mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-03-03 05:55:16 +01:00
Compare commits
2 Commits
master
...
copilot/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c84911b7ea | ||
|
|
3fc0d95b18 |
@@ -12,7 +12,7 @@ opcache.max_accelerated_files = 20000
|
|||||||
opcache.memory_consumption = 256
|
opcache.memory_consumption = 256
|
||||||
opcache.enable_file_override = 1
|
opcache.enable_file_override = 1
|
||||||
|
|
||||||
memory_limit = 512M
|
memory_limit = 256M
|
||||||
|
|
||||||
upload_max_filesize=256M
|
upload_max_filesize=256M
|
||||||
post_max_size=300M
|
post_max_size=300M
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
worker {
|
worker {
|
||||||
file ./public/index.php
|
file ./public/index.php
|
||||||
|
env APP_RUNTIME Runtime\FrankenPhpSymfony\Runtime
|
||||||
}
|
}
|
||||||
|
|||||||
11
.env
11
.env
@@ -59,17 +59,6 @@ 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...
|
# 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
|
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
|
|
||||||
|
|
||||||
###################################################################################
|
###################################################################################
|
||||||
# SAML Single sign on-settings
|
# SAML Single sign on-settings
|
||||||
|
|||||||
113
.github/workflows/docker_build.yml
vendored
113
.github/workflows/docker_build.yml
vendored
@@ -15,20 +15,8 @@ on:
|
|||||||
- 'v*.*.*-**'
|
- 'v*.*.*-**'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
docker:
|
||||||
strategy:
|
runs-on: ubuntu-latest
|
||||||
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:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
@@ -44,12 +32,13 @@ jobs:
|
|||||||
# Mark the image build from master as latest (as we dont have really releases yet)
|
# Mark the image build from master as latest (as we dont have really releases yet)
|
||||||
tags: |
|
tags: |
|
||||||
type=edge,branch=master
|
type=edge,branch=master
|
||||||
type=ref,event=branch
|
type=ref,event=branch,
|
||||||
type=ref,event=tag
|
type=ref,event=tag,
|
||||||
type=schedule
|
type=schedule
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
type=semver,pattern={{major}}
|
type=semver,pattern={{major}}
|
||||||
|
type=ref,event=branch
|
||||||
type=ref,event=pr
|
type=ref,event=pr
|
||||||
labels: |
|
labels: |
|
||||||
org.opencontainers.image.source=${{ github.event.repository.clone_url }}
|
org.opencontainers.image.source=${{ github.event.repository.clone_url }}
|
||||||
@@ -60,10 +49,12 @@ jobs:
|
|||||||
org.opencontainers.image.source=https://github.com/Part-DB/Part-DB-symfony
|
org.opencontainers.image.source=https://github.com/Part-DB/Part-DB-symfony
|
||||||
org.opencontainers.image.authors=Jan Böhmer
|
org.opencontainers.image.authors=Jan Böhmer
|
||||||
org.opencontainers.licenses=AGPL-3.0-or-later
|
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
|
name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
@@ -76,85 +67,13 @@ jobs:
|
|||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
-
|
-
|
||||||
name: Build and push by digest
|
name: Build and push
|
||||||
id: build
|
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: ${{ matrix.platform }}
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||||
outputs: type=image,name=jbtronics/part-db1,push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
|
cache-from: type=gha
|
||||||
cache-from: type=gha,scope=build-${{ matrix.platform }}
|
cache-to: type=gha,mode=max
|
||||||
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@v6
|
|
||||||
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@v7
|
|
||||||
with:
|
|
||||||
path: /tmp/digests
|
|
||||||
pattern: digests-*
|
|
||||||
merge-multiple: true
|
|
||||||
|
|
||||||
-
|
|
||||||
name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
-
|
|
||||||
name: Docker meta
|
|
||||||
id: docker_meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
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@v3
|
|
||||||
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 }}
|
|
||||||
|
|||||||
113
.github/workflows/docker_frankenphp.yml
vendored
113
.github/workflows/docker_frankenphp.yml
vendored
@@ -15,20 +15,8 @@ on:
|
|||||||
- 'v*.*.*-**'
|
- 'v*.*.*-**'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
docker:
|
||||||
strategy:
|
runs-on: ubuntu-latest
|
||||||
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:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
@@ -44,12 +32,13 @@ jobs:
|
|||||||
# Mark the image build from master as latest (as we dont have really releases yet)
|
# Mark the image build from master as latest (as we dont have really releases yet)
|
||||||
tags: |
|
tags: |
|
||||||
type=edge,branch=master
|
type=edge,branch=master
|
||||||
type=ref,event=branch
|
type=ref,event=branch,
|
||||||
type=ref,event=tag
|
type=ref,event=tag,
|
||||||
type=schedule
|
type=schedule
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
type=semver,pattern={{major}}
|
type=semver,pattern={{major}}
|
||||||
|
type=ref,event=branch
|
||||||
type=ref,event=pr
|
type=ref,event=pr
|
||||||
labels: |
|
labels: |
|
||||||
org.opencontainers.image.source=${{ github.event.repository.clone_url }}
|
org.opencontainers.image.source=${{ github.event.repository.clone_url }}
|
||||||
@@ -60,10 +49,12 @@ jobs:
|
|||||||
org.opencontainers.image.source=https://github.com/Part-DB/Part-DB-server
|
org.opencontainers.image.source=https://github.com/Part-DB/Part-DB-server
|
||||||
org.opencontainers.image.authors=Jan Böhmer
|
org.opencontainers.image.authors=Jan Böhmer
|
||||||
org.opencontainers.licenses=AGPL-3.0-or-later
|
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
|
name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
@@ -76,86 +67,14 @@ jobs:
|
|||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
-
|
-
|
||||||
name: Build and push by digest
|
name: Build and push
|
||||||
id: build
|
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: Dockerfile-frankenphp
|
file: Dockerfile-frankenphp
|
||||||
platforms: ${{ matrix.platform }}
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||||
outputs: type=image,name=partdborg/part-db,push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
|
cache-from: type=gha
|
||||||
cache-from: type=gha,scope=build-${{ matrix.platform }}
|
cache-to: type=gha,mode=max
|
||||||
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@v6
|
|
||||||
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@v7
|
|
||||||
with:
|
|
||||||
path: /tmp/digests
|
|
||||||
pattern: digests-*
|
|
||||||
merge-multiple: true
|
|
||||||
|
|
||||||
-
|
|
||||||
name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
-
|
|
||||||
name: Docker meta
|
|
||||||
id: docker_meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
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@v3
|
|
||||||
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 }}
|
|
||||||
|
|||||||
117
Dockerfile
117
Dockerfile
@@ -1,75 +1,15 @@
|
|||||||
# syntax=docker/dockerfile:1
|
|
||||||
ARG BASE_IMAGE=debian:bookworm-slim
|
ARG BASE_IMAGE=debian:bookworm-slim
|
||||||
ARG PHP_VERSION=8.4
|
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
|
FROM ${BASE_IMAGE} AS base
|
||||||
ARG PHP_VERSION
|
ARG PHP_VERSION
|
||||||
ARG TARGETARCH
|
|
||||||
|
|
||||||
# Use BuildKit cache mounts for apt in base stage
|
# Install needed dependencies for PHP build
|
||||||
RUN --mount=type=cache,id=apt-cache-$TARGETARCH,target=/var/cache/apt \
|
#RUN apt-get update && apt-get install -y pkg-config curl libcurl4-openssl-dev libicu-dev \
|
||||||
--mount=type=cache,id=apt-lists-$TARGETARCH,target=/var/lib/apt/lists \
|
# libpng-dev libjpeg-dev libfreetype6-dev gnupg zip libzip-dev libjpeg62-turbo-dev libonig-dev libxslt-dev libwebp-dev vim \
|
||||||
apt-get update && apt-get -y install \
|
# && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get -y install \
|
||||||
apt-transport-https \
|
apt-transport-https \
|
||||||
lsb-release \
|
lsb-release \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
@@ -99,10 +39,21 @@ RUN --mount=type=cache,id=apt-cache-$TARGETARCH,target=/var/cache/apt \
|
|||||||
gpg \
|
gpg \
|
||||||
sudo \
|
sudo \
|
||||||
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/* \
|
&& 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 \
|
&& mkdir -p /var/www/html \
|
||||||
&& chown -R www-data:www-data /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/*
|
&& 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
|
# Install composer
|
||||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
@@ -116,12 +67,14 @@ ENV APACHE_ENVVARS=$APACHE_CONFDIR/envvars
|
|||||||
# : ${APACHE_RUN_USER:=www-data}
|
# : ${APACHE_RUN_USER:=www-data}
|
||||||
# export APACHE_RUN_USER
|
# export APACHE_RUN_USER
|
||||||
# so that they can be overridden at runtime ("-e 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" && \
|
RUN sed -ri 's/^export ([^=]+)=(.*)$/: ${\1:=\2}\nexport \1/' "$APACHE_ENVVARS"; \
|
||||||
set -eux; . "$APACHE_ENVVARS" && \
|
set -eux; . "$APACHE_ENVVARS"; \
|
||||||
ln -sfT /dev/stderr "$APACHE_LOG_DIR/error.log" && \
|
\
|
||||||
ln -sfT /dev/stdout "$APACHE_LOG_DIR/access.log" && \
|
# logs should go to stdout / stderr
|
||||||
ln -sfT /dev/stdout "$APACHE_LOG_DIR/other_vhosts_access.log" && \
|
ln -sfT /dev/stderr "$APACHE_LOG_DIR/error.log"; \
|
||||||
chown -R --no-dereference "$APACHE_RUN_USER:$APACHE_RUN_GROUP" "$APACHE_LOG_DIR"
|
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";
|
||||||
|
|
||||||
# ---
|
# ---
|
||||||
|
|
||||||
@@ -190,6 +143,7 @@ COPY --chown=www-data:www-data . .
|
|||||||
# Setup apache2
|
# Setup apache2
|
||||||
RUN a2dissite 000-default.conf && \
|
RUN a2dissite 000-default.conf && \
|
||||||
a2ensite symfony.conf && \
|
a2ensite symfony.conf && \
|
||||||
|
# Enable php-fpm
|
||||||
a2enmod proxy_fcgi setenvif && \
|
a2enmod proxy_fcgi setenvif && \
|
||||||
a2enconf php${PHP_VERSION}-fpm && \
|
a2enconf php${PHP_VERSION}-fpm && \
|
||||||
a2enconf docker-php && \
|
a2enconf docker-php && \
|
||||||
@@ -197,13 +151,12 @@ RUN a2dissite 000-default.conf && \
|
|||||||
|
|
||||||
# Install composer and yarn dependencies for Part-DB
|
# Install composer and yarn dependencies for Part-DB
|
||||||
USER www-data
|
USER www-data
|
||||||
# Use BuildKit cache for Composer when running as www-data by setting COMPOSER_CACHE_DIR
|
RUN composer install -a --no-dev && \
|
||||||
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
|
composer clear-cache
|
||||||
|
RUN yarn install --network-timeout 600000 && \
|
||||||
# Copy built frontend assets from node-builder stage
|
yarn build && \
|
||||||
COPY --from=node-builder --chown=www-data:www-data /app/public/build ./public/build
|
yarn cache clean && \
|
||||||
|
rm -rf node_modules/
|
||||||
|
|
||||||
# Use docker env to output logs to stdout
|
# Use docker env to output logs to stdout
|
||||||
ENV APP_ENV=docker
|
ENV APP_ENV=docker
|
||||||
@@ -215,12 +168,10 @@ USER root
|
|||||||
RUN sed -i "s/PHP_VERSION/${PHP_VERSION}/g" ./.docker/partdb-entrypoint.sh
|
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
|
# Copy entrypoint and apache2-foreground to /usr/local/bin and make it executable
|
||||||
# Convert CRLF -> LF and install entrypoint scripts with executable mode
|
RUN install ./.docker/partdb-entrypoint.sh /usr/local/bin && \
|
||||||
RUN sed -i 's/\r$//' ./.docker/partdb-entrypoint.sh ./.docker/apache2-foreground && \
|
install ./.docker/apache2-foreground /usr/local/bin
|
||||||
install -m 0755 ./.docker/partdb-entrypoint.sh /usr/local/bin/ && \
|
|
||||||
install -m 0755 ./.docker/apache2-foreground /usr/local/bin/
|
|
||||||
ENTRYPOINT ["partdb-entrypoint.sh"]
|
ENTRYPOINT ["partdb-entrypoint.sh"]
|
||||||
CMD ["/usr/local/bin/apache2-foreground"]
|
CMD ["apache2-foreground"]
|
||||||
|
|
||||||
# https://httpd.apache.org/docs/2.4/stopping.html#gracefulstop
|
# https://httpd.apache.org/docs/2.4/stopping.html#gracefulstop
|
||||||
STOPSIGNAL SIGWINCH
|
STOPSIGNAL SIGWINCH
|
||||||
|
|||||||
@@ -1,72 +1,6 @@
|
|||||||
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/
|
|
||||||
|
|
||||||
# FrankenPHP base stage
|
|
||||||
FROM dunglas/frankenphp:1-php8.4 AS frankenphp_upstream
|
FROM dunglas/frankenphp:1-php8.4 AS frankenphp_upstream
|
||||||
ARG TARGETARCH
|
|
||||||
RUN --mount=type=cache,id=apt-cache-$TARGETARCH,target=/var/cache/apt \
|
RUN apt-get update && apt-get -y install \
|
||||||
--mount=type=cache,id=apt-lists-$TARGETARCH,target=/var/lib/apt/lists \
|
|
||||||
apt-get update && apt-get -y install \
|
|
||||||
curl \
|
curl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
mariadb-client \
|
mariadb-client \
|
||||||
@@ -79,6 +13,34 @@ RUN --mount=type=cache,id=apt-cache-$TARGETARCH,target=/var/cache/apt \
|
|||||||
zip \
|
zip \
|
||||||
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*;
|
&& 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
|
# Install PHP
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
install-php-extensions \
|
install-php-extensions \
|
||||||
@@ -124,11 +86,14 @@ COPY --link . ./
|
|||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
mkdir -p var/cache var/log; \
|
mkdir -p var/cache var/log; \
|
||||||
composer dump-autoload --classmap-authoritative --no-dev; \
|
composer dump-autoload --classmap-authoritative --no-dev; \
|
||||||
|
composer dump-env prod; \
|
||||||
composer run-script --no-dev post-install-cmd; \
|
composer run-script --no-dev post-install-cmd; \
|
||||||
chmod +x bin/console; sync;
|
chmod +x bin/console; sync;
|
||||||
|
|
||||||
# Copy built frontend assets from node-builder stage
|
RUN yarn install --network-timeout 600000 && \
|
||||||
COPY --from=node-builder /app/public/build ./public/build
|
yarn build && \
|
||||||
|
yarn cache clean && \
|
||||||
|
rm -rf node_modules/
|
||||||
|
|
||||||
# Use docker env to output logs to stdout
|
# Use docker env to output logs to stdout
|
||||||
ENV APP_ENV=docker
|
ENV APP_ENV=docker
|
||||||
@@ -147,8 +112,8 @@ VOLUME ["/var/www/html/uploads", "/var/www/html/public/media"]
|
|||||||
HEALTHCHECK --start-period=60s CMD curl -f http://localhost:2019/metrics || exit 1
|
HEALTHCHECK --start-period=60s CMD curl -f http://localhost:2019/metrics || exit 1
|
||||||
|
|
||||||
# See https://caddyserver.com/docs/conventions#file-locations for details
|
# See https://caddyserver.com/docs/conventions#file-locations for details
|
||||||
ENV XDG_CONFIG_HOME=/config
|
ENV XDG_CONFIG_HOME /config
|
||||||
ENV XDG_DATA_HOME=/data
|
ENV XDG_DATA_HOME /data
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
EXPOSE 443
|
EXPOSE 443
|
||||||
|
|||||||
@@ -1,206 +0,0 @@
|
|||||||
{
|
|
||||||
"_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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,10 +20,6 @@
|
|||||||
import { Controller } from '@hotwired/stimulus';
|
import { Controller } from '@hotwired/stimulus';
|
||||||
import { Toast } from 'bootstrap';
|
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 {
|
export default class extends Controller {
|
||||||
connect() {
|
connect() {
|
||||||
//Move all toasts from the page into our toast container and show them
|
//Move all toasts from the page into our toast container and show them
|
||||||
@@ -37,4 +33,4 @@ export default class extends Controller {
|
|||||||
const toast = new Toast(this.element);
|
const toast = new Toast(this.element);
|
||||||
toast.show();
|
toast.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,6 @@ import {Controller} from "@hotwired/stimulus";
|
|||||||
|
|
||||||
import * as bootbox from "bootbox";
|
import * as bootbox from "bootbox";
|
||||||
import "../../css/components/bootbox_extensions.css";
|
import "../../css/components/bootbox_extensions.css";
|
||||||
import accept from "attr-accept";
|
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static values = {
|
static values = {
|
||||||
@@ -74,33 +73,15 @@ export default class extends Controller {
|
|||||||
const newElementStr = this.htmlDecode(prototype.replace(regex, this.generateUID()));
|
const newElementStr = this.htmlDecode(prototype.replace(regex, this.generateUID()));
|
||||||
|
|
||||||
|
|
||||||
let ret = null;
|
|
||||||
|
|
||||||
//Insert new html after the last child element
|
//Insert new html after the last child element
|
||||||
//If the table has a tbody, insert it there
|
//If the table has a tbody, insert it there
|
||||||
//Afterwards return the newly created row
|
//Afterwards return the newly created row
|
||||||
if(targetTable.tBodies[0]) {
|
if(targetTable.tBodies[0]) {
|
||||||
targetTable.tBodies[0].insertAdjacentHTML('beforeend', newElementStr);
|
targetTable.tBodies[0].insertAdjacentHTML('beforeend', newElementStr);
|
||||||
ret = targetTable.tBodies[0].lastElementChild;
|
return targetTable.tBodies[0].lastElementChild;
|
||||||
} else { //Otherwise just insert it
|
} else { //Otherwise just insert it
|
||||||
targetTable.insertAdjacentHTML('beforeend', newElementStr);
|
targetTable.insertAdjacentHTML('beforeend', newElementStr);
|
||||||
ret = targetTable.lastElementChild;
|
return 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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,33 +112,6 @@ export default class extends Controller {
|
|||||||
dataTransfer.items.add(file);
|
dataTransfer.items.add(file);
|
||||||
|
|
||||||
rowInput.files = dataTransfer.files;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -235,4 +189,4 @@ export default class extends Controller {
|
|||||||
del();
|
del();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,19 +108,11 @@ export default class extends Controller {
|
|||||||
const raw_order = saved_state.order;
|
const raw_order = saved_state.order;
|
||||||
|
|
||||||
settings.initial_order = raw_order.map((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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
column: order[0],
|
column: order[0],
|
||||||
dir: order[1]
|
dir: order[1]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//Remove null values from the initial_order array
|
|
||||||
settings.initial_order = settings.initial_order.filter(order => order !== null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let options = {
|
let options = {
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,7 @@ export default class extends Controller {
|
|||||||
|
|
||||||
let settings = {
|
let settings = {
|
||||||
allowEmptyOption: true,
|
allowEmptyOption: true,
|
||||||
plugins: ['dropdown_input', this.element.required ? null : 'clear_button'],
|
plugins: ['dropdown_input'],
|
||||||
searchField: ["name", "description", "category", "footprint"],
|
searchField: ["name", "description", "category", "footprint"],
|
||||||
valueField: "id",
|
valueField: "id",
|
||||||
labelField: "name",
|
labelField: "name",
|
||||||
|
|||||||
@@ -62,6 +62,6 @@ export default class extends Controller {
|
|||||||
element.disabled = true;
|
element.disabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
form.requestSubmit();
|
form.submit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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,31 +21,17 @@ import {Controller} from "@hotwired/stimulus";
|
|||||||
//import * as ZXing from "@zxing/library";
|
//import * as ZXing from "@zxing/library";
|
||||||
|
|
||||||
import {Html5QrcodeScanner, Html5Qrcode} from "@part-db/html5-qrcode";
|
import {Html5QrcodeScanner, Html5Qrcode} from "@part-db/html5-qrcode";
|
||||||
import { generateCsrfToken, generateCsrfHeaders } from "../csrf_protection_controller";
|
|
||||||
|
|
||||||
/* stimulusFetch: 'lazy' */
|
/* stimulusFetch: 'lazy' */
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
|
|
||||||
|
//codeReader = null;
|
||||||
|
|
||||||
_scanner = null;
|
_scanner = null;
|
||||||
_submitting = false;
|
|
||||||
_lastDecodedText = "";
|
|
||||||
_onInfoChange = null;
|
|
||||||
|
|
||||||
connect() {
|
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
|
//This function ensures, that the qrbox is 70% of the total viewport
|
||||||
let qrboxFunction = function(viewfinderWidth, viewfinderHeight) {
|
let qrboxFunction = function(viewfinderWidth, viewfinderHeight) {
|
||||||
@@ -59,67 +45,31 @@ 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
|
//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(() => {
|
Html5Qrcode.getCameras().catch((devices) => {
|
||||||
document.getElementById("scanner-warning")?.classList.remove("d-none");
|
document.getElementById('scanner-warning').classList.remove('d-none');
|
||||||
});
|
});
|
||||||
|
|
||||||
this._scanner = new Html5QrcodeScanner(this.element.id, {
|
this._scanner = new Html5QrcodeScanner(this.element.id, {
|
||||||
fps: 10,
|
fps: 10,
|
||||||
qrbox: qrboxFunction,
|
qrbox: qrboxFunction,
|
||||||
// Key change: shrink preview height on mobile
|
|
||||||
...(isMobile ? { aspectRatio: 1.0 } : {}),
|
|
||||||
experimentalFeatures: {
|
experimentalFeatures: {
|
||||||
//This option improves reading quality on android chrome
|
//This option improves reading quality on android chrome
|
||||||
useBarCodeDetectorIfSupported: true,
|
useBarCodeDetectorIfSupported: true
|
||||||
},
|
}
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
this._scanner.render(this.onScanSuccess.bind(this));
|
this._scanner.render(this.onScanSuccess.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
|
this._scanner.pause();
|
||||||
// If we already stopped/cleared before submit, nothing to do.
|
this._scanner.clear();
|
||||||
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) {
|
||||||
onScanSuccess(decodedText) {
|
//Put our decoded Text into the input box
|
||||||
if (!decodedText) return;
|
document.getElementById('scan_dialog_input').value = decodedText;
|
||||||
|
|
||||||
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
|
//Submit form
|
||||||
document.getElementById('scan_dialog_form').requestSubmit();
|
document.getElementById('scan_dialog_form').submit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
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,7 +5,6 @@ export default class extends Controller
|
|||||||
{
|
{
|
||||||
connect() {
|
connect() {
|
||||||
this.element.addEventListener('show.bs.modal', event => this._handleModalOpen(event));
|
this.element.addEventListener('show.bs.modal', event => this._handleModalOpen(event));
|
||||||
this.element.addEventListener('shown.bs.modal', event => this._handleModalShown(event));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleModalOpen(event) {
|
_handleModalOpen(event) {
|
||||||
@@ -62,8 +61,4 @@ export default class extends Controller
|
|||||||
amountInput.setAttribute('max', lotAmount);
|
amountInput.setAttribute('max', lotAmount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleModalShown(event) {
|
|
||||||
this.element.querySelector('input[name="amount"]').focus();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
<?php
|
|
||||||
/*
|
/*
|
||||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
* 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)
|
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -18,18 +17,11 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
declare(strict_types=1);
|
import { Controller } from '@hotwired/stimulus';
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
namespace App\Validator\Constraints;
|
connect() {
|
||||||
|
//If we encounter an element with global reload controller, then reload the whole page
|
||||||
use Symfony\Component\Validator\Constraint;
|
window.location.reload();
|
||||||
|
}
|
||||||
/**
|
}
|
||||||
* A constraint to ensure that a GTIN is valid.
|
|
||||||
*/
|
|
||||||
#[\Attribute(\Attribute::TARGET_PROPERTY)]
|
|
||||||
class ValidGTIN extends Constraint
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
<?php
|
|
||||||
/*
|
/*
|
||||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
* 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)
|
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -18,15 +17,11 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
declare(strict_types=1);
|
import { Controller } from '@hotwired/stimulus';
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
namespace App\Exceptions;
|
connect() {
|
||||||
|
const menu = document.getElementById('locale-select-menu');
|
||||||
class ProviderIDNotSupportedException extends \RuntimeException
|
menu.innerHTML = this.element.innerHTML;
|
||||||
{
|
|
||||||
public function fromProvider(string $providerKey, string $id): self
|
|
||||||
{
|
|
||||||
return new self(sprintf('The given ID %s is not supported by the provider %s.', $id, $providerKey,));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
<?php
|
|
||||||
/*
|
/*
|
||||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
* 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)
|
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@@ -18,29 +17,15 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
declare(strict_types=1);
|
import { Controller } from '@hotwired/stimulus';
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
namespace App\Services\LabelSystem\BarcodeScanner;
|
connect() {
|
||||||
|
//If we encounter an element with this, then change the title of our document according to data-title
|
||||||
final readonly class AmazonBarcodeScanResult implements BarcodeScanResultInterface
|
this.changeTitle(this.element.dataset.title);
|
||||||
{
|
|
||||||
public function __construct(public string $asin) {
|
|
||||||
if (!self::isAmazonBarcode($asin)) {
|
|
||||||
throw new \InvalidArgumentException("The provided input '$asin' is not a valid Amazon barcode (ASIN)");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function isAmazonBarcode(string $input): bool
|
changeTitle(title) {
|
||||||
{
|
document.title = title;
|
||||||
//Amazon barcodes are 10 alphanumeric characters
|
|
||||||
return preg_match('/^[A-Z0-9]{10}$/i', $input) === 1;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
public function getDecodedForInfoMode(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'ASIN' => $this->asin,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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,12 +58,6 @@
|
|||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.part-info-image {
|
|
||||||
max-height: 100px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.object-fit-cover {
|
.object-fit-cover {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,25 +125,3 @@ Classes for Datatables export
|
|||||||
.export-helper{
|
.export-helper{
|
||||||
display: none;
|
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -27,12 +27,7 @@ class RegisterEventHelper {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.registerTooltips();
|
this.registerTooltips();
|
||||||
this.configureDropdowns();
|
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
|
//Initialize ClipboardJS
|
||||||
this.registerLoadHandler(() => {
|
this.registerLoadHandler(() => {
|
||||||
|
|||||||
@@ -56,8 +56,7 @@ class TristateHelper {
|
|||||||
|
|
||||||
document.addEventListener("turbo:load", listener);
|
document.addEventListener("turbo:load", listener);
|
||||||
document.addEventListener("turbo:render", listener);
|
document.addEventListener("turbo:render", listener);
|
||||||
document.addEventListener("collection:elementAdded", listener);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new TristateHelper();
|
export default new TristateHelper();
|
||||||
@@ -198,7 +198,6 @@ class WebauthnTFA {
|
|||||||
{
|
{
|
||||||
const resultField = document.getElementById('_auth_code');
|
const resultField = document.getElementById('_auth_code');
|
||||||
resultField.value = JSON.stringify(data)
|
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();
|
form.submit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,4 +232,4 @@ class WebauthnTFA {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.webauthnTFA = new WebauthnTFA();
|
window.webauthnTFA = new WebauthnTFA();
|
||||||
@@ -11,14 +11,12 @@
|
|||||||
"ext-intl": "*",
|
"ext-intl": "*",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-mbstring": "*",
|
"ext-mbstring": "*",
|
||||||
"ext-zip": "*",
|
|
||||||
"amphp/http-client": "^5.1",
|
"amphp/http-client": "^5.1",
|
||||||
"api-platform/doctrine-orm": "^4.1",
|
"api-platform/doctrine-orm": "^4.1",
|
||||||
"api-platform/json-api": "^4.0.0",
|
"api-platform/json-api": "^4.0.0",
|
||||||
"api-platform/symfony": "^4.0.0",
|
"api-platform/symfony": "^4.0.0",
|
||||||
"beberlei/doctrineextensions": "^1.2",
|
"beberlei/doctrineextensions": "^1.2",
|
||||||
"brick/math": "^0.14.8",
|
"brick/math": "^0.13.1",
|
||||||
"brick/schema": "^0.2.0",
|
|
||||||
"composer/ca-bundle": "^1.5",
|
"composer/ca-bundle": "^1.5",
|
||||||
"composer/package-versions-deprecated": "^1.11.99.5",
|
"composer/package-versions-deprecated": "^1.11.99.5",
|
||||||
"doctrine/data-fixtures": "^2.0.0",
|
"doctrine/data-fixtures": "^2.0.0",
|
||||||
@@ -28,7 +26,7 @@
|
|||||||
"doctrine/orm": "^3.2.0",
|
"doctrine/orm": "^3.2.0",
|
||||||
"dompdf/dompdf": "^3.1.2",
|
"dompdf/dompdf": "^3.1.2",
|
||||||
"gregwar/captcha-bundle": "^2.1.0",
|
"gregwar/captcha-bundle": "^2.1.0",
|
||||||
"hshn/base64-encoded-file": "^6.0",
|
"hshn/base64-encoded-file": "^5.0",
|
||||||
"jbtronics/2fa-webauthn": "^3.0.0",
|
"jbtronics/2fa-webauthn": "^3.0.0",
|
||||||
"jbtronics/dompdf-font-loader-bundle": "^1.0.0",
|
"jbtronics/dompdf-font-loader-bundle": "^1.0.0",
|
||||||
"jbtronics/settings-bundle": "^3.0.0",
|
"jbtronics/settings-bundle": "^3.0.0",
|
||||||
@@ -45,6 +43,7 @@
|
|||||||
"nelmio/security-bundle": "^3.0",
|
"nelmio/security-bundle": "^3.0",
|
||||||
"nyholm/psr7": "^1.1",
|
"nyholm/psr7": "^1.1",
|
||||||
"omines/datatables-bundle": "^0.10.0",
|
"omines/datatables-bundle": "^0.10.0",
|
||||||
|
"paragonie/sodium_compat": "^1.21",
|
||||||
"part-db/label-fonts": "^1.0",
|
"part-db/label-fonts": "^1.0",
|
||||||
"part-db/swap-bundle": "^6.0.0",
|
"part-db/swap-bundle": "^6.0.0",
|
||||||
"phpoffice/phpspreadsheet": "^5.0.0",
|
"phpoffice/phpspreadsheet": "^5.0.0",
|
||||||
@@ -69,7 +68,7 @@
|
|||||||
"symfony/http-client": "7.4.*",
|
"symfony/http-client": "7.4.*",
|
||||||
"symfony/http-kernel": "7.4.*",
|
"symfony/http-kernel": "7.4.*",
|
||||||
"symfony/mailer": "7.4.*",
|
"symfony/mailer": "7.4.*",
|
||||||
"symfony/monolog-bundle": "^4.0",
|
"symfony/monolog-bundle": "^3.1",
|
||||||
"symfony/process": "7.4.*",
|
"symfony/process": "7.4.*",
|
||||||
"symfony/property-access": "7.4.*",
|
"symfony/property-access": "7.4.*",
|
||||||
"symfony/property-info": "7.4.*",
|
"symfony/property-info": "7.4.*",
|
||||||
@@ -80,16 +79,15 @@
|
|||||||
"symfony/string": "7.4.*",
|
"symfony/string": "7.4.*",
|
||||||
"symfony/translation": "7.4.*",
|
"symfony/translation": "7.4.*",
|
||||||
"symfony/twig-bundle": "7.4.*",
|
"symfony/twig-bundle": "7.4.*",
|
||||||
"symfony/type-info": "7.4.*",
|
"symfony/type-info": "7.4.0",
|
||||||
"symfony/ux-translator": "^2.32.0",
|
"symfony/ux-translator": "^2.32.0",
|
||||||
"symfony/ux-turbo": "^2.0",
|
"symfony/ux-turbo": "^2.0",
|
||||||
"symfony/validator": "7.4.*",
|
"symfony/validator": "7.4.*",
|
||||||
"symfony/web-link": "7.4.*",
|
"symfony/web-link": "7.4.*",
|
||||||
"symfony/webpack-encore-bundle": "^v2.0.1",
|
"symfony/webpack-encore-bundle": "^v2.0.1",
|
||||||
"symfony/yaml": "7.4.*",
|
"symfony/yaml": "7.4.*",
|
||||||
"symplify/easy-coding-standard": "^13.0",
|
"symplify/easy-coding-standard": "^12.5.20",
|
||||||
"tecnickcom/tc-lib-barcode": "^2.1.4",
|
"tecnickcom/tc-lib-barcode": "^2.1.4",
|
||||||
"tiendanube/gtinvalidation": "^1.0",
|
|
||||||
"twig/cssinliner-extra": "^3.0",
|
"twig/cssinliner-extra": "^3.0",
|
||||||
"twig/extra-bundle": "^3.8",
|
"twig/extra-bundle": "^3.8",
|
||||||
"twig/html-extra": "^3.8",
|
"twig/html-extra": "^3.8",
|
||||||
@@ -128,7 +126,7 @@
|
|||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
"ext-bcmath": "Used to improve price calculation performance",
|
"ext-bcmath": "Used to improve price calculation performance",
|
||||||
"ext-gmp": "Used to improve price calculation performance"
|
"ext-gmp": "Used to improve price calculation performanice"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"preferred-install": {
|
"preferred-install": {
|
||||||
|
|||||||
1883
composer.lock
generated
1883
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,3 @@ framework:
|
|||||||
|
|
||||||
info_provider.cache:
|
info_provider.cache:
|
||||||
adapter: cache.app
|
adapter: cache.app
|
||||||
|
|
||||||
cache.settings:
|
|
||||||
adapter: cache.app
|
|
||||||
tags: true
|
|
||||||
|
|||||||
@@ -20,14 +20,12 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
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+.
|
* 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.
|
* We have to do this in a PHP file, because the yaml file does not support conditionals on PHP version.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
return static function(DoctrineConfig $doctrine) {
|
return static function(\Symfony\Config\DoctrineConfig $doctrine) {
|
||||||
//On PHP 8.4+ we can use native lazy objects, which are much more efficient than proxies.
|
//On PHP 8.4+ we can use native lazy objects, which are much more efficient than proxies.
|
||||||
if (PHP_VERSION_ID >= 80400) {
|
if (PHP_VERSION_ID >= 80400) {
|
||||||
$doctrine->orm()->enableNativeLazyObjects(true);
|
$doctrine->orm()->enableNativeLazyObjects(true);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
# yaml-language-server: $schema=../../vendor/symfony/dependency-injection/Loader/schema/services.schema.json
|
|
||||||
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
||||||
framework:
|
framework:
|
||||||
secret: '%env(APP_SECRET)%'
|
secret: '%env(APP_SECRET)%'
|
||||||
@@ -9,7 +8,6 @@ framework:
|
|||||||
# Must be set to true, to enable the change of HTTP method via _method parameter, otherwise our delete routines does not work anymore
|
# 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)
|
# 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
|
http_method_override: true
|
||||||
allowed_http_method_override: ['DELETE']
|
|
||||||
|
|
||||||
# Allow users to configure trusted hosts via .env variables
|
# Allow users to configure trusted hosts via .env variables
|
||||||
# see https://symfony.com/doc/current/reference/configuration/framework.html#trusted-hosts
|
# see https://symfony.com/doc/current/reference/configuration/framework.html#trusted-hosts
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ jbtronics_settings:
|
|||||||
|
|
||||||
cache:
|
cache:
|
||||||
default_cacheable: true
|
default_cacheable: true
|
||||||
service: 'cache.settings'
|
|
||||||
|
|
||||||
orm_storage:
|
orm_storage:
|
||||||
default_entity_class: App\Entity\SettingsEntry
|
default_entity_class: App\Entity\SettingsEntry
|
||||||
|
|||||||
@@ -68,9 +68,6 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co
|
|||||||
move:
|
move:
|
||||||
label: "perm.parts_stock.move"
|
label: "perm.parts_stock.move"
|
||||||
apiTokenRole: ROLE_API_EDIT
|
apiTokenRole: ROLE_API_EDIT
|
||||||
stocktake:
|
|
||||||
label: "perm.parts_stock.stocktake"
|
|
||||||
apiTokenRole: ROLE_API_EDIT
|
|
||||||
|
|
||||||
|
|
||||||
storelocations: &PART_CONTAINING
|
storelocations: &PART_CONTAINING
|
||||||
@@ -300,10 +297,6 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co
|
|||||||
show_updates:
|
show_updates:
|
||||||
label: "perm.system.show_available_updates"
|
label: "perm.system.show_available_updates"
|
||||||
apiTokenRole: ROLE_API_ADMIN
|
apiTokenRole: ROLE_API_ADMIN
|
||||||
manage_updates:
|
|
||||||
label: "perm.system.manage_updates"
|
|
||||||
alsoSet: ['show_updates', 'server_infos']
|
|
||||||
apiTokenRole: ROLE_API_ADMIN
|
|
||||||
|
|
||||||
|
|
||||||
attachments:
|
attachments:
|
||||||
|
|||||||
2259
config/reference.php
2259
config/reference.php
File diff suppressed because it is too large
Load Diff
@@ -5,5 +5,3 @@ files:
|
|||||||
translation: /translations/validators.%two_letters_code%.xlf
|
translation: /translations/validators.%two_letters_code%.xlf
|
||||||
- source: /translations/security.en.xlf
|
- source: /translations/security.en.xlf
|
||||||
translation: /translations/security.%two_letters_code%.xlf
|
translation: /translations/security.%two_letters_code%.xlf
|
||||||
- source: /translations/frontend.en.xlf
|
|
||||||
translation: /translations/frontend.%two_letters_code%.xlf
|
|
||||||
|
|||||||
@@ -86,9 +86,6 @@ 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
|
* `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
|
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.
|
locally by default. Attachment download is only possible, when `ALLOW_ATTACHMENT_DOWNLOADS` is set to 1.
|
||||||
* `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
|
* `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
|
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.
|
this might be a privacy risk.
|
||||||
@@ -129,8 +126,6 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept
|
|||||||
unique increments for parts within a category hierarchy, ensuring consistency and uniqueness in IPN generation.
|
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
|
* `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.
|
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)
|
### E-Mail settings (all env only)
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 142 KiB |
@@ -50,14 +50,6 @@ docker exec --user=www-data partdb php bin/console cache:clear
|
|||||||
* `php bin/console partdb:currencies:update-exchange-rates`: Update the exchange rates of all currencies from the
|
* `php bin/console partdb:currencies:update-exchange-rates`: Update the exchange rates of all currencies from the
|
||||||
internet
|
internet
|
||||||
|
|
||||||
## Update Manager commands
|
|
||||||
|
|
||||||
{: .note }
|
|
||||||
> The Update Manager is an experimental feature. See the [Update Manager documentation](update_manager.md) for details.
|
|
||||||
|
|
||||||
* `php bin/console partdb:update`: Check for and perform updates to Part-DB. Use `--check` to only check for updates without installing.
|
|
||||||
* `php bin/console partdb:maintenance-mode`: Enable, disable, or check the status of maintenance mode. Use `--enable`, `--disable`, or `--status`.
|
|
||||||
|
|
||||||
## Installation/Maintenance commands
|
## Installation/Maintenance commands
|
||||||
|
|
||||||
* `php bin/console partdb:backup`: Backup the database and the attachments
|
* `php bin/console partdb:backup`: Backup the database and the attachments
|
||||||
@@ -88,6 +80,3 @@ The value of the environment variable is copied to the settings database, so the
|
|||||||
* `php bin/console partdb:attachments:download`: Download all attachments that are not already downloaded to the
|
* `php bin/console partdb:attachments:download`: Download all attachments that are not already downloaded to the
|
||||||
local filesystem. This is useful to create local backups of the attachments, no matter what happens on the remote, and
|
local filesystem. This is useful to create local backups of the attachments, no matter what happens on the remote, and
|
||||||
also makes picture thumbnails available for the frontend for them.
|
also makes picture thumbnails available for the frontend for them.
|
||||||
|
|
||||||
## EDA integration commands
|
|
||||||
* `php bin/console partdb:kicad:populate`: Populate KiCad footprint paths and symbol paths for footprints and categories based on their names. Use `--dry-run` to preview changes without applying them, and `--list` to list current values. See the [EDA integration documentation](eda_integration.md) for more details.
|
|
||||||
|
|||||||
@@ -87,31 +87,3 @@ To show more levels of categories, you can set this value to a higher number.
|
|||||||
If you set this value to -1, all parts are shown inside a single category in KiCad, without any subcategories.
|
If you set this value to -1, all parts are shown inside a single category in KiCad, without any subcategories.
|
||||||
|
|
||||||
You can view the "real" category path of a part in the part details dialog in KiCad.
|
You can view the "real" category path of a part in the part details dialog in KiCad.
|
||||||
|
|
||||||
### Kicad:populate command
|
|
||||||
|
|
||||||
Part-DB also provides a command that attempts to automatically populate the KiCad symbol and footprint fields based on the part's category and footprint names.
|
|
||||||
This is especially useful if you have a large database and want to quickly assign symbols and footprints to parts without doing it manually.
|
|
||||||
|
|
||||||
For this run `bin/console partdb:kicad:populate --dry-run` in the terminal, it will show you a list of suggestions for mappings for your existing categories and footprints.
|
|
||||||
It uses names and alternative names, when the primary name doesn't match, to find the right mapping.
|
|
||||||
If you are happy with the suggestions, you can run the command without the `--dry-run` option to apply the changes to your database. By default, only empty values are updated, but you can use the `--force` option to overwrite existing values as well.
|
|
||||||
|
|
||||||
It uses the mapping under `assets/commands/kicad_populate_default_mappings.json` by default, but you can extend/override it by providing your own mapping file
|
|
||||||
with the `--mapping-file` option.
|
|
||||||
The mapping file is a JSON file with the following structure, where the key is the name of the footprint or category, and the value is the corresponding KiCad library path:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"footprints": {
|
|
||||||
"MyCustomPackage": "MyLibrary:MyFootprint",
|
|
||||||
"0805": "Capacitor_SMD:C_0805_2012Metric"
|
|
||||||
},
|
|
||||||
"categories": {
|
|
||||||
"Sensor": "Sensor:Sensor_Temperature",
|
|
||||||
"MCU": "MCU_Microchip:PIC16F877A"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Its okay if the file contains just one of the `footprints` or `categories` keys, so you can choose to only provide mappings for one of them if you want.
|
|
||||||
|
|
||||||
It is recommended to take a backup of your database before running this command.
|
|
||||||
|
|||||||
@@ -96,21 +96,6 @@ The following providers are currently available and shipped with Part-DB:
|
|||||||
|
|
||||||
(All trademarks are property of their respective owners. Part-DB is not affiliated with any of the companies.)
|
(All trademarks are property of their respective owners. Part-DB is not affiliated with any of the companies.)
|
||||||
|
|
||||||
### Generic Web URL Provider
|
|
||||||
The Generic Web URL Provider can extract part information from any webpage that contains structured data in the form of
|
|
||||||
[Schema.org](https://schema.org/) format. Many e-commerce websites use this format to provide detailed product information
|
|
||||||
for search engines and other services. Therefore it allows Part-DB to retrieve rudimentary part information (like name, image and price)
|
|
||||||
from a wide range of websites without the need for a dedicated API integration.
|
|
||||||
To use the Generic Web URL Provider, simply enable it in the information provider settings. No additional configuration
|
|
||||||
is required. Afterwards you can enter any product URL in the search field, and Part-DB will attempt to extract the relevant part information
|
|
||||||
from the webpage.
|
|
||||||
|
|
||||||
Please note that if this provider is enabled, Part-DB will make HTTP requests to external websites to fetch product data, which
|
|
||||||
may have privacy and security implications.
|
|
||||||
|
|
||||||
Following env configuration options are available:
|
|
||||||
* `PROVIDER_GENERIC_WEB_ENABLED`: Set this to `1` to enable the Generic Web URL Provider (optional, default: `0`)
|
|
||||||
|
|
||||||
### Octopart
|
### Octopart
|
||||||
|
|
||||||
The Octopart provider uses the [Octopart / Nexar API](https://nexar.com/api) to search for parts and get information.
|
The Octopart provider uses the [Octopart / Nexar API](https://nexar.com/api) to search for parts and get information.
|
||||||
@@ -293,27 +278,7 @@ The following env configuration options are available:
|
|||||||
* `PROVIDER_BUERKLIN_CURRENCY`: The currency you want to get prices in if available (optional, 3 letter ISO-code, default: `EUR`).
|
* `PROVIDER_BUERKLIN_CURRENCY`: The currency you want to get prices in if available (optional, 3 letter ISO-code, default: `EUR`).
|
||||||
* `PROVIDER_BUERKLIN_LANGUAGE`: The language you want to get the descriptions in. Possible values: `de` = German, `en` = English. (optional, default: `en`)
|
* `PROVIDER_BUERKLIN_LANGUAGE`: The language you want to get the descriptions in. Possible values: `de` = German, `en` = English. (optional, default: `en`)
|
||||||
|
|
||||||
### Conrad
|
### Custom provider
|
||||||
|
|
||||||
The conrad provider the [Conrad API](https://developer.conrad.com/) to search for parts and retried their information.
|
|
||||||
To use it you have to request access to the API, however it seems currently your mail address needs to be allowlisted before you can register for an account.
|
|
||||||
The conrad webpages uses the API key in the requests, so you might be able to extract a working API key by listening to browser requests.
|
|
||||||
That method is not officially supported nor encouraged by Part-DB, and might break at any moment.
|
|
||||||
|
|
||||||
The following env configuration options are available:
|
|
||||||
* `PROVIDER_CONRAD_API_KEY`: The API key you got from Conrad (mandatory)
|
|
||||||
|
|
||||||
### Canopy / Amazon
|
|
||||||
The Canopy provider uses the [Canopy API](https://www.canopyapi.co/) to search for parts and get shopping information from Amazon.
|
|
||||||
Canopy is a third-party service that provides access to Amazon product data through their API. Their trial plan offers 100 requests per month for free,
|
|
||||||
and they also offer paid plans with higher limits. To use the Canopy provider, you need to create an account on the Canopy website and obtain an API key.
|
|
||||||
Once you have the API key, you can configure the Canopy provider in Part-DB using the web UI or environment variables:
|
|
||||||
|
|
||||||
* `PROVIDER_CANOPY_API_KEY`: The API key you got from Canopy (mandatory)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Custom providers
|
|
||||||
|
|
||||||
To create a custom provider, you have to create a new class implementing the `InfoProviderInterface` interface. As long
|
To create a custom provider, you have to create a new class implementing the `InfoProviderInterface` interface. As long
|
||||||
as it is a valid Symfony service, it will be automatically loaded and can be used.
|
as it is a valid Symfony service, it will be automatically loaded and can be used.
|
||||||
|
|||||||
@@ -8,21 +8,6 @@ parent: Usage
|
|||||||
|
|
||||||
This page lists all the keybindings of Part-DB. Currently, there are only the special character keybindings.
|
This page lists all the keybindings of Part-DB. Currently, there are only the special character keybindings.
|
||||||
|
|
||||||
## Disabling keybindings
|
|
||||||
|
|
||||||
If you want to disable the special character keybindings (for example, because they conflict with your keyboard layout or system shortcuts), you can do so in two ways:
|
|
||||||
|
|
||||||
### Via the System Settings UI (recommended)
|
|
||||||
|
|
||||||
1. Navigate to **System Settings** (Tools → System Settings)
|
|
||||||
2. Go to **Behavior** → **Keybindings**
|
|
||||||
3. Uncheck **Enable special character keybindings**
|
|
||||||
4. Save the settings
|
|
||||||
|
|
||||||
### Via Environment Variable
|
|
||||||
|
|
||||||
Alternatively, you can set the environment variable `KEYBINDINGS_SPECIAL_CHARS_ENABLED=0` in your `.env.local` file or your server environment configuration.
|
|
||||||
|
|
||||||
## Special characters
|
## Special characters
|
||||||
|
|
||||||
Using the keybindings below (Alt + key) you can insert special characters into the text fields of Part-DB. This works on
|
Using the keybindings below (Alt + key) you can insert special characters into the text fields of Part-DB. This works on
|
||||||
|
|||||||
@@ -91,20 +91,18 @@ in [official documentation](https://twig.symfony.com/doc/3.x/).
|
|||||||
|
|
||||||
Twig allows you for much more complex and dynamic label generation. You can use loops, conditions, and functions to create
|
Twig allows you for much more complex and dynamic label generation. You can use loops, conditions, and functions to create
|
||||||
the label content and you can access almost all data Part-DB offers. The label templates are evaluated in a special sandboxed environment,
|
the label content and you can access almost all data Part-DB offers. The label templates are evaluated in a special sandboxed environment,
|
||||||
where only certain operations are allowed. Only read access to entities is allowed. However, as it circumvents Part-DB normal permission system,
|
where only certain operations are allowed. Only read access to entities is allowed. However as it circumvents Part-DB normal permission system,
|
||||||
the twig mode is only available to users with the "Twig mode" permission.
|
the twig mode is only available to users with the "Twig mode" permission.
|
||||||
|
|
||||||
It is useful to use the HTML embed feature of the editor, to have a block where you can write the twig code without worrying about the WYSIWYG editor messing with your code.
|
|
||||||
|
|
||||||
The following variables are in injected into Twig and can be accessed using `{% raw %}{{ variable }}{% endraw %}` (
|
The following variables are in injected into Twig and can be accessed using `{% raw %}{{ variable }}{% endraw %}` (
|
||||||
or `{% raw %}{{ variable.property }}{% endraw %}`):
|
or `{% raw %}{{ variable.property }}{% endraw %}`):
|
||||||
|
|
||||||
| Variable name | Description |
|
| Variable name | Description |
|
||||||
|--------------------------------------------|--------------------------------------------------------------------------------------|
|
|--------------------------------------------|--------------------------------------------------------------------------------------|
|
||||||
| `{% raw %}{{ element }}{% endraw %}` | The target element, selected in label dialog. |
|
| `{% raw %}{{ element }}{% endraw %}` | The target element, selected in label dialog. |
|
||||||
| `{% raw %}{{ user }}{% endraw %}` | The current logged in user. Null if you are not logged in |
|
| `{% raw %}{{ user }}{% endraw %}` | The current logged in user. Null if you are not logged in |
|
||||||
| `{% raw %}{{ install_title }}{% endraw %}` | The name of the current Part-DB instance (similar to [[INSTALL_NAME]] placeholder). |
|
| `{% raw %}{{ install_title }}{% endraw %}` | The name of the current Part-DB instance (similar to [[INSTALL_NAME]] placeholder). |
|
||||||
| `{% raw %}{{ page }}{% endraw %}` | The page number (the nth-element for which the label is generated ) |
|
| `{% raw %}{{ page }}{% endraw %}` | The page number (the nth-element for which the label is generated |
|
||||||
| `{% raw %}{{ last_page }}{% endraw %}` | The page number of the last element. Equals the number of all pages / element labels |
|
| `{% raw %}{{ last_page }}{% endraw %}` | The page number of the last element. Equals the number of all pages / element labels |
|
||||||
| `{% raw %}{{ paper_width }}{% endraw %}` | The width of the label paper in mm |
|
| `{% raw %}{{ paper_width }}{% endraw %}` | The width of the label paper in mm |
|
||||||
| `{% raw %}{{ paper_height }}{% endraw %}` | The height of the label paper in mm |
|
| `{% raw %}{{ paper_height }}{% endraw %}` | The height of the label paper in mm |
|
||||||
@@ -238,18 +236,12 @@ certain data:
|
|||||||
|
|
||||||
#### Functions
|
#### Functions
|
||||||
|
|
||||||
| Function name | Description |
|
| Function name | Description |
|
||||||
|------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|----------------------------------------------|-----------------------------------------------------------------------------------------------|
|
||||||
| `placeholder(placeholder, element)` | Get the value of a placeholder of an element |
|
| `placeholder(placeholder, element)` | Get the value of a placeholder of an element |
|
||||||
| `entity_type(element)` | Get the type of an entity as string |
|
| `entity_type(element)` | Get the type of an entity as string |
|
||||||
| `entity_url(element, type)` | Get the URL to a specific entity type page (e.g. `info`, `edit`, etc.) |
|
| `entity_url(element, type)` | Get the URL to a specific entity type page (e.g. `info`, `edit`, etc.) |
|
||||||
| `barcode_svg(content, type)` | Generate a barcode SVG from the content and type (e.g. `QRCODE`, `CODE128` etc.). A svg string is returned, which you need to data uri encode to inline it. |
|
| `barcode_svg(content, type)` | Generate a barcode SVG from the content and type (e.g. `QRCODE`, `CODE128` etc.). A svg string is returned, which you need to data uri encode to inline it. |
|
||||||
| `associated_parts(element)` | Get the associated parts of an element like a storagelocation, footprint, etc. Only the directly associated parts are returned |
|
|
||||||
| `associated_parts_r(element)` | Get the associated parts of an element like a storagelocation, footprint, etc. including all sub-entities recursively (e.g. sub-locations) |
|
|
||||||
| `associated_parts_count(element)` | Get the count of associated parts of an element like a storagelocation, footprint, excluding sub-entities |
|
|
||||||
| `associated_parts_count_r(element)` | Get the count of associated parts of an element like a storagelocation, footprint, including all sub-entities recursively (e.g. sub-locations) |
|
|
||||||
| `type_label(element)` | Get the name of the type of an element (e.g. "Part", "Storage location", etc.) |
|
|
||||||
| `type_label_p(element)` | Get the name of the type of an element in plural form (e.g. "Parts", "Storage locations", etc.) |
|
|
||||||
|
|
||||||
### Filters
|
### Filters
|
||||||
|
|
||||||
@@ -293,5 +285,5 @@ If you want to use a different (more beautiful) font, you can use the [custom fo
|
|||||||
feature.
|
feature.
|
||||||
There is the [Noto](https://www.google.com/get/noto/) font family from Google, which supports a lot of languages and is
|
There is the [Noto](https://www.google.com/get/noto/) font family from Google, which supports a lot of languages and is
|
||||||
available in different styles (regular, bold, italic, bold-italic).
|
available in different styles (regular, bold, italic, bold-italic).
|
||||||
For example, you can use [Noto CJK](https://github.com/notofonts/noto-cjk) for more beautiful Chinese, Japanese,
|
For example, you can use [Noto CJK](https://github.com/notofonts/noto-cjk) for more beautiful Chinese, Japanese,
|
||||||
and Korean characters.
|
and Korean characters.
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
---
|
|
||||||
title: Barcode Scanner
|
|
||||||
layout: default
|
|
||||||
parent: Usage
|
|
||||||
---
|
|
||||||
|
|
||||||
# Barcode scanner
|
|
||||||
|
|
||||||
When the user has the correct permission there will be a barcode scanner button in the navbar.
|
|
||||||
On this page you can either input a barcode code by hand, use an external barcode scanner, or use your devices camera to
|
|
||||||
scan a barcode.
|
|
||||||
|
|
||||||
In info mode (when the "Info" toggle is enabled) you can scan a barcode and Part-DB will parse it and show information
|
|
||||||
about it.
|
|
||||||
|
|
||||||
Without info mode, the barcode will directly redirect you to the corresponding page.
|
|
||||||
|
|
||||||
### Barcode matching
|
|
||||||
|
|
||||||
When you scan a barcode, Part-DB will try to match it to an existing part, part lot or storage location first.
|
|
||||||
For Part-DB generated barcodes, it will use the internal ID of a part. Alternatively you can also scan a barcode that contains the part's IPN.
|
|
||||||
|
|
||||||
You can set a GTIN/EAN code in the part properties and Part-DB will open the part page when you scan the corresponding GTIN/EAN barcode.
|
|
||||||
|
|
||||||
On a part lot you can under "Advanced" set a user barcode, that will redirect you to the part lot page when scanned. This allows to reuse
|
|
||||||
arbitrary existing barcodes that already exist on the part lots (for example, from the manufacturer) and link them to the part lot in Part-DB.
|
|
||||||
|
|
||||||
Part-DB can also parse various distributor barcodes (for example from Digikey and Mouser) and will try to redirect you to the corresponding
|
|
||||||
part page based on the distributor part number in the barcode.
|
|
||||||
|
|
||||||
### Part creation from barcodes
|
|
||||||
For certain barcodes Part-DB can automatically create a new part, when it cannot find a matching part.
|
|
||||||
Part-DB will try to retrieve the part information from an information provider and redirects you to the part creation page
|
|
||||||
with the retrieved information pre-filled.
|
|
||||||
|
|
||||||
## Using an external barcode scanner
|
|
||||||
|
|
||||||
Part-DB supports the use of external barcode scanners that emulate keyboard input. To use a barcode scanner with Part-DB,
|
|
||||||
simply connect the scanner to your computer and scan a barcode while the cursor is in a text field in Part-DB.
|
|
||||||
The scanned barcode will be entered into the text field as if you had typed it on the keyboard.
|
|
||||||
|
|
||||||
In scanner fields, it will also try to insert special non-printable characters the scanner send via Alt + key combinations.
|
|
||||||
This is required for EIGP114 datamatrix codes.
|
|
||||||
|
|
||||||
### Automatically redirect on barcode scanning
|
|
||||||
|
|
||||||
If you configure your barcode scanner to send a <SOH> (Start of heading, 0x01) non-printable character at the beginning
|
|
||||||
of the scanned barcode, Part-DB will automatically scan the barcode that comes afterward (and is ended with an enter key)
|
|
||||||
and redirects you to the corresponding page.
|
|
||||||
This allows you to quickly scan a barcode from anywhere in Part-DB without the need to first open the scanner page.
|
|
||||||
If an input field is focused, the barcode will be entered into the field as usual and no redirection will happen.
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
---
|
|
||||||
title: Update Manager
|
|
||||||
layout: default
|
|
||||||
parent: Usage
|
|
||||||
---
|
|
||||||
|
|
||||||
# Update Manager (Experimental)
|
|
||||||
|
|
||||||
{: .warning }
|
|
||||||
> The Update Manager is currently an **experimental feature**. It is disabled by default while user experience data is being gathered. Use with caution and always ensure you have proper backups before updating.
|
|
||||||
|
|
||||||
Part-DB includes an Update Manager that can automatically update Git-based installations to newer versions. The Update Manager provides both a web interface and CLI commands for managing updates, backups, and maintenance mode.
|
|
||||||
|
|
||||||
## Supported Installation Types
|
|
||||||
|
|
||||||
The Update Manager currently supports automatic updates only for **Git clone** installations. Other installation types show manual update instructions:
|
|
||||||
|
|
||||||
| Installation Type | Auto-Update | Instructions |
|
|
||||||
|-------------------|-------------|--------------|
|
|
||||||
| Git Clone | Yes | Automatic via CLI or Web UI |
|
|
||||||
| Docker | No | Pull new image: `docker-compose pull && docker-compose up -d` |
|
|
||||||
| ZIP Release | No | Download and extract new release manually |
|
|
||||||
|
|
||||||
## Enabling the Update Manager
|
|
||||||
|
|
||||||
By default, web-based updates and backup restore are **disabled** for security reasons. To enable them, add these settings to your `.env.local` file:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Enable web-based updates (default: disabled)
|
|
||||||
DISABLE_WEB_UPDATES=0
|
|
||||||
|
|
||||||
# Enable backup restore via web interface (default: disabled)
|
|
||||||
DISABLE_BACKUP_RESTORE=0
|
|
||||||
```
|
|
||||||
|
|
||||||
{: .note }
|
|
||||||
> Even with web updates disabled, you can still use the CLI commands to perform updates.
|
|
||||||
|
|
||||||
## CLI Commands
|
|
||||||
|
|
||||||
### Update Command
|
|
||||||
|
|
||||||
Check for updates or perform an update:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check for available updates
|
|
||||||
php bin/console partdb:update --check
|
|
||||||
|
|
||||||
# Update to the latest version
|
|
||||||
php bin/console partdb:update
|
|
||||||
|
|
||||||
# Update to a specific version
|
|
||||||
php bin/console partdb:update v2.6.0
|
|
||||||
|
|
||||||
# Update without creating a backup first
|
|
||||||
php bin/console partdb:update --no-backup
|
|
||||||
|
|
||||||
# Force update without confirmation prompt
|
|
||||||
php bin/console partdb:update --force
|
|
||||||
```
|
|
||||||
|
|
||||||
### Maintenance Mode Command
|
|
||||||
|
|
||||||
Manually enable or disable maintenance mode:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Enable maintenance mode with default message
|
|
||||||
php bin/console partdb:maintenance-mode --enable
|
|
||||||
|
|
||||||
# Enable with custom message
|
|
||||||
php bin/console partdb:maintenance-mode --enable "System maintenance until 6 PM"
|
|
||||||
php bin/console partdb:maintenance-mode --enable --message="Updating to v2.6.0"
|
|
||||||
|
|
||||||
# Disable maintenance mode
|
|
||||||
php bin/console partdb:maintenance-mode --disable
|
|
||||||
|
|
||||||
# Check current status
|
|
||||||
php bin/console partdb:maintenance-mode --status
|
|
||||||
```
|
|
||||||
|
|
||||||
## Web Interface
|
|
||||||
|
|
||||||
When web updates are enabled, the Update Manager is accessible at **System > Update Manager** (URL: `/system/update-manager`).
|
|
||||||
|
|
||||||
The web interface shows:
|
|
||||||
- Current version and installation type
|
|
||||||
- Available updates with release notes
|
|
||||||
- Precondition validation (Git, Composer, Yarn, permissions)
|
|
||||||
- Update history and logs
|
|
||||||
- Backup management
|
|
||||||
|
|
||||||
### Required Permissions
|
|
||||||
|
|
||||||
Users need the following permissions to access the Update Manager:
|
|
||||||
|
|
||||||
| Permission | Description |
|
|
||||||
|------------|-------------|
|
|
||||||
| `@system.show_updates` | View update status and available versions |
|
|
||||||
| `@system.manage_updates` | Perform updates and restore backups |
|
|
||||||
|
|
||||||
## Update Process
|
|
||||||
|
|
||||||
When an update is performed, the following steps are executed:
|
|
||||||
|
|
||||||
1. **Lock** - Acquire exclusive lock to prevent concurrent updates
|
|
||||||
2. **Maintenance Mode** - Enable maintenance mode to block user access
|
|
||||||
3. **Rollback Tag** - Create a Git tag for potential rollback
|
|
||||||
4. **Backup** - Create a full backup (optional but recommended)
|
|
||||||
5. **Git Fetch** - Fetch latest changes from origin
|
|
||||||
6. **Git Checkout** - Checkout the target version
|
|
||||||
7. **Composer Install** - Install/update PHP dependencies
|
|
||||||
8. **Yarn Install** - Install frontend dependencies
|
|
||||||
9. **Yarn Build** - Compile frontend assets
|
|
||||||
10. **Database Migrations** - Run any new migrations
|
|
||||||
11. **Cache Clear** - Clear the application cache
|
|
||||||
12. **Cache Warmup** - Rebuild the cache
|
|
||||||
13. **Maintenance Off** - Disable maintenance mode
|
|
||||||
14. **Unlock** - Release the update lock
|
|
||||||
|
|
||||||
If any step fails, the system automatically attempts to rollback to the previous version.
|
|
||||||
|
|
||||||
## Backup Management
|
|
||||||
|
|
||||||
The Update Manager automatically creates backups before updates. These backups are stored in `var/backups/` and include:
|
|
||||||
|
|
||||||
- Database dump (SQL file or SQLite database)
|
|
||||||
- Configuration files (`.env.local`, `parameters.yaml`, `banner.md`)
|
|
||||||
- Attachment files (`uploads/`, `public/media/`)
|
|
||||||
|
|
||||||
### Restoring from Backup
|
|
||||||
|
|
||||||
{: .warning }
|
|
||||||
> Backup restore is a destructive operation that will overwrite your current database. Only use this if you need to recover from a failed update.
|
|
||||||
|
|
||||||
If web restore is enabled (`DISABLE_BACKUP_RESTORE=0`), you can restore backups from the web interface. The restore process:
|
|
||||||
|
|
||||||
1. Enables maintenance mode
|
|
||||||
2. Extracts the backup
|
|
||||||
3. Restores the database
|
|
||||||
4. Optionally restores config and attachments
|
|
||||||
5. Clears and warms up the cache
|
|
||||||
6. Disables maintenance mode
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Precondition Errors
|
|
||||||
|
|
||||||
Before updating, the system validates:
|
|
||||||
|
|
||||||
- **Git available**: Git must be installed and in PATH
|
|
||||||
- **No local changes**: Uncommitted changes must be committed or stashed
|
|
||||||
- **Composer available**: Composer must be installed and in PATH
|
|
||||||
- **Yarn available**: Yarn must be installed and in PATH
|
|
||||||
- **Write permissions**: `var/`, `vendor/`, and `public/` must be writable
|
|
||||||
- **Not already locked**: No other update can be in progress
|
|
||||||
|
|
||||||
### Stale Lock
|
|
||||||
|
|
||||||
If an update was interrupted and the lock file remains, it will automatically be removed after 1 hour. You can also manually delete `var/update.lock`.
|
|
||||||
|
|
||||||
### Viewing Update Logs
|
|
||||||
|
|
||||||
Update logs are stored in `var/log/updates/` and can be viewed from the web interface or directly on the server.
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
- **Disable web updates in production** unless you specifically need them
|
|
||||||
- The Update Manager requires shell access to run Git, Composer, and Yarn
|
|
||||||
- Backup files may contain sensitive data (database, config) - secure the `var/backups/` directory
|
|
||||||
- Consider running updates during maintenance windows with low user activity
|
|
||||||
91
makefile
Normal file
91
makefile
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# PartDB Makefile for Test Environment Management
|
||||||
|
|
||||||
|
.PHONY: help deps-install lint format format-check test coverage pre-commit all test-typecheck \
|
||||||
|
test-setup test-clean test-db-create test-db-migrate test-cache-clear test-fixtures test-run test-reset \
|
||||||
|
section-dev dev-setup dev-clean dev-db-create dev-db-migrate dev-cache-clear dev-warmup dev-reset
|
||||||
|
|
||||||
|
# Default target
|
||||||
|
help: ## Show this help
|
||||||
|
@awk 'BEGIN {FS = ":.*##"}; /^[a-zA-Z0-9][a-zA-Z0-9_-]+:.*##/ {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
deps-install: ## Install PHP dependencies with unlimited memory
|
||||||
|
@echo "📦 Installing PHP dependencies..."
|
||||||
|
COMPOSER_MEMORY_LIMIT=-1 composer install
|
||||||
|
yarn install
|
||||||
|
@echo "✅ Dependencies installed"
|
||||||
|
|
||||||
|
# Complete test environment setup
|
||||||
|
test-setup: test-clean test-db-create test-db-migrate test-fixtures ## Complete test setup (clean, create DB, migrate, fixtures)
|
||||||
|
@echo "✅ Test environment setup complete!"
|
||||||
|
|
||||||
|
# Clean test environment
|
||||||
|
test-clean: ## Clean test cache and database files
|
||||||
|
@echo "🧹 Cleaning test environment..."
|
||||||
|
rm -rf var/cache/test
|
||||||
|
rm -f var/app_test.db
|
||||||
|
@echo "✅ Test environment cleaned"
|
||||||
|
|
||||||
|
# Create test database
|
||||||
|
test-db-create: ## Create test database (if not exists)
|
||||||
|
@echo "🗄️ Creating test database..."
|
||||||
|
-php bin/console doctrine:database:create --if-not-exists --env test || echo "⚠️ Database creation failed (expected for SQLite) - continuing..."
|
||||||
|
|
||||||
|
# Run database migrations for test environment
|
||||||
|
test-db-migrate: ## Run database migrations for test environment
|
||||||
|
@echo "🔄 Running database migrations..."
|
||||||
|
COMPOSER_MEMORY_LIMIT=-1 php bin/console doctrine:migrations:migrate -n --env test
|
||||||
|
|
||||||
|
# Clear test cache
|
||||||
|
test-cache-clear: ## Clear test cache
|
||||||
|
@echo "🗑️ Clearing test cache..."
|
||||||
|
rm -rf var/cache/test
|
||||||
|
@echo "✅ Test cache cleared"
|
||||||
|
|
||||||
|
# Load test fixtures
|
||||||
|
test-fixtures: ## Load test fixtures
|
||||||
|
@echo "📦 Loading test fixtures..."
|
||||||
|
php bin/console partdb:fixtures:load -n --env test
|
||||||
|
|
||||||
|
# Run PHPUnit tests
|
||||||
|
test-run: ## Run PHPUnit tests
|
||||||
|
@echo "🧪 Running tests..."
|
||||||
|
php bin/phpunit
|
||||||
|
|
||||||
|
# Quick test reset (clean + migrate + fixtures, skip DB creation)
|
||||||
|
test-reset: test-cache-clear test-db-migrate test-fixtures
|
||||||
|
@echo "✅ Test environment reset complete!"
|
||||||
|
|
||||||
|
test-typecheck: ## Run static analysis (PHPStan)
|
||||||
|
@echo "🧪 Running type checks..."
|
||||||
|
COMPOSER_MEMORY_LIMIT=-1 composer phpstan
|
||||||
|
|
||||||
|
# Development helpers
|
||||||
|
dev-setup: dev-clean dev-db-create dev-db-migrate dev-warmup ## Complete development setup (clean, create DB, migrate, warmup)
|
||||||
|
@echo "✅ Development environment setup complete!"
|
||||||
|
|
||||||
|
dev-clean: ## Clean development cache and database files
|
||||||
|
@echo "🧹 Cleaning development environment..."
|
||||||
|
rm -rf var/cache/dev
|
||||||
|
rm -f var/app_dev.db
|
||||||
|
@echo "✅ Development environment cleaned"
|
||||||
|
|
||||||
|
dev-db-create: ## Create development database (if not exists)
|
||||||
|
@echo "🗄️ Creating development database..."
|
||||||
|
-php bin/console doctrine:database:create --if-not-exists --env dev || echo "⚠️ Database creation failed (expected for SQLite) - continuing..."
|
||||||
|
|
||||||
|
dev-db-migrate: ## Run database migrations for development environment
|
||||||
|
@echo "🔄 Running database migrations..."
|
||||||
|
COMPOSER_MEMORY_LIMIT=-1 php bin/console doctrine:migrations:migrate -n --env dev
|
||||||
|
|
||||||
|
dev-cache-clear: ## Clear development cache
|
||||||
|
@echo "🗑️ Clearing development cache..."
|
||||||
|
rm -rf var/cache/dev
|
||||||
|
@echo "✅ Development cache cleared"
|
||||||
|
|
||||||
|
dev-warmup: ## Warm up development cache
|
||||||
|
@echo "🔥 Warming up development cache..."
|
||||||
|
COMPOSER_MEMORY_LIMIT=-1 php -d memory_limit=1G bin/console cache:warmup --env dev -n
|
||||||
|
|
||||||
|
dev-reset: dev-cache-clear dev-db-migrate ## Quick development reset (cache clear + migrate)
|
||||||
|
@echo "✅ Development environment reset complete!"
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace DoctrineMigrations;
|
|
||||||
|
|
||||||
use App\Migration\AbstractMultiPlatformMigration;
|
|
||||||
use Doctrine\DBAL\Schema\Schema;
|
|
||||||
|
|
||||||
|
|
||||||
final class Version20260208131116 extends AbstractMultiPlatformMigration
|
|
||||||
{
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return 'Add GTIN fields, allowed targets for attachment types and last stocktake date for part lots and add include_vat field for price details.';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function mySQLUp(Schema $schema): void
|
|
||||||
{
|
|
||||||
// this up() migration is auto-generated, please modify it to your needs
|
|
||||||
$this->addSql('ALTER TABLE attachment_types ADD allowed_targets LONGTEXT DEFAULT NULL');
|
|
||||||
$this->addSql('ALTER TABLE part_lots ADD last_stocktake_at DATETIME DEFAULT NULL');
|
|
||||||
$this->addSql('ALTER TABLE parts ADD gtin VARCHAR(255) DEFAULT NULL');
|
|
||||||
$this->addSql('CREATE INDEX parts_idx_gtin ON parts (gtin)');
|
|
||||||
$this->addSql('ALTER TABLE orderdetails ADD prices_includes_vat TINYINT DEFAULT NULL');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function mySQLDown(Schema $schema): void
|
|
||||||
{
|
|
||||||
// this down() migration is auto-generated, please modify it to your needs
|
|
||||||
$this->addSql('ALTER TABLE `attachment_types` DROP allowed_targets');
|
|
||||||
$this->addSql('DROP INDEX parts_idx_gtin ON `parts`');
|
|
||||||
$this->addSql('ALTER TABLE `parts` DROP gtin');
|
|
||||||
$this->addSql('ALTER TABLE part_lots DROP last_stocktake_at');
|
|
||||||
$this->addSql('ALTER TABLE `orderdetails` DROP prices_includes_vat');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function sqLiteUp(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('ALTER TABLE attachment_types ADD COLUMN allowed_targets CLOB DEFAULT NULL');
|
|
||||||
$this->addSql('ALTER TABLE part_lots ADD COLUMN last_stocktake_at DATETIME DEFAULT NULL');
|
|
||||||
$this->addSql('CREATE TEMPORARY TABLE __temp__parts AS SELECT id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, id_part_custom_state, order_orderdetails_id, built_project_id, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint FROM parts');
|
|
||||||
$this->addSql('DROP TABLE parts');
|
|
||||||
$this->addSql('CREATE TABLE parts (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_preview_attachment INTEGER DEFAULT NULL, id_category INTEGER NOT NULL, id_footprint INTEGER DEFAULT NULL, id_part_unit INTEGER DEFAULT NULL, id_manufacturer INTEGER DEFAULT NULL, id_part_custom_state INTEGER DEFAULT NULL, order_orderdetails_id INTEGER DEFAULT NULL, built_project_id INTEGER DEFAULT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, needs_review BOOLEAN NOT NULL, tags CLOB NOT NULL, mass DOUBLE PRECISION DEFAULT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, visible BOOLEAN NOT NULL, favorite BOOLEAN NOT NULL, minamount DOUBLE PRECISION NOT NULL, manufacturer_product_url CLOB NOT NULL, manufacturer_product_number VARCHAR(255) NOT NULL, manufacturing_status VARCHAR(255) DEFAULT NULL, order_quantity INTEGER NOT NULL, manual_order BOOLEAN NOT NULL, ipn VARCHAR(100) DEFAULT NULL, provider_reference_provider_key VARCHAR(255) DEFAULT NULL, provider_reference_provider_id VARCHAR(255) DEFAULT NULL, provider_reference_provider_url VARCHAR(2048) DEFAULT NULL, provider_reference_last_updated DATETIME DEFAULT NULL, eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, eda_info_value VARCHAR(255) DEFAULT NULL, eda_info_invisible BOOLEAN DEFAULT NULL, eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, eda_info_exclude_from_board BOOLEAN DEFAULT NULL, eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL, gtin VARCHAR(255) DEFAULT NULL, CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES categories (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES footprints (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES measurement_units (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES manufacturers (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES part_custom_states (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES orderdetails (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
|
||||||
$this->addSql('INSERT INTO parts (id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, id_part_custom_state, order_orderdetails_id, built_project_id, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint) SELECT id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, id_part_custom_state, order_orderdetails_id, built_project_id, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint FROM __temp__parts');
|
|
||||||
$this->addSql('DROP TABLE __temp__parts');
|
|
||||||
$this->addSql('CREATE INDEX parts_idx_name ON parts (name)');
|
|
||||||
$this->addSql('CREATE INDEX parts_idx_ipn ON parts (ipn)');
|
|
||||||
$this->addSql('CREATE INDEX parts_idx_datet_name_last_id_needs ON parts (datetime_added, name, last_modified, id, needs_review)');
|
|
||||||
$this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON parts (built_project_id)');
|
|
||||||
$this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON parts (order_orderdetails_id)');
|
|
||||||
$this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON parts (ipn)');
|
|
||||||
$this->addSql('CREATE INDEX IDX_6940A7FEEA7100A1 ON parts (id_preview_attachment)');
|
|
||||||
$this->addSql('CREATE INDEX IDX_6940A7FE7E371A10 ON parts (id_footprint)');
|
|
||||||
$this->addSql('CREATE INDEX IDX_6940A7FE5697F554 ON parts (id_category)');
|
|
||||||
$this->addSql('CREATE INDEX IDX_6940A7FE2626CEF9 ON parts (id_part_unit)');
|
|
||||||
$this->addSql('CREATE INDEX IDX_6940A7FE1ECB93AE ON parts (id_manufacturer)');
|
|
||||||
$this->addSql('CREATE INDEX IDX_6940A7FEA3ED1215 ON parts (id_part_custom_state)');
|
|
||||||
$this->addSql('CREATE INDEX parts_idx_gtin ON parts (gtin)');
|
|
||||||
$this->addSql('ALTER TABLE orderdetails ADD COLUMN prices_includes_vat BOOLEAN DEFAULT NULL');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function sqLiteDown(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('CREATE TEMPORARY TABLE __temp__attachment_types AS SELECT id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, filetype_filter, parent_id, id_preview_attachment FROM "attachment_types"');
|
|
||||||
$this->addSql('DROP TABLE "attachment_types"');
|
|
||||||
$this->addSql('CREATE TABLE "attachment_types" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, alternative_names CLOB DEFAULT NULL, filetype_filter CLOB NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, CONSTRAINT FK_EFAED719727ACA70 FOREIGN KEY (parent_id) REFERENCES "attachment_types" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EFAED719EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
|
||||||
$this->addSql('INSERT INTO "attachment_types" (id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, filetype_filter, parent_id, id_preview_attachment) SELECT id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, filetype_filter, parent_id, id_preview_attachment FROM __temp__attachment_types');
|
|
||||||
$this->addSql('DROP TABLE __temp__attachment_types');
|
|
||||||
$this->addSql('CREATE INDEX IDX_EFAED719727ACA70 ON "attachment_types" (parent_id)');
|
|
||||||
$this->addSql('CREATE INDEX IDX_EFAED719EA7100A1 ON "attachment_types" (id_preview_attachment)');
|
|
||||||
$this->addSql('CREATE INDEX attachment_types_idx_name ON "attachment_types" (name)');
|
|
||||||
$this->addSql('CREATE INDEX attachment_types_idx_parent_name ON "attachment_types" (parent_id, name)');
|
|
||||||
$this->addSql('CREATE TEMPORARY TABLE __temp__part_lots AS SELECT id, description, comment, expiration_date, instock_unknown, amount, needs_refill, vendor_barcode, last_modified, datetime_added, id_store_location, id_part, id_owner FROM part_lots');
|
|
||||||
$this->addSql('DROP TABLE part_lots');
|
|
||||||
$this->addSql('CREATE TABLE part_lots (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, expiration_date DATETIME DEFAULT NULL, instock_unknown BOOLEAN NOT NULL, amount DOUBLE PRECISION NOT NULL, needs_refill BOOLEAN NOT NULL, vendor_barcode VARCHAR(255) DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, id_store_location INTEGER DEFAULT NULL, id_part INTEGER NOT NULL, id_owner INTEGER DEFAULT NULL, CONSTRAINT FK_EBC8F9435D8F4B37 FOREIGN KEY (id_store_location) REFERENCES "storelocations" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F943C22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F94321E5A74C FOREIGN KEY (id_owner) REFERENCES "users" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
|
||||||
$this->addSql('INSERT INTO part_lots (id, description, comment, expiration_date, instock_unknown, amount, needs_refill, vendor_barcode, last_modified, datetime_added, id_store_location, id_part, id_owner) SELECT id, description, comment, expiration_date, instock_unknown, amount, needs_refill, vendor_barcode, last_modified, datetime_added, id_store_location, id_part, id_owner FROM __temp__part_lots');
|
|
||||||
$this->addSql('DROP TABLE __temp__part_lots');
|
|
||||||
$this->addSql('CREATE INDEX IDX_EBC8F9435D8F4B37 ON part_lots (id_store_location)');
|
|
||||||
$this->addSql('CREATE INDEX IDX_EBC8F943C22F6CC4 ON part_lots (id_part)');
|
|
||||||
$this->addSql('CREATE INDEX IDX_EBC8F94321E5A74C ON part_lots (id_owner)');
|
|
||||||
$this->addSql('CREATE INDEX part_lots_idx_instock_un_expiration_id_part ON part_lots (instock_unknown, expiration_date, id_part)');
|
|
||||||
$this->addSql('CREATE INDEX part_lots_idx_needs_refill ON part_lots (needs_refill)');
|
|
||||||
$this->addSql('CREATE INDEX part_lots_idx_barcode ON part_lots (vendor_barcode)');
|
|
||||||
$this->addSql('CREATE TEMPORARY TABLE __temp__parts AS SELECT id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint, id_preview_attachment, id_part_custom_state, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id FROM "parts"');
|
|
||||||
$this->addSql('DROP TABLE "parts"');
|
|
||||||
$this->addSql('CREATE TABLE "parts" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, needs_review BOOLEAN NOT NULL, tags CLOB NOT NULL, mass DOUBLE PRECISION DEFAULT NULL, ipn VARCHAR(100) DEFAULT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, visible BOOLEAN NOT NULL, favorite BOOLEAN NOT NULL, minamount DOUBLE PRECISION NOT NULL, manufacturer_product_url CLOB NOT NULL, manufacturer_product_number VARCHAR(255) NOT NULL, manufacturing_status VARCHAR(255) DEFAULT NULL, order_quantity INTEGER NOT NULL, manual_order BOOLEAN NOT NULL, provider_reference_provider_key VARCHAR(255) DEFAULT NULL, provider_reference_provider_id VARCHAR(255) DEFAULT NULL, provider_reference_provider_url VARCHAR(2048) DEFAULT NULL, provider_reference_last_updated DATETIME DEFAULT NULL, eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, eda_info_value VARCHAR(255) DEFAULT NULL, eda_info_invisible BOOLEAN DEFAULT NULL, eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, eda_info_exclude_from_board BOOLEAN DEFAULT NULL, eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, id_part_custom_state INTEGER DEFAULT NULL, id_category INTEGER NOT NULL, id_footprint INTEGER DEFAULT NULL, id_part_unit INTEGER DEFAULT NULL, id_manufacturer INTEGER DEFAULT NULL, order_orderdetails_id INTEGER DEFAULT NULL, built_project_id INTEGER DEFAULT NULL, CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES "part_custom_states" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES "categories" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES "footprints" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES "manufacturers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES "orderdetails" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
|
||||||
$this->addSql('INSERT INTO "parts" (id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint, id_preview_attachment, id_part_custom_state, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id) SELECT id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint, id_preview_attachment, id_part_custom_state, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id FROM __temp__parts');
|
|
||||||
$this->addSql('DROP TABLE __temp__parts');
|
|
||||||
$this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON "parts" (ipn)');
|
|
||||||
$this->addSql('CREATE INDEX IDX_6940A7FEEA7100A1 ON "parts" (id_preview_attachment)');
|
|
||||||
$this->addSql('CREATE INDEX IDX_6940A7FEA3ED1215 ON "parts" (id_part_custom_state)');
|
|
||||||
$this->addSql('CREATE INDEX IDX_6940A7FE5697F554 ON "parts" (id_category)');
|
|
||||||
$this->addSql('CREATE INDEX IDX_6940A7FE7E371A10 ON "parts" (id_footprint)');
|
|
||||||
$this->addSql('CREATE INDEX IDX_6940A7FE2626CEF9 ON "parts" (id_part_unit)');
|
|
||||||
$this->addSql('CREATE INDEX IDX_6940A7FE1ECB93AE ON "parts" (id_manufacturer)');
|
|
||||||
$this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON "parts" (order_orderdetails_id)');
|
|
||||||
$this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON "parts" (built_project_id)');
|
|
||||||
$this->addSql('CREATE INDEX parts_idx_datet_name_last_id_needs ON "parts" (datetime_added, name, last_modified, id, needs_review)');
|
|
||||||
$this->addSql('CREATE INDEX parts_idx_name ON "parts" (name)');
|
|
||||||
$this->addSql('CREATE INDEX parts_idx_ipn ON "parts" (ipn)');
|
|
||||||
$this->addSql('CREATE TEMPORARY TABLE __temp__orderdetails AS SELECT id, supplierpartnr, obsolete, supplier_product_url, last_modified, datetime_added, part_id, id_supplier FROM "orderdetails"');
|
|
||||||
$this->addSql('DROP TABLE "orderdetails"');
|
|
||||||
$this->addSql('CREATE TABLE "orderdetails" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, supplierpartnr VARCHAR(255) NOT NULL, obsolete BOOLEAN NOT NULL, supplier_product_url CLOB NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, part_id INTEGER NOT NULL, id_supplier INTEGER DEFAULT NULL, CONSTRAINT FK_489AFCDC4CE34BEC FOREIGN KEY (part_id) REFERENCES "parts" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_489AFCDCCBF180EB FOREIGN KEY (id_supplier) REFERENCES "suppliers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
|
||||||
$this->addSql('INSERT INTO "orderdetails" (id, supplierpartnr, obsolete, supplier_product_url, last_modified, datetime_added, part_id, id_supplier) SELECT id, supplierpartnr, obsolete, supplier_product_url, last_modified, datetime_added, part_id, id_supplier FROM __temp__orderdetails');
|
|
||||||
$this->addSql('DROP TABLE __temp__orderdetails');
|
|
||||||
$this->addSql('CREATE INDEX IDX_489AFCDC4CE34BEC ON "orderdetails" (part_id)');
|
|
||||||
$this->addSql('CREATE INDEX IDX_489AFCDCCBF180EB ON "orderdetails" (id_supplier)');
|
|
||||||
$this->addSql('CREATE INDEX orderdetails_supplier_part_nr ON "orderdetails" (supplierpartnr)');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function postgreSQLUp(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('ALTER TABLE attachment_types ADD allowed_targets TEXT DEFAULT NULL');
|
|
||||||
$this->addSql('ALTER TABLE part_lots ADD last_stocktake_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
|
||||||
$this->addSql('ALTER TABLE parts ADD gtin VARCHAR(255) DEFAULT NULL');
|
|
||||||
$this->addSql('CREATE INDEX parts_idx_gtin ON parts (gtin)');
|
|
||||||
$this->addSql('ALTER TABLE orderdetails ADD prices_includes_vat BOOLEAN DEFAULT NULL');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function postgreSQLDown(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('ALTER TABLE "attachment_types" DROP allowed_targets');
|
|
||||||
$this->addSql('ALTER TABLE part_lots DROP last_stocktake_at');
|
|
||||||
$this->addSql('DROP INDEX parts_idx_gtin');
|
|
||||||
$this->addSql('ALTER TABLE "parts" DROP gtin');
|
|
||||||
$this->addSql('ALTER TABLE "orderdetails" DROP prices_includes_vat');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace DoctrineMigrations;
|
|
||||||
|
|
||||||
use App\Migration\AbstractMultiPlatformMigration;
|
|
||||||
use Doctrine\DBAL\Schema\Schema;
|
|
||||||
|
|
||||||
final class Version20260211000000 extends AbstractMultiPlatformMigration
|
|
||||||
{
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return 'Add eda_visibility nullable boolean column to parameters and orderdetails tables';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function mySQLUp(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('ALTER TABLE parameters ADD eda_visibility TINYINT(1) DEFAULT NULL');
|
|
||||||
$this->addSql('ALTER TABLE `orderdetails` ADD eda_visibility TINYINT(1) DEFAULT NULL');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function mySQLDown(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('ALTER TABLE parameters DROP COLUMN eda_visibility');
|
|
||||||
$this->addSql('ALTER TABLE `orderdetails` DROP COLUMN eda_visibility');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function sqLiteUp(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('ALTER TABLE parameters ADD COLUMN eda_visibility BOOLEAN DEFAULT NULL');
|
|
||||||
$this->addSql('ALTER TABLE orderdetails ADD COLUMN eda_visibility BOOLEAN DEFAULT NULL');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function sqLiteDown(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('ALTER TABLE parameters DROP COLUMN eda_visibility');
|
|
||||||
$this->addSql('ALTER TABLE orderdetails DROP COLUMN eda_visibility');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function postgreSQLUp(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('ALTER TABLE parameters ADD eda_visibility BOOLEAN DEFAULT NULL');
|
|
||||||
$this->addSql('ALTER TABLE orderdetails ADD eda_visibility BOOLEAN DEFAULT NULL');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function postgreSQLDown(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('ALTER TABLE parameters DROP COLUMN eda_visibility');
|
|
||||||
$this->addSql('ALTER TABLE orderdetails DROP COLUMN eda_visibility');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -46,7 +46,6 @@
|
|||||||
"@zxcvbn-ts/language-en": "^3.0.1",
|
"@zxcvbn-ts/language-en": "^3.0.1",
|
||||||
"@zxcvbn-ts/language-fr": "^3.0.1",
|
"@zxcvbn-ts/language-fr": "^3.0.1",
|
||||||
"@zxcvbn-ts/language-ja": "^3.0.1",
|
"@zxcvbn-ts/language-ja": "^3.0.1",
|
||||||
"attr-accept": "^2.2.5",
|
|
||||||
"barcode-detector": "^3.0.5",
|
"barcode-detector": "^3.0.5",
|
||||||
"bootbox": "^6.0.0",
|
"bootbox": "^6.0.0",
|
||||||
"bootswatch": "^5.1.3",
|
"bootswatch": "^5.1.3",
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ parameters:
|
|||||||
- src
|
- src
|
||||||
# - tests
|
# - tests
|
||||||
|
|
||||||
banned_code:
|
|
||||||
non_ignorable: false # Allow to ignore some banned code
|
|
||||||
|
|
||||||
excludePaths:
|
excludePaths:
|
||||||
- src/DataTables/Adapter/*
|
- src/DataTables/Adapter/*
|
||||||
- src/Configuration/*
|
- src/Configuration/*
|
||||||
@@ -64,9 +61,3 @@ parameters:
|
|||||||
|
|
||||||
# Ignore error of unused WithPermPresetsTrait, as it is used in the migrations which are not analyzed by Phpstan
|
# Ignore error of unused WithPermPresetsTrait, as it is used in the migrations which are not analyzed by Phpstan
|
||||||
- '#Trait App\\Migration\\WithPermPresetsTrait is used zero times and is not analysed#'
|
- '#Trait App\\Migration\\WithPermPresetsTrait is used zero times and is not analysed#'
|
||||||
-
|
|
||||||
message: '#Should not use function "shell_exec"#'
|
|
||||||
path: src/Services/System/UpdateExecutor.php
|
|
||||||
|
|
||||||
- message: '#Access to an undefined property Brick\\Schema\\Interfaces\\#'
|
|
||||||
path: src/Services/InfoProviderSystem/Providers/GenericWebProvider.php
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ use Rector\Symfony\Set\SymfonySetList;
|
|||||||
use Rector\TypeDeclaration\Rector\StmtsAwareInterface\DeclareStrictTypesRector;
|
use Rector\TypeDeclaration\Rector\StmtsAwareInterface\DeclareStrictTypesRector;
|
||||||
|
|
||||||
return RectorConfig::configure()
|
return RectorConfig::configure()
|
||||||
->withComposerBased(phpunit: true, symfony: true)
|
->withComposerBased(phpunit: true)
|
||||||
|
|
||||||
->withSymfonyContainerPhp(__DIR__ . '/tests/symfony-container.php')
|
->withSymfonyContainerPhp(__DIR__ . '/tests/symfony-container.php')
|
||||||
->withSymfonyContainerXml(__DIR__ . '/var/cache/dev/App_KernelDevDebugContainer.xml')
|
->withSymfonyContainerXml(__DIR__ . '/var/cache/dev/App_KernelDevDebugContainer.xml')
|
||||||
@@ -36,6 +36,8 @@ return RectorConfig::configure()
|
|||||||
PHPUnitSetList::PHPUNIT_90,
|
PHPUnitSetList::PHPUNIT_90,
|
||||||
PHPUnitSetList::PHPUNIT_110,
|
PHPUnitSetList::PHPUNIT_110,
|
||||||
PHPUnitSetList::PHPUNIT_CODE_QUALITY,
|
PHPUnitSetList::PHPUNIT_CODE_QUALITY,
|
||||||
|
|
||||||
|
|
||||||
])
|
])
|
||||||
|
|
||||||
->withRules([
|
->withRules([
|
||||||
@@ -57,9 +59,6 @@ return RectorConfig::configure()
|
|||||||
PreferPHPUnitThisCallRector::class,
|
PreferPHPUnitThisCallRector::class,
|
||||||
//Do not replace 'GET' with class constant,
|
//Do not replace 'GET' with class constant,
|
||||||
LiteralGetToRequestClassConstantRector::class,
|
LiteralGetToRequestClassConstantRector::class,
|
||||||
|
|
||||||
//Do not move help text of commands to the command class, as we want to keep the help text in the command definition for better readability
|
|
||||||
\Rector\Symfony\Symfony73\Rector\Class_\CommandHelpToAttributeRector::class
|
|
||||||
])
|
])
|
||||||
|
|
||||||
//Do not apply rules to Symfony own files
|
//Do not apply rules to Symfony own files
|
||||||
@@ -68,7 +67,6 @@ return RectorConfig::configure()
|
|||||||
__DIR__ . '/src/Kernel.php',
|
__DIR__ . '/src/Kernel.php',
|
||||||
__DIR__ . '/config/preload.php',
|
__DIR__ . '/config/preload.php',
|
||||||
__DIR__ . '/config/bundles.php',
|
__DIR__ . '/config/bundles.php',
|
||||||
__DIR__ . '/config/reference.php'
|
|
||||||
])
|
])
|
||||||
|
|
||||||
;
|
;
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace App\ApiResource;
|
|
||||||
|
|
||||||
use ApiPlatform\Metadata\ApiProperty;
|
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
|
||||||
use ApiPlatform\Metadata\Post;
|
|
||||||
use ApiPlatform\OpenApi\Model\Operation;
|
|
||||||
use ApiPlatform\OpenApi\Model\RequestBody;
|
|
||||||
use ApiPlatform\OpenApi\Model\Response;
|
|
||||||
use App\Entity\LabelSystem\LabelSupportedElement;
|
|
||||||
use App\State\LabelGenerationProcessor;
|
|
||||||
use App\Validator\Constraints\Misc\ValidRange;
|
|
||||||
use Symfony\Component\Validator\Constraints as Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* API Resource for generating PDF labels for parts, part lots, or storage locations.
|
|
||||||
* This endpoint allows generating labels using saved label profiles.
|
|
||||||
*/
|
|
||||||
#[ApiResource(
|
|
||||||
uriTemplate: '/labels/generate',
|
|
||||||
description: 'Generate PDF labels for parts, part lots, or storage locations using label profiles.',
|
|
||||||
operations: [
|
|
||||||
new Post(
|
|
||||||
inputFormats: ['json' => ['application/json']],
|
|
||||||
outputFormats: [],
|
|
||||||
openapi: new Operation(
|
|
||||||
responses: [
|
|
||||||
"200" => new Response(description: "PDF file containing the generated labels"),
|
|
||||||
],
|
|
||||||
summary: 'Generate PDF labels',
|
|
||||||
description: 'Generate PDF labels for one or more elements using a label profile. Returns a PDF file.',
|
|
||||||
requestBody: new RequestBody(
|
|
||||||
description: 'Label generation request',
|
|
||||||
required: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
processor: LabelGenerationProcessor::class,
|
|
||||||
)]
|
|
||||||
class LabelGenerationRequest
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var int The ID of the label profile to use for generation
|
|
||||||
*/
|
|
||||||
#[Assert\NotBlank(message: 'Profile ID is required')]
|
|
||||||
#[Assert\Positive(message: 'Profile ID must be a positive integer')]
|
|
||||||
public int $profileId = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string Comma-separated list of element IDs or ranges (e.g., "1,2,5-10,15")
|
|
||||||
*/
|
|
||||||
#[Assert\NotBlank(message: 'Element IDs are required')]
|
|
||||||
#[ValidRange()]
|
|
||||||
#[ApiProperty(example: "1,2,5-10,15")]
|
|
||||||
public string $elementIds = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var LabelSupportedElement|null Optional: Override the element type. If not provided, uses profile's default.
|
|
||||||
*/
|
|
||||||
public ?LabelSupportedElement $elementType = null;
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*
|
|
||||||
* 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
|
|
||||||
namespace App\Command;
|
|
||||||
|
|
||||||
use App\Services\System\UpdateExecutor;
|
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
|
||||||
use Symfony\Component\Console\Command\Command;
|
|
||||||
use Symfony\Component\Console\Input\InputArgument;
|
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
||||||
|
|
||||||
#[AsCommand('partdb:maintenance-mode', 'Enable/disable maintenance mode and set a message')]
|
|
||||||
class MaintenanceModeCommand extends Command
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly UpdateExecutor $updateExecutor
|
|
||||||
) {
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function configure(): void
|
|
||||||
{
|
|
||||||
$this
|
|
||||||
->setDefinition([
|
|
||||||
new InputOption('enable', null, InputOption::VALUE_NONE, 'Enable maintenance mode'),
|
|
||||||
new InputOption('disable', null, InputOption::VALUE_NONE, 'Disable maintenance mode'),
|
|
||||||
new InputOption('status', null, InputOption::VALUE_NONE, 'Show current maintenance mode status'),
|
|
||||||
new InputOption('message', null, InputOption::VALUE_REQUIRED, 'Optional maintenance message (explicit option)'),
|
|
||||||
new InputArgument('message_arg', InputArgument::OPTIONAL, 'Optional maintenance message as a positional argument (preferred when writing message directly)')
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function execute(InputInterface $input, OutputInterface $output): int
|
|
||||||
{
|
|
||||||
$io = new SymfonyStyle($input, $output);
|
|
||||||
|
|
||||||
$enable = (bool)$input->getOption('enable');
|
|
||||||
$disable = (bool)$input->getOption('disable');
|
|
||||||
$status = (bool)$input->getOption('status');
|
|
||||||
|
|
||||||
// Accept message either via --message option or as positional argument
|
|
||||||
$optionMessage = $input->getOption('message');
|
|
||||||
$argumentMessage = $input->getArgument('message_arg');
|
|
||||||
|
|
||||||
// Prefer explicit --message option, otherwise use positional argument if provided
|
|
||||||
$message = null;
|
|
||||||
if (is_string($optionMessage) && $optionMessage !== '') {
|
|
||||||
$message = $optionMessage;
|
|
||||||
} elseif (is_string($argumentMessage) && $argumentMessage !== '') {
|
|
||||||
$message = $argumentMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no action provided, show help
|
|
||||||
if (!$enable && !$disable && !$status) {
|
|
||||||
$io->text('Maintenance mode command. See usage below:');
|
|
||||||
$this->printHelp($io);
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($enable && $disable) {
|
|
||||||
$io->error('Conflicting options: specify either --enable or --disable, not both.');
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if ($status) {
|
|
||||||
if ($this->updateExecutor->isMaintenanceMode()) {
|
|
||||||
$info = $this->updateExecutor->getMaintenanceInfo();
|
|
||||||
$reason = $info['reason'] ?? 'Unknown reason';
|
|
||||||
$enabledAt = $info['enabled_at'] ?? 'Unknown time';
|
|
||||||
|
|
||||||
$io->success(sprintf('Maintenance mode is ENABLED (since %s).', $enabledAt));
|
|
||||||
$io->text(sprintf('Reason: %s', $reason));
|
|
||||||
} else {
|
|
||||||
$io->success('Maintenance mode is DISABLED.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// If only status requested, exit
|
|
||||||
if (!$enable && !$disable) {
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($enable) {
|
|
||||||
// Use provided message or fallback to a default English message
|
|
||||||
$reason = is_string($message)
|
|
||||||
? $message
|
|
||||||
: 'The system is temporarily unavailable due to maintenance.';
|
|
||||||
|
|
||||||
$this->updateExecutor->enableMaintenanceMode($reason);
|
|
||||||
|
|
||||||
$io->success(sprintf('Maintenance mode enabled. Reason: %s', $reason));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($disable) {
|
|
||||||
$this->updateExecutor->disableMaintenanceMode();
|
|
||||||
$io->success('Maintenance mode disabled.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$io->error(sprintf('Unexpected error: %s', $e->getMessage()));
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function printHelp(SymfonyStyle $io): void
|
|
||||||
{
|
|
||||||
$io->writeln('');
|
|
||||||
$io->writeln('Usage:');
|
|
||||||
$io->writeln(' php bin/console partdb:maintenance_mode --enable [--message="Maintenance message"]');
|
|
||||||
$io->writeln(' php bin/console partdb:maintenance_mode --enable "Maintenance message"');
|
|
||||||
$io->writeln(' php bin/console partdb:maintenance_mode --disable');
|
|
||||||
$io->writeln(' php bin/console partdb:maintenance_mode --status');
|
|
||||||
$io->writeln('');
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,364 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Command;
|
|
||||||
|
|
||||||
use App\Entity\Parts\Category;
|
|
||||||
use App\Entity\Parts\Footprint;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
|
||||||
use Symfony\Component\Console\Command\Command;
|
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
|
||||||
|
|
||||||
#[AsCommand('partdb:kicad:populate', 'Populate KiCad footprint paths and symbol paths for footprints and categories')]
|
|
||||||
final class PopulateKicadCommand extends Command
|
|
||||||
{
|
|
||||||
private const DEFAULT_MAPPING_FILE = 'assets/commands/kicad_populate_default_mappings.json';
|
|
||||||
|
|
||||||
public function __construct(private readonly EntityManagerInterface $entityManager, #[Autowire("%kernel.project_dir%")] private readonly string $projectDir)
|
|
||||||
{
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function configure(): void
|
|
||||||
{
|
|
||||||
$this->setHelp('This command populates KiCad footprint paths on Footprint entities and KiCad symbol paths on Category entities based on their names.');
|
|
||||||
|
|
||||||
$this
|
|
||||||
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Preview changes without applying them')
|
|
||||||
->addOption('footprints', null, InputOption::VALUE_NONE, 'Only update footprint entities')
|
|
||||||
->addOption('categories', null, InputOption::VALUE_NONE, 'Only update category entities')
|
|
||||||
->addOption('force', null, InputOption::VALUE_NONE, 'Overwrite existing values (by default, only empty values are updated)')
|
|
||||||
->addOption('list', null, InputOption::VALUE_NONE, 'List all footprints and categories with their current KiCad values')
|
|
||||||
->addOption('mapping-file', null, InputOption::VALUE_REQUIRED, 'Path to a JSON file with custom mappings (merges with built-in defaults)')
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
|
||||||
{
|
|
||||||
$io = new SymfonyStyle($input, $output);
|
|
||||||
$dryRun = $input->getOption('dry-run');
|
|
||||||
$footprintsOnly = $input->getOption('footprints');
|
|
||||||
$categoriesOnly = $input->getOption('categories');
|
|
||||||
$force = $input->getOption('force');
|
|
||||||
$list = $input->getOption('list');
|
|
||||||
$mappingFile = $input->getOption('mapping-file');
|
|
||||||
|
|
||||||
// If neither specified, do both
|
|
||||||
$doFootprints = !$categoriesOnly || $footprintsOnly;
|
|
||||||
$doCategories = !$footprintsOnly || $categoriesOnly;
|
|
||||||
|
|
||||||
if ($list) {
|
|
||||||
$this->listCurrentValues($io);
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load mappings: start with built-in defaults, then merge user-supplied file
|
|
||||||
['footprints' => $footprintMappings, 'categories' => $categoryMappings] = $this->getDefaultMappings();
|
|
||||||
|
|
||||||
if ($mappingFile !== null) {
|
|
||||||
$customMappings = $this->loadMappingFile($mappingFile, $io);
|
|
||||||
if ($customMappings === null) {
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
if (isset($customMappings['footprints']) && is_array($customMappings['footprints'])) {
|
|
||||||
// User mappings take priority (overwrite defaults)
|
|
||||||
$footprintMappings = array_merge($footprintMappings, $customMappings['footprints']);
|
|
||||||
$io->text(sprintf('Loaded %d custom footprint mappings from %s', count($customMappings['footprints']), $mappingFile));
|
|
||||||
}
|
|
||||||
if (isset($customMappings['categories']) && is_array($customMappings['categories'])) {
|
|
||||||
$categoryMappings = array_merge($categoryMappings, $customMappings['categories']);
|
|
||||||
$io->text(sprintf('Loaded %d custom category mappings from %s', count($customMappings['categories']), $mappingFile));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($dryRun) {
|
|
||||||
$io->note('DRY RUN MODE - No changes will be made');
|
|
||||||
}
|
|
||||||
|
|
||||||
$totalUpdated = 0;
|
|
||||||
|
|
||||||
if ($doFootprints) {
|
|
||||||
$updated = $this->updateFootprints($io, $dryRun, $force, $footprintMappings);
|
|
||||||
$totalUpdated += $updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($doCategories) {
|
|
||||||
$updated = $this->updateCategories($io, $dryRun, $force, $categoryMappings);
|
|
||||||
$totalUpdated += $updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$dryRun && $totalUpdated > 0) {
|
|
||||||
$this->entityManager->flush();
|
|
||||||
$io->success(sprintf('Updated %d entities. Run "php bin/console cache:clear" to clear the cache.', $totalUpdated));
|
|
||||||
} elseif ($dryRun && $totalUpdated > 0) {
|
|
||||||
$io->info(sprintf('DRY RUN: Would update %d entities. Run without --dry-run to apply changes.', $totalUpdated));
|
|
||||||
} else {
|
|
||||||
$io->info('No entities needed updating.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function listCurrentValues(SymfonyStyle $io): void
|
|
||||||
{
|
|
||||||
$io->section('Current Footprint KiCad Values');
|
|
||||||
|
|
||||||
$footprintRepo = $this->entityManager->getRepository(Footprint::class);
|
|
||||||
/** @var Footprint[] $footprints */
|
|
||||||
$footprints = $footprintRepo->findAll();
|
|
||||||
|
|
||||||
$rows = [];
|
|
||||||
foreach ($footprints as $footprint) {
|
|
||||||
$kicadValue = $footprint->getEdaInfo()->getKicadFootprint();
|
|
||||||
$rows[] = [
|
|
||||||
$footprint->getId(),
|
|
||||||
$footprint->getName(),
|
|
||||||
$kicadValue ?? '(empty)',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$io->table(['ID', 'Name', 'KiCad Footprint'], $rows);
|
|
||||||
|
|
||||||
$io->section('Current Category KiCad Values');
|
|
||||||
|
|
||||||
$categoryRepo = $this->entityManager->getRepository(Category::class);
|
|
||||||
/** @var Category[] $categories */
|
|
||||||
$categories = $categoryRepo->findAll();
|
|
||||||
|
|
||||||
$rows = [];
|
|
||||||
foreach ($categories as $category) {
|
|
||||||
$kicadValue = $category->getEdaInfo()->getKicadSymbol();
|
|
||||||
$rows[] = [
|
|
||||||
$category->getId(),
|
|
||||||
$category->getName(),
|
|
||||||
$kicadValue ?? '(empty)',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$io->table(['ID', 'Name', 'KiCad Symbol'], $rows);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function updateFootprints(SymfonyStyle $io, bool $dryRun, bool $force, array $mappings): int
|
|
||||||
{
|
|
||||||
$io->section('Updating Footprint Entities');
|
|
||||||
|
|
||||||
$footprintRepo = $this->entityManager->getRepository(Footprint::class);
|
|
||||||
/** @var Footprint[] $footprints */
|
|
||||||
$footprints = $footprintRepo->findAll();
|
|
||||||
|
|
||||||
$updated = 0;
|
|
||||||
$skipped = [];
|
|
||||||
|
|
||||||
foreach ($footprints as $footprint) {
|
|
||||||
$name = $footprint->getName();
|
|
||||||
$currentValue = $footprint->getEdaInfo()->getKicadFootprint();
|
|
||||||
|
|
||||||
// Skip if already has value and not forcing
|
|
||||||
if (!$force && $currentValue !== null && $currentValue !== '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for exact match on name first, then try alternative names
|
|
||||||
$matchedValue = $this->findFootprintMapping($mappings, $name, $footprint->getAlternativeNames());
|
|
||||||
|
|
||||||
if ($matchedValue !== null) {
|
|
||||||
$io->text(sprintf(' %s: %s -> %s', $name, $currentValue ?? '(empty)', $matchedValue));
|
|
||||||
|
|
||||||
if (!$dryRun) {
|
|
||||||
$footprint->getEdaInfo()->setKicadFootprint($matchedValue);
|
|
||||||
}
|
|
||||||
$updated++;
|
|
||||||
} else {
|
|
||||||
// No mapping found
|
|
||||||
$skipped[] = $name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$io->newLine();
|
|
||||||
$io->text(sprintf('Updated: %d footprints', $updated));
|
|
||||||
|
|
||||||
if (count($skipped) > 0) {
|
|
||||||
$io->warning(sprintf('No mapping found for %d footprints:', count($skipped)));
|
|
||||||
foreach ($skipped as $name) {
|
|
||||||
$io->text(' - ' . $name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function updateCategories(SymfonyStyle $io, bool $dryRun, bool $force, array $mappings): int
|
|
||||||
{
|
|
||||||
$io->section('Updating Category Entities');
|
|
||||||
|
|
||||||
$categoryRepo = $this->entityManager->getRepository(Category::class);
|
|
||||||
/** @var Category[] $categories */
|
|
||||||
$categories = $categoryRepo->findAll();
|
|
||||||
|
|
||||||
$updated = 0;
|
|
||||||
$skipped = [];
|
|
||||||
|
|
||||||
foreach ($categories as $category) {
|
|
||||||
$name = $category->getName();
|
|
||||||
$currentValue = $category->getEdaInfo()->getKicadSymbol();
|
|
||||||
|
|
||||||
// Skip if already has value and not forcing
|
|
||||||
if (!$force && $currentValue !== null && $currentValue !== '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for matches using the pattern-based mappings (also check alternative names)
|
|
||||||
$matchedValue = $this->findCategoryMapping($mappings, $name, $category->getAlternativeNames());
|
|
||||||
|
|
||||||
if ($matchedValue !== null) {
|
|
||||||
$io->text(sprintf(' %s: %s -> %s', $name, $currentValue ?? '(empty)', $matchedValue));
|
|
||||||
|
|
||||||
if (!$dryRun) {
|
|
||||||
$category->getEdaInfo()->setKicadSymbol($matchedValue);
|
|
||||||
}
|
|
||||||
$updated++;
|
|
||||||
} else {
|
|
||||||
$skipped[] = $name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$io->newLine();
|
|
||||||
$io->text(sprintf('Updated: %d categories', $updated));
|
|
||||||
|
|
||||||
if (count($skipped) > 0) {
|
|
||||||
$io->note(sprintf('No mapping found for %d categories (this is often expected):', count($skipped)));
|
|
||||||
foreach ($skipped as $name) {
|
|
||||||
$io->text(' - ' . $name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads a JSON mapping file and returns the parsed data.
|
|
||||||
* Expected format: {"footprints": {"Name": "KiCad:Path"}, "categories": {"Pattern": "KiCad:Path"}}
|
|
||||||
*
|
|
||||||
* @return array|null The parsed mappings, or null on error
|
|
||||||
*/
|
|
||||||
private function loadMappingFile(string $path, SymfonyStyle $io): ?array
|
|
||||||
{
|
|
||||||
if (!file_exists($path)) {
|
|
||||||
$io->error(sprintf('Mapping file not found: %s', $path));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$content = file_get_contents($path);
|
|
||||||
if ($content === false) {
|
|
||||||
$io->error(sprintf('Could not read mapping file: %s', $path));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = json_decode($content, true);
|
|
||||||
if (!is_array($data)) {
|
|
||||||
$io->error(sprintf('Invalid JSON in mapping file: %s', $path));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function matchesPattern(string $name, string $pattern): bool
|
|
||||||
{
|
|
||||||
// Check for exact match
|
|
||||||
if ($pattern === $name) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for case-insensitive contains
|
|
||||||
if (stripos($name, $pattern) !== false) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a footprint mapping by checking the entity name and its alternative names.
|
|
||||||
* Footprints use exact matching.
|
|
||||||
*
|
|
||||||
* @param array<string, string> $mappings
|
|
||||||
* @param string $name The primary name of the footprint
|
|
||||||
* @param string|null $alternativeNames Comma-separated alternative names
|
|
||||||
* @return string|null The matched KiCad path, or null if no match found
|
|
||||||
*/
|
|
||||||
private function findFootprintMapping(array $mappings, string $name, ?string $alternativeNames): ?string
|
|
||||||
{
|
|
||||||
// Check primary name
|
|
||||||
if (isset($mappings[$name])) {
|
|
||||||
return $mappings[$name];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check alternative names
|
|
||||||
if ($alternativeNames !== null && $alternativeNames !== '') {
|
|
||||||
foreach (explode(',', $alternativeNames) as $altName) {
|
|
||||||
$altName = trim($altName);
|
|
||||||
if ($altName !== '' && isset($mappings[$altName])) {
|
|
||||||
return $mappings[$altName];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a category mapping by checking the entity name and its alternative names.
|
|
||||||
* Categories use pattern-based matching (case-insensitive contains).
|
|
||||||
*
|
|
||||||
* @param array<string, string> $mappings
|
|
||||||
* @param string $name The primary name of the category
|
|
||||||
* @param string|null $alternativeNames Comma-separated alternative names
|
|
||||||
* @return string|null The matched KiCad symbol path, or null if no match found
|
|
||||||
*/
|
|
||||||
private function findCategoryMapping(array $mappings, string $name, ?string $alternativeNames): ?string
|
|
||||||
{
|
|
||||||
// Check primary name against all patterns
|
|
||||||
foreach ($mappings as $pattern => $kicadSymbol) {
|
|
||||||
if ($this->matchesPattern($name, $pattern)) {
|
|
||||||
return $kicadSymbol;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check alternative names against all patterns
|
|
||||||
if ($alternativeNames !== null && $alternativeNames !== '') {
|
|
||||||
foreach (explode(',', $alternativeNames) as $altName) {
|
|
||||||
$altName = trim($altName);
|
|
||||||
if ($altName === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
foreach ($mappings as $pattern => $kicadSymbol) {
|
|
||||||
if ($this->matchesPattern($altName, $pattern)) {
|
|
||||||
return $kicadSymbol;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the default mappings for footprints and categories.
|
|
||||||
* @return array{footprints: array<string, string>, categories: array<string, string>}
|
|
||||||
* @throws \JsonException
|
|
||||||
*/
|
|
||||||
private function getDefaultMappings(): array
|
|
||||||
{
|
|
||||||
$path = $this->projectDir . '/' . self::DEFAULT_MAPPING_FILE;
|
|
||||||
$content = file_get_contents($path);
|
|
||||||
|
|
||||||
return json_decode($content, true, 512, JSON_THROW_ON_ERROR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,445 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*
|
|
||||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 - 2024 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
|
|
||||||
namespace App\Command;
|
|
||||||
|
|
||||||
use App\Services\System\UpdateChecker;
|
|
||||||
use App\Services\System\UpdateExecutor;
|
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
|
||||||
use Symfony\Component\Console\Command\Command;
|
|
||||||
use Symfony\Component\Console\Helper\Table;
|
|
||||||
use Symfony\Component\Console\Input\InputArgument;
|
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
||||||
|
|
||||||
#[AsCommand(name: 'partdb:update', description: 'Check for and install Part-DB updates', aliases: ['app:update'])]
|
|
||||||
class UpdateCommand extends Command
|
|
||||||
{
|
|
||||||
public function __construct(private readonly UpdateChecker $updateChecker,
|
|
||||||
private readonly UpdateExecutor $updateExecutor)
|
|
||||||
{
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function configure(): void
|
|
||||||
{
|
|
||||||
$this
|
|
||||||
->setHelp(<<<'HELP'
|
|
||||||
The <info>%command.name%</info> command checks for Part-DB updates and can install them.
|
|
||||||
|
|
||||||
<comment>Check for updates:</comment>
|
|
||||||
<info>php %command.full_name% --check</info>
|
|
||||||
|
|
||||||
<comment>List available versions:</comment>
|
|
||||||
<info>php %command.full_name% --list</info>
|
|
||||||
|
|
||||||
<comment>Update to the latest version:</comment>
|
|
||||||
<info>php %command.full_name%</info>
|
|
||||||
|
|
||||||
<comment>Update to a specific version:</comment>
|
|
||||||
<info>php %command.full_name% v2.6.0</info>
|
|
||||||
|
|
||||||
<comment>Update without creating a backup (faster but riskier):</comment>
|
|
||||||
<info>php %command.full_name% --no-backup</info>
|
|
||||||
|
|
||||||
<comment>Non-interactive update for scripts:</comment>
|
|
||||||
<info>php %command.full_name% --force</info>
|
|
||||||
|
|
||||||
<comment>View update logs:</comment>
|
|
||||||
<info>php %command.full_name% --logs</info>
|
|
||||||
HELP
|
|
||||||
)
|
|
||||||
->addArgument(
|
|
||||||
'version',
|
|
||||||
InputArgument::OPTIONAL,
|
|
||||||
'Target version to update to (e.g., v2.6.0). If not specified, updates to the latest stable version.'
|
|
||||||
)
|
|
||||||
->addOption(
|
|
||||||
'check',
|
|
||||||
'c',
|
|
||||||
InputOption::VALUE_NONE,
|
|
||||||
'Only check for updates without installing'
|
|
||||||
)
|
|
||||||
->addOption(
|
|
||||||
'list',
|
|
||||||
'l',
|
|
||||||
InputOption::VALUE_NONE,
|
|
||||||
'List all available versions'
|
|
||||||
)
|
|
||||||
->addOption(
|
|
||||||
'no-backup',
|
|
||||||
null,
|
|
||||||
InputOption::VALUE_NONE,
|
|
||||||
'Skip creating a backup before updating (not recommended)'
|
|
||||||
)
|
|
||||||
->addOption(
|
|
||||||
'force',
|
|
||||||
'f',
|
|
||||||
InputOption::VALUE_NONE,
|
|
||||||
'Skip confirmation prompts'
|
|
||||||
)
|
|
||||||
->addOption(
|
|
||||||
'include-prerelease',
|
|
||||||
null,
|
|
||||||
InputOption::VALUE_NONE,
|
|
||||||
'Include pre-release versions'
|
|
||||||
)
|
|
||||||
->addOption(
|
|
||||||
'logs',
|
|
||||||
null,
|
|
||||||
InputOption::VALUE_NONE,
|
|
||||||
'Show recent update logs'
|
|
||||||
)
|
|
||||||
->addOption(
|
|
||||||
'refresh',
|
|
||||||
'r',
|
|
||||||
InputOption::VALUE_NONE,
|
|
||||||
'Force refresh of cached version information'
|
|
||||||
)
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
|
||||||
{
|
|
||||||
$io = new SymfonyStyle($input, $output);
|
|
||||||
|
|
||||||
// Handle --logs option
|
|
||||||
if ($input->getOption('logs')) {
|
|
||||||
return $this->showLogs($io);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle --refresh option
|
|
||||||
if ($input->getOption('refresh')) {
|
|
||||||
$io->text('Refreshing version information...');
|
|
||||||
$this->updateChecker->refreshVersionInfo();
|
|
||||||
$io->success('Version cache cleared.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle --list option
|
|
||||||
if ($input->getOption('list')) {
|
|
||||||
return $this->listVersions($io, $input->getOption('include-prerelease'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get update status
|
|
||||||
$status = $this->updateChecker->getUpdateStatus();
|
|
||||||
|
|
||||||
// Display current status
|
|
||||||
$io->title('Part-DB Update Manager');
|
|
||||||
|
|
||||||
$this->displayStatus($io, $status);
|
|
||||||
|
|
||||||
// Handle --check option
|
|
||||||
if ($input->getOption('check')) {
|
|
||||||
return $this->checkOnly($io, $status);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate we can update
|
|
||||||
$validationResult = $this->validateUpdate($io, $status);
|
|
||||||
if ($validationResult !== null) {
|
|
||||||
return $validationResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine target version
|
|
||||||
$targetVersion = $input->getArgument('version');
|
|
||||||
$includePrerelease = $input->getOption('include-prerelease');
|
|
||||||
|
|
||||||
if (!$targetVersion) {
|
|
||||||
$latest = $this->updateChecker->getLatestVersion($includePrerelease);
|
|
||||||
if (!$latest) {
|
|
||||||
$io->error('Could not determine the latest version. Please specify a version manually.');
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
$targetVersion = $latest['tag'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate target version
|
|
||||||
if (!$this->updateChecker->isNewerVersionThanCurrent($targetVersion)) {
|
|
||||||
$io->warning(sprintf(
|
|
||||||
'Version %s is not newer than the current version %s.',
|
|
||||||
$targetVersion,
|
|
||||||
$status['current_version']
|
|
||||||
));
|
|
||||||
|
|
||||||
if (!$input->getOption('force')) {
|
|
||||||
if (!$io->confirm('Do you want to proceed anyway?', false)) {
|
|
||||||
$io->info('Update cancelled.');
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirm update
|
|
||||||
if (!$input->getOption('force')) {
|
|
||||||
$io->section('Update Plan');
|
|
||||||
|
|
||||||
$io->listing([
|
|
||||||
sprintf('Target version: <info>%s</info>', $targetVersion),
|
|
||||||
$input->getOption('no-backup')
|
|
||||||
? '<fg=yellow>Backup will be SKIPPED</>'
|
|
||||||
: 'A full backup will be created before updating',
|
|
||||||
'Maintenance mode will be enabled during update',
|
|
||||||
'Database migrations will be run automatically',
|
|
||||||
'Cache will be cleared and rebuilt',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$io->warning('The update process may take several minutes. Do not interrupt it.');
|
|
||||||
|
|
||||||
if (!$io->confirm('Do you want to proceed with the update?', false)) {
|
|
||||||
$io->info('Update cancelled.');
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute update
|
|
||||||
return $this->executeUpdate($io, $targetVersion, !$input->getOption('no-backup'));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function displayStatus(SymfonyStyle $io, array $status): void
|
|
||||||
{
|
|
||||||
$io->definitionList(
|
|
||||||
['Current Version' => sprintf('<info>%s</info>', $status['current_version'])],
|
|
||||||
['Latest Version' => $status['latest_version']
|
|
||||||
? sprintf('<info>%s</info>', $status['latest_version'])
|
|
||||||
: '<fg=yellow>Unknown</>'],
|
|
||||||
['Installation Type' => $status['installation']['type_name']],
|
|
||||||
['Git Branch' => $status['git']['branch'] ?? '<fg=gray>N/A</>'],
|
|
||||||
['Git Commit' => $status['git']['commit'] ?? '<fg=gray>N/A</>'],
|
|
||||||
['Local Changes' => $status['git']['has_local_changes']
|
|
||||||
? '<fg=yellow>Yes (update blocked)</>'
|
|
||||||
: '<fg=green>No</>'],
|
|
||||||
['Commits Behind' => $status['git']['commits_behind'] > 0
|
|
||||||
? sprintf('<fg=yellow>%d</>', $status['git']['commits_behind'])
|
|
||||||
: '<fg=green>0</>'],
|
|
||||||
['Update Available' => $status['update_available']
|
|
||||||
? '<fg=green>Yes</>'
|
|
||||||
: 'No'],
|
|
||||||
['Can Auto-Update' => $status['can_auto_update']
|
|
||||||
? '<fg=green>Yes</>'
|
|
||||||
: '<fg=yellow>No</>'],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!empty($status['update_blockers'])) {
|
|
||||||
$io->warning('Update blockers: ' . implode(', ', $status['update_blockers']));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function checkOnly(SymfonyStyle $io, array $status): int
|
|
||||||
{
|
|
||||||
if (!$status['check_enabled']) {
|
|
||||||
$io->warning('Update checking is disabled in privacy settings.');
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($status['update_available']) {
|
|
||||||
$io->success(sprintf(
|
|
||||||
'A new version is available: %s (current: %s)',
|
|
||||||
$status['latest_version'],
|
|
||||||
$status['current_version']
|
|
||||||
));
|
|
||||||
|
|
||||||
if ($status['release_url']) {
|
|
||||||
$io->text(sprintf('Release notes: <href=%s>%s</>', $status['release_url'], $status['release_url']));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($status['can_auto_update']) {
|
|
||||||
$io->text('');
|
|
||||||
$io->text('Run <info>php bin/console partdb:update</info> to update.');
|
|
||||||
} else {
|
|
||||||
$io->text('');
|
|
||||||
$io->text($status['installation']['update_instructions']);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
$io->success('You are running the latest version.');
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function validateUpdate(SymfonyStyle $io, array $status): ?int
|
|
||||||
{
|
|
||||||
// Check if update checking is enabled
|
|
||||||
if (!$status['check_enabled']) {
|
|
||||||
$io->error('Update checking is disabled in privacy settings. Enable it to use automatic updates.');
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check installation type
|
|
||||||
if (!$status['can_auto_update']) {
|
|
||||||
$io->error('Automatic updates are not supported for this installation type.');
|
|
||||||
$io->text($status['installation']['update_instructions']);
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate preconditions
|
|
||||||
$validation = $this->updateExecutor->validateUpdatePreconditions();
|
|
||||||
if (!$validation['valid']) {
|
|
||||||
$io->error('Cannot proceed with update:');
|
|
||||||
$io->listing($validation['errors']);
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function executeUpdate(SymfonyStyle $io, string $targetVersion, bool $createBackup): int
|
|
||||||
{
|
|
||||||
$io->section('Executing Update');
|
|
||||||
$io->text(sprintf('Updating to version: <info>%s</info>', $targetVersion));
|
|
||||||
$io->text('');
|
|
||||||
|
|
||||||
$progressCallback = function (array $step) use ($io): void {
|
|
||||||
$icon = $step['success'] ? '<fg=green>✓</>' : '<fg=red>✗</>';
|
|
||||||
$duration = $step['duration'] ? sprintf(' <fg=gray>(%.1fs)</>', $step['duration']) : '';
|
|
||||||
$io->text(sprintf(' %s <info>%s</info>: %s%s', $icon, $step['step'], $step['message'], $duration));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use executeUpdateWithProgress to update the progress file for web UI
|
|
||||||
$result = $this->updateExecutor->executeUpdateWithProgress($targetVersion, $createBackup, $progressCallback);
|
|
||||||
|
|
||||||
$io->text('');
|
|
||||||
|
|
||||||
if ($result['success']) {
|
|
||||||
$io->success(sprintf(
|
|
||||||
'Successfully updated to %s in %.1f seconds!',
|
|
||||||
$targetVersion,
|
|
||||||
$result['duration']
|
|
||||||
));
|
|
||||||
|
|
||||||
$io->text([
|
|
||||||
sprintf('Rollback tag: <info>%s</info>', $result['rollback_tag']),
|
|
||||||
sprintf('Log file: <info>%s</info>', $result['log_file']),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$io->note('If you encounter any issues, you can rollback using: git checkout ' . $result['rollback_tag']);
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
$io->error('Update failed: ' . $result['error']);
|
|
||||||
|
|
||||||
if ($result['rollback_tag']) {
|
|
||||||
$io->warning(sprintf('System was rolled back to: %s', $result['rollback_tag']));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($result['log_file']) {
|
|
||||||
$io->text(sprintf('See log file for details: %s', $result['log_file']));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function listVersions(SymfonyStyle $io, bool $includePrerelease): int
|
|
||||||
{
|
|
||||||
$releases = $this->updateChecker->getAvailableReleases(15);
|
|
||||||
$currentVersion = $this->updateChecker->getCurrentVersionString();
|
|
||||||
|
|
||||||
if (empty($releases)) {
|
|
||||||
$io->warning('Could not fetch available versions. Check your internet connection.');
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
$io->title('Available Part-DB Versions');
|
|
||||||
|
|
||||||
$table = new Table($io);
|
|
||||||
$table->setHeaders(['Tag', 'Version', 'Released', 'Status']);
|
|
||||||
|
|
||||||
foreach ($releases as $release) {
|
|
||||||
if (!$includePrerelease && $release['prerelease']) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$version = $release['version'];
|
|
||||||
$status = [];
|
|
||||||
|
|
||||||
if (version_compare($version, $currentVersion, '=')) {
|
|
||||||
$status[] = '<fg=cyan>current</>';
|
|
||||||
} elseif (version_compare($version, $currentVersion, '>')) {
|
|
||||||
$status[] = '<fg=green>newer</>';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($release['prerelease']) {
|
|
||||||
$status[] = '<fg=yellow>pre-release</>';
|
|
||||||
}
|
|
||||||
|
|
||||||
$table->addRow([
|
|
||||||
$release['tag'],
|
|
||||||
$version,
|
|
||||||
(new \DateTime($release['published_at']))->format('Y-m-d'),
|
|
||||||
implode(' ', $status) ?: '-',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$table->render();
|
|
||||||
|
|
||||||
$io->text('');
|
|
||||||
$io->text('Use <info>php bin/console partdb:update [tag]</info> to update to a specific version.');
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function showLogs(SymfonyStyle $io): int
|
|
||||||
{
|
|
||||||
$logs = $this->updateExecutor->getUpdateLogs();
|
|
||||||
|
|
||||||
if (empty($logs)) {
|
|
||||||
$io->info('No update logs found.');
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
$io->title('Recent Update Logs');
|
|
||||||
|
|
||||||
$table = new Table($io);
|
|
||||||
$table->setHeaders(['Date', 'File', 'Size']);
|
|
||||||
|
|
||||||
foreach (array_slice($logs, 0, 10) as $log) {
|
|
||||||
$table->addRow([
|
|
||||||
date('Y-m-d H:i:s', $log['date']),
|
|
||||||
$log['file'],
|
|
||||||
$this->formatBytes($log['size']),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$table->render();
|
|
||||||
|
|
||||||
$io->text('');
|
|
||||||
$io->text('Log files are stored in: <info>var/log/updates/</info>');
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function formatBytes(int $bytes): string
|
|
||||||
{
|
|
||||||
$units = ['B', 'KB', 'MB', 'GB'];
|
|
||||||
$unitIndex = 0;
|
|
||||||
|
|
||||||
while ($bytes >= 1024 && $unitIndex < count($units) - 1) {
|
|
||||||
$bytes /= 1024;
|
|
||||||
$unitIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sprintf('%.1f %s', $bytes, $units[$unitIndex]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -22,9 +22,9 @@ declare(strict_types=1);
|
|||||||
*/
|
*/
|
||||||
namespace App\Command;
|
namespace App\Command;
|
||||||
|
|
||||||
use App\Services\System\GitVersionInfoProvider;
|
|
||||||
use Shivas\VersioningBundle\Service\VersionManagerInterface;
|
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use App\Services\Misc\GitVersionInfo;
|
||||||
|
use Shivas\VersioningBundle\Service\VersionManagerInterface;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
@@ -33,7 +33,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
|||||||
#[AsCommand('partdb:version|app:version', 'Shows the currently installed version of Part-DB.')]
|
#[AsCommand('partdb:version|app:version', 'Shows the currently installed version of Part-DB.')]
|
||||||
class VersionCommand extends Command
|
class VersionCommand extends Command
|
||||||
{
|
{
|
||||||
public function __construct(protected VersionManagerInterface $versionManager, protected GitVersionInfoProvider $gitVersionInfo)
|
public function __construct(protected VersionManagerInterface $versionManager, protected GitVersionInfo $gitVersionInfo)
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
@@ -48,9 +48,9 @@ class VersionCommand extends Command
|
|||||||
|
|
||||||
$message = 'Part-DB version: '. $this->versionManager->getVersion()->toString();
|
$message = 'Part-DB version: '. $this->versionManager->getVersion()->toString();
|
||||||
|
|
||||||
if ($this->gitVersionInfo->getBranchName() !== null) {
|
if ($this->gitVersionInfo->getGitBranchName() !== null) {
|
||||||
$message .= ' Git branch: '. $this->gitVersionInfo->getBranchName();
|
$message .= ' Git branch: '. $this->gitVersionInfo->getGitBranchName();
|
||||||
$message .= ', Git commit: '. $this->gitVersionInfo->getCommitHash();
|
$message .= ', Git commit: '. $this->gitVersionInfo->getGitCommitHash();
|
||||||
}
|
}
|
||||||
|
|
||||||
$io->success($message);
|
$io->success($message);
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ use App\Form\Filters\AttachmentFilterType;
|
|||||||
use App\Services\Attachments\AttachmentManager;
|
use App\Services\Attachments\AttachmentManager;
|
||||||
use App\Services\Trees\NodesListBuilder;
|
use App\Services\Trees\NodesListBuilder;
|
||||||
use App\Settings\BehaviorSettings\TableSettings;
|
use App\Settings\BehaviorSettings\TableSettings;
|
||||||
use App\Settings\SystemSettings\AttachmentsSettings;
|
|
||||||
use Omines\DataTablesBundle\DataTableFactory;
|
use Omines\DataTablesBundle\DataTableFactory;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
@@ -42,93 +41,11 @@ use Symfony\Component\Routing\Attribute\Route;
|
|||||||
|
|
||||||
class AttachmentFileController extends AbstractController
|
class AttachmentFileController extends AbstractController
|
||||||
{
|
{
|
||||||
|
|
||||||
public function __construct(private readonly AttachmentManager $helper)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route(path: '/attachment/{id}/sandbox', name: 'attachment_html_sandbox')]
|
|
||||||
public function htmlSandbox(Attachment $attachment, AttachmentsSettings $attachmentsSettings): Response
|
|
||||||
{
|
|
||||||
//Check if the sandbox is enabled in the settings, as it can be a security risk if used without proper precautions, so it should be opt-in
|
|
||||||
if (!$attachmentsSettings->showHTMLAttachments) {
|
|
||||||
throw $this->createAccessDeniedException('The HTML sandbox for attachments is disabled in the settings, as it can be a security risk if used without proper precautions. Please enable it in the settings if you want to use it.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->checkPermissions($attachment);
|
|
||||||
|
|
||||||
$file_path = $this->helper->toAbsoluteInternalFilePath($attachment);
|
|
||||||
|
|
||||||
$attachmentContent = file_get_contents($file_path);
|
|
||||||
|
|
||||||
$response = $this->render('attachments/html_sandbox.html.twig', [
|
|
||||||
'attachment' => $attachment,
|
|
||||||
'content' => $attachmentContent,
|
|
||||||
]);
|
|
||||||
|
|
||||||
//Set an CSP that allows to run inline scripts, styles and images from external ressources, but does not allow any connections or others.
|
|
||||||
//Also set the sandbox CSP directive with only "allow-script" to run basic scripts
|
|
||||||
$response->headers->set('Content-Security-Policy', "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline' *; img-src data: *; sandbox allow-scripts allow-downloads allow-modals;");
|
|
||||||
|
|
||||||
//Forbid to embed the attachment render page in an iframe to prevent clickjacking, as it is not used anywhere else for now
|
|
||||||
$response->headers->set('X-Frame-Options', 'DENY');
|
|
||||||
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download the selected attachment.
|
* Download the selected attachment.
|
||||||
*/
|
*/
|
||||||
#[Route(path: '/attachment/{id}/download', name: 'attachment_download')]
|
#[Route(path: '/attachment/{id}/download', name: 'attachment_download')]
|
||||||
public function download(Attachment $attachment): BinaryFileResponse
|
public function download(Attachment $attachment, AttachmentManager $helper): BinaryFileResponse
|
||||||
{
|
|
||||||
$this->checkPermissions($attachment);
|
|
||||||
|
|
||||||
$file_path = $this->helper->toAbsoluteInternalFilePath($attachment);
|
|
||||||
$response = new BinaryFileResponse($file_path);
|
|
||||||
|
|
||||||
$response = $this->forbidHTMLContentType($response);
|
|
||||||
|
|
||||||
//Set header content disposition, so that the file will be downloaded
|
|
||||||
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $attachment->getFilename());
|
|
||||||
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* View the attachment.
|
|
||||||
*/
|
|
||||||
#[Route(path: '/attachment/{id}/view', name: 'attachment_view')]
|
|
||||||
public function view(Attachment $attachment): BinaryFileResponse
|
|
||||||
{
|
|
||||||
$this->checkPermissions($attachment);
|
|
||||||
|
|
||||||
$file_path = $this->helper->toAbsoluteInternalFilePath($attachment);
|
|
||||||
$response = new BinaryFileResponse($file_path);
|
|
||||||
|
|
||||||
$response = $this->forbidHTMLContentType($response);
|
|
||||||
|
|
||||||
//Set header content disposition, so that the file will be downloaded
|
|
||||||
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_INLINE, $attachment->getFilename());
|
|
||||||
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function forbidHTMLContentType(BinaryFileResponse $response): BinaryFileResponse
|
|
||||||
{
|
|
||||||
$mimeType = $response->getFile()->getMimeType();
|
|
||||||
|
|
||||||
if ($mimeType === 'text/html') {
|
|
||||||
$mimeType = 'text/plain';
|
|
||||||
}
|
|
||||||
|
|
||||||
$response->headers->set('Content-Type', $mimeType);
|
|
||||||
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function checkPermissions(Attachment $attachment): void
|
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('read', $attachment);
|
$this->denyAccessUnlessGranted('read', $attachment);
|
||||||
|
|
||||||
@@ -140,9 +57,46 @@ class AttachmentFileController extends AbstractController
|
|||||||
throw $this->createNotFoundException('The file for this attachment is external and not stored locally!');
|
throw $this->createNotFoundException('The file for this attachment is external and not stored locally!');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->helper->isInternalFileExisting($attachment)) {
|
if (!$helper->isInternalFileExisting($attachment)) {
|
||||||
throw $this->createNotFoundException('The file associated with the attachment is not existing!');
|
throw $this->createNotFoundException('The file associated with the attachment is not existing!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$file_path = $helper->toAbsoluteInternalFilePath($attachment);
|
||||||
|
$response = new BinaryFileResponse($file_path);
|
||||||
|
|
||||||
|
//Set header content disposition, so that the file will be downloaded
|
||||||
|
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View the attachment.
|
||||||
|
*/
|
||||||
|
#[Route(path: '/attachment/{id}/view', name: 'attachment_view')]
|
||||||
|
public function view(Attachment $attachment, AttachmentManager $helper): BinaryFileResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('read', $attachment);
|
||||||
|
|
||||||
|
if ($attachment->isSecure()) {
|
||||||
|
$this->denyAccessUnlessGranted('show_private', $attachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$attachment->hasInternal()) {
|
||||||
|
throw $this->createNotFoundException('The file for this attachment is external and not stored locally!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$helper->isInternalFileExisting($attachment)) {
|
||||||
|
throw $this->createNotFoundException('The file associated with the attachment is not existing!');
|
||||||
|
}
|
||||||
|
|
||||||
|
$file_path = $helper->toAbsoluteInternalFilePath($attachment);
|
||||||
|
$response = new BinaryFileResponse($file_path);
|
||||||
|
|
||||||
|
//Set header content disposition, so that the file will be downloaded
|
||||||
|
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_INLINE);
|
||||||
|
|
||||||
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route(path: '/attachment/list', name: 'attachment_list')]
|
#[Route(path: '/attachment/list', name: 'attachment_list')]
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Controller;
|
|
||||||
|
|
||||||
use App\Entity\Parts\Part;
|
|
||||||
use App\Form\Part\EDA\BatchEdaType;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
|
||||||
|
|
||||||
class BatchEdaController extends AbstractController
|
|
||||||
{
|
|
||||||
public function __construct(private readonly EntityManagerInterface $entityManager)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute shared EDA values across all parts. If all parts have the same value for a field, return it.
|
|
||||||
* @param Part[] $parts
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
private function getSharedEdaValues(array $parts): array
|
|
||||||
{
|
|
||||||
$fields = [
|
|
||||||
'reference_prefix' => static fn (Part $p) => $p->getEdaInfo()->getReferencePrefix(),
|
|
||||||
'value' => static fn (Part $p) => $p->getEdaInfo()->getValue(),
|
|
||||||
'kicad_symbol' => static fn (Part $p) => $p->getEdaInfo()->getKicadSymbol(),
|
|
||||||
'kicad_footprint' => static fn (Part $p) => $p->getEdaInfo()->getKicadFootprint(),
|
|
||||||
'visibility' => static fn (Part $p) => $p->getEdaInfo()->getVisibility(),
|
|
||||||
'exclude_from_bom' => static fn (Part $p) => $p->getEdaInfo()->getExcludeFromBom(),
|
|
||||||
'exclude_from_board' => static fn (Part $p) => $p->getEdaInfo()->getExcludeFromBoard(),
|
|
||||||
'exclude_from_sim' => static fn (Part $p) => $p->getEdaInfo()->getExcludeFromSim(),
|
|
||||||
];
|
|
||||||
|
|
||||||
$data = [];
|
|
||||||
foreach ($fields as $key => $getter) {
|
|
||||||
$values = array_map($getter, $parts);
|
|
||||||
$unique = array_unique($values, SORT_REGULAR);
|
|
||||||
if (count($unique) === 1) {
|
|
||||||
$data[$key] = $unique[array_key_first($unique)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/tools/batch_eda_edit', name: 'batch_eda_edit')]
|
|
||||||
public function batchEdaEdit(Request $request): Response
|
|
||||||
{
|
|
||||||
$this->denyAccessUnlessGranted('@parts.edit');
|
|
||||||
|
|
||||||
$ids = $request->query->getString('ids', '');
|
|
||||||
$redirectUrl = $request->query->getString('_redirect', '');
|
|
||||||
|
|
||||||
//Parse part IDs and load parts
|
|
||||||
$idArray = array_filter(array_map(intval(...), explode(',', $ids)), static fn (int $id): bool => $id > 0);
|
|
||||||
$parts = $this->entityManager->getRepository(Part::class)->findBy(['id' => $idArray]);
|
|
||||||
|
|
||||||
if ($parts === []) {
|
|
||||||
$this->addFlash('error', 'batch_eda.no_parts_selected');
|
|
||||||
|
|
||||||
return $redirectUrl !== '' ? $this->redirect($redirectUrl) : $this->redirectToRoute('parts_show_all');
|
|
||||||
}
|
|
||||||
|
|
||||||
//Pre-populate form with shared values (when all parts have the same value)
|
|
||||||
$initialData = $this->getSharedEdaValues($parts);
|
|
||||||
$form = $this->createForm(BatchEdaType::class, $initialData);
|
|
||||||
$form->handleRequest($request);
|
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
|
||||||
foreach ($parts as $part) {
|
|
||||||
$this->denyAccessUnlessGranted('edit', $part);
|
|
||||||
$edaInfo = $part->getEdaInfo();
|
|
||||||
|
|
||||||
if ($form->get('apply_reference_prefix')->getData()) {
|
|
||||||
$edaInfo->setReferencePrefix($form->get('reference_prefix')->getData() ?: null);
|
|
||||||
}
|
|
||||||
if ($form->get('apply_value')->getData()) {
|
|
||||||
$edaInfo->setValue($form->get('value')->getData() ?: null);
|
|
||||||
}
|
|
||||||
if ($form->get('apply_kicad_symbol')->getData()) {
|
|
||||||
$edaInfo->setKicadSymbol($form->get('kicad_symbol')->getData() ?: null);
|
|
||||||
}
|
|
||||||
if ($form->get('apply_kicad_footprint')->getData()) {
|
|
||||||
$edaInfo->setKicadFootprint($form->get('kicad_footprint')->getData() ?: null);
|
|
||||||
}
|
|
||||||
if ($form->get('apply_visibility')->getData()) {
|
|
||||||
$edaInfo->setVisibility($form->get('visibility')->getData());
|
|
||||||
}
|
|
||||||
if ($form->get('apply_exclude_from_bom')->getData()) {
|
|
||||||
$edaInfo->setExcludeFromBom($form->get('exclude_from_bom')->getData());
|
|
||||||
}
|
|
||||||
if ($form->get('apply_exclude_from_board')->getData()) {
|
|
||||||
$edaInfo->setExcludeFromBoard($form->get('exclude_from_board')->getData());
|
|
||||||
}
|
|
||||||
if ($form->get('apply_exclude_from_sim')->getData()) {
|
|
||||||
$edaInfo->setExcludeFromSim($form->get('exclude_from_sim')->getData());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->entityManager->flush();
|
|
||||||
$this->addFlash('success', 'batch_eda.success');
|
|
||||||
|
|
||||||
return $redirectUrl !== '' ? $this->redirect($redirectUrl) : $this->redirectToRoute('parts_show_all');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->render('parts/batch_eda_edit.html.twig', [
|
|
||||||
'form' => $form->createView(),
|
|
||||||
'parts' => $parts,
|
|
||||||
'redirect_url' => $redirectUrl,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -24,9 +24,9 @@ namespace App\Controller;
|
|||||||
|
|
||||||
use App\DataTables\LogDataTable;
|
use App\DataTables\LogDataTable;
|
||||||
use App\Entity\Parts\Part;
|
use App\Entity\Parts\Part;
|
||||||
|
use App\Services\Misc\GitVersionInfo;
|
||||||
use App\Services\System\BannerHelper;
|
use App\Services\System\BannerHelper;
|
||||||
use App\Services\System\GitVersionInfoProvider;
|
use App\Services\System\UpdateAvailableManager;
|
||||||
use App\Services\System\UpdateAvailableFacade;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Omines\DataTablesBundle\DataTableFactory;
|
use Omines\DataTablesBundle\DataTableFactory;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
@@ -43,8 +43,8 @@ class HomepageController extends AbstractController
|
|||||||
|
|
||||||
|
|
||||||
#[Route(path: '/', name: 'homepage')]
|
#[Route(path: '/', name: 'homepage')]
|
||||||
public function homepage(Request $request, GitVersionInfoProvider $versionInfo, EntityManagerInterface $entityManager,
|
public function homepage(Request $request, GitVersionInfo $versionInfo, EntityManagerInterface $entityManager,
|
||||||
UpdateAvailableFacade $updateAvailableManager): Response
|
UpdateAvailableManager $updateAvailableManager): Response
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('HAS_ACCESS_PERMISSIONS');
|
$this->denyAccessUnlessGranted('HAS_ACCESS_PERMISSIONS');
|
||||||
|
|
||||||
@@ -77,8 +77,8 @@ class HomepageController extends AbstractController
|
|||||||
|
|
||||||
return $this->render('homepage.html.twig', [
|
return $this->render('homepage.html.twig', [
|
||||||
'banner' => $this->bannerHelper->getBanner(),
|
'banner' => $this->bannerHelper->getBanner(),
|
||||||
'git_branch' => $versionInfo->getBranchName(),
|
'git_branch' => $versionInfo->getGitBranchName(),
|
||||||
'git_commit' => $versionInfo->getCommitHash(),
|
'git_commit' => $versionInfo->getGitCommitHash(),
|
||||||
'show_first_steps' => $show_first_steps,
|
'show_first_steps' => $show_first_steps,
|
||||||
'datatable' => $table,
|
'datatable' => $table,
|
||||||
'new_version_available' => $updateAvailableManager->isUpdateAvailable(),
|
'new_version_available' => $updateAvailableManager->isUpdateAvailable(),
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ use App\Form\InfoProviderSystem\PartSearchType;
|
|||||||
use App\Services\InfoProviderSystem\ExistingPartFinder;
|
use App\Services\InfoProviderSystem\ExistingPartFinder;
|
||||||
use App\Services\InfoProviderSystem\PartInfoRetriever;
|
use App\Services\InfoProviderSystem\PartInfoRetriever;
|
||||||
use App\Services\InfoProviderSystem\ProviderRegistry;
|
use App\Services\InfoProviderSystem\ProviderRegistry;
|
||||||
use App\Services\InfoProviderSystem\Providers\GenericWebProvider;
|
|
||||||
use App\Settings\AppSettings;
|
use App\Settings\AppSettings;
|
||||||
use App\Settings\InfoProviderSystem\InfoProviderGeneralSettings;
|
use App\Settings\InfoProviderSystem\InfoProviderGeneralSettings;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
@@ -40,15 +39,11 @@ use Psr\Log\LoggerInterface;
|
|||||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\UrlType;
|
|
||||||
use Symfony\Component\HttpClient\Exception\ClientException;
|
use Symfony\Component\HttpClient\Exception\ClientException;
|
||||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
|
|
||||||
|
|
||||||
use function Symfony\Component\Translation\t;
|
use function Symfony\Component\Translation\t;
|
||||||
|
|
||||||
#[Route('/tools/info_providers')]
|
#[Route('/tools/info_providers')]
|
||||||
@@ -183,13 +178,6 @@ class InfoProviderController extends AbstractController
|
|||||||
$exceptionLogger->error('Error during info provider search: ' . $e->getMessage(), ['exception' => $e]);
|
$exceptionLogger->error('Error during info provider search: ' . $e->getMessage(), ['exception' => $e]);
|
||||||
} catch (OAuthReconnectRequiredException $e) {
|
} catch (OAuthReconnectRequiredException $e) {
|
||||||
$this->addFlash('error', t('info_providers.search.error.oauth_reconnect', ['%provider%' => $e->getProviderName()]));
|
$this->addFlash('error', t('info_providers.search.error.oauth_reconnect', ['%provider%' => $e->getProviderName()]));
|
||||||
} catch (TransportException $e) {
|
|
||||||
$this->addFlash('error', t('info_providers.search.error.transport_exception'));
|
|
||||||
$exceptionLogger->error('Transport error during info provider search: ' . $e->getMessage(), ['exception' => $e]);
|
|
||||||
} catch (\RuntimeException $e) {
|
|
||||||
$this->addFlash('error', t('info_providers.search.error.general_exception', ['%type%' => (new \ReflectionClass($e))->getShortName()]));
|
|
||||||
//Log the exception
|
|
||||||
$exceptionLogger->error('Error during info provider search: ' . $e->getMessage(), ['exception' => $e]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -210,58 +198,4 @@ class InfoProviderController extends AbstractController
|
|||||||
'update_target' => $update_target
|
'update_target' => $update_target
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/from_url', name: 'info_providers_from_url')]
|
|
||||||
public function fromURL(Request $request, GenericWebProvider $provider): Response
|
|
||||||
{
|
|
||||||
$this->denyAccessUnlessGranted('@info_providers.create_parts');
|
|
||||||
|
|
||||||
if (!$provider->isActive()) {
|
|
||||||
$this->addFlash('error', "Generic Web Provider is not active. Please enable it in the provider settings.");
|
|
||||||
return $this->redirectToRoute('info_providers_list');
|
|
||||||
}
|
|
||||||
|
|
||||||
$formBuilder = $this->createFormBuilder();
|
|
||||||
$formBuilder->add('url', UrlType::class, [
|
|
||||||
'label' => 'info_providers.from_url.url.label',
|
|
||||||
'required' => true,
|
|
||||||
]);
|
|
||||||
$formBuilder->add('submit', SubmitType::class, [
|
|
||||||
'label' => 'info_providers.search.submit',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$form = $formBuilder->getForm();
|
|
||||||
$form->handleRequest($request);
|
|
||||||
|
|
||||||
$partDetail = null;
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
|
||||||
//Try to retrieve the part detail from the given URL
|
|
||||||
$url = $form->get('url')->getData();
|
|
||||||
try {
|
|
||||||
$searchResult = $this->infoRetriever->searchByKeyword(
|
|
||||||
keyword: $url,
|
|
||||||
providers: [$provider]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (count($searchResult) === 0) {
|
|
||||||
$this->addFlash('warning', t('info_providers.from_url.no_part_found'));
|
|
||||||
} else {
|
|
||||||
$searchResult = $searchResult[0];
|
|
||||||
//Redirect to the part creation page with the found part detail
|
|
||||||
return $this->redirectToRoute('info_providers_create_part', [
|
|
||||||
'providerKey' => $searchResult->provider_key,
|
|
||||||
'providerId' => $searchResult->provider_id,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
} catch (ExceptionInterface $e) {
|
|
||||||
$this->addFlash('error', t('info_providers.search.error.general_exception', ['%type%' => (new \ReflectionClass($e))->getShortName()]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->render('info_providers/from_url/from_url.html.twig', [
|
|
||||||
'form' => $form,
|
|
||||||
'partDetail' => $partDetail,
|
|
||||||
]);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ use App\Entity\Parts\Category;
|
|||||||
use App\Entity\Parts\Part;
|
use App\Entity\Parts\Part;
|
||||||
use App\Services\EDA\KiCadHelper;
|
use App\Services\EDA\KiCadHelper;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
@@ -57,16 +55,15 @@ class KiCadApiController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/categories.json', name: 'kicad_api_categories')]
|
#[Route('/categories.json', name: 'kicad_api_categories')]
|
||||||
public function categories(Request $request): Response
|
public function categories(): Response
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('@categories.read');
|
$this->denyAccessUnlessGranted('@categories.read');
|
||||||
|
|
||||||
$data = $this->kiCADHelper->getCategories();
|
return $this->json($this->kiCADHelper->getCategories());
|
||||||
return $this->createCacheableJsonResponse($request, $data, 300);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/parts/category/{category}.json', name: 'kicad_api_category')]
|
#[Route('/parts/category/{category}.json', name: 'kicad_api_category')]
|
||||||
public function categoryParts(Request $request, ?Category $category): Response
|
public function categoryParts(?Category $category): Response
|
||||||
{
|
{
|
||||||
if ($category !== null) {
|
if ($category !== null) {
|
||||||
$this->denyAccessUnlessGranted('read', $category);
|
$this->denyAccessUnlessGranted('read', $category);
|
||||||
@@ -75,31 +72,14 @@ class KiCadApiController extends AbstractController
|
|||||||
}
|
}
|
||||||
$this->denyAccessUnlessGranted('@parts.read');
|
$this->denyAccessUnlessGranted('@parts.read');
|
||||||
|
|
||||||
$minimal = $request->query->getBoolean('minimal', false);
|
return $this->json($this->kiCADHelper->getCategoryParts($category));
|
||||||
$data = $this->kiCADHelper->getCategoryParts($category, $minimal);
|
|
||||||
return $this->createCacheableJsonResponse($request, $data, 300);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/parts/{part}.json', name: 'kicad_api_part')]
|
#[Route('/parts/{part}.json', name: 'kicad_api_part')]
|
||||||
public function partDetails(Request $request, Part $part): Response
|
public function partDetails(Part $part): Response
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('read', $part);
|
$this->denyAccessUnlessGranted('read', $part);
|
||||||
|
|
||||||
$data = $this->kiCADHelper->getKiCADPart($part);
|
return $this->json($this->kiCADHelper->getKiCADPart($part));
|
||||||
return $this->createCacheableJsonResponse($request, $data, 60);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a JSON response with HTTP cache headers (ETag and Cache-Control).
|
|
||||||
* Returns 304 Not Modified if the client's ETag matches.
|
|
||||||
*/
|
|
||||||
private function createCacheableJsonResponse(Request $request, array $data, int $maxAge): Response
|
|
||||||
{
|
|
||||||
$response = new JsonResponse($data);
|
|
||||||
$response->setEtag(md5(json_encode($data)));
|
|
||||||
$response->headers->set('Cache-Control', 'private, max-age=' . $maxAge);
|
|
||||||
$response->isNotModified($request);
|
|
||||||
|
|
||||||
return $response;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
use App\Entity\InfoProviderSystem\BulkInfoProviderImportJob;
|
|
||||||
use App\DataTables\LogDataTable;
|
use App\DataTables\LogDataTable;
|
||||||
use App\Entity\Attachments\AttachmentUpload;
|
use App\Entity\Attachments\AttachmentUpload;
|
||||||
use App\Entity\Parts\Category;
|
use App\Entity\Parts\Category;
|
||||||
@@ -55,14 +54,12 @@ use Exception;
|
|||||||
use Omines\DataTablesBundle\DataTableFactory;
|
use Omines\DataTablesBundle\DataTableFactory;
|
||||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\ExpressionLanguage\Expression;
|
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||||
use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid;
|
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
use function Symfony\Component\Translation\t;
|
use function Symfony\Component\Translation\t;
|
||||||
@@ -138,7 +135,6 @@ final class PartController extends AbstractController
|
|||||||
'description_params' => $this->partInfoSettings->extractParamsFromDescription ? $parameterExtractor->extractParameters($part->getDescription()) : [],
|
'description_params' => $this->partInfoSettings->extractParamsFromDescription ? $parameterExtractor->extractParameters($part->getDescription()) : [],
|
||||||
'comment_params' => $this->partInfoSettings->extractParamsFromNotes ? $parameterExtractor->extractParameters($part->getComment()) : [],
|
'comment_params' => $this->partInfoSettings->extractParamsFromNotes ? $parameterExtractor->extractParameters($part->getComment()) : [],
|
||||||
'withdraw_add_helper' => $withdrawAddHelper,
|
'withdraw_add_helper' => $withdrawAddHelper,
|
||||||
'highlightLotId' => $request->query->getInt('highlightLot', 0),
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -152,7 +148,7 @@ final class PartController extends AbstractController
|
|||||||
$jobId = $request->query->get('jobId');
|
$jobId = $request->query->get('jobId');
|
||||||
$bulkJob = null;
|
$bulkJob = null;
|
||||||
if ($jobId) {
|
if ($jobId) {
|
||||||
$bulkJob = $this->em->getRepository(BulkInfoProviderImportJob::class)->find($jobId);
|
$bulkJob = $this->em->getRepository(\App\Entity\InfoProviderSystem\BulkInfoProviderImportJob::class)->find($jobId);
|
||||||
// Verify user owns this job
|
// Verify user owns this job
|
||||||
if ($bulkJob && $bulkJob->getCreatedBy() !== $this->getUser()) {
|
if ($bulkJob && $bulkJob->getCreatedBy() !== $this->getUser()) {
|
||||||
$bulkJob = null;
|
$bulkJob = null;
|
||||||
@@ -173,7 +169,7 @@ final class PartController extends AbstractController
|
|||||||
throw $this->createAccessDeniedException('Invalid CSRF token');
|
throw $this->createAccessDeniedException('Invalid CSRF token');
|
||||||
}
|
}
|
||||||
|
|
||||||
$bulkJob = $this->em->getRepository(BulkInfoProviderImportJob::class)->find($jobId);
|
$bulkJob = $this->em->getRepository(\App\Entity\InfoProviderSystem\BulkInfoProviderImportJob::class)->find($jobId);
|
||||||
if (!$bulkJob || $bulkJob->getCreatedBy() !== $this->getUser()) {
|
if (!$bulkJob || $bulkJob->getCreatedBy() !== $this->getUser()) {
|
||||||
throw $this->createNotFoundException('Bulk import job not found');
|
throw $this->createNotFoundException('Bulk import job not found');
|
||||||
}
|
}
|
||||||
@@ -339,7 +335,7 @@ final class PartController extends AbstractController
|
|||||||
$jobId = $request->query->get('jobId');
|
$jobId = $request->query->get('jobId');
|
||||||
$bulkJob = null;
|
$bulkJob = null;
|
||||||
if ($jobId) {
|
if ($jobId) {
|
||||||
$bulkJob = $this->em->getRepository(BulkInfoProviderImportJob::class)->find($jobId);
|
$bulkJob = $this->em->getRepository(\App\Entity\InfoProviderSystem\BulkInfoProviderImportJob::class)->find($jobId);
|
||||||
// Verify user owns this job
|
// Verify user owns this job
|
||||||
if ($bulkJob && $bulkJob->getCreatedBy() !== $this->getUser()) {
|
if ($bulkJob && $bulkJob->getCreatedBy() !== $this->getUser()) {
|
||||||
$bulkJob = null;
|
$bulkJob = null;
|
||||||
@@ -466,54 +462,6 @@ final class PartController extends AbstractController
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route(path: '/{id}/stocktake', name: 'part_stocktake', methods: ['POST'])]
|
|
||||||
#[IsCsrfTokenValid(new Expression("'part_stocktake-' ~ args['part'].getid()"), '_token')]
|
|
||||||
public function stocktakeHandler(Part $part, EntityManagerInterface $em, PartLotWithdrawAddHelper $withdrawAddHelper,
|
|
||||||
Request $request,
|
|
||||||
): Response
|
|
||||||
{
|
|
||||||
$partLot = $em->find(PartLot::class, $request->request->get('lot_id'));
|
|
||||||
|
|
||||||
//Check that the user is allowed to stocktake the partlot
|
|
||||||
$this->denyAccessUnlessGranted('stocktake', $partLot);
|
|
||||||
|
|
||||||
if (!$partLot instanceof PartLot) {
|
|
||||||
throw new \RuntimeException('Part lot not found!');
|
|
||||||
}
|
|
||||||
//Ensure that the partlot belongs to the part
|
|
||||||
if ($partLot->getPart() !== $part) {
|
|
||||||
throw new \RuntimeException("The origin partlot does not belong to the part!");
|
|
||||||
}
|
|
||||||
|
|
||||||
$actualAmount = (float) $request->request->get('actual_amount');
|
|
||||||
$comment = $request->request->get('comment');
|
|
||||||
|
|
||||||
$timestamp = null;
|
|
||||||
$timestamp_str = $request->request->getString('timestamp', '');
|
|
||||||
//Try to parse the timestamp
|
|
||||||
if ($timestamp_str !== '') {
|
|
||||||
$timestamp = new DateTime($timestamp_str);
|
|
||||||
}
|
|
||||||
|
|
||||||
$withdrawAddHelper->stocktake($partLot, $actualAmount, $comment, $timestamp);
|
|
||||||
|
|
||||||
//Ensure that the timestamp is not in the future
|
|
||||||
if ($timestamp !== null && $timestamp > new DateTime("+20min")) {
|
|
||||||
throw new \LogicException("The timestamp must not be in the future!");
|
|
||||||
}
|
|
||||||
|
|
||||||
//Save the changes to the DB
|
|
||||||
$em->flush();
|
|
||||||
$this->addFlash('success', 'part.withdraw.success');
|
|
||||||
|
|
||||||
//If a redirect was passed, then redirect there
|
|
||||||
if ($request->request->get('_redirect')) {
|
|
||||||
return $this->redirect($request->request->get('_redirect'));
|
|
||||||
}
|
|
||||||
//Otherwise just redirect to the part page
|
|
||||||
return $this->redirectToRoute('part_info', ['id' => $part->getID()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route(path: '/{id}/add_withdraw', name: 'part_add_withdraw', methods: ['POST'])]
|
#[Route(path: '/{id}/add_withdraw', name: 'part_add_withdraw', methods: ['POST'])]
|
||||||
public function withdrawAddHandler(Part $part, Request $request, EntityManagerInterface $em, PartLotWithdrawAddHelper $withdrawAddHelper): Response
|
public function withdrawAddHandler(Part $part, Request $request, EntityManagerInterface $em, PartLotWithdrawAddHelper $withdrawAddHelper): Response
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -41,16 +41,11 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
use App\Exceptions\InfoProviderNotActiveException;
|
|
||||||
use App\Form\LabelSystem\ScanDialogType;
|
use App\Form\LabelSystem\ScanDialogType;
|
||||||
use App\Services\InfoProviderSystem\Providers\LCSCProvider;
|
use App\Services\LabelSystem\BarcodeScanner\BarcodeRedirector;
|
||||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeScanResultHandler;
|
|
||||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeScanHelper;
|
use App\Services\LabelSystem\BarcodeScanner\BarcodeScanHelper;
|
||||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeScanResultInterface;
|
|
||||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType;
|
use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType;
|
||||||
use App\Services\LabelSystem\BarcodeScanner\LocalBarcodeScanResult;
|
use App\Services\LabelSystem\BarcodeScanner\LocalBarcodeScanResult;
|
||||||
use App\Services\LabelSystem\BarcodeScanner\LCSCBarcodeScanResult;
|
|
||||||
use App\Services\LabelSystem\BarcodeScanner\EIGP114BarcodeScanResult;
|
|
||||||
use Doctrine\ORM\EntityNotFoundException;
|
use Doctrine\ORM\EntityNotFoundException;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
@@ -58,13 +53,6 @@ use Symfony\Component\HttpFoundation\Request;
|
|||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
|
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
use App\Services\InfoProviderSystem\PartInfoRetriever;
|
|
||||||
use App\Services\InfoProviderSystem\ProviderRegistry;
|
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
|
||||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
|
||||||
use App\Entity\Parts\Part;
|
|
||||||
use \App\Entity\Parts\StorageLocation;
|
|
||||||
use Symfony\UX\Turbo\TurboBundle;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Tests\Controller\ScanControllerTest
|
* @see \App\Tests\Controller\ScanControllerTest
|
||||||
@@ -72,10 +60,9 @@ use Symfony\UX\Turbo\TurboBundle;
|
|||||||
#[Route(path: '/scan')]
|
#[Route(path: '/scan')]
|
||||||
class ScanController extends AbstractController
|
class ScanController extends AbstractController
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(protected BarcodeRedirector $barcodeParser, protected BarcodeScanHelper $barcodeNormalizer)
|
||||||
protected BarcodeScanResultHandler $resultHandler,
|
{
|
||||||
protected BarcodeScanHelper $barcodeNormalizer,
|
}
|
||||||
) {}
|
|
||||||
|
|
||||||
#[Route(path: '', name: 'scan_dialog')]
|
#[Route(path: '', name: 'scan_dialog')]
|
||||||
public function dialog(Request $request, #[MapQueryParameter] ?string $input = null): Response
|
public function dialog(Request $request, #[MapQueryParameter] ?string $input = null): Response
|
||||||
@@ -85,86 +72,35 @@ class ScanController extends AbstractController
|
|||||||
$form = $this->createForm(ScanDialogType::class);
|
$form = $this->createForm(ScanDialogType::class);
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
|
|
||||||
// If JS is working, scanning uses /scan/lookup and this action just renders the page.
|
|
||||||
// This fallback only runs if user submits the form manually or uses ?input=...
|
|
||||||
if ($input === null && $form->isSubmitted() && $form->isValid()) {
|
if ($input === null && $form->isSubmitted() && $form->isValid()) {
|
||||||
$input = $form['input']->getData();
|
$input = $form['input']->getData();
|
||||||
|
$mode = $form['mode']->getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$infoModeData = null;
|
||||||
|
|
||||||
if ($input !== null && $input !== '') {
|
if ($input !== null) {
|
||||||
$mode = $form->isSubmitted() ? $form['mode']->getData() : null;
|
|
||||||
$infoMode = $form->isSubmitted() && $form['info_mode']->getData();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$scan = $this->barcodeNormalizer->scanBarcodeContent($input, $mode ?? null);
|
$scan_result = $this->barcodeNormalizer->scanBarcodeContent($input, $mode ?? null);
|
||||||
|
//Perform a redirect if the info mode is not enabled
|
||||||
// If not in info mode, mimic “normal scan” behavior: redirect if possible.
|
if (!$form['info_mode']->getData()) {
|
||||||
if (!$infoMode) {
|
try {
|
||||||
|
return $this->redirect($this->barcodeParser->getRedirectURL($scan_result));
|
||||||
// Try to get an Info URL if possible
|
} catch (EntityNotFoundException) {
|
||||||
$url = $this->resultHandler->getInfoURL($scan);
|
$this->addFlash('success', 'scan.qr_not_found');
|
||||||
if ($url !== null) {
|
|
||||||
return $this->redirect($url);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Try to get an creation URL if possible (only for vendor codes)
|
|
||||||
$createUrl = $this->buildCreateUrlForScanResult($scan);
|
|
||||||
if ($createUrl !== null) {
|
|
||||||
return $this->redirect($createUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
//// Otherwise: show “not found” (not “format unknown”)
|
|
||||||
$this->addFlash('warning', 'scan.qr_not_found');
|
|
||||||
} else { // Info mode
|
|
||||||
// Info mode fallback: render page with prefilled result
|
|
||||||
$decoded = $scan->getDecodedForInfoMode();
|
|
||||||
|
|
||||||
//Try to resolve to an entity, to enhance info mode with entity-specific data
|
|
||||||
$dbEntity = $this->resultHandler->resolveEntity($scan);
|
|
||||||
$resolvedPart = $this->resultHandler->resolvePart($scan);
|
|
||||||
$openUrl = $this->resultHandler->getInfoURL($scan);
|
|
||||||
|
|
||||||
//If no entity is found, try to create an URL for creating a new part (only for vendor codes)
|
|
||||||
$createUrl = null;
|
|
||||||
if ($dbEntity === null) {
|
|
||||||
$createUrl = $this->buildCreateUrlForScanResult($scan);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TurboBundle::STREAM_FORMAT === $request->getPreferredFormat()) {
|
|
||||||
$request->setRequestFormat(TurboBundle::STREAM_FORMAT);
|
|
||||||
return $this->renderBlock('label_system/scanner/scanner.html.twig', 'scan_results', [
|
|
||||||
'decoded' => $decoded,
|
|
||||||
'entity' => $dbEntity,
|
|
||||||
'part' => $resolvedPart,
|
|
||||||
'openUrl' => $openUrl,
|
|
||||||
'createUrl' => $createUrl,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
} else { //Otherwise retrieve infoModeData
|
||||||
|
$infoModeData = $scan_result->getDecodedForInfoMode();
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (InvalidArgumentException) {
|
||||||
// Keep fallback user-friendly; avoid 500
|
$this->addFlash('error', 'scan.format_unknown');
|
||||||
$this->addFlash('warning', 'scan.format_unknown');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//When we reach here, only the flash messages are relevant, so if it's a Turbo request, only send the flash message fragment, so the client can show it without a full page reload
|
|
||||||
if (TurboBundle::STREAM_FORMAT === $request->getPreferredFormat()) {
|
|
||||||
$request->setRequestFormat(TurboBundle::STREAM_FORMAT);
|
|
||||||
//Only send our flash message, so the client can show it without a full page reload
|
|
||||||
return $this->renderBlock('_turbo_control.html.twig', 'flashes');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->render('label_system/scanner/scanner.html.twig', [
|
return $this->render('label_system/scanner/scanner.html.twig', [
|
||||||
'form' => $form,
|
'form' => $form,
|
||||||
|
'infoModeData' => $infoModeData,
|
||||||
//Info mode
|
|
||||||
'decoded' => $decoded ?? null,
|
|
||||||
'entity' => $dbEntity ?? null,
|
|
||||||
'part' => $resolvedPart ?? null,
|
|
||||||
'openUrl' => $openUrl ?? null,
|
|
||||||
'createUrl' => $createUrl ?? null,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,30 +125,11 @@ class ScanController extends AbstractController
|
|||||||
source_type: BarcodeSourceType::INTERNAL
|
source_type: BarcodeSourceType::INTERNAL
|
||||||
);
|
);
|
||||||
|
|
||||||
return $this->redirect($this->resultHandler->getInfoURL($scan_result) ?? throw new EntityNotFoundException("Not found"));
|
return $this->redirect($this->barcodeParser->getRedirectURL($scan_result));
|
||||||
} catch (EntityNotFoundException) {
|
} catch (EntityNotFoundException) {
|
||||||
$this->addFlash('success', 'scan.qr_not_found');
|
$this->addFlash('success', 'scan.qr_not_found');
|
||||||
|
|
||||||
return $this->redirectToRoute('homepage');
|
return $this->redirectToRoute('homepage');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a URL for creating a new part based on the barcode data, handles exceptions and shows user-friendly error messages if the provider is not active or if there is an error during URL generation.
|
|
||||||
* @param BarcodeScanResultInterface $scanResult
|
|
||||||
* @return string|null
|
|
||||||
*/
|
|
||||||
private function buildCreateUrlForScanResult(BarcodeScanResultInterface $scanResult): ?string
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
return $this->resultHandler->getCreationURL($scanResult);
|
|
||||||
} catch (InfoProviderNotActiveException $e) {
|
|
||||||
$this->addFlash('error', $e->getMessage());
|
|
||||||
} catch (\Throwable) {
|
|
||||||
// Don’t break scanning UX if provider lookup fails
|
|
||||||
$this->addFlash('error', 'An error occurred while looking up the provider for this barcode. Please try again later.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,7 +147,10 @@ class SecurityController extends AbstractController
|
|||||||
'label' => 'user.settings.pw_confirm.label',
|
'label' => 'user.settings.pw_confirm.label',
|
||||||
],
|
],
|
||||||
'invalid_message' => 'password_must_match',
|
'invalid_message' => 'password_must_match',
|
||||||
'constraints' => [new Length(min: 6, max: 128)],
|
'constraints' => [new Length([
|
||||||
|
'min' => 6,
|
||||||
|
'max' => 128,
|
||||||
|
])],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$builder->add('submit', SubmitType::class, [
|
$builder->add('submit', SubmitType::class, [
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ use App\Services\Attachments\AttachmentURLGenerator;
|
|||||||
use App\Services\Attachments\BuiltinAttachmentsFinder;
|
use App\Services\Attachments\BuiltinAttachmentsFinder;
|
||||||
use App\Services\Doctrine\DBInfoHelper;
|
use App\Services\Doctrine\DBInfoHelper;
|
||||||
use App\Services\Doctrine\NatsortDebugHelper;
|
use App\Services\Doctrine\NatsortDebugHelper;
|
||||||
use App\Services\System\GitVersionInfoProvider;
|
use App\Services\Misc\GitVersionInfo;
|
||||||
use App\Services\System\UpdateAvailableFacade;
|
use App\Services\System\UpdateAvailableManager;
|
||||||
use App\Settings\AppSettings;
|
use App\Settings\AppSettings;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
@@ -47,16 +47,16 @@ class ToolsController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[Route(path: '/server_infos', name: 'tools_server_infos')]
|
#[Route(path: '/server_infos', name: 'tools_server_infos')]
|
||||||
public function systemInfos(GitVersionInfoProvider $versionInfo, DBInfoHelper $DBInfoHelper, NatsortDebugHelper $natsortDebugHelper,
|
public function systemInfos(GitVersionInfo $versionInfo, DBInfoHelper $DBInfoHelper, NatsortDebugHelper $natsortDebugHelper,
|
||||||
AttachmentSubmitHandler $attachmentSubmitHandler, UpdateAvailableFacade $updateAvailableManager,
|
AttachmentSubmitHandler $attachmentSubmitHandler, UpdateAvailableManager $updateAvailableManager,
|
||||||
AppSettings $settings): Response
|
AppSettings $settings): Response
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('@system.server_infos');
|
$this->denyAccessUnlessGranted('@system.server_infos');
|
||||||
|
|
||||||
return $this->render('tools/server_infos/server_infos.html.twig', [
|
return $this->render('tools/server_infos/server_infos.html.twig', [
|
||||||
//Part-DB section
|
//Part-DB section
|
||||||
'git_branch' => $versionInfo->getBranchName(),
|
'git_branch' => $versionInfo->getGitBranchName(),
|
||||||
'git_commit' => $versionInfo->getCommitHash(),
|
'git_commit' => $versionInfo->getGitCommitHash(),
|
||||||
'default_locale' => $settings->system->localization->locale,
|
'default_locale' => $settings->system->localization->locale,
|
||||||
'default_timezone' => $settings->system->localization->timezone,
|
'default_timezone' => $settings->system->localization->timezone,
|
||||||
'default_currency' => $settings->system->localization->baseCurrency,
|
'default_currency' => $settings->system->localization->baseCurrency,
|
||||||
|
|||||||
@@ -1,371 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*
|
|
||||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 - 2024 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
|
|
||||||
namespace App\Controller;
|
|
||||||
|
|
||||||
use App\Services\System\BackupManager;
|
|
||||||
use App\Services\System\UpdateChecker;
|
|
||||||
use App\Services\System\UpdateExecutor;
|
|
||||||
use Shivas\VersioningBundle\Service\VersionManagerInterface;
|
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Controller for the Update Manager web interface.
|
|
||||||
*
|
|
||||||
* This provides a read-only view of update status and instructions.
|
|
||||||
* Actual updates should be performed via the CLI command for safety.
|
|
||||||
*/
|
|
||||||
#[Route('/system/update-manager')]
|
|
||||||
class UpdateManagerController extends AbstractController
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly UpdateChecker $updateChecker,
|
|
||||||
private readonly UpdateExecutor $updateExecutor,
|
|
||||||
private readonly VersionManagerInterface $versionManager,
|
|
||||||
private readonly BackupManager $backupManager,
|
|
||||||
#[Autowire(env: 'bool:DISABLE_WEB_UPDATES')]
|
|
||||||
private readonly bool $webUpdatesDisabled = false,
|
|
||||||
#[Autowire(env: 'bool:DISABLE_BACKUP_RESTORE')]
|
|
||||||
private readonly bool $backupRestoreDisabled = false,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if web updates are disabled and throw exception if so.
|
|
||||||
*/
|
|
||||||
private function denyIfWebUpdatesDisabled(): void
|
|
||||||
{
|
|
||||||
if ($this->webUpdatesDisabled) {
|
|
||||||
throw new AccessDeniedHttpException('Web-based updates are disabled by server configuration. Please use the CLI command instead.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if backup restore is disabled and throw exception if so.
|
|
||||||
*/
|
|
||||||
private function denyIfBackupRestoreDisabled(): void
|
|
||||||
{
|
|
||||||
if ($this->backupRestoreDisabled) {
|
|
||||||
throw new AccessDeniedHttpException('Backup restore is disabled by server configuration.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main update manager page.
|
|
||||||
*/
|
|
||||||
#[Route('', name: 'admin_update_manager', methods: ['GET'])]
|
|
||||||
public function index(): Response
|
|
||||||
{
|
|
||||||
$this->denyAccessUnlessGranted('@system.show_updates');
|
|
||||||
|
|
||||||
$status = $this->updateChecker->getUpdateStatus();
|
|
||||||
$availableUpdates = $this->updateChecker->getAvailableUpdates();
|
|
||||||
$validation = $this->updateExecutor->validateUpdatePreconditions();
|
|
||||||
|
|
||||||
return $this->render('admin/update_manager/index.html.twig', [
|
|
||||||
'status' => $status,
|
|
||||||
'available_updates' => $availableUpdates,
|
|
||||||
'all_releases' => $this->updateChecker->getAvailableReleases(10),
|
|
||||||
'validation' => $validation,
|
|
||||||
'is_locked' => $this->updateExecutor->isLocked(),
|
|
||||||
'lock_info' => $this->updateExecutor->getLockInfo(),
|
|
||||||
'is_maintenance' => $this->updateExecutor->isMaintenanceMode(),
|
|
||||||
'maintenance_info' => $this->updateExecutor->getMaintenanceInfo(),
|
|
||||||
'update_logs' => $this->updateExecutor->getUpdateLogs(),
|
|
||||||
'backups' => $this->backupManager->getBackups(),
|
|
||||||
'web_updates_disabled' => $this->webUpdatesDisabled,
|
|
||||||
'backup_restore_disabled' => $this->backupRestoreDisabled,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AJAX endpoint to check update status.
|
|
||||||
*/
|
|
||||||
#[Route('/status', name: 'admin_update_manager_status', methods: ['GET'])]
|
|
||||||
public function status(): JsonResponse
|
|
||||||
{
|
|
||||||
$this->denyAccessUnlessGranted('@system.show_updates');
|
|
||||||
|
|
||||||
return $this->json([
|
|
||||||
'status' => $this->updateChecker->getUpdateStatus(),
|
|
||||||
'is_locked' => $this->updateExecutor->isLocked(),
|
|
||||||
'is_maintenance' => $this->updateExecutor->isMaintenanceMode(),
|
|
||||||
'lock_info' => $this->updateExecutor->getLockInfo(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AJAX endpoint to refresh version information.
|
|
||||||
*/
|
|
||||||
#[Route('/refresh', name: 'admin_update_manager_refresh', methods: ['POST'])]
|
|
||||||
public function refresh(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
$this->denyAccessUnlessGranted('@system.show_updates');
|
|
||||||
|
|
||||||
// Validate CSRF token
|
|
||||||
if (!$this->isCsrfTokenValid('update_manager_refresh', $request->request->get('_token'))) {
|
|
||||||
return $this->json(['error' => 'Invalid CSRF token'], Response::HTTP_FORBIDDEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->updateChecker->refreshVersionInfo();
|
|
||||||
|
|
||||||
return $this->json([
|
|
||||||
'success' => true,
|
|
||||||
'status' => $this->updateChecker->getUpdateStatus(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* View release notes for a specific version.
|
|
||||||
*/
|
|
||||||
#[Route('/release/{tag}', name: 'admin_update_manager_release', methods: ['GET'])]
|
|
||||||
public function releaseNotes(string $tag): Response
|
|
||||||
{
|
|
||||||
$this->denyAccessUnlessGranted('@system.show_updates');
|
|
||||||
|
|
||||||
$releases = $this->updateChecker->getAvailableReleases(20);
|
|
||||||
$release = null;
|
|
||||||
|
|
||||||
foreach ($releases as $r) {
|
|
||||||
if ($r['tag'] === $tag) {
|
|
||||||
$release = $r;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$release) {
|
|
||||||
throw $this->createNotFoundException('Release not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->render('admin/update_manager/release_notes.html.twig', [
|
|
||||||
'release' => $release,
|
|
||||||
'current_version' => $this->updateChecker->getCurrentVersionString(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* View an update log file.
|
|
||||||
*/
|
|
||||||
#[Route('/log/{filename}', name: 'admin_update_manager_log', methods: ['GET'])]
|
|
||||||
public function viewLog(string $filename): Response
|
|
||||||
{
|
|
||||||
$this->denyAccessUnlessGranted('@system.manage_updates');
|
|
||||||
|
|
||||||
// Security: Only allow viewing files from the update logs directory
|
|
||||||
$logs = $this->updateExecutor->getUpdateLogs();
|
|
||||||
$logPath = null;
|
|
||||||
|
|
||||||
foreach ($logs as $log) {
|
|
||||||
if ($log['file'] === $filename) {
|
|
||||||
$logPath = $log['path'];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$logPath || !file_exists($logPath)) {
|
|
||||||
throw $this->createNotFoundException('Log file not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
$content = file_get_contents($logPath);
|
|
||||||
|
|
||||||
return $this->render('admin/update_manager/log_viewer.html.twig', [
|
|
||||||
'filename' => $filename,
|
|
||||||
'content' => $content,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start an update process.
|
|
||||||
*/
|
|
||||||
#[Route('/start', name: 'admin_update_manager_start', methods: ['POST'])]
|
|
||||||
public function startUpdate(Request $request): Response
|
|
||||||
{
|
|
||||||
$this->denyAccessUnlessGranted('@system.manage_updates');
|
|
||||||
$this->denyIfWebUpdatesDisabled();
|
|
||||||
|
|
||||||
// Validate CSRF token
|
|
||||||
if (!$this->isCsrfTokenValid('update_manager_start', $request->request->get('_token'))) {
|
|
||||||
$this->addFlash('error', 'Invalid CSRF token');
|
|
||||||
return $this->redirectToRoute('admin_update_manager');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if update is already running
|
|
||||||
if ($this->updateExecutor->isLocked() || $this->updateExecutor->isUpdateRunning()) {
|
|
||||||
$this->addFlash('error', 'An update is already in progress.');
|
|
||||||
return $this->redirectToRoute('admin_update_manager');
|
|
||||||
}
|
|
||||||
|
|
||||||
$targetVersion = $request->request->get('version');
|
|
||||||
$createBackup = $request->request->getBoolean('backup', true);
|
|
||||||
|
|
||||||
if (!$targetVersion) {
|
|
||||||
// Get latest version if not specified
|
|
||||||
$latest = $this->updateChecker->getLatestVersion();
|
|
||||||
if (!$latest) {
|
|
||||||
$this->addFlash('error', 'Could not determine target version.');
|
|
||||||
return $this->redirectToRoute('admin_update_manager');
|
|
||||||
}
|
|
||||||
$targetVersion = $latest['tag'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate preconditions
|
|
||||||
$validation = $this->updateExecutor->validateUpdatePreconditions();
|
|
||||||
if (!$validation['valid']) {
|
|
||||||
$this->addFlash('error', implode(' ', $validation['errors']));
|
|
||||||
return $this->redirectToRoute('admin_update_manager');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the background update
|
|
||||||
$pid = $this->updateExecutor->startBackgroundUpdate($targetVersion, $createBackup);
|
|
||||||
|
|
||||||
if (!$pid) {
|
|
||||||
$this->addFlash('error', 'Failed to start update process.');
|
|
||||||
return $this->redirectToRoute('admin_update_manager');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect to progress page
|
|
||||||
return $this->redirectToRoute('admin_update_manager_progress');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update progress page.
|
|
||||||
*/
|
|
||||||
#[Route('/progress', name: 'admin_update_manager_progress', methods: ['GET'])]
|
|
||||||
public function progress(): Response
|
|
||||||
{
|
|
||||||
$this->denyAccessUnlessGranted('@system.manage_updates');
|
|
||||||
|
|
||||||
$progress = $this->updateExecutor->getProgress();
|
|
||||||
$currentVersion = $this->versionManager->getVersion()->toString();
|
|
||||||
|
|
||||||
// Determine if this is a downgrade
|
|
||||||
$isDowngrade = false;
|
|
||||||
if ($progress && isset($progress['target_version'])) {
|
|
||||||
$targetVersion = ltrim($progress['target_version'], 'v');
|
|
||||||
$isDowngrade = version_compare($targetVersion, $currentVersion, '<');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->render('admin/update_manager/progress.html.twig', [
|
|
||||||
'progress' => $progress,
|
|
||||||
'is_locked' => $this->updateExecutor->isLocked(),
|
|
||||||
'is_maintenance' => $this->updateExecutor->isMaintenanceMode(),
|
|
||||||
'is_downgrade' => $isDowngrade,
|
|
||||||
'current_version' => $currentVersion,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AJAX endpoint to get update progress.
|
|
||||||
*/
|
|
||||||
#[Route('/progress/status', name: 'admin_update_manager_progress_status', methods: ['GET'])]
|
|
||||||
public function progressStatus(): JsonResponse
|
|
||||||
{
|
|
||||||
$this->denyAccessUnlessGranted('@system.show_updates');
|
|
||||||
|
|
||||||
$progress = $this->updateExecutor->getProgress();
|
|
||||||
|
|
||||||
return $this->json([
|
|
||||||
'progress' => $progress,
|
|
||||||
'is_locked' => $this->updateExecutor->isLocked(),
|
|
||||||
'is_maintenance' => $this->updateExecutor->isMaintenanceMode(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get backup details for restore confirmation.
|
|
||||||
*/
|
|
||||||
#[Route('/backup/{filename}', name: 'admin_update_manager_backup_details', methods: ['GET'])]
|
|
||||||
public function backupDetails(string $filename): JsonResponse
|
|
||||||
{
|
|
||||||
$this->denyAccessUnlessGranted('@system.manage_updates');
|
|
||||||
|
|
||||||
$details = $this->backupManager->getBackupDetails($filename);
|
|
||||||
|
|
||||||
if (!$details) {
|
|
||||||
return $this->json(['error' => 'Backup not found'], 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->json($details);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore from a backup.
|
|
||||||
*/
|
|
||||||
#[Route('/restore', name: 'admin_update_manager_restore', methods: ['POST'])]
|
|
||||||
public function restore(Request $request): Response
|
|
||||||
{
|
|
||||||
$this->denyAccessUnlessGranted('@system.manage_updates');
|
|
||||||
$this->denyIfBackupRestoreDisabled();
|
|
||||||
|
|
||||||
// Validate CSRF token
|
|
||||||
if (!$this->isCsrfTokenValid('update_manager_restore', $request->request->get('_token'))) {
|
|
||||||
$this->addFlash('error', 'Invalid CSRF token.');
|
|
||||||
return $this->redirectToRoute('admin_update_manager');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if already locked
|
|
||||||
if ($this->updateExecutor->isLocked()) {
|
|
||||||
$this->addFlash('error', 'An update or restore is already in progress.');
|
|
||||||
return $this->redirectToRoute('admin_update_manager');
|
|
||||||
}
|
|
||||||
|
|
||||||
$filename = $request->request->get('filename');
|
|
||||||
$restoreDatabase = $request->request->getBoolean('restore_database', true);
|
|
||||||
$restoreConfig = $request->request->getBoolean('restore_config', false);
|
|
||||||
$restoreAttachments = $request->request->getBoolean('restore_attachments', false);
|
|
||||||
|
|
||||||
if (!$filename) {
|
|
||||||
$this->addFlash('error', 'No backup file specified.');
|
|
||||||
return $this->redirectToRoute('admin_update_manager');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the backup exists
|
|
||||||
$backupDetails = $this->backupManager->getBackupDetails($filename);
|
|
||||||
if (!$backupDetails) {
|
|
||||||
$this->addFlash('error', 'Backup file not found.');
|
|
||||||
return $this->redirectToRoute('admin_update_manager');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute restore (this is a synchronous operation for now - could be made async later)
|
|
||||||
$result = $this->updateExecutor->restoreBackup(
|
|
||||||
$filename,
|
|
||||||
$restoreDatabase,
|
|
||||||
$restoreConfig,
|
|
||||||
$restoreAttachments
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($result['success']) {
|
|
||||||
$this->addFlash('success', 'Backup restored successfully.');
|
|
||||||
} else {
|
|
||||||
$this->addFlash('error', 'Restore failed: ' . ($result['error'] ?? 'Unknown error'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->redirectToRoute('admin_update_manager');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -295,7 +295,10 @@ class UserSettingsController extends AbstractController
|
|||||||
'autocomplete' => 'new-password',
|
'autocomplete' => 'new-password',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'constraints' => [new Length(min: 6, max: 128)],
|
'constraints' => [new Length([
|
||||||
|
'min' => 6,
|
||||||
|
'max' => 128,
|
||||||
|
])],
|
||||||
])
|
])
|
||||||
->add('submit', SubmitType::class, [
|
->add('submit', SubmitType::class, [
|
||||||
'label' => 'save',
|
'label' => 'save',
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ class PartFilter implements FilterInterface
|
|||||||
public readonly BooleanConstraint $favorite;
|
public readonly BooleanConstraint $favorite;
|
||||||
public readonly BooleanConstraint $needsReview;
|
public readonly BooleanConstraint $needsReview;
|
||||||
public readonly NumberConstraint $mass;
|
public readonly NumberConstraint $mass;
|
||||||
public readonly TextConstraint $gtin;
|
|
||||||
public readonly DateTimeConstraint $lastModified;
|
public readonly DateTimeConstraint $lastModified;
|
||||||
public readonly DateTimeConstraint $addedDate;
|
public readonly DateTimeConstraint $addedDate;
|
||||||
public readonly EntityConstraint $category;
|
public readonly EntityConstraint $category;
|
||||||
@@ -133,7 +132,6 @@ class PartFilter implements FilterInterface
|
|||||||
$this->measurementUnit = new EntityConstraint($nodesListBuilder, MeasurementUnit::class, 'part.partUnit');
|
$this->measurementUnit = new EntityConstraint($nodesListBuilder, MeasurementUnit::class, 'part.partUnit');
|
||||||
$this->partCustomState = new EntityConstraint($nodesListBuilder, PartCustomState::class, 'part.partCustomState');
|
$this->partCustomState = new EntityConstraint($nodesListBuilder, PartCustomState::class, 'part.partCustomState');
|
||||||
$this->mass = new NumberConstraint('part.mass');
|
$this->mass = new NumberConstraint('part.mass');
|
||||||
$this->gtin = new TextConstraint('part.gtin');
|
|
||||||
$this->dbId = new IntConstraint('part.id');
|
$this->dbId = new IntConstraint('part.id');
|
||||||
$this->ipn = new TextConstraint('part.ipn');
|
$this->ipn = new TextConstraint('part.ipn');
|
||||||
$this->addedDate = new DateTimeConstraint('part.addedDate');
|
$this->addedDate = new DateTimeConstraint('part.addedDate');
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ class PartSearchFilter implements FilterInterface
|
|||||||
if ($search_dbId) {
|
if ($search_dbId) {
|
||||||
$expressions[] = $queryBuilder->expr()->eq('part.id', ':id_exact');
|
$expressions[] = $queryBuilder->expr()->eq('part.id', ':id_exact');
|
||||||
$queryBuilder->setParameter('id_exact', (int) $this->keyword,
|
$queryBuilder->setParameter('id_exact', (int) $this->keyword,
|
||||||
ParameterType::INTEGER);
|
\Doctrine\DBAL\ParameterType::INTEGER);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Guard condition
|
//Guard condition
|
||||||
|
|||||||
@@ -115,61 +115,6 @@ class PartDataTableHelper
|
|||||||
return implode('<br>', $tmp);
|
return implode('<br>', $tmp);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders an EDA/KiCad completeness indicator for the given part.
|
|
||||||
* Shows icons for symbol, footprint, and value status.
|
|
||||||
*/
|
|
||||||
public function renderEdaStatus(Part $context): string
|
|
||||||
{
|
|
||||||
$edaInfo = $context->getEdaInfo();
|
|
||||||
$category = $context->getCategory();
|
|
||||||
$footprint = $context->getFootprint();
|
|
||||||
|
|
||||||
// Determine effective values (direct or inherited)
|
|
||||||
$hasSymbol = $edaInfo->getKicadSymbol() !== null || $category?->getEdaInfo()->getKicadSymbol() !== null;
|
|
||||||
$hasFootprint = $edaInfo->getKicadFootprint() !== null || $footprint?->getEdaInfo()->getKicadFootprint() !== null;
|
|
||||||
$hasReference = $edaInfo->getReferencePrefix() !== null || $category?->getEdaInfo()->getReferencePrefix() !== null;
|
|
||||||
|
|
||||||
$symbolInherited = $edaInfo->getKicadSymbol() === null && $category?->getEdaInfo()->getKicadSymbol() !== null;
|
|
||||||
$footprintInherited = $edaInfo->getKicadFootprint() === null && $footprint?->getEdaInfo()->getKicadFootprint() !== null;
|
|
||||||
|
|
||||||
$icons = [];
|
|
||||||
|
|
||||||
// Symbol status
|
|
||||||
if ($hasSymbol) {
|
|
||||||
$title = $this->translator->trans('eda.status.symbol_set');
|
|
||||||
$class = $symbolInherited ? 'text-info' : 'text-success';
|
|
||||||
$icons[] = sprintf('<i class="fa-solid fa-microchip fa-fw %s" title="%s"></i>', $class, $title);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Footprint status
|
|
||||||
if ($hasFootprint) {
|
|
||||||
$title = $this->translator->trans('eda.status.footprint_set');
|
|
||||||
$class = $footprintInherited ? 'text-info' : 'text-success';
|
|
||||||
$icons[] = sprintf('<i class="fa-solid fa-stamp fa-fw %s" title="%s"></i>', $class, $title);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reference prefix status
|
|
||||||
if ($hasReference) {
|
|
||||||
$icons[] = sprintf('<i class="fa-solid fa-font fa-fw text-success" title="%s"></i>',
|
|
||||||
$this->translator->trans('eda.status.reference_set'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($icons)) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overall status: all 3 = green check, partial = yellow
|
|
||||||
$allSet = $hasSymbol && $hasFootprint && $hasReference;
|
|
||||||
$statusIcon = $allSet
|
|
||||||
? sprintf('<i class="fa-solid fa-bolt fa-fw text-success" title="%s"></i>', $this->translator->trans('eda.status.complete'))
|
|
||||||
: sprintf('<i class="fa-solid fa-bolt fa-fw text-warning" title="%s"></i>', $this->translator->trans('eda.status.partial'));
|
|
||||||
|
|
||||||
// Wrap in link to EDA settings tab (data-turbo=false to ensure hash is read on page load)
|
|
||||||
$editUrl = $this->entityURLGenerator->editURL($context) . '#eda';
|
|
||||||
return sprintf('<a href="%s" data-turbo="false">%s</a>', $editUrl, $statusIcon);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function renderAmount(Part $context): string
|
public function renderAmount(Part $context): string
|
||||||
{
|
{
|
||||||
$amount = $context->getAmountSum();
|
$amount = $context->getAmountSum();
|
||||||
|
|||||||
@@ -208,7 +208,6 @@ class LogDataTable implements DataTableTypeInterface
|
|||||||
|
|
||||||
$dataTable->add('extra', LogEntryExtraColumn::class, [
|
$dataTable->add('extra', LogEntryExtraColumn::class, [
|
||||||
'label' => 'log.extra',
|
'label' => 'log.extra',
|
||||||
'orderable' => false, //Sorting the JSON column makes no sense: MySQL/Sqlite does it via the string representation, PostgreSQL errors out
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$dataTable->add('timeTravel', IconLinkColumn::class, [
|
$dataTable->add('timeTravel', IconLinkColumn::class, [
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ use App\Services\EntityURLGenerator;
|
|||||||
use App\Services\Formatters\AmountFormatter;
|
use App\Services\Formatters\AmountFormatter;
|
||||||
use App\Settings\BehaviorSettings\TableSettings;
|
use App\Settings\BehaviorSettings\TableSettings;
|
||||||
use Doctrine\ORM\AbstractQuery;
|
use Doctrine\ORM\AbstractQuery;
|
||||||
use Doctrine\ORM\Query;
|
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider;
|
use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider;
|
||||||
use Omines\DataTablesBundle\Column\TextColumn;
|
use Omines\DataTablesBundle\Column\TextColumn;
|
||||||
@@ -89,10 +88,6 @@ final class PartsDataTable implements DataTableTypeInterface
|
|||||||
$this->configureOptions($resolver);
|
$this->configureOptions($resolver);
|
||||||
$options = $resolver->resolve($options);
|
$options = $resolver->resolve($options);
|
||||||
|
|
||||||
/*************************************************************************************************************
|
|
||||||
* When adding columns here, add them also to PartTableColumns enum, to make them configurable in the settings!
|
|
||||||
*************************************************************************************************************/
|
|
||||||
|
|
||||||
$this->csh
|
$this->csh
|
||||||
//Color the table rows depending on the review and favorite status
|
//Color the table rows depending on the review and favorite status
|
||||||
->add('row_color', RowClassColumn::class, [
|
->add('row_color', RowClassColumn::class, [
|
||||||
@@ -223,30 +218,11 @@ final class PartsDataTable implements DataTableTypeInterface
|
|||||||
'label' => $this->translator->trans('part.table.mass'),
|
'label' => $this->translator->trans('part.table.mass'),
|
||||||
'unit' => 'g'
|
'unit' => 'g'
|
||||||
])
|
])
|
||||||
->add('gtin', TextColumn::class, [
|
|
||||||
'label' => $this->translator->trans('part.table.gtin'),
|
|
||||||
'orderField' => 'NATSORT(part.gtin)'
|
|
||||||
])
|
|
||||||
->add('tags', TagsColumn::class, [
|
->add('tags', TagsColumn::class, [
|
||||||
'label' => $this->translator->trans('part.table.tags'),
|
'label' => $this->translator->trans('part.table.tags'),
|
||||||
])
|
])
|
||||||
->add('attachments', PartAttachmentsColumn::class, [
|
->add('attachments', PartAttachmentsColumn::class, [
|
||||||
'label' => $this->translator->trans('part.table.attachments'),
|
'label' => $this->translator->trans('part.table.attachments'),
|
||||||
])
|
|
||||||
->add('eda_reference', TextColumn::class, [
|
|
||||||
'label' => $this->translator->trans('part.table.eda_reference'),
|
|
||||||
'render' => static fn($value, Part $context) => htmlspecialchars($context->getEdaInfo()->getReferencePrefix() ?? ''),
|
|
||||||
'orderField' => 'NATSORT(part.eda_info.reference_prefix)'
|
|
||||||
])
|
|
||||||
->add('eda_value', TextColumn::class, [
|
|
||||||
'label' => $this->translator->trans('part.table.eda_value'),
|
|
||||||
'render' => static fn($value, Part $context) => htmlspecialchars($context->getEdaInfo()->getValue() ?? ''),
|
|
||||||
'orderField' => 'NATSORT(part.eda_info.value)'
|
|
||||||
])
|
|
||||||
->add('eda_status', TextColumn::class, [
|
|
||||||
'label' => $this->translator->trans('part.table.eda_status'),
|
|
||||||
'render' => fn($value, Part $context) => $this->partDataTableHelper->renderEdaStatus($context),
|
|
||||||
'className' => 'text-center',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
//Add a column to list the projects where the part is used, when the user has the permission to see the projects
|
//Add a column to list the projects where the part is used, when the user has the permission to see the projects
|
||||||
@@ -353,7 +329,6 @@ final class PartsDataTable implements DataTableTypeInterface
|
|||||||
->addSelect('orderdetails')
|
->addSelect('orderdetails')
|
||||||
->addSelect('attachments')
|
->addSelect('attachments')
|
||||||
->addSelect('storelocations')
|
->addSelect('storelocations')
|
||||||
->addSelect('projectBomEntries')
|
|
||||||
->from(Part::class, 'part')
|
->from(Part::class, 'part')
|
||||||
->leftJoin('part.category', 'category')
|
->leftJoin('part.category', 'category')
|
||||||
->leftJoin('part.master_picture_attachment', 'master_picture_attachment')
|
->leftJoin('part.master_picture_attachment', 'master_picture_attachment')
|
||||||
@@ -368,7 +343,6 @@ final class PartsDataTable implements DataTableTypeInterface
|
|||||||
->leftJoin('part.partUnit', 'partUnit')
|
->leftJoin('part.partUnit', 'partUnit')
|
||||||
->leftJoin('part.partCustomState', 'partCustomState')
|
->leftJoin('part.partCustomState', 'partCustomState')
|
||||||
->leftJoin('part.parameters', 'parameters')
|
->leftJoin('part.parameters', 'parameters')
|
||||||
->leftJoin('part.project_bom_entries', 'projectBomEntries')
|
|
||||||
->where('part.id IN (:ids)')
|
->where('part.id IN (:ids)')
|
||||||
->setParameter('ids', $ids)
|
->setParameter('ids', $ids)
|
||||||
|
|
||||||
@@ -386,12 +360,7 @@ final class PartsDataTable implements DataTableTypeInterface
|
|||||||
->addGroupBy('attachments')
|
->addGroupBy('attachments')
|
||||||
->addGroupBy('partUnit')
|
->addGroupBy('partUnit')
|
||||||
->addGroupBy('partCustomState')
|
->addGroupBy('partCustomState')
|
||||||
->addGroupBy('parameters')
|
->addGroupBy('parameters');
|
||||||
->addGroupBy('projectBomEntries')
|
|
||||||
|
|
||||||
->setHint(Query::HINT_READ_ONLY, true)
|
|
||||||
->setHint(Query::HINT_FORCE_PARTIAL_LOAD, false)
|
|
||||||
;
|
|
||||||
|
|
||||||
//Get the results in the same order as the IDs were passed
|
//Get the results in the same order as the IDs were passed
|
||||||
FieldHelper::addOrderByFieldParam($builder, 'part.id', 'ids');
|
FieldHelper::addOrderByFieldParam($builder, 'part.id', 'ids');
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ use function in_array;
|
|||||||
#[DiscriminatorMap(typeProperty: '_type', mapping: self::API_DISCRIMINATOR_MAP)]
|
#[DiscriminatorMap(typeProperty: '_type', mapping: self::API_DISCRIMINATOR_MAP)]
|
||||||
abstract class Attachment extends AbstractNamedDBElement
|
abstract class Attachment extends AbstractNamedDBElement
|
||||||
{
|
{
|
||||||
final public const ORM_DISCRIMINATOR_MAP = ['Part' => PartAttachment::class, 'PartCustomState' => PartCustomStateAttachment::class, 'Device' => ProjectAttachment::class,
|
private const ORM_DISCRIMINATOR_MAP = ['Part' => PartAttachment::class, 'PartCustomState' => PartCustomStateAttachment::class, 'Device' => ProjectAttachment::class,
|
||||||
'AttachmentType' => AttachmentTypeAttachment::class,
|
'AttachmentType' => AttachmentTypeAttachment::class,
|
||||||
'Category' => CategoryAttachment::class, 'Footprint' => FootprintAttachment::class, 'Manufacturer' => ManufacturerAttachment::class,
|
'Category' => CategoryAttachment::class, 'Footprint' => FootprintAttachment::class, 'Manufacturer' => ManufacturerAttachment::class,
|
||||||
'Currency' => CurrencyAttachment::class, 'Group' => GroupAttachment::class, 'MeasurementUnit' => MeasurementUnitAttachment::class,
|
'Currency' => CurrencyAttachment::class, 'Group' => GroupAttachment::class, 'MeasurementUnit' => MeasurementUnitAttachment::class,
|
||||||
@@ -136,7 +136,7 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||||||
* @var string The class of the element that can be passed to this attachment. Must be overridden in subclasses.
|
* @var string The class of the element that can be passed to this attachment. Must be overridden in subclasses.
|
||||||
* @phpstan-var class-string<T>
|
* @phpstan-var class-string<T>
|
||||||
*/
|
*/
|
||||||
public const ALLOWED_ELEMENT_CLASS = AttachmentContainingDBElement::class;
|
protected const ALLOWED_ELEMENT_CLASS = AttachmentContainingDBElement::class;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var AttachmentUpload|null The options used for uploading a file to this attachment or modify it.
|
* @var AttachmentUpload|null The options used for uploading a file to this attachment or modify it.
|
||||||
@@ -296,22 +296,6 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||||||
return in_array(strtolower($extension), static::MODEL_EXTS, true);
|
return in_array(strtolower($extension), static::MODEL_EXTS, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if this is a locally stored HTML file, which can be shown by the sandbox viewer.
|
|
||||||
* This is the case if we have an internal path with a html extension.
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function isLocalHTMLFile(): bool
|
|
||||||
{
|
|
||||||
if($this->hasInternal()){
|
|
||||||
|
|
||||||
$extension = pathinfo($this->getFilename(), PATHINFO_EXTENSION);
|
|
||||||
|
|
||||||
return in_array(strtolower($extension), ['html', 'htm'], true);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if this attachment has a path to an external file
|
* Checks if this attachment has a path to an external file
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -134,17 +134,6 @@ class AttachmentType extends AbstractStructuralDBElement
|
|||||||
#[ORM\OneToMany(mappedBy: 'attachment_type', targetEntity: Attachment::class)]
|
#[ORM\OneToMany(mappedBy: 'attachment_type', targetEntity: Attachment::class)]
|
||||||
protected Collection $attachments_with_type;
|
protected Collection $attachments_with_type;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string[]|null A list of allowed targets where this attachment type can be assigned to, as a list of portable names
|
|
||||||
*/
|
|
||||||
#[ORM\Column(type: Types::SIMPLE_ARRAY, nullable: true)]
|
|
||||||
protected ?array $allowed_targets = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var class-string<Attachment>[]|null
|
|
||||||
*/
|
|
||||||
protected ?array $allowed_targets_parsed_cache = null;
|
|
||||||
|
|
||||||
#[Groups(['attachment_type:read'])]
|
#[Groups(['attachment_type:read'])]
|
||||||
protected ?\DateTimeImmutable $addedDate = null;
|
protected ?\DateTimeImmutable $addedDate = null;
|
||||||
#[Groups(['attachment_type:read'])]
|
#[Groups(['attachment_type:read'])]
|
||||||
@@ -195,81 +184,4 @@ class AttachmentType extends AbstractStructuralDBElement
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list of allowed targets as class names (e.g. PartAttachment::class), where this attachment type can be assigned to. If null, there are no restrictions.
|
|
||||||
* @return class-string<Attachment>[]|null
|
|
||||||
*/
|
|
||||||
public function getAllowedTargets(): ?array
|
|
||||||
{
|
|
||||||
//Use cached value if available
|
|
||||||
if ($this->allowed_targets_parsed_cache !== null) {
|
|
||||||
return $this->allowed_targets_parsed_cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($this->allowed_targets)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$tmp = [];
|
|
||||||
foreach ($this->allowed_targets as $target) {
|
|
||||||
if (isset(Attachment::ORM_DISCRIMINATOR_MAP[$target])) {
|
|
||||||
$tmp[] = Attachment::ORM_DISCRIMINATOR_MAP[$target];
|
|
||||||
}
|
|
||||||
//Otherwise ignore the entry, as it is invalid
|
|
||||||
}
|
|
||||||
|
|
||||||
//Cache the parsed value
|
|
||||||
$this->allowed_targets_parsed_cache = $tmp;
|
|
||||||
return $tmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the allowed targets for this attachment type. Allowed targets are specified as a list of class names (e.g. PartAttachment::class). If null is passed, there are no restrictions.
|
|
||||||
* @param class-string<Attachment>[]|null $allowed_targets
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function setAllowedTargets(?array $allowed_targets): self
|
|
||||||
{
|
|
||||||
if ($allowed_targets === null) {
|
|
||||||
$this->allowed_targets = null;
|
|
||||||
} else {
|
|
||||||
$tmp = [];
|
|
||||||
foreach ($allowed_targets as $target) {
|
|
||||||
$discriminator = array_search($target, Attachment::ORM_DISCRIMINATOR_MAP, true);
|
|
||||||
if ($discriminator !== false) {
|
|
||||||
$tmp[] = $discriminator;
|
|
||||||
} else {
|
|
||||||
throw new \InvalidArgumentException("Invalid allowed target: $target. Allowed targets must be a class name of an Attachment subclass.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$this->allowed_targets = $tmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Reset the cache
|
|
||||||
$this->allowed_targets_parsed_cache = null;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if this attachment type is allowed for the given attachment target.
|
|
||||||
* @param Attachment|string $attachment
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function isAllowedForTarget(Attachment|string $attachment): bool
|
|
||||||
{
|
|
||||||
//If no restrictions are set, allow all targets
|
|
||||||
if ($this->getAllowedTargets() === null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Iterate over all allowed targets and check if the attachment is an instance of any of them
|
|
||||||
foreach ($this->getAllowedTargets() as $allowed_target) {
|
|
||||||
if (is_a($attachment, $allowed_target, true)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class EDACategoryInfo
|
|||||||
/** @var bool|null If this is set to true, then this part will be excluded in the simulation */
|
/** @var bool|null If this is set to true, then this part will be excluded in the simulation */
|
||||||
#[Column(type: Types::BOOLEAN, nullable: true)]
|
#[Column(type: Types::BOOLEAN, nullable: true)]
|
||||||
#[Groups(['full', 'category:read', 'category:write', 'import'])]
|
#[Groups(['full', 'category:read', 'category:write', 'import'])]
|
||||||
private ?bool $exclude_from_sim = null;
|
private ?bool $exclude_from_sim = true;
|
||||||
|
|
||||||
/** @var string|null The KiCAD schematic symbol, which should be used (the path to the library) */
|
/** @var string|null The KiCAD schematic symbol, which should be used (the path to the library) */
|
||||||
#[Column(type: Types::STRING, nullable: true)]
|
#[Column(type: Types::STRING, nullable: true)]
|
||||||
|
|||||||
@@ -41,12 +41,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Entity\LabelSystem;
|
namespace App\Entity\LabelSystem;
|
||||||
|
|
||||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
|
||||||
use ApiPlatform\Metadata\ApiFilter;
|
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
|
||||||
use ApiPlatform\Metadata\Get;
|
|
||||||
use ApiPlatform\Metadata\GetCollection;
|
|
||||||
use ApiPlatform\OpenApi\Model\Operation;
|
|
||||||
use Doctrine\Common\Collections\Criteria;
|
use Doctrine\Common\Collections\Criteria;
|
||||||
use App\Entity\Attachments\Attachment;
|
use App\Entity\Attachments\Attachment;
|
||||||
use App\Repository\LabelProfileRepository;
|
use App\Repository\LabelProfileRepository;
|
||||||
@@ -64,22 +58,6 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||||||
/**
|
/**
|
||||||
* @extends AttachmentContainingDBElement<LabelAttachment>
|
* @extends AttachmentContainingDBElement<LabelAttachment>
|
||||||
*/
|
*/
|
||||||
#[ApiResource(
|
|
||||||
operations: [
|
|
||||||
new Get(
|
|
||||||
normalizationContext: ['groups' => ['label_profile:read', 'simple']],
|
|
||||||
security: "is_granted('read', object)",
|
|
||||||
openapi: new Operation(summary: 'Get a label profile by ID')
|
|
||||||
),
|
|
||||||
new GetCollection(
|
|
||||||
normalizationContext: ['groups' => ['label_profile:read', 'simple']],
|
|
||||||
security: "is_granted('@labels.create_labels')",
|
|
||||||
openapi: new Operation(summary: 'List all available label profiles')
|
|
||||||
),
|
|
||||||
],
|
|
||||||
paginationEnabled: false,
|
|
||||||
)]
|
|
||||||
#[ApiFilter(SearchFilter::class, properties: ['options.supported_element' => 'exact', 'show_in_dropdown' => 'exact'])]
|
|
||||||
#[UniqueEntity(['name', 'options.supported_element'])]
|
#[UniqueEntity(['name', 'options.supported_element'])]
|
||||||
#[ORM\Entity(repositoryClass: LabelProfileRepository::class)]
|
#[ORM\Entity(repositoryClass: LabelProfileRepository::class)]
|
||||||
#[ORM\EntityListeners([TreeCacheInvalidationListener::class])]
|
#[ORM\EntityListeners([TreeCacheInvalidationListener::class])]
|
||||||
@@ -102,21 +80,20 @@ class LabelProfile extends AttachmentContainingDBElement
|
|||||||
*/
|
*/
|
||||||
#[Assert\Valid]
|
#[Assert\Valid]
|
||||||
#[ORM\Embedded(class: 'LabelOptions')]
|
#[ORM\Embedded(class: 'LabelOptions')]
|
||||||
#[Groups(["extended", "full", "import", "label_profile:read"])]
|
#[Groups(["extended", "full", "import"])]
|
||||||
protected LabelOptions $options;
|
protected LabelOptions $options;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string The comment info for this element
|
* @var string The comment info for this element
|
||||||
*/
|
*/
|
||||||
#[ORM\Column(type: Types::TEXT)]
|
#[ORM\Column(type: Types::TEXT)]
|
||||||
#[Groups(["extended", "full", "import", "label_profile:read"])]
|
|
||||||
protected string $comment = '';
|
protected string $comment = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var bool determines, if this label profile should be shown in the dropdown quick menu
|
* @var bool determines, if this label profile should be shown in the dropdown quick menu
|
||||||
*/
|
*/
|
||||||
#[ORM\Column(type: Types::BOOLEAN)]
|
#[ORM\Column(type: Types::BOOLEAN)]
|
||||||
#[Groups(["extended", "full", "import", "label_profile:read"])]
|
#[Groups(["extended", "full", "import"])]
|
||||||
protected bool $show_in_dropdown = true;
|
protected bool $show_in_dropdown = true;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ enum PartStockChangeType: string
|
|||||||
case WITHDRAW = "withdraw";
|
case WITHDRAW = "withdraw";
|
||||||
case MOVE = "move";
|
case MOVE = "move";
|
||||||
|
|
||||||
case STOCKTAKE = "stock_take";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the type to a short representation usable in the extra field of the log entry.
|
* Converts the type to a short representation usable in the extra field of the log entry.
|
||||||
* @return string
|
* @return string
|
||||||
@@ -40,7 +38,6 @@ enum PartStockChangeType: string
|
|||||||
self::ADD => 'a',
|
self::ADD => 'a',
|
||||||
self::WITHDRAW => 'w',
|
self::WITHDRAW => 'w',
|
||||||
self::MOVE => 'm',
|
self::MOVE => 'm',
|
||||||
self::STOCKTAKE => 's',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +52,6 @@ enum PartStockChangeType: string
|
|||||||
'a' => self::ADD,
|
'a' => self::ADD,
|
||||||
'w' => self::WITHDRAW,
|
'w' => self::WITHDRAW,
|
||||||
'm' => self::MOVE,
|
'm' => self::MOVE,
|
||||||
's' => self::STOCKTAKE,
|
|
||||||
default => throw new \InvalidArgumentException("Invalid short type: $value"),
|
default => throw new \InvalidArgumentException("Invalid short type: $value"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,11 +122,6 @@ class PartStockChangedLogEntry extends AbstractLogEntry
|
|||||||
return new self(PartStockChangeType::MOVE, $lot, $old_stock, $new_stock, $new_total_part_instock, $comment, $move_to_target, action_timestamp: $action_timestamp);
|
return new self(PartStockChangeType::MOVE, $lot, $old_stock, $new_stock, $new_total_part_instock, $comment, $move_to_target, action_timestamp: $action_timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function stocktake(PartLot $lot, float $old_stock, float $new_stock, float $new_total_part_instock, string $comment, ?\DateTimeInterface $action_timestamp = null): self
|
|
||||||
{
|
|
||||||
return new self(PartStockChangeType::STOCKTAKE, $lot, $old_stock, $new_stock, $new_total_part_instock, $comment, action_timestamp: $action_timestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the instock change type of this entry
|
* Returns the instock change type of this entry
|
||||||
* @return PartStockChangeType
|
* @return PartStockChangeType
|
||||||
|
|||||||
@@ -172,13 +172,6 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
|
|||||||
#[Assert\Length(max: 255)]
|
#[Assert\Length(max: 255)]
|
||||||
protected string $group = '';
|
protected string $group = '';
|
||||||
|
|
||||||
/**
|
|
||||||
* @var bool|null Whether this parameter should be exported as a field in the EDA HTTP library API. Null means use system default.
|
|
||||||
*/
|
|
||||||
#[Groups(['full', 'parameter:read', 'parameter:write', 'import'])]
|
|
||||||
#[ORM\Column(type: Types::BOOLEAN, nullable: true, options: ['default' => null])]
|
|
||||||
protected ?bool $eda_visibility = null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapping is done in subclasses.
|
* Mapping is done in subclasses.
|
||||||
*
|
*
|
||||||
@@ -478,21 +471,6 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
|
|||||||
return static::ALLOWED_ELEMENT_CLASS;
|
return static::ALLOWED_ELEMENT_CLASS;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isEdaVisibility(): ?bool
|
|
||||||
{
|
|
||||||
return $this->eda_visibility;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function setEdaVisibility(?bool $eda_visibility): self
|
|
||||||
{
|
|
||||||
$this->eda_visibility = $eda_visibility;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getComparableFields(): array
|
public function getComparableFields(): array
|
||||||
{
|
{
|
||||||
return ['name' => $this->name, 'group' => $this->group, 'element' => $this->element?->getId()];
|
return ['name' => $this->name, 'group' => $this->group, 'element' => $this->element?->getId()];
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
|||||||
#[ORM\Index(columns: ['datetime_added', 'name', 'last_modified', 'id', 'needs_review'], name: 'parts_idx_datet_name_last_id_needs')]
|
#[ORM\Index(columns: ['datetime_added', 'name', 'last_modified', 'id', 'needs_review'], name: 'parts_idx_datet_name_last_id_needs')]
|
||||||
#[ORM\Index(columns: ['name'], name: 'parts_idx_name')]
|
#[ORM\Index(columns: ['name'], name: 'parts_idx_name')]
|
||||||
#[ORM\Index(columns: ['ipn'], name: 'parts_idx_ipn')]
|
#[ORM\Index(columns: ['ipn'], name: 'parts_idx_ipn')]
|
||||||
#[ORM\Index(columns: ['gtin'], name: 'parts_idx_gtin')]
|
|
||||||
#[ApiResource(
|
#[ApiResource(
|
||||||
operations: [
|
operations: [
|
||||||
new Get(normalizationContext: [
|
new Get(normalizationContext: [
|
||||||
|
|||||||
@@ -171,14 +171,6 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
|
|||||||
#[Length(max: 255)]
|
#[Length(max: 255)]
|
||||||
protected ?string $user_barcode = null;
|
protected ?string $user_barcode = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \DateTimeImmutable|null The date when the last stocktake was performed for this part lot. Set to null, if no stocktake was performed yet.
|
|
||||||
*/
|
|
||||||
#[Groups(['extended', 'full', 'import', 'part_lot:read', 'part_lot:write'])]
|
|
||||||
#[ORM\Column( type: Types::DATETIME_IMMUTABLE, nullable: true)]
|
|
||||||
#[Year2038BugWorkaround]
|
|
||||||
protected ?\DateTimeImmutable $last_stocktake_at = null;
|
|
||||||
|
|
||||||
public function __clone()
|
public function __clone()
|
||||||
{
|
{
|
||||||
if ($this->id) {
|
if ($this->id) {
|
||||||
@@ -399,26 +391,6 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the date when the last stocktake was performed for this part lot. Returns null, if no stocktake was performed yet.
|
|
||||||
* @return \DateTimeImmutable|null
|
|
||||||
*/
|
|
||||||
public function getLastStocktakeAt(): ?\DateTimeImmutable
|
|
||||||
{
|
|
||||||
return $this->last_stocktake_at;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the date when the last stocktake was performed for this part lot. Set to null, if no stocktake was performed yet.
|
|
||||||
* @param \DateTimeImmutable|null $last_stocktake_at
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function setLastStocktakeAt(?\DateTimeImmutable $last_stocktake_at): self
|
|
||||||
{
|
|
||||||
$this->last_stocktake_at = $last_stocktake_at;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[Assert\Callback]
|
#[Assert\Callback]
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ namespace App\Entity\Parts\PartTraits;
|
|||||||
|
|
||||||
use App\Entity\Parts\InfoProviderReference;
|
use App\Entity\Parts\InfoProviderReference;
|
||||||
use App\Entity\Parts\PartCustomState;
|
use App\Entity\Parts\PartCustomState;
|
||||||
use App\Validator\Constraints\ValidGTIN;
|
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use App\Entity\Parts\Part;
|
use App\Entity\Parts\Part;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
@@ -85,14 +84,6 @@ trait AdvancedPropertyTrait
|
|||||||
#[ORM\JoinColumn(name: 'id_part_custom_state')]
|
#[ORM\JoinColumn(name: 'id_part_custom_state')]
|
||||||
protected ?PartCustomState $partCustomState = null;
|
protected ?PartCustomState $partCustomState = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string|null The GTIN (Global Trade Item Number) of the part, for example a UPC or EAN code
|
|
||||||
*/
|
|
||||||
#[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])]
|
|
||||||
#[ORM\Column(type: Types::STRING, nullable: true)]
|
|
||||||
#[ValidGTIN]
|
|
||||||
protected ?string $gtin = null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if this part is marked, for that it needs further review.
|
* Checks if this part is marked, for that it needs further review.
|
||||||
*/
|
*/
|
||||||
@@ -220,26 +211,4 @@ trait AdvancedPropertyTrait
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the GTIN (Global Trade Item Number) of the part, for example a UPC or EAN code.
|
|
||||||
* Returns null if no GTIN is set.
|
|
||||||
*/
|
|
||||||
public function getGtin(): ?string
|
|
||||||
{
|
|
||||||
return $this->gtin;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the GTIN (Global Trade Item Number) of the part, for example a UPC or EAN code.
|
|
||||||
*
|
|
||||||
* @param string|null $gtin The new GTIN of the part
|
|
||||||
*
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function setGtin(?string $gtin): self
|
|
||||||
{
|
|
||||||
$this->gtin = $gtin;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ use Doctrine\Common\Collections\Collection;
|
|||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||||
use Symfony\Component\Serializer\Annotation\Groups;
|
use Symfony\Component\Serializer\Annotation\Groups;
|
||||||
use Symfony\Component\Serializer\Annotation\SerializedName;
|
|
||||||
use Symfony\Component\Validator\Constraints as Assert;
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
use Symfony\Component\Validator\Constraints\Length;
|
use Symfony\Component\Validator\Constraints\Length;
|
||||||
|
|
||||||
@@ -122,13 +121,6 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N
|
|||||||
#[ORM\Column(type: Types::BOOLEAN)]
|
#[ORM\Column(type: Types::BOOLEAN)]
|
||||||
protected bool $obsolete = false;
|
protected bool $obsolete = false;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var bool|null Whether this orderdetail's supplier part number should be exported as an EDA field. Null means use system default.
|
|
||||||
*/
|
|
||||||
#[Groups(['full', 'import', 'orderdetail:read', 'orderdetail:write'])]
|
|
||||||
#[ORM\Column(type: Types::BOOLEAN, nullable: true, options: ['default' => null])]
|
|
||||||
protected ?bool $eda_visibility = null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string The URL to the product on the supplier's website
|
* @var string The URL to the product on the supplier's website
|
||||||
*/
|
*/
|
||||||
@@ -155,13 +147,6 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N
|
|||||||
#[ORM\JoinColumn(name: 'id_supplier')]
|
#[ORM\JoinColumn(name: 'id_supplier')]
|
||||||
protected ?Supplier $supplier = null;
|
protected ?Supplier $supplier = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var bool|null Whether the prices includes VAT or not. Null means, that it is not specified, if the prices includes VAT or not.
|
|
||||||
*/
|
|
||||||
#[ORM\Column(type: Types::BOOLEAN, nullable: true)]
|
|
||||||
#[Groups(['extended', 'full', 'import', 'orderdetail:read', 'orderdetail:write'])]
|
|
||||||
protected ?bool $prices_includes_vat = null;
|
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->pricedetails = new ArrayCollection();
|
$this->pricedetails = new ArrayCollection();
|
||||||
@@ -403,43 +388,6 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the prices of this orderdetail include VAT. Null means, that it is not specified, if the prices includes
|
|
||||||
* VAT or not.
|
|
||||||
* @return bool|null
|
|
||||||
*/
|
|
||||||
public function getPricesIncludesVAT(): ?bool
|
|
||||||
{
|
|
||||||
return $this->prices_includes_vat;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether the prices of this orderdetail include VAT.
|
|
||||||
* @param bool|null $includesVat
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function setPricesIncludesVAT(?bool $includesVat): self
|
|
||||||
{
|
|
||||||
$this->prices_includes_vat = $includesVat;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isEdaVisibility(): ?bool
|
|
||||||
{
|
|
||||||
return $this->eda_visibility;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function setEdaVisibility(?bool $eda_visibility): self
|
|
||||||
{
|
|
||||||
$this->eda_visibility = $eda_visibility;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getName(): string
|
public function getName(): string
|
||||||
{
|
{
|
||||||
return $this->getSupplierPartNr();
|
return $this->getSupplierPartNr();
|
||||||
|
|||||||
@@ -121,8 +121,6 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface
|
|||||||
#[Groups(['pricedetail:read:standalone', 'pricedetail:write'])]
|
#[Groups(['pricedetail:read:standalone', 'pricedetail:write'])]
|
||||||
protected ?Orderdetail $orderdetail = null;
|
protected ?Orderdetail $orderdetail = null;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->price = BigDecimal::zero()->toScale(self::PRICE_PRECISION);
|
$this->price = BigDecimal::zero()->toScale(self::PRICE_PRECISION);
|
||||||
@@ -266,15 +264,6 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface
|
|||||||
return $this->currency?->getIsoCode();
|
return $this->currency?->getIsoCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether the price includes VAT or not. Null means, that it is not specified, if the price includes VAT or not.
|
|
||||||
* @return bool|null
|
|
||||||
*/
|
|
||||||
public function getIncludesVat(): ?bool
|
|
||||||
{
|
|
||||||
return $this->orderdetail?->getPricesIncludesVAT();
|
|
||||||
}
|
|
||||||
|
|
||||||
/********************************************************************************
|
/********************************************************************************
|
||||||
*
|
*
|
||||||
* Setters
|
* Setters
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ final class PermissionData implements \JsonSerializable
|
|||||||
/**
|
/**
|
||||||
* The current schema version of the permission data
|
* The current schema version of the permission data
|
||||||
*/
|
*/
|
||||||
public const CURRENT_SCHEMA_VERSION = 4;
|
public const CURRENT_SCHEMA_VERSION = 3;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new Permission Data Instance using the given data.
|
* Creates a new Permission Data Instance using the given data.
|
||||||
|
|||||||
@@ -1,230 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*
|
|
||||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 - 2024 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
|
|
||||||
namespace App\EventSubscriber;
|
|
||||||
|
|
||||||
use App\Services\System\UpdateExecutor;
|
|
||||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
|
|
||||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
|
||||||
use Symfony\Component\HttpKernel\Event\ResponseEvent;
|
|
||||||
use Symfony\Component\HttpKernel\KernelEvents;
|
|
||||||
use Twig\Environment;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Blocks all web requests when maintenance mode is enabled during updates.
|
|
||||||
*/
|
|
||||||
readonly class MaintenanceModeSubscriber implements EventSubscriberInterface
|
|
||||||
{
|
|
||||||
public function __construct(private UpdateExecutor $updateExecutor)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getSubscribedEvents(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
// High priority to run before other listeners
|
|
||||||
KernelEvents::REQUEST => ['onKernelRequest', 512], //High priority to run before other listeners
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function onKernelRequest(RequestEvent $event): void
|
|
||||||
{
|
|
||||||
// Only handle main requests
|
|
||||||
if (!$event->isMainRequest()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip if not in maintenance mode
|
|
||||||
if (!$this->updateExecutor->isMaintenanceMode()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Allow to view the progress page
|
|
||||||
if (preg_match('#^/\w{2}/system/update-manager/progress#', $event->getRequest()->getPathInfo())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow CLI requests
|
|
||||||
if (PHP_SAPI === 'cli') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get maintenance info
|
|
||||||
$maintenanceInfo = $this->updateExecutor->getMaintenanceInfo();
|
|
||||||
|
|
||||||
// Calculate how long the update has been running
|
|
||||||
$duration = null;
|
|
||||||
if ($maintenanceInfo && isset($maintenanceInfo['enabled_at'])) {
|
|
||||||
try {
|
|
||||||
$startedAt = new \DateTime($maintenanceInfo['enabled_at']);
|
|
||||||
$now = new \DateTime();
|
|
||||||
$duration = $now->getTimestamp() - $startedAt->getTimestamp();
|
|
||||||
} catch (\Exception) {
|
|
||||||
// Ignore date parsing errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$content = $this->getSimpleMaintenanceHtml($maintenanceInfo, $duration);
|
|
||||||
|
|
||||||
$response = new Response($content, Response::HTTP_SERVICE_UNAVAILABLE);
|
|
||||||
$response->headers->set('Retry-After', '30');
|
|
||||||
$response->headers->set('Cache-Control', 'no-store, no-cache, must-revalidate');
|
|
||||||
|
|
||||||
$event->setResponse($response);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a simple maintenance page HTML without Twig.
|
|
||||||
*/
|
|
||||||
private function getSimpleMaintenanceHtml(?array $maintenanceInfo, ?int $duration): string
|
|
||||||
{
|
|
||||||
$reason = htmlspecialchars($maintenanceInfo['reason'] ?? 'Update in progress');
|
|
||||||
$durationText = $duration !== null ? sprintf('%d seconds', $duration) : 'a moment';
|
|
||||||
|
|
||||||
$startDateStr = $maintenanceInfo['enabled_at'] ?? 'unknown time';
|
|
||||||
|
|
||||||
return <<<HTML
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="refresh" content="15">
|
|
||||||
<title>Part-DB - Maintenance</title>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
||||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
text-align: center;
|
|
||||||
padding: 40px;
|
|
||||||
max-width: 600px;
|
|
||||||
}
|
|
||||||
.icon {
|
|
||||||
font-size: 80px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
animation: pulse 2s infinite;
|
|
||||||
}
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% { transform: scale(1); opacity: 1; }
|
|
||||||
50% { transform: scale(1.1); opacity: 0.8; }
|
|
||||||
}
|
|
||||||
@keyframes spin {
|
|
||||||
from { transform: rotate(0deg); }
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
.spinner {
|
|
||||||
display: inline-block;
|
|
||||||
animation: spin 2s linear infinite;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
color: #00d4ff;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
color: #b8c5d6;
|
|
||||||
}
|
|
||||||
.reason {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
padding: 15px 25px;
|
|
||||||
border-radius: 10px;
|
|
||||||
margin: 20px 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
.progress-bar {
|
|
||||||
width: 100%;
|
|
||||||
height: 6px;
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
border-radius: 3px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin: 30px 0;
|
|
||||||
}
|
|
||||||
.progress-bar-inner {
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(90deg, #00d4ff, #00ff88);
|
|
||||||
border-radius: 3px;
|
|
||||||
animation: progress 3s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
@keyframes progress {
|
|
||||||
0% { width: 0%; margin-left: 0%; }
|
|
||||||
50% { width: 50%; margin-left: 25%; }
|
|
||||||
100% { width: 0%; margin-left: 100%; }
|
|
||||||
}
|
|
||||||
.info {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #8899aa;
|
|
||||||
margin-top: 30px;
|
|
||||||
}
|
|
||||||
.duration {
|
|
||||||
font-family: monospace;
|
|
||||||
background: rgba(0, 212, 255, 0.2);
|
|
||||||
padding: 3px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="icon">
|
|
||||||
<span class="spinner">⚙️</span>
|
|
||||||
</div>
|
|
||||||
<h1>Part-DB is under maintenance</h1>
|
|
||||||
<p>We're making things better. This should only take a moment.</p>
|
|
||||||
|
|
||||||
<div class="reason">
|
|
||||||
<strong>{$reason}</strong>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="progress-bar">
|
|
||||||
<div class="progress-bar-inner"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="info">
|
|
||||||
Maintenance mode active since <span class="duration">{$startDateStr}</span><br>
|
|
||||||
<br>
|
|
||||||
Started <span class="duration">{$durationText}</span> ago<br>
|
|
||||||
<small>This page will automatically refresh every 15 seconds.</small>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
HTML;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\EventSubscriber\UserSystem;
|
namespace App\EventSubscriber\UserSystem;
|
||||||
|
|
||||||
use App\Entity\Parts\Part;
|
use App\Entity\Parts\Part;
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*
|
|
||||||
* 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
|
|
||||||
namespace App\Exceptions;
|
|
||||||
|
|
||||||
use App\Services\InfoProviderSystem\Providers\InfoProviderInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An exception denoting that a required info provider is not active. This can be used to display a user-friendly error message,
|
|
||||||
* when a user tries to use an info provider that is not active.
|
|
||||||
*/
|
|
||||||
class InfoProviderNotActiveException extends \RuntimeException
|
|
||||||
{
|
|
||||||
public function __construct(public readonly string $providerKey, public readonly string $friendlyName)
|
|
||||||
{
|
|
||||||
parent::__construct(sprintf('The info provider "%s" (%s) is not active.', $this->friendlyName, $this->providerKey));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an instance of this exception from an info provider instance
|
|
||||||
* @param InfoProviderInterface $provider
|
|
||||||
* @return self
|
|
||||||
*/
|
|
||||||
public static function fromProvider(InfoProviderInterface $provider): self
|
|
||||||
{
|
|
||||||
return new self($provider->getProviderKey(), $provider->getProviderInfo()['name'] ?? '???');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -42,14 +42,15 @@ declare(strict_types=1);
|
|||||||
namespace App\Exceptions;
|
namespace App\Exceptions;
|
||||||
|
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
use Twig\Error\Error;
|
||||||
|
|
||||||
class TwigModeException extends RuntimeException
|
class TwigModeException extends RuntimeException
|
||||||
{
|
{
|
||||||
private const PROJECT_PATH = __DIR__ . '/../../';
|
private const PROJECT_PATH = __DIR__ . '/../../';
|
||||||
|
|
||||||
public function __construct(?\Throwable $previous = null)
|
public function __construct(?Error $previous = null)
|
||||||
{
|
{
|
||||||
parent::__construct($previous?->getMessage() ?? "Unknown message", 0, $previous);
|
parent::__construct($previous->getMessage(), 0, $previous);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -22,23 +22,17 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Form\AdminPages;
|
namespace App\Form\AdminPages;
|
||||||
|
|
||||||
use App\Entity\Attachments\Attachment;
|
|
||||||
use App\Entity\Attachments\PartAttachment;
|
|
||||||
use App\Entity\Attachments\ProjectAttachment;
|
|
||||||
use App\Services\ElementTypeNameGenerator;
|
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
use App\Entity\Base\AbstractNamedDBElement;
|
use App\Entity\Base\AbstractNamedDBElement;
|
||||||
use App\Services\Attachments\FileTypeFilterTools;
|
use App\Services\Attachments\FileTypeFilterTools;
|
||||||
use App\Services\LogSystem\EventCommentNeededHelper;
|
use App\Services\LogSystem\EventCommentNeededHelper;
|
||||||
use Symfony\Component\Form\CallbackTransformer;
|
use Symfony\Component\Form\CallbackTransformer;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
|
||||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Component\Translation\StaticMessage;
|
|
||||||
|
|
||||||
class AttachmentTypeAdminForm extends BaseEntityAdminForm
|
class AttachmentTypeAdminForm extends BaseEntityAdminForm
|
||||||
{
|
{
|
||||||
public function __construct(Security $security, protected FileTypeFilterTools $filterTools, EventCommentNeededHelper $eventCommentNeededHelper, private readonly ElementTypeNameGenerator $elementTypeNameGenerator)
|
public function __construct(Security $security, protected FileTypeFilterTools $filterTools, EventCommentNeededHelper $eventCommentNeededHelper)
|
||||||
{
|
{
|
||||||
parent::__construct($security, $eventCommentNeededHelper);
|
parent::__construct($security, $eventCommentNeededHelper);
|
||||||
}
|
}
|
||||||
@@ -47,25 +41,6 @@ class AttachmentTypeAdminForm extends BaseEntityAdminForm
|
|||||||
{
|
{
|
||||||
$is_new = null === $entity->getID();
|
$is_new = null === $entity->getID();
|
||||||
|
|
||||||
|
|
||||||
$choiceLabel = function (string $class) {
|
|
||||||
if (!is_a($class, Attachment::class, true)) {
|
|
||||||
return $class;
|
|
||||||
}
|
|
||||||
return new StaticMessage($this->elementTypeNameGenerator->typeLabelPlural($class::ALLOWED_ELEMENT_CLASS));
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
$builder->add('allowed_targets', ChoiceType::class, [
|
|
||||||
'required' => false,
|
|
||||||
'choices' => array_values(Attachment::ORM_DISCRIMINATOR_MAP),
|
|
||||||
'choice_label' => $choiceLabel,
|
|
||||||
'preferred_choices' => [PartAttachment::class, ProjectAttachment::class],
|
|
||||||
'label' => 'attachment_type.edit.allowed_targets',
|
|
||||||
'help' => 'attachment_type.edit.allowed_targets.help',
|
|
||||||
'multiple' => true,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$builder->add('filetype_filter', TextType::class, [
|
$builder->add('filetype_filter', TextType::class, [
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'label' => 'attachment_type.edit.filetype_filter',
|
'label' => 'attachment_type.edit.filetype_filter',
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ class BaseEntityAdminForm extends AbstractType
|
|||||||
'label' => 'name.label',
|
'label' => 'name.label',
|
||||||
'attr' => [
|
'attr' => [
|
||||||
'placeholder' => 'part.name.placeholder',
|
'placeholder' => 'part.name.placeholder',
|
||||||
'autofocus' => $is_new,
|
|
||||||
],
|
],
|
||||||
'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity),
|
'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Form;
|
namespace App\Form;
|
||||||
|
|
||||||
use App\Form\Type\AttachmentTypeType;
|
|
||||||
use App\Settings\SystemSettings\AttachmentsSettings;
|
use App\Settings\SystemSettings\AttachmentsSettings;
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
use App\Entity\Attachments\Attachment;
|
use App\Entity\Attachments\Attachment;
|
||||||
@@ -68,10 +67,10 @@ class AttachmentFormType extends AbstractType
|
|||||||
'required' => false,
|
'required' => false,
|
||||||
'empty_data' => '',
|
'empty_data' => '',
|
||||||
])
|
])
|
||||||
->add('attachment_type', AttachmentTypeType::class, [
|
->add('attachment_type', StructuralEntityType::class, [
|
||||||
'label' => 'attachment.edit.attachment_type',
|
'label' => 'attachment.edit.attachment_type',
|
||||||
|
'class' => AttachmentType::class,
|
||||||
'disable_not_selectable' => true,
|
'disable_not_selectable' => true,
|
||||||
'attachment_filter_class' => $options['data_class'] ?? null,
|
|
||||||
'allow_add' => $this->security->isGranted('@attachment_types.create'),
|
'allow_add' => $this->security->isGranted('@attachment_types.create'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -122,7 +121,9 @@ class AttachmentFormType extends AbstractType
|
|||||||
],
|
],
|
||||||
'constraints' => [
|
'constraints' => [
|
||||||
//new AllowedFileExtension(),
|
//new AllowedFileExtension(),
|
||||||
new File(maxSize: $options['max_file_size']),
|
new File([
|
||||||
|
'maxSize' => $options['max_file_size'],
|
||||||
|
]),
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -135,10 +135,6 @@ class PartFilterType extends AbstractType
|
|||||||
'min' => 0,
|
'min' => 0,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$builder->add('gtin', TextConstraintType::class, [
|
|
||||||
'label' => 'part.gtin',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$builder->add('measurementUnit', StructuralEntityConstraintType::class, [
|
$builder->add('measurementUnit', StructuralEntityConstraintType::class, [
|
||||||
'label' => 'part.edit.partUnit',
|
'label' => 'part.edit.partUnit',
|
||||||
'entity_class' => MeasurementUnit::class
|
'entity_class' => MeasurementUnit::class
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Form\InfoProviderSystem;
|
namespace App\Form\InfoProviderSystem;
|
||||||
|
|
||||||
use Symfony\Component\Validator\Constraints\Range;
|
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||||
@@ -62,7 +61,7 @@ class FieldToProviderMappingType extends AbstractType
|
|||||||
'style' => 'width: 80px;'
|
'style' => 'width: 80px;'
|
||||||
],
|
],
|
||||||
'constraints' => [
|
'constraints' => [
|
||||||
new Range(min: 1, max: 10),
|
new \Symfony\Component\Validator\Constraints\Range(['min' => 1, 'max' => 10]),
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,8 +61,6 @@ class ScanDialogType extends AbstractType
|
|||||||
'attr' => [
|
'attr' => [
|
||||||
'autofocus' => true,
|
'autofocus' => true,
|
||||||
'id' => 'scan_dialog_input',
|
'id' => 'scan_dialog_input',
|
||||||
'style' => 'font-family: var(--bs-font-monospace)',
|
|
||||||
'data-controller' => 'elements--nonprintable-char-input',
|
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -74,7 +72,10 @@ class ScanDialogType extends AbstractType
|
|||||||
'placeholder' => 'scan_dialog.mode.auto',
|
'placeholder' => 'scan_dialog.mode.auto',
|
||||||
'choice_label' => fn (?BarcodeSourceType $enum) => match($enum) {
|
'choice_label' => fn (?BarcodeSourceType $enum) => match($enum) {
|
||||||
null => 'scan_dialog.mode.auto',
|
null => 'scan_dialog.mode.auto',
|
||||||
default => 'scan_dialog.mode.' . $enum->value,
|
BarcodeSourceType::INTERNAL => 'scan_dialog.mode.internal',
|
||||||
|
BarcodeSourceType::IPN => 'scan_dialog.mode.ipn',
|
||||||
|
BarcodeSourceType::USER_DEFINED => 'scan_dialog.mode.user',
|
||||||
|
BarcodeSourceType::EIGP114 => 'scan_dialog.mode.eigp'
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -54,9 +54,7 @@ use App\Entity\Parameters\StorageLocationParameter;
|
|||||||
use App\Entity\Parameters\SupplierParameter;
|
use App\Entity\Parameters\SupplierParameter;
|
||||||
use App\Entity\Parts\MeasurementUnit;
|
use App\Entity\Parts\MeasurementUnit;
|
||||||
use App\Form\Type\ExponentialNumberType;
|
use App\Form\Type\ExponentialNumberType;
|
||||||
use App\Form\Type\TriStateCheckboxType;
|
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
|
||||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
@@ -149,14 +147,6 @@ class ParameterType extends AbstractType
|
|||||||
'class' => 'form-control-sm',
|
'class' => 'form-control-sm',
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Only show the EDA visibility field for part parameters, as it has no function for other entities
|
|
||||||
if ($options['data_class'] === PartParameter::class) {
|
|
||||||
$builder->add('eda_visibility', TriStateCheckboxType::class, [
|
|
||||||
'label' => false,
|
|
||||||
'required' => false,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function finishView(FormView $view, FormInterface $form, array $options): void
|
public function finishView(FormView $view, FormInterface $form, array $options): void
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user