feat/docker-compose-update (#18)
Co-authored-by: jon brookes <marshyon@gmail.com> Reviewed-on: https://codeberg.org/headshed/share-lt/pulls/18
This commit is contained in:
parent
fd43495e2d
commit
1a22fd156d
70 changed files with 1068 additions and 745 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
# Ignore .env files
|
# Ignore .env files
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
.envrc
|
||||||
|
|
||||||
# Ignore node_modules
|
# Ignore node_modules
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
@ -26,3 +27,6 @@ Thumbs.db
|
||||||
/storage/framework/sessions/*
|
/storage/framework/sessions/*
|
||||||
/storage/framework/views/*
|
/storage/framework/views/*
|
||||||
/storage/logs/*
|
/storage/logs/*
|
||||||
|
|
||||||
|
# Ignore database files if in development
|
||||||
|
database/database.sqlite
|
||||||
|
|
@ -66,5 +66,7 @@ AWS_DEFAULT_REGION=us-east-1
|
||||||
AWS_BUCKET=
|
AWS_BUCKET=
|
||||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
|
MEDIA_DISK=s3
|
||||||
|
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
|
|
||||||
9
.github/copilot-instructions.md
vendored
9
.github/copilot-instructions.md
vendored
|
|
@ -499,3 +499,12 @@ Fortify is a headless authentication backend that provides authentication routes
|
||||||
- `Features::updatePasswords()` to let users change their passwords.
|
- `Features::updatePasswords()` to let users change their passwords.
|
||||||
- `Features::resetPasswords()` for password reset via email.
|
- `Features::resetPasswords()` for password reset via email.
|
||||||
</laravel-boost-guidelines>
|
</laravel-boost-guidelines>
|
||||||
|
|
||||||
|
|
||||||
|
=== docker/core rules ===
|
||||||
|
## Over-Engineering & Bloat
|
||||||
|
- Do not add unnecessary boilerplate or "nice-to-have" features.
|
||||||
|
- Only implement what solves the immediate problem.
|
||||||
|
- Ask before adding optional infrastructure or configuration sections.
|
||||||
|
- If a system worked before without something, don't add it "just in case".
|
||||||
|
- Minimize configuration, complexity, and dependencies.
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -9,6 +9,7 @@
|
||||||
.env
|
.env
|
||||||
.env.backup
|
.env.backup
|
||||||
.env.production
|
.env.production
|
||||||
|
.env.dev
|
||||||
.phpactor.json
|
.phpactor.json
|
||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
Homestead.json
|
Homestead.json
|
||||||
|
|
@ -28,3 +29,6 @@ log*.txt
|
||||||
.envrc
|
.envrc
|
||||||
database/backups
|
database/backups
|
||||||
*backup.tar.gz
|
*backup.tar.gz
|
||||||
|
public/css
|
||||||
|
public/js
|
||||||
|
notes
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
when:
|
when:
|
||||||
- event: push
|
- event: push
|
||||||
branch: feat/docker-compose-update
|
branch: dev
|
||||||
steps:
|
steps:
|
||||||
build-local:
|
build-local:
|
||||||
image: docker:24-dind
|
image: docker:24-dind
|
||||||
|
|
@ -14,10 +14,10 @@ steps:
|
||||||
- docker pull quay.io/marshyon/share-lt:latest || true
|
- docker pull quay.io/marshyon/share-lt:latest || true
|
||||||
- echo "Building image for testing (amd64 only for CI compatibility)..."
|
- echo "Building image for testing (amd64 only for CI compatibility)..."
|
||||||
- docker build --platform linux/amd64 --cache-from=quay.io/marshyon/share-lt:latest -t share-lt:test .
|
- docker build --platform linux/amd64 --cache-from=quay.io/marshyon/share-lt:latest -t share-lt:test .
|
||||||
- echo "Tagging test image as quay.io/marshyon/share-lt:v0.0.2..."
|
- echo "Tagging test image as quay.io/marshyon/share-lt:v0.0.5..."
|
||||||
- docker tag share-lt:test quay.io/marshyon/share-lt:v0.0.2
|
- docker tag share-lt:test quay.io/marshyon/share-lt:v0.0.5
|
||||||
- echo "Generating SBOM..."
|
- echo "Generating SBOM..."
|
||||||
- docker run --rm -v /var/run/docker.sock:/var/run/docker.sock anchore/syft:latest scan quay.io/marshyon/share-lt:v0.0.2 -o cyclonedx-json > sbom.json
|
- docker run --rm -v /var/run/docker.sock:/var/run/docker.sock anchore/syft:latest scan quay.io/marshyon/share-lt:v0.0.5 -o cyclonedx-json > sbom.json
|
||||||
scan-vulnerabilities:
|
scan-vulnerabilities:
|
||||||
image: aquasec/trivy:0.67.2
|
image: aquasec/trivy:0.67.2
|
||||||
volumes:
|
volumes:
|
||||||
|
|
@ -41,7 +41,7 @@ steps:
|
||||||
repo: quay.io/marshyon/share-lt
|
repo: quay.io/marshyon/share-lt
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
tags:
|
tags:
|
||||||
- v0.0.2
|
- v0.0.5
|
||||||
- latest
|
- latest
|
||||||
username:
|
username:
|
||||||
from_secret: QUAY_USERNAME
|
from_secret: QUAY_USERNAME
|
||||||
|
|
@ -57,5 +57,6 @@ steps:
|
||||||
COSIGN_REGISTRY_PASSWORD:
|
COSIGN_REGISTRY_PASSWORD:
|
||||||
from_secret: QUAY_PASSWORD
|
from_secret: QUAY_PASSWORD
|
||||||
commands:
|
commands:
|
||||||
- cosign attach sbom --sbom sbom.json quay.io/marshyon/share-lt:v0.0.2 || echo "SBOM attach failed"
|
- cosign attach sbom --sbom sbom.json quay.io/marshyon/share-lt:v0.0.5 || echo "SBOM attach failed"
|
||||||
- echo "Done - trivy report saved to workspace for manual review"
|
- echo "Done - trivy report saved to workspace for manual review"
|
||||||
|
|
||||||
|
|
|
||||||
124
Dockerfile
124
Dockerfile
|
|
@ -1,12 +1,8 @@
|
||||||
FROM php:8.5-fpm-alpine
|
FROM php:8.4-fpm-alpine3.23
|
||||||
|
|
||||||
# Copy composer.lock and composer.json
|
ENV APP_ENV=production
|
||||||
COPY composer.lock composer.json /var/www/
|
ENV APP_DEBUG=false
|
||||||
|
|
||||||
# Set working directory
|
|
||||||
WORKDIR /var/www
|
WORKDIR /var/www
|
||||||
|
|
||||||
# # Install dependencies
|
|
||||||
RUN apk update && apk add --no-cache \
|
RUN apk update && apk add --no-cache \
|
||||||
build-base \
|
build-base \
|
||||||
libpng-dev \
|
libpng-dev \
|
||||||
|
|
@ -20,70 +16,86 @@ RUN apk update && apk add --no-cache \
|
||||||
curl \
|
curl \
|
||||||
libzip-dev \
|
libzip-dev \
|
||||||
oniguruma-dev \
|
oniguruma-dev \
|
||||||
icu-dev \
|
|
||||||
sqlite \
|
|
||||||
sqlite-dev \
|
|
||||||
nodejs \
|
nodejs \
|
||||||
npm
|
npm \
|
||||||
|
icu-dev \
|
||||||
|
sqlite-dev \
|
||||||
|
sqlite-libs \
|
||||||
|
nginx \
|
||||||
|
supervisor \
|
||||||
|
su-exec \
|
||||||
|
tini \
|
||||||
|
unzip \
|
||||||
|
bash \
|
||||||
|
jq \
|
||||||
|
&& rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
|
RUN curl -sSL https://github.com/nats-io/natscli/releases/download/v0.3.1/nats-0.3.1-linux-amd64.zip -o /tmp/nats.zip \
|
||||||
|
&& unzip /tmp/nats.zip -d /tmp/nats \
|
||||||
|
&& mv /tmp/nats/nats-0.3.1-linux-amd64/nats /usr/local/bin/nats \
|
||||||
|
&& chmod +x /usr/local/bin/nats \
|
||||||
|
&& rm -rf /tmp/nats /tmp/nats.zip
|
||||||
|
|
||||||
# # Clear cache
|
|
||||||
RUN rm -rf /var/cache/apk/*
|
RUN rm -rf /var/cache/apk/*
|
||||||
|
RUN docker-php-ext-install mbstring zip exif pcntl intl gd pdo pdo_sqlite bcmath
|
||||||
# # Install extensions
|
|
||||||
RUN docker-php-ext-install mbstring zip exif pcntl intl pdo_sqlite
|
|
||||||
|
|
||||||
RUN docker-php-ext-install gd
|
|
||||||
|
|
||||||
# # Install composer
|
|
||||||
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
||||||
|
|
||||||
# Add user for laravel application
|
# Copy entrypoint script
|
||||||
RUN addgroup -g 1000 www
|
COPY cmd/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||||
RUN adduser -u 1000 -G www -s /bin/sh -D www
|
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||||
|
|
||||||
# # Copy existing application directory contents
|
# Copy supervisord configuration
|
||||||
COPY . /var/www
|
COPY ./docker/supervisord.conf /etc/supervisord.conf
|
||||||
|
RUN mkdir -p /var/log/supervisor \
|
||||||
|
&& mkdir -p /run/nginx /var/cache/nginx /var/lib/nginx /var/tmp/nginx \
|
||||||
|
&& chown -R root:root /run/nginx /var/cache/nginx /var/lib/nginx /var/tmp/nginx
|
||||||
|
|
||||||
# # Copy existing application directory permissions
|
# Create www user and add to www-data group
|
||||||
COPY --chown=www-data:www-data . /var/www
|
RUN adduser -u 1000 -G www-data -s /bin/sh -D www
|
||||||
|
|
||||||
# # Install PHP dependencies
|
# Configure PHP-FPM to run as www user
|
||||||
RUN composer install --no-dev --optimize-autoloader
|
RUN sed -i 's/user = www-data/user = www/g' /usr/local/etc/php-fpm.d/www.conf
|
||||||
|
|
||||||
# Install Node.js dependencies
|
# Remove the semicolon to uncomment the listen directive
|
||||||
RUN npm install
|
RUN sed -i 's/;listen = 127.0.0.1:9000/listen = 9000/' /usr/local/etc/php-fpm.d/www.conf
|
||||||
|
|
||||||
# Build assets
|
# Ensure the worker running the code is correct (usually www-data or nginx)
|
||||||
RUN npm run build
|
RUN sed -i 's/;listen.owner = www-data/listen.owner = www/' /usr/local/etc/php-fpm.d/www.conf
|
||||||
|
RUN sed -i 's/;listen.group = www-data/listen.group = www-data/' /usr/local/etc/php-fpm.d/www.conf
|
||||||
|
|
||||||
# Change ownership of /var/www to www-data
|
# Update nginx.conf to use 'www' user instead of 'nginx'
|
||||||
RUN chmod -R u+rw /var/www && chown -R www:www /var/www
|
RUN sed -i 's/user nginx;/user www;/' /etc/nginx/nginx.conf
|
||||||
|
|
||||||
# # RUN php artisan optimize
|
# Remove user and group directives from nginx and php-fpm configs to avoid conflicts
|
||||||
# # removed as it calls all 4 commands below and fails du to
|
RUN sed -i '/^user /d' /etc/nginx/nginx.conf
|
||||||
# # multi-site configuration
|
RUN sed -i '/^user = /d' /usr/local/etc/php-fpm.d/www.conf
|
||||||
|
RUN sed -i '/^group = /d' /usr/local/etc/php-fpm.d/www.conf
|
||||||
|
|
||||||
# # Clear any cached configurations
|
# Set permissions for nginx directories
|
||||||
RUN php artisan config:clear
|
RUN mkdir -p /var/lib/nginx/tmp/client_body /var/log/nginx \
|
||||||
# RUN php artisan cache:clear
|
&& chown -R www:www-data /var/lib/nginx /var/log/nginx \
|
||||||
RUN php artisan route:clear
|
&& chmod -R 755 /var/lib/nginx /var/log/nginx \
|
||||||
RUN php artisan view:clear
|
&& touch /run/nginx/nginx.pid \
|
||||||
|
&& chown www:www-data /run/nginx/nginx.pid
|
||||||
|
|
||||||
# # Build optimizations
|
# Copy application code (includes database/migrations/) and excluding
|
||||||
RUN php artisan config:cache
|
# files in .dockerignore
|
||||||
RUN php artisan event:cache
|
COPY --chown=www:www-data . /var/www
|
||||||
RUN php artisan view:cache
|
RUN chown -R www:www-data /var/www
|
||||||
|
RUN chown -R www:www-data /var/log/supervisor
|
||||||
|
|
||||||
# # RUN php artisan route:cache
|
# Switch to www user
|
||||||
|
|
||||||
# # Run Laravel artisan command
|
|
||||||
RUN php artisan storage:link
|
|
||||||
|
|
||||||
# # RUN composer install --optimize-autoloader --no-dev
|
|
||||||
|
|
||||||
# # Change current user to www
|
|
||||||
USER www
|
USER www
|
||||||
|
|
||||||
# # Expose port 9000 and start php-fpm server
|
# Install app dependencies
|
||||||
# EXPOSE 9000
|
RUN composer install --optimize-autoloader --no-dev
|
||||||
|
RUN npm ci
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# run laravel cache optimization
|
||||||
|
RUN php artisan optimize
|
||||||
|
|
||||||
|
EXPOSE 8889
|
||||||
|
|
||||||
|
ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/docker-entrypoint.sh"]
|
||||||
|
CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisord.conf"]
|
||||||
|
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
FROM php:8.4-fpm-alpine3.23
|
|
||||||
|
|
||||||
ENV APP_ENV=production
|
|
||||||
ENV APP_DEBUG=false
|
|
||||||
|
|
||||||
WORKDIR /var/www
|
|
||||||
|
|
||||||
RUN apk update && apk add --no-cache \
|
|
||||||
build-base \
|
|
||||||
libpng-dev \
|
|
||||||
libjpeg-turbo-dev \
|
|
||||||
freetype-dev \
|
|
||||||
zip \
|
|
||||||
jpegoptim optipng pngquant gifsicle \
|
|
||||||
vim \
|
|
||||||
unzip \
|
|
||||||
git \
|
|
||||||
curl \
|
|
||||||
libzip-dev \
|
|
||||||
oniguruma-dev \
|
|
||||||
nodejs \
|
|
||||||
npm \
|
|
||||||
icu-dev \
|
|
||||||
sqlite-dev \
|
|
||||||
sqlite-libs \
|
|
||||||
nginx \
|
|
||||||
supervisor \
|
|
||||||
su-exec \
|
|
||||||
tini
|
|
||||||
|
|
||||||
RUN rm -rf /var/cache/apk/*
|
|
||||||
|
|
||||||
RUN docker-php-ext-install mbstring zip exif pcntl intl gd pdo pdo_sqlite bcmath
|
|
||||||
|
|
||||||
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
|
||||||
|
|
||||||
# RUN addgroup -g 1000 www
|
|
||||||
# RUN adduser -u 1000 -G www -s /bin/sh -D www
|
|
||||||
|
|
||||||
# # Configure PHP-FPM to run as www user
|
|
||||||
# RUN sed -i 's/user = www-data/user = www/g' /usr/local/etc/php-fpm.d/www.conf && \
|
|
||||||
# sed -i 's/group = www-data/group = www/g' /usr/local/etc/php-fpm.d/www.conf
|
|
||||||
|
|
||||||
# Copy application code (includes database/migrations/)
|
|
||||||
COPY . /var/www
|
|
||||||
|
|
||||||
|
|
||||||
# DEBUG - SHOW ME WHAT WAS COPIED
|
|
||||||
# RUN echo "===== CONTENTS OF /var/www/database =====" && ls -la /var/www/database/
|
|
||||||
# RUN echo "===== CONTENTS OF /var/www/database/migrations =====" && ls -la /var/www/database/migrations/
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
RUN composer install --optimize-autoloader --no-dev
|
|
||||||
RUN npm install
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Copy entrypoint script
|
|
||||||
COPY cmd/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
|
||||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
|
||||||
|
|
||||||
|
|
||||||
COPY ./docker/supervisord.conf /etc/supervisord.conf
|
|
||||||
RUN mkdir -p /var/log/supervisor \
|
|
||||||
&& mkdir -p /run/nginx /var/cache/nginx /var/lib/nginx /var/tmp/nginx \
|
|
||||||
&& chown -R root:root /run/nginx /var/cache/nginx /var/lib/nginx /var/tmp/nginx
|
|
||||||
|
|
||||||
# Test nginx config at build time
|
|
||||||
RUN nginx -t
|
|
||||||
|
|
||||||
# keep running as root so supervisord starts nginx/php-fpm as root (nginx needs root for master process)
|
|
||||||
# we will use su-exec in entrypoint to run maintenance steps as www, preserving previous behaviour
|
|
||||||
|
|
||||||
EXPOSE 8889
|
|
||||||
|
|
||||||
# Keep entrypoint script as before; entrypoint runs startup tasks then supervisord becomes PID 1
|
|
||||||
# ENTRYPOINT ["docker-entrypoint.sh"]
|
|
||||||
# CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisord.conf"]
|
|
||||||
|
|
||||||
ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/docker-entrypoint.sh"]
|
|
||||||
CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisord.conf"]
|
|
||||||
50
app/Filament/Resources/Assets/AssetResource.php
Normal file
50
app/Filament/Resources/Assets/AssetResource.php
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Assets;
|
||||||
|
|
||||||
|
use App\Filament\Resources\Assets\Pages\CreateAsset;
|
||||||
|
use App\Filament\Resources\Assets\Pages\EditAsset;
|
||||||
|
use App\Filament\Resources\Assets\Pages\ListAssets;
|
||||||
|
use App\Filament\Resources\Assets\Schemas\AssetForm;
|
||||||
|
use App\Filament\Resources\Assets\Tables\AssetsTable;
|
||||||
|
use App\Models\Asset;
|
||||||
|
use BackedEnum;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Support\Icons\Heroicon;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
|
class AssetResource extends Resource
|
||||||
|
{
|
||||||
|
protected static bool $shouldRegisterNavigation = false;
|
||||||
|
|
||||||
|
protected static ?string $model = Asset::class;
|
||||||
|
|
||||||
|
protected static string|BackedEnum|null $navigationIcon = Heroicon::ArrowUpTray;
|
||||||
|
|
||||||
|
public static function form(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return AssetForm::configure($schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return AssetsTable::configure($table);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => ListAssets::route('/'),
|
||||||
|
'create' => CreateAsset::route('/create'),
|
||||||
|
'edit' => EditAsset::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
11
app/Filament/Resources/Assets/Pages/CreateAsset.php
Normal file
11
app/Filament/Resources/Assets/Pages/CreateAsset.php
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Assets\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\Assets\AssetResource;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
|
class CreateAsset extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = AssetResource::class;
|
||||||
|
}
|
||||||
19
app/Filament/Resources/Assets/Pages/EditAsset.php
Normal file
19
app/Filament/Resources/Assets/Pages/EditAsset.php
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Assets\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\Assets\AssetResource;
|
||||||
|
use Filament\Actions\DeleteAction;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
|
class EditAsset extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = AssetResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
DeleteAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
19
app/Filament/Resources/Assets/Pages/ListAssets.php
Normal file
19
app/Filament/Resources/Assets/Pages/ListAssets.php
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Assets\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\Assets\AssetResource;
|
||||||
|
use Filament\Actions\CreateAction;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListAssets extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = AssetResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
CreateAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/Filament/Resources/Assets/Schemas/AssetForm.php
Normal file
24
app/Filament/Resources/Assets/Schemas/AssetForm.php
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Assets\Schemas;
|
||||||
|
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Components\SpatieMediaLibraryFileUpload;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
|
||||||
|
class AssetForm
|
||||||
|
{
|
||||||
|
public static function configure(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return $schema
|
||||||
|
->components([
|
||||||
|
TextInput::make('alt_text'),
|
||||||
|
SpatieMediaLibraryFileUpload::make('image')
|
||||||
|
->collection('default')
|
||||||
|
->image()
|
||||||
|
->disk('s3')
|
||||||
|
->visibility('public')
|
||||||
|
->label('Upload Image'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
40
app/Filament/Resources/Assets/Tables/AssetsTable.php
Normal file
40
app/Filament/Resources/Assets/Tables/AssetsTable.php
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Assets\Tables;
|
||||||
|
|
||||||
|
use Filament\Actions\BulkActionGroup;
|
||||||
|
use Filament\Actions\DeleteBulkAction;
|
||||||
|
use Filament\Actions\EditAction;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
|
class AssetsTable
|
||||||
|
{
|
||||||
|
public static function configure(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
TextColumn::make('alt_text')
|
||||||
|
->searchable(),
|
||||||
|
TextColumn::make('created_at')
|
||||||
|
->dateTime()
|
||||||
|
->sortable()
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
TextColumn::make('updated_at')
|
||||||
|
->dateTime()
|
||||||
|
->sortable()
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
//
|
||||||
|
])
|
||||||
|
->recordActions([
|
||||||
|
EditAction::make(),
|
||||||
|
])
|
||||||
|
->toolbarActions([
|
||||||
|
BulkActionGroup::make([
|
||||||
|
DeleteBulkAction::make(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -52,6 +52,7 @@ class EntryForm
|
||||||
->visible(fn($get) => $get('type') === 'article')
|
->visible(fn($get) => $get('type') === 'article')
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
SpatieMediaLibraryFileUpload::make('featured_image')
|
SpatieMediaLibraryFileUpload::make('featured_image')
|
||||||
|
->multiple() // <- force array handling for Filament v4 bug
|
||||||
->visible(
|
->visible(
|
||||||
fn($get) =>
|
fn($get) =>
|
||||||
$get('type') === 'article' || $get('type') === 'image'
|
$get('type') === 'article' || $get('type') === 'image'
|
||||||
|
|
@ -64,9 +65,63 @@ class EntryForm
|
||||||
->columnSpanFull()
|
->columnSpanFull()
|
||||||
->dehydrated(false)
|
->dehydrated(false)
|
||||||
->saveUploadedFileUsing(function ($file, $record) {
|
->saveUploadedFileUsing(function ($file, $record) {
|
||||||
$diskName = config('media-library.disk_name', 'public');
|
if (is_array($file)) {
|
||||||
|
$file = reset($file);
|
||||||
|
}
|
||||||
|
|
||||||
if (config('app.env') === 'local') {
|
// Validate upload object early
|
||||||
|
if (
|
||||||
|
! is_object($file) ||
|
||||||
|
! (method_exists($file, 'getRealPath') || method_exists($file, 'getPathname') || method_exists($file, 'getStream') || method_exists($file, 'store'))
|
||||||
|
) {
|
||||||
|
Log::error('Invalid upload object', ['type' => gettype($file)]);
|
||||||
|
throw new \Exception('Invalid upload object provided to saveUploadedFileUsing');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use safe variables for further calls
|
||||||
|
$realPath = method_exists($file, 'getRealPath') ? $file->getRealPath() : null;
|
||||||
|
$exists = $realPath ? file_exists($realPath) : false;
|
||||||
|
$name = method_exists($file, 'getClientOriginalName') ? $file->getClientOriginalName() : null;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Log::info('TemporaryUploadedFile Debug', [
|
||||||
|
'path' => $file->getRealPath(),
|
||||||
|
'exists' => file_exists($file->getRealPath()),
|
||||||
|
'name' => $file->getClientOriginalName(),
|
||||||
|
'temp_dir' => sys_get_temp_dir(),
|
||||||
|
'disk_root' => config('filesystems.disks.local.root'),
|
||||||
|
'is_readable' => is_readable($file->getRealPath()),
|
||||||
|
'is_writable' => is_writable($file->getRealPath()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Additional debug: Check if the file is being moved to livewire-tmp
|
||||||
|
$livewireTmpPath = storage_path('framework/livewire-tmp');
|
||||||
|
Log::info('Livewire Temp Directory Debug', [
|
||||||
|
'livewire_tmp_path' => $livewireTmpPath,
|
||||||
|
'exists' => file_exists($livewireTmpPath),
|
||||||
|
'is_writable' => is_writable($livewireTmpPath),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Check if the file is being moved
|
||||||
|
$tempFilePath = $file->getRealPath();
|
||||||
|
$newFilePath = $livewireTmpPath . '/' . $file->getClientOriginalName();
|
||||||
|
if (file_exists($tempFilePath)) {
|
||||||
|
Log::info('File exists in temp directory', ['temp_file_path' => $tempFilePath]);
|
||||||
|
} else {
|
||||||
|
Log::error('File does not exist in temp directory', ['temp_file_path' => $tempFilePath]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// $diskName = config('media-library.disk_name', 'public');
|
||||||
|
$diskName = config('media-library.disk_name');
|
||||||
|
|
||||||
|
if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') {
|
||||||
Log::info('Featured Image Upload Debug', [
|
Log::info('Featured Image Upload Debug', [
|
||||||
'disk' => $diskName,
|
'disk' => $diskName,
|
||||||
'file_name' => $file->getClientOriginalName(),
|
'file_name' => $file->getClientOriginalName(),
|
||||||
|
|
@ -97,7 +152,7 @@ class EntryForm
|
||||||
$disk->put($testFile, 'test content');
|
$disk->put($testFile, 'test content');
|
||||||
$disk->delete($testFile);
|
$disk->delete($testFile);
|
||||||
|
|
||||||
if (config('app.env') === 'local') {
|
if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') {
|
||||||
Log::info('S3 connectivity test passed');
|
Log::info('S3 connectivity test passed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -112,12 +167,114 @@ class EntryForm
|
||||||
$encodedName = base64_encode($originalName);
|
$encodedName = base64_encode($originalName);
|
||||||
$secureFileName = Str::random(32) . '-meta' . $encodedName . '-.' . $extension;
|
$secureFileName = Str::random(32) . '-meta' . $encodedName . '-.' . $extension;
|
||||||
|
|
||||||
$media = $record->addMedia($file->getRealPath())
|
|
||||||
->usingName($baseName) // Keep original name for display/search
|
|
||||||
->usingFileName($secureFileName) // Use secure filename for storage
|
|
||||||
|
|
||||||
|
// Resolve possibly-relative Livewire temp path + safe fallbacks
|
||||||
|
$realPath = $realPath ?: (method_exists($file, 'getRealPath') ? $file->getRealPath() : null);
|
||||||
|
$candidates = [];
|
||||||
|
|
||||||
|
if ($realPath && str_starts_with($realPath, '/')) {
|
||||||
|
$candidates[] = $realPath;
|
||||||
|
} else {
|
||||||
|
$candidates[] = sys_get_temp_dir() . '/' . ltrim((string)$realPath, '/');
|
||||||
|
$candidates[] = storage_path('framework/' . ltrim((string)$realPath, '/'));
|
||||||
|
$candidates[] = storage_path(ltrim((string)$realPath, '/'));
|
||||||
|
$candidates[] = base_path(ltrim((string)$realPath, '/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($realPath) {
|
||||||
|
$candidates[] = storage_path('framework/livewire-tmp/' . basename($realPath));
|
||||||
|
$candidates[] = sys_get_temp_dir() . '/' . basename($realPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Try storing to local disk (creates an absolute path we control)
|
||||||
|
$stored = null;
|
||||||
|
if (method_exists($file, 'store')) {
|
||||||
|
try {
|
||||||
|
$stored = $file->store('livewire-temp', 'local'); // storage/app/livewire-temp/...
|
||||||
|
if ($stored && \Storage::disk('local')->exists($stored)) {
|
||||||
|
$resolved = \Storage::disk('local')->path($stored);
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::debug('store() fallback failed', ['err' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) If not resolved, check candidates
|
||||||
|
if (! isset($resolved)) {
|
||||||
|
foreach ($candidates as $p) {
|
||||||
|
if ($p && file_exists($p)) {
|
||||||
|
$resolved = $p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) If still not resolved, try stream -> temp file copy
|
||||||
|
$is_tmp_copy = false;
|
||||||
|
if (! isset($resolved)) {
|
||||||
|
try {
|
||||||
|
$stream = null;
|
||||||
|
if (method_exists($file, 'getStream')) {
|
||||||
|
$stream = $file->getStream();
|
||||||
|
} elseif (method_exists($file, 'getRealPath') && is_readable($file->getRealPath())) {
|
||||||
|
$stream = fopen($file->getRealPath(), 'r');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($stream) {
|
||||||
|
$tmpPath = tempnam(sys_get_temp_dir(), 'filament-upload-');
|
||||||
|
$out = fopen($tmpPath, 'w');
|
||||||
|
stream_copy_to_stream($stream, $out);
|
||||||
|
fclose($out);
|
||||||
|
if (is_resource($stream)) {
|
||||||
|
@fclose($stream);
|
||||||
|
}
|
||||||
|
$resolved = $tmpPath;
|
||||||
|
$is_tmp_copy = true;
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::debug('stream fallback failed', ['err' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Still nothing -> error
|
||||||
|
if (empty($resolved)) {
|
||||||
|
Log::error('Featured Image Upload: could not resolve temp path', [
|
||||||
|
'original' => $realPath,
|
||||||
|
'checked_candidates' => $candidates,
|
||||||
|
'stored' => $stored,
|
||||||
|
]);
|
||||||
|
throw new \Exception("File `{$realPath}` does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) Use resolved absolute path
|
||||||
|
$media = $record->addMedia($resolved)
|
||||||
|
->usingName($baseName)
|
||||||
|
->usingFileName($secureFileName)
|
||||||
->toMediaCollection('featured-image', $diskName);
|
->toMediaCollection('featured-image', $diskName);
|
||||||
|
|
||||||
if (config('app.env') === 'local') {
|
// 6) Cleanup short-lived artifacts
|
||||||
|
if (! empty($is_tmp_copy) && file_exists($resolved)) {
|
||||||
|
@unlink($resolved);
|
||||||
|
}
|
||||||
|
if (! empty($stored) && \Storage::disk('local')->exists($stored)) {
|
||||||
|
\Storage::disk('local')->delete($stored);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('Featured image resolved', ['resolved' => $resolved, 'media_id' => $media->id ?? null]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') {
|
||||||
Log::info('Featured Image Upload Success', [
|
Log::info('Featured Image Upload Success', [
|
||||||
'media_id' => $media->id,
|
'media_id' => $media->id,
|
||||||
'media_url' => $media->getUrl(),
|
'media_url' => $media->getUrl(),
|
||||||
|
|
@ -189,7 +346,7 @@ class EntryForm
|
||||||
$record = $component->getRecord();
|
$record = $component->getRecord();
|
||||||
$diskName = config('media-library.disk_name', 'public');
|
$diskName = config('media-library.disk_name', 'public');
|
||||||
|
|
||||||
if (config('app.env') === 'local') {
|
if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') {
|
||||||
Log::info('Featured Image Picker Action Debug', [
|
Log::info('Featured Image Picker Action Debug', [
|
||||||
'disk' => $diskName,
|
'disk' => $diskName,
|
||||||
'record_id' => $record?->id,
|
'record_id' => $record?->id,
|
||||||
|
|
@ -223,7 +380,7 @@ class EntryForm
|
||||||
try {
|
try {
|
||||||
// For S3, we need to handle file copying differently
|
// For S3, we need to handle file copying differently
|
||||||
if ($sourceMedia->disk === 's3') {
|
if ($sourceMedia->disk === 's3') {
|
||||||
if (config('app.env') === 'local') {
|
if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') {
|
||||||
Log::info('Copying S3 media to new collection', [
|
Log::info('Copying S3 media to new collection', [
|
||||||
'source_disk' => $sourceMedia->disk,
|
'source_disk' => $sourceMedia->disk,
|
||||||
'source_path' => $sourceMedia->getPathRelativeToRoot(),
|
'source_path' => $sourceMedia->getPathRelativeToRoot(),
|
||||||
|
|
@ -267,7 +424,7 @@ class EntryForm
|
||||||
->usingFileName($sourceMedia->file_name)
|
->usingFileName($sourceMedia->file_name)
|
||||||
->toMediaCollection('featured-image', $diskName);
|
->toMediaCollection('featured-image', $diskName);
|
||||||
|
|
||||||
if (config('app.env') === 'local') {
|
if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') {
|
||||||
Log::info('Featured Image Picker Success', [
|
Log::info('Featured Image Picker Success', [
|
||||||
'new_media_id' => $newMedia->id,
|
'new_media_id' => $newMedia->id,
|
||||||
'new_media_disk' => $newMedia->disk,
|
'new_media_disk' => $newMedia->disk,
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ class ListMedia extends ListRecords
|
||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
CreateAction::make(),
|
// CreateAction::make(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ use Filament\Forms\Components\Hidden;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
use Spatie\MediaLibrary\MediaCollections\Models\Media as SpatieMedia;
|
use Spatie\MediaLibrary\MediaCollections\Models\Media as SpatieMedia;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class MediaForm
|
class MediaForm
|
||||||
{
|
{
|
||||||
|
|
@ -24,6 +26,7 @@ class MediaForm
|
||||||
Hidden::make('disk')
|
Hidden::make('disk')
|
||||||
->default('public'),
|
->default('public'),
|
||||||
FileUpload::make('file')
|
FileUpload::make('file')
|
||||||
|
->multiple() // workaround for Filament v4 single-file bug
|
||||||
->label('File')
|
->label('File')
|
||||||
->imageEditor()
|
->imageEditor()
|
||||||
->imageEditorAspectRatios([
|
->imageEditorAspectRatios([
|
||||||
|
|
@ -38,13 +41,17 @@ class MediaForm
|
||||||
->acceptedFileTypes(['image/*', 'application/pdf'])
|
->acceptedFileTypes(['image/*', 'application/pdf'])
|
||||||
->maxSize(10240)
|
->maxSize(10240)
|
||||||
->required(fn($context) => $context === 'create')
|
->required(fn($context) => $context === 'create')
|
||||||
|
|
||||||
|
|
||||||
->afterStateHydrated(function (FileUpload $component, $state, $record): void {
|
->afterStateHydrated(function (FileUpload $component, $state, $record): void {
|
||||||
|
Log::info('MediaForm afterStateHydrated invoked', ['record_id' => $record?->id, 'state' => $state]);
|
||||||
|
|
||||||
|
try {
|
||||||
if (! $record) {
|
if (! $record) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$media = $record;
|
$media = $record;
|
||||||
|
|
||||||
if (! $media instanceof SpatieMedia) {
|
if (! $media instanceof SpatieMedia) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -52,8 +59,32 @@ class MediaForm
|
||||||
// Construct the correct path: {media_id}/{filename}
|
// Construct the correct path: {media_id}/{filename}
|
||||||
$path = $media->id . '/' . $media->file_name;
|
$path = $media->id . '/' . $media->file_name;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$disk = $media->disk ?? 'public';
|
||||||
|
if (Storage::disk($disk)->exists($path)) {
|
||||||
$component->state($path);
|
$component->state($path);
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::error('MediaForm afterStateHydrated storage check failed', [
|
||||||
|
'err' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
'disk' => $media->disk ?? null,
|
||||||
|
'path' => $path,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::error('MediaForm afterStateHydrated unhandled', [
|
||||||
|
'err' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
'record' => $record?->id,
|
||||||
|
'state' => $state,
|
||||||
|
]);
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace App\Filament\Resources\Media\Tables;
|
namespace App\Filament\Resources\Media\Tables;
|
||||||
|
|
||||||
|
use Filament\Actions\Action;
|
||||||
use Filament\Actions\BulkActionGroup;
|
use Filament\Actions\BulkActionGroup;
|
||||||
use Filament\Actions\DeleteAction;
|
use Filament\Actions\DeleteAction;
|
||||||
use Filament\Actions\DeleteBulkAction;
|
use Filament\Actions\DeleteBulkAction;
|
||||||
|
|
@ -23,7 +24,8 @@ class MediaTable
|
||||||
->columns([
|
->columns([
|
||||||
ImageColumn::make('url')
|
ImageColumn::make('url')
|
||||||
->label('Preview')
|
->label('Preview')
|
||||||
->getStateUsing(fn ($record) =>
|
->getStateUsing(
|
||||||
|
fn($record) =>
|
||||||
// Prefer the stored path produced by Filament's FileUpload (saved in custom_properties),
|
// Prefer the stored path produced by Filament's FileUpload (saved in custom_properties),
|
||||||
// fall back to Spatie's getUrl() when no stored_path exists.
|
// fall back to Spatie's getUrl() when no stored_path exists.
|
||||||
($record->getCustomProperty('stored_path'))
|
($record->getCustomProperty('stored_path'))
|
||||||
|
|
@ -52,7 +54,7 @@ class MediaTable
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
->recordActions([
|
->recordActions([
|
||||||
EditAction::make(),
|
// EditAction::make(),
|
||||||
DeleteAction::make()
|
DeleteAction::make()
|
||||||
->action(function (Media $record) {
|
->action(function (Media $record) {
|
||||||
// Delete the actual stored file path if we saved one, otherwise fall back to the Spatie path.
|
// Delete the actual stored file path if we saved one, otherwise fall back to the Spatie path.
|
||||||
|
|
@ -67,6 +69,13 @@ class MediaTable
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
->toolbarActions([
|
->toolbarActions([
|
||||||
|
|
||||||
|
Action::make('add')
|
||||||
|
->label('Add')
|
||||||
|
->icon('heroicon-o-plus')
|
||||||
|
->url(fn() => url('admin/assets/create'))
|
||||||
|
->color('primary'),
|
||||||
|
|
||||||
BulkActionGroup::make([
|
BulkActionGroup::make([
|
||||||
DeleteBulkAction::make()
|
DeleteBulkAction::make()
|
||||||
->action(function (Collection $records) {
|
->action(function (Collection $records) {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ use Filament\Tables\Table;
|
||||||
|
|
||||||
class TextWidgetResource extends Resource
|
class TextWidgetResource extends Resource
|
||||||
{
|
{
|
||||||
|
|
||||||
|
protected static bool $shouldRegisterNavigation = false;
|
||||||
|
|
||||||
protected static ?string $model = TextWidget::class;
|
protected static ?string $model = TextWidget::class;
|
||||||
|
|
||||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::DocumentText;
|
protected static string|BackedEnum|null $navigationIcon = Heroicon::DocumentText;
|
||||||
|
|
|
||||||
98
app/Jobs/ProcessEntryUpdate.php
Normal file
98
app/Jobs/ProcessEntryUpdate.php
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\Entry;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Queue\Queueable;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\Process;
|
||||||
|
use Symfony\Component\Console\Exception\RuntimeException;
|
||||||
|
|
||||||
|
class ProcessEntryUpdate implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public int $entryId,
|
||||||
|
public string $action
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
|
||||||
|
$incoming = [
|
||||||
|
'id' => $this->entryId,
|
||||||
|
'action' => $this->action,
|
||||||
|
];
|
||||||
|
|
||||||
|
$jsonData = json_encode($incoming, JSON_THROW_ON_ERROR);
|
||||||
|
$tempFile = tempnam(sys_get_temp_dir(), 'entry_update_');
|
||||||
|
file_put_contents($tempFile, $jsonData);
|
||||||
|
|
||||||
|
$scriptPath = env('HANDLE_ENTRY_UPDATES_SCRIPT', base_path('cmd/handle_cms_updates.sh'));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Log what we're about to execute
|
||||||
|
Log::info("Executing script: {$scriptPath} with action: {$this->action}", [
|
||||||
|
'entry_id' => $this->entryId,
|
||||||
|
'temp_file' => $tempFile,
|
||||||
|
'script_exists' => file_exists($scriptPath),
|
||||||
|
'script_executable' => is_executable($scriptPath),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = Process::run([
|
||||||
|
'bash',
|
||||||
|
$scriptPath,
|
||||||
|
$this->action,
|
||||||
|
$tempFile
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($result->failed()) {
|
||||||
|
$errorDetails = [
|
||||||
|
'exit_code' => $result->exitCode(),
|
||||||
|
'stdout' => $result->output(),
|
||||||
|
'stderr' => $result->errorOutput(),
|
||||||
|
'command' => ['bash', $scriptPath, $this->action, $tempFile],
|
||||||
|
'script_path' => $scriptPath,
|
||||||
|
'script_exists' => file_exists($scriptPath),
|
||||||
|
'temp_file_exists' => file_exists($tempFile),
|
||||||
|
'temp_file_contents' => file_exists($tempFile) ? file_get_contents($tempFile) : 'N/A',
|
||||||
|
];
|
||||||
|
|
||||||
|
Log::error("Script execution failed", $errorDetails);
|
||||||
|
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Script execution failed with exit code {$result->exitCode()}. " .
|
||||||
|
"STDOUT: " . ($result->output() ?: 'empty') . " " .
|
||||||
|
"STDERR: " . ($result->errorOutput() ?: 'empty')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info("Script executed successfully", [
|
||||||
|
'stdout' => $result->output(),
|
||||||
|
'entry_id' => $this->entryId,
|
||||||
|
'action' => $this->action,
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
// Clean up temp file
|
||||||
|
if (file_exists($tempFile)) {
|
||||||
|
unlink($tempFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
app/Models/Asset.php
Normal file
16
app/Models/Asset.php
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Spatie\MediaLibrary\HasMedia;
|
||||||
|
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||||
|
|
||||||
|
class Asset extends Model implements HasMedia
|
||||||
|
{
|
||||||
|
use InteractsWithMedia;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'alt_text',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -73,6 +73,6 @@ class User extends Authenticatable implements FilamentUser
|
||||||
*/
|
*/
|
||||||
public function canAccessPanel(Panel $panel): bool
|
public function canAccessPanel(Panel $panel): bool
|
||||||
{
|
{
|
||||||
return $this->email === config('app.admin_email');
|
return $this->email === config('app.admin_email') || $this->role === 'admin';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
50
app/Observers/EntryObserver.php
Normal file
50
app/Observers/EntryObserver.php
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Observers;
|
||||||
|
|
||||||
|
use App\Jobs\ProcessEntryUpdate;
|
||||||
|
use App\Models\Entry;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class EntryObserver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle the Entry "created" event.
|
||||||
|
*/
|
||||||
|
public function created(Entry $entry): void
|
||||||
|
{
|
||||||
|
ProcessEntryUpdate::dispatch($entry->id, 'created');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the Entry "updated" event.
|
||||||
|
*/
|
||||||
|
public function updated(Entry $entry): void
|
||||||
|
{
|
||||||
|
ProcessEntryUpdate::dispatch($entry->id, 'updated');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the Entry "deleted" event.
|
||||||
|
*/
|
||||||
|
public function deleted(Entry $entry): void
|
||||||
|
{
|
||||||
|
ProcessEntryUpdate::dispatch($entry->id, 'deleted');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the Entry "restored" event.
|
||||||
|
*/
|
||||||
|
public function restored(Entry $entry): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the Entry "force deleted" event.
|
||||||
|
*/
|
||||||
|
public function forceDeleted(Entry $entry): void
|
||||||
|
{
|
||||||
|
// ProcessEntryUpdate::dispatch($entry, 'force deleted');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,10 @@
|
||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Models\Entry;
|
||||||
|
use App\Observers\EntryObserver;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
|
|
@ -19,6 +22,12 @@ class AppServiceProvider extends ServiceProvider
|
||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
//
|
// Ensure the livewire-tmp directory exists
|
||||||
|
$livewireTmpPath = storage_path('framework/livewire-tmp');
|
||||||
|
if (!File::exists($livewireTmpPath)) {
|
||||||
|
File::makeDirectory($livewireTmpPath, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Entry::observe(EntryObserver::class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,10 +36,10 @@ class AdminPanelProvider extends PanelProvider
|
||||||
])
|
])
|
||||||
->resources([
|
->resources([
|
||||||
\App\Filament\Resources\Entries\EntryResource::class,
|
\App\Filament\Resources\Entries\EntryResource::class,
|
||||||
\App\Filament\Resources\TextWidgets\TextWidgetResource::class,
|
|
||||||
\App\Filament\Resources\Media\MediaResource::class,
|
\App\Filament\Resources\Media\MediaResource::class,
|
||||||
\App\Filament\Resources\Categroys\CategroyResource::class,
|
\App\Filament\Resources\Categroys\CategroyResource::class,
|
||||||
])
|
])
|
||||||
|
->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources')
|
||||||
->discoverPages(in: app_path('Filament/Pages'), for: 'App\Filament\Pages')
|
->discoverPages(in: app_path('Filament/Pages'), for: 'App\Filament\Pages')
|
||||||
->pages([
|
->pages([
|
||||||
Dashboard::class,
|
Dashboard::class,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
LARAVEL_CONTAINER_NAME="quay.io/marshyon/share-lt"
|
LARAVEL_CONTAINER_NAME="quay.io/marshyon/share-lt"
|
||||||
CONTAINER_LABEL="0.0.3"
|
CONTAINER_LABEL="v0.0.5"
|
||||||
CACHE="--no-cache"
|
CACHE="--no-cache"
|
||||||
CACHE=""
|
# CACHE=""
|
||||||
|
|
||||||
docker build \
|
docker build \
|
||||||
$CACHE \
|
$CACHE \
|
||||||
-t ${LARAVEL_CONTAINER_NAME}:${CONTAINER_LABEL} \
|
-t ${LARAVEL_CONTAINER_NAME}:${CONTAINER_LABEL} .
|
||||||
-f Dockerfile.phpfpm .
|
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,7 @@
|
||||||
# tokens need to be created with tinker or similar method
|
# tokens need to be created with tinker or similar method
|
||||||
|
|
||||||
|
|
||||||
URL='http://127.0.0.1:8000/api/entries'
|
URL="${ADDRESS:-http://127.0.0.1:8000/api/entries}"
|
||||||
|
|
||||||
curl -s -X GET \
|
curl -s -X GET \
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
-H "Accept: application/json" \
|
-H "Accept: application/json" \
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,7 @@
|
||||||
# granted access to view entries by being given
|
# granted access to view entries by being given
|
||||||
# a token
|
# a token
|
||||||
|
|
||||||
URL='http://127.0.0.1:8000/api/entries'
|
URL="${ADDRESS:-http://127.0.0.1:8000/api/entries}"
|
||||||
|
|
||||||
curl -s -X GET \
|
curl -s -X GET \
|
||||||
-H "Accept: application/json" \
|
-H "Accept: application/json" \
|
||||||
$URL
|
$URL
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,37 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# run_as_www() {
|
# Ensure APP_KEY is set and persisted
|
||||||
# # prefer su-exec (alpine), fallback to runuser if available, otherwise run directly
|
PERSISTED_KEY="/var/www/storage/.app_key"
|
||||||
# if command -v su-exec >/dev/null 2>&1; then
|
|
||||||
# su-exec www "$@"
|
|
||||||
# elif command -v runuser >/dev/null 2>&1; then
|
|
||||||
# runuser -u www -- "$@"
|
|
||||||
# else
|
|
||||||
# "$@"
|
|
||||||
# fi
|
|
||||||
# }
|
|
||||||
|
|
||||||
# Build front-end assets if Vite manifest is missing
|
if [ -z "$APP_KEY" ]; then
|
||||||
if [ ! -f /var/www/public/build/manifest.json ]; then
|
if [ -f "$PERSISTED_KEY" ]; then
|
||||||
echo "Building front-end assets (vite)..."
|
echo "Using persisted APP_KEY from: $PERSISTED_KEY"
|
||||||
run_as_www npm ci
|
export APP_KEY=$(cat "$PERSISTED_KEY")
|
||||||
run_as_www npm run build
|
else
|
||||||
|
# Generate key, strip "base64:", and save
|
||||||
|
NEW_KEY=$(php artisan key:generate --show --no-interaction)
|
||||||
|
echo "Generated new APP_KEY to: $PERSISTED_KEY"
|
||||||
|
echo "$NEW_KEY" > "$PERSISTED_KEY"
|
||||||
|
export APP_KEY="$NEW_KEY"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Wait for database directory to be mounted
|
# check to see if /var/www/database/database.sqlite exists
|
||||||
# if [ ! -f /var/www/database/database.sqlite ]; then
|
# if not, run migrations
|
||||||
# echo "Creating database..."
|
if [ ! -f /var/www/database/database.sqlite ]; then
|
||||||
# # create the sqlite file as the www user so ownership matches app files
|
php artisan migrate --force
|
||||||
# run_as_www sh -c 'touch /var/www/database/database.sqlite'
|
fi
|
||||||
# run_as_www php artisan migrate --force
|
|
||||||
# fi
|
|
||||||
|
|
||||||
# Fix storage permissions
|
# check to see if /var/www/public/storage exists
|
||||||
# echo "Fixing storage permissions..."
|
# if not, run storage:link
|
||||||
# chown -R www:www /var/www/storage /var/www/bootstrap/cache
|
if [ ! -d /var/www/public/storage ]; then
|
||||||
# chmod -R 775 /var/www/storage /var/www/bootstrap/cache
|
php artisan storage:link
|
||||||
|
fi
|
||||||
|
|
||||||
# Execute the main command
|
php artisan config:clear
|
||||||
|
|
||||||
|
|
||||||
|
# Start supervisord directly
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|
||||||
|
|
|
||||||
33
cmd/handle_cms_updates.sh
Executable file
33
cmd/handle_cms_updates.sh
Executable file
|
|
@ -0,0 +1,33 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
LOG_FILE="/tmp/logfile.log"
|
||||||
|
|
||||||
|
# Redirect all output to both stdout and the log file
|
||||||
|
exec > >(tee -a "$LOG_FILE") 2>&1
|
||||||
|
|
||||||
|
# Read the first two command line arguments
|
||||||
|
ACTION=$1
|
||||||
|
FILENAME=$2
|
||||||
|
|
||||||
|
# Check if the file exists and echo its contents
|
||||||
|
if [[ -f "$FILENAME" ]]; then
|
||||||
|
echo "Contents of the file $FILENAME:"
|
||||||
|
cat "$FILENAME" | jq
|
||||||
|
else
|
||||||
|
echo "Error: File $FILENAME does not exist."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
# Read and print command line arguments
|
||||||
|
echo "=============================="
|
||||||
|
echo "ACTION: $ACTION"
|
||||||
|
echo "FILENAME: $FILENAME"
|
||||||
|
echo "=============================="
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Publish message and check return code
|
||||||
|
if nats pub $NATS_SUBJECT "$(cat "$FILENAME")" --server $NATS_URL --user $NATS_USERNAME --password $NATS_PASSWORD; then
|
||||||
|
echo "Success: Message published to NATS successfully."
|
||||||
|
else
|
||||||
|
echo "Error: Failed to publish message to NATS."
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('assets', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('alt_text')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('assets');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->string('role')->default('user')->after('email');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('role');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
20
docker/php/php.ini
Normal file
20
docker/php/php.ini
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
; PHP Configuration File
|
||||||
|
|
||||||
|
; Maximum allowed size for uploaded files.
|
||||||
|
upload_max_filesize = 10M
|
||||||
|
|
||||||
|
; Maximum size of POST data that PHP will accept.
|
||||||
|
post_max_size = 12M
|
||||||
|
|
||||||
|
; Maximum execution time of each script, in seconds.
|
||||||
|
max_execution_time = 120
|
||||||
|
|
||||||
|
; Maximum amount of memory a script may consume.
|
||||||
|
memory_limit = 256M
|
||||||
|
|
||||||
|
; Maximum number of files that can be uploaded via a single request.
|
||||||
|
max_file_uploads = 20
|
||||||
|
|
||||||
|
; File upload temporary directory (optional, defaults to system temp directory).
|
||||||
|
; upload_tmp_dir = /tmp
|
||||||
|
|
||||||
|
|
@ -24,7 +24,7 @@ stderr_logfile=/dev/stderr
|
||||||
stderr_logfile_maxbytes=0
|
stderr_logfile_maxbytes=0
|
||||||
|
|
||||||
[program:queue-worker]
|
[program:queue-worker]
|
||||||
command=su-exec www /usr/local/bin/php /var/www/artisan queue:work --sleep=3 --tries=3 --max-time=3600
|
command=/usr/local/bin/php /var/www/artisan queue:work --sleep=3 --tries=3 --max-time=3600
|
||||||
autostart=true
|
autostart=true
|
||||||
autorestart=true
|
autorestart=true
|
||||||
startretries=3
|
startretries=3
|
||||||
|
|
|
||||||
17
docs/decisions/007-docker-build.md
Normal file
17
docs/decisions/007-docker-build.md
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
The ENTRYPOINT script (/usr/local/bin/docker-entrypoint.sh) is executed first.
|
||||||
|
|
||||||
|
The CMD (/usr/bin/supervisord -n -c /etc/supervisord.conf) is passed as arguments to the ENTRYPOINT script.
|
||||||
|
|
||||||
|
The ENTRYPOINT script can choose to execute the CMD or perform other tasks before running the CMD.
|
||||||
|
|
||||||
|
for now, supervisord is used in its current form, that of a python based application
|
||||||
|
|
||||||
|
this is ok for now but other options could be considered, such as replacing it with a [go binary port of supervisord](https://github.com/ochinchina/supervisord), using [s6-overlay](https://github.com/just-containers/s6-overlay), [dumb-init](https://github.com/Yelp/dumb-init) or similar process management tools designed for containers.
|
||||||
|
|
||||||
|
In the dockerfile, a user directive is used to lower from default root to that of a user that is created - 'www' and which is, for now, given a shell by which it is possible to log into this container for debugging and maintenance.
|
||||||
|
|
||||||
|
further on, this can be removed somehow - likely a case for k8s as there are patterns for this that are perhaps not so much for docker alone.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
103
docs/docker/docker-compose-cloudflard.yaml
Normal file
103
docs/docker/docker-compose-cloudflard.yaml
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
services:
|
||||||
|
nats:
|
||||||
|
image: nats:2.9.19-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
#ports:
|
||||||
|
# - 4222:4222
|
||||||
|
# - 8222:8222
|
||||||
|
volumes:
|
||||||
|
- ./nats/nats-server.conf:/nats-server.conf
|
||||||
|
- nats-data:/opt/storage
|
||||||
|
command: ["-c", "/nats-server.conf"]
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
- nats
|
||||||
|
|
||||||
|
# nats-cli:
|
||||||
|
# image: natsio/nats-box
|
||||||
|
# container_name: nats-cli
|
||||||
|
# depends_on:
|
||||||
|
# - nats
|
||||||
|
# command: sleep infinity # Keeps container running
|
||||||
|
# networks:
|
||||||
|
# - app-network
|
||||||
|
# - nats
|
||||||
|
|
||||||
|
app:
|
||||||
|
image: quay.io/marshyon/share-lt:v0.0.5
|
||||||
|
restart: unless-stopped
|
||||||
|
tty: false
|
||||||
|
working_dir: /var/www
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
- nats
|
||||||
|
ports:
|
||||||
|
- 8889:8889
|
||||||
|
volumes:
|
||||||
|
- storage-data:/var/www/storage
|
||||||
|
- database-data:/var/www/database
|
||||||
|
- ./nginx/conf.d/app.conf:/etc/nginx/http.d/app.conf
|
||||||
|
- ./docker/php/php.ini:/usr/local/etc/php/php.ini
|
||||||
|
|
||||||
|
environment:
|
||||||
|
- "LIVEWIRE_TEMPORARY_FILE_UPLOAD_MAX_FILE_UPLOAD_TIME=5"
|
||||||
|
- "ADMIN_EMAIL=${ADMIN_EMAIL}"
|
||||||
|
- "APP_NAME=${APP_NAME}"
|
||||||
|
- "APP_ENV=production"
|
||||||
|
- "APP_KEY=${APP_KEY}"
|
||||||
|
- "APP_DEBUG=false"
|
||||||
|
- "APP_URL=${APP_URL}"
|
||||||
|
- "APP_LOCALE=en"
|
||||||
|
- "APP_FALLBACK_LOCALE=en"
|
||||||
|
- "APP_MAINTENANCE_DRIVER=file"
|
||||||
|
- "PHP_CLI_SERVER_WORKERS=4"
|
||||||
|
- "BCRYPT_ROUNDS=12"
|
||||||
|
- "LOG_CHANNEL=stack"
|
||||||
|
- "LOG_STACK=single"
|
||||||
|
- "LOG_DEPRECATIONS_CHANNEL=null"
|
||||||
|
- "LOG_LEVEL=${LOG_LEVEL}"
|
||||||
|
- "DB_CONNECTION=sqlite"
|
||||||
|
- "SESSION_DRIVER=database"
|
||||||
|
- "SESSION_LIFETIME=120"
|
||||||
|
- "SESSION_ENCRYPT=false"
|
||||||
|
- "SESSION_PATH=/"
|
||||||
|
- "SESSION_DOMAIN=null"
|
||||||
|
- "BROADCAST_CONNECTION=log"
|
||||||
|
- "FILESYSTEM_DISK=s3"
|
||||||
|
- "QUEUE_CONNECTION=database"
|
||||||
|
- "CACHE_STORE=database"
|
||||||
|
- "CACHE_PREFIX=laravel_"
|
||||||
|
- "MAIL_MAILER=smtp"
|
||||||
|
- "MAIL_SCHEME=smtp"
|
||||||
|
- "MAIL_HOST=${MAIL_HOST}"
|
||||||
|
- "MAIL_PORT=${MAIL_PORT}"
|
||||||
|
- "MAIL_USERNAME=${MAIL_USERNAME}"
|
||||||
|
- "MAIL_PASSWORD=${MAIL_PASSWORD}"
|
||||||
|
- "MAIL_FROM_ADDRESS=${MAIL_FROM_ADDRESS}"
|
||||||
|
- "MAIL_FROM_NAME=${APP_NAME}"
|
||||||
|
- "AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}"
|
||||||
|
- "AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}"
|
||||||
|
- "AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION}"
|
||||||
|
- "AWS_BUCKET=${AWS_BUCKET}"
|
||||||
|
- "AWS_USE_PATH_STYLE_ENDPOINT=${AWS_USE_PATH_STYLE_ENDPOINT}"
|
||||||
|
- "AWS_DIRECTORY=${AWS_DIRECTORY}"
|
||||||
|
- "MEDIA_DISK=${MEDIA_DISK}"
|
||||||
|
- "VITE_APP_NAME=${APP_NAME}"
|
||||||
|
- "NATS_URL=${NATS_URL}"
|
||||||
|
- "NATS_USERNAME=${NATS_USERNAME}"
|
||||||
|
- "NATS_PASSWORD=${NATS_PASSWORD}"
|
||||||
|
- "NATS_STREAM=${NATS_STREAM}"
|
||||||
|
- "NATS_SUBJECT=${NATS_SUBJECT}"
|
||||||
|
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
storage-data:
|
||||||
|
database-data:
|
||||||
|
nats-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
app-network:
|
||||||
|
nats:
|
||||||
|
external: true
|
||||||
|
|
||||||
84
docs/docker/docker-compose-prod.yaml
Normal file
84
docs/docker/docker-compose-prod.yaml
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: quay.io/marshyon/share-lt:v0.0.5
|
||||||
|
restart: unless-stopped
|
||||||
|
tty: false
|
||||||
|
working_dir: /var/www
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
- traefik-net9
|
||||||
|
volumes:
|
||||||
|
- storage-data:/var/www/storage
|
||||||
|
- database-data:/var/www/database
|
||||||
|
- ./nginx/conf.d/app.conf:/etc/nginx/http.d/app.conf
|
||||||
|
- ./docker/php/php.ini:/usr/local/etc/php/php.ini
|
||||||
|
|
||||||
|
environment:
|
||||||
|
- "LIVEWIRE_TEMPORARY_FILE_UPLOAD_MAX_FILE_UPLOAD_TIME=5"
|
||||||
|
- "ADMIN_EMAIL=${ADMIN_EMAIL}"
|
||||||
|
- "APP_NAME=${APP_NAME}"
|
||||||
|
- "APP_ENV=production"
|
||||||
|
- "APP_KEY=${APP_KEY}"
|
||||||
|
- "APP_DEBUG=false"
|
||||||
|
- "APP_URL=${APP_URL}"
|
||||||
|
- "APP_LOCALE=en"
|
||||||
|
- "APP_FALLBACK_LOCALE=en"
|
||||||
|
- "APP_MAINTENANCE_DRIVER=file"
|
||||||
|
- "PHP_CLI_SERVER_WORKERS=4"
|
||||||
|
- "BCRYPT_ROUNDS=12"
|
||||||
|
- "LOG_CHANNEL=stack"
|
||||||
|
- "LOG_STACK=single"
|
||||||
|
- "LOG_DEPRECATIONS_CHANNEL=null"
|
||||||
|
- "LOG_LEVEL=${LOG_LEVEL}"
|
||||||
|
- "DB_CONNECTION=sqlite"
|
||||||
|
- "SESSION_DRIVER=database"
|
||||||
|
- "SESSION_LIFETIME=120"
|
||||||
|
- "SESSION_ENCRYPT=false"
|
||||||
|
- "SESSION_PATH=/"
|
||||||
|
- "SESSION_DOMAIN=null"
|
||||||
|
- "BROADCAST_CONNECTION=log"
|
||||||
|
- "FILESYSTEM_DISK=s3"
|
||||||
|
- "QUEUE_CONNECTION=database"
|
||||||
|
- "CACHE_STORE=database"
|
||||||
|
- "CACHE_PREFIX=laravel_"
|
||||||
|
- "MAIL_MAILER=smtp"
|
||||||
|
- "MAIL_SCHEME=smtp"
|
||||||
|
- "MAIL_HOST=${MAIL_HOST}"
|
||||||
|
- "MAIL_PORT=${MAIL_PORT}"
|
||||||
|
- "MAIL_USERNAME=${MAIL_USERNAME}"
|
||||||
|
- "MAIL_PASSWORD=${MAIL_PASSWORD}"
|
||||||
|
- "MAIL_FROM_ADDRESS=${MAIL_FROM_ADDRESS}"
|
||||||
|
- "MAIL_FROM_NAME=${APP_NAME}"
|
||||||
|
- "AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}"
|
||||||
|
- "AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}"
|
||||||
|
- "AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION}"
|
||||||
|
- "AWS_BUCKET=${AWS_BUCKET}"
|
||||||
|
- "AWS_USE_PATH_STYLE_ENDPOINT=${AWS_USE_PATH_STYLE_ENDPOINT}"
|
||||||
|
- "AWS_DIRECTORY=${AWS_DIRECTORY}"
|
||||||
|
- "MEDIA_DISK=${MEDIA_DISK}"
|
||||||
|
- "VITE_APP_NAME=${APP_NAME}"
|
||||||
|
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=traefik-net9"
|
||||||
|
- "traefik.http.services.${APP_SERVICE_NAME}.loadbalancer.server.port=${APP_PORT}"
|
||||||
|
- "traefik.http.services.${APP_SERVICE_NAME}.loadbalancer.server.scheme=http"
|
||||||
|
- "traefik.http.routers.${APP_SERVICE_NAME}.service=${APP_SERVICE_NAME}"
|
||||||
|
- "traefik.http.routers.${APP_SERVICE_NAME}.tls.certresolver=le"
|
||||||
|
- "traefik.http.routers.${APP_SERVICE_NAME}.rule=Host(`${APP_DOMAIN}`)"
|
||||||
|
- "traefik.http.routers.${APP_SERVICE_NAME}.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.${APP_SERVICE_NAME}.tls=true"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
storage-data:
|
||||||
|
database-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
app-network:
|
||||||
|
traefik-net9:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
13
nats/nats-server.conf
Normal file
13
nats/nats-server.conf
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Client port of 4222 on all interfaces
|
||||||
|
port: 4222
|
||||||
|
|
||||||
|
# HTTP monitoring port
|
||||||
|
monitor_port: 8222
|
||||||
|
|
||||||
|
# Enable JetStream and specify the storage directory
|
||||||
|
jetstream: {
|
||||||
|
store_dir: "/opt/storage"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set a unique server name for this instance
|
||||||
|
server_name: "nats-server-1"
|
||||||
31
nginx/conf.d/app.conf
Normal file
31
nginx/conf.d/app.conf
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
server {
|
||||||
|
listen 8889;
|
||||||
|
# Redirect all HTTP requests to HTTPS
|
||||||
|
# return 301 https://$host$request_uri;
|
||||||
|
|
||||||
|
index index.php index.html;
|
||||||
|
# error_log /var/log/nginx/error.log;
|
||||||
|
error_log /dev/stderr debug;
|
||||||
|
# access_log /var/log/nginx/access.log;
|
||||||
|
access_log /dev/stdout;
|
||||||
|
root /var/www/public;
|
||||||
|
location ~ \.php$ {
|
||||||
|
try_files $uri =404;
|
||||||
|
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||||
|
fastcgi_pass 127.0.0.1:9000;
|
||||||
|
fastcgi_index index.php;
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||||
|
fastcgi_param HTTPS on;
|
||||||
|
fastcgi_param HTTP_X_FORWARDED_PROTO https;
|
||||||
|
}
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
gzip_static on;
|
||||||
|
}
|
||||||
|
location /storage/ {
|
||||||
|
alias /var/www/storage/app/public/;
|
||||||
|
autoindex on;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
||||||
(()=>{var n=({livewireId:e})=>({actionNestingIndex:null,init(){window.addEventListener("sync-action-modals",t=>{t.detail.id===e&&this.syncActionModals(t.detail.newActionNestingIndex)})},syncActionModals(t){if(this.actionNestingIndex===t){this.actionNestingIndex!==null&&this.$nextTick(()=>this.openModal());return}if(this.actionNestingIndex!==null&&this.closeModal(),this.actionNestingIndex=t,this.actionNestingIndex!==null){if(!this.$el.querySelector(`#${this.generateModalId(t)}`)){this.$nextTick(()=>this.openModal());return}this.openModal()}},generateModalId(t){return`fi-${e}-action-`+t},openModal(){let t=this.generateModalId(this.actionNestingIndex);document.dispatchEvent(new CustomEvent("open-modal",{bubbles:!0,composed:!0,detail:{id:t}}))},closeModal(){let t=this.generateModalId(this.actionNestingIndex);document.dispatchEvent(new CustomEvent("close-modal-quietly",{bubbles:!0,composed:!0,detail:{id:t}}))}});document.addEventListener("alpine:init",()=>{window.Alpine.data("filamentActionModals",n)});})();
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
||||||
function c({livewireId:s}){return{areAllCheckboxesChecked:!1,checkboxListOptions:[],search:"",visibleCheckboxListOptions:[],init(){this.checkboxListOptions=Array.from(this.$root.querySelectorAll(".fi-fo-checkbox-list-option")),this.updateVisibleCheckboxListOptions(),this.$nextTick(()=>{this.checkIfAllCheckboxesAreChecked()}),Livewire.hook("commit",({component:e,commit:t,succeed:i,fail:o,respond:h})=>{i(({snapshot:r,effect:l})=>{this.$nextTick(()=>{e.id===s&&(this.checkboxListOptions=Array.from(this.$root.querySelectorAll(".fi-fo-checkbox-list-option")),this.updateVisibleCheckboxListOptions(),this.checkIfAllCheckboxesAreChecked())})})}),this.$watch("search",()=>{this.updateVisibleCheckboxListOptions(),this.checkIfAllCheckboxesAreChecked()})},checkIfAllCheckboxesAreChecked(){this.areAllCheckboxesChecked=this.visibleCheckboxListOptions.length===this.visibleCheckboxListOptions.filter(e=>e.querySelector("input[type=checkbox]:checked, input[type=checkbox]:disabled")).length},toggleAllCheckboxes(){this.checkIfAllCheckboxesAreChecked();let e=!this.areAllCheckboxesChecked;this.visibleCheckboxListOptions.forEach(t=>{let i=t.querySelector("input[type=checkbox]");i.disabled||i.checked!==e&&(i.checked=e,i.dispatchEvent(new Event("change")))}),this.areAllCheckboxesChecked=e},updateVisibleCheckboxListOptions(){this.visibleCheckboxListOptions=this.checkboxListOptions.filter(e=>["",null,void 0].includes(this.search)||e.querySelector(".fi-fo-checkbox-list-option-label")?.innerText.toLowerCase().includes(this.search.toLowerCase())?!0:e.querySelector(".fi-fo-checkbox-list-option-description")?.innerText.toLowerCase().includes(this.search.toLowerCase()))}}}export{c as default};
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
||||||
function h({state:r}){return{state:r,rows:[],init(){this.updateRows(),this.rows.length<=0?this.rows.push({key:"",value:""}):this.updateState(),this.$watch("state",(e,t)=>{let s=i=>i===null?0:Array.isArray(i)?i.length:typeof i!="object"?0:Object.keys(i).length;s(e)===0&&s(t)===0||this.updateRows()})},addRow(){this.rows.push({key:"",value:""}),this.updateState()},deleteRow(e){this.rows.splice(e,1),this.rows.length<=0&&this.addRow(),this.updateState()},reorderRows(e){let t=Alpine.raw(this.rows);this.rows=[];let s=t.splice(e.oldIndex,1)[0];t.splice(e.newIndex,0,s),this.$nextTick(()=>{this.rows=t,this.updateState()})},updateRows(){let t=Alpine.raw(this.state).map(({key:s,value:i})=>({key:s,value:i}));this.rows.forEach(s=>{(s.key===""||s.key===null)&&t.push({key:"",value:s.value})}),this.rows=t},updateState(){let e=[];this.rows.forEach(t=>{t.key===""||t.key===null||e.push({key:t.key,value:t.value})}),JSON.stringify(this.state)!==JSON.stringify(e)&&(this.state=e)}}}export{h as default};
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
||||||
function s({state:n,splitKeys:a}){return{newTag:"",state:n,createTag(){if(this.newTag=this.newTag.trim(),this.newTag!==""){if(this.state.includes(this.newTag)){this.newTag="";return}this.state.push(this.newTag),this.newTag=""}},deleteTag(t){this.state=this.state.filter(e=>e!==t)},reorderTags(t){let e=this.state.splice(t.oldIndex,1)[0];this.state.splice(t.newIndex,0,e),this.state=[...this.state]},input:{"x-on:blur":"createTag()","x-model":"newTag","x-on:keydown"(t){["Enter",...a].includes(t.key)&&(t.preventDefault(),t.stopPropagation(),this.createTag())},"x-on:paste"(){this.$nextTick(()=>{if(a.length===0){this.createTag();return}let t=a.map(e=>e.replace(/[/\-\\^$*+?.()|[\]{}]/g,"\\$&")).join("|");this.newTag.split(new RegExp(t,"g")).forEach(e=>{this.newTag=e,this.createTag()})})}}}}export{s as default};
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
function r({initialHeight:i,shouldAutosize:s,state:h}){return{state:h,wrapperEl:null,init(){this.wrapperEl=this.$el.parentNode,this.setInitialHeight(),s?this.$watch("state",()=>{this.resize()}):this.setUpResizeObserver()},setInitialHeight(){this.$el.scrollHeight<=0||(this.wrapperEl.style.height=i+"rem")},resize(){if(this.$el.scrollHeight<=0)return;let e=this.$el.style.height;this.$el.style.height="0px";let t=this.$el.scrollHeight+"px";this.$el.style.height=e,this.wrapperEl.style.height!==t&&(this.wrapperEl.style.height=t)},setUpResizeObserver(){new ResizeObserver(()=>{this.wrapperEl.style.height=this.$el.style.height}).observe(this.$el)}}}export{r as default};
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
||||||
var i=()=>({isSticky:!1,width:0,resizeObserver:null,boundUpdateWidth:null,init(){let e=this.$el.parentElement;e&&(this.updateWidth(),this.resizeObserver=new ResizeObserver(()=>this.updateWidth()),this.resizeObserver.observe(e),this.boundUpdateWidth=this.updateWidth.bind(this),window.addEventListener("resize",this.boundUpdateWidth))},enableSticky(){this.isSticky=this.$el.getBoundingClientRect().top>0},disableSticky(){this.isSticky=!1},updateWidth(){let e=this.$el.parentElement;if(!e)return;let t=getComputedStyle(this.$root.querySelector(".fi-ac"));this.width=e.offsetWidth+parseInt(t.marginInlineStart,10)*-1+parseInt(t.marginInlineEnd,10)*-1},destroy(){this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),this.boundUpdateWidth&&(window.removeEventListener("resize",this.boundUpdateWidth),this.boundUpdateWidth=null)}});export{i as default};
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
function I({activeTab:w,isScrollable:f,isTabPersistedInQueryString:m,livewireId:g,tab:T,tabQueryStringKey:c}){return{boundResizeHandler:null,isScrollable:f,resizeDebounceTimer:null,tab:T,withinDropdownIndex:null,withinDropdownMounted:!1,init(){let t=this.getTabs(),e=new URLSearchParams(window.location.search);m&&e.has(c)&&t.includes(e.get(c))&&(this.tab=e.get(c)),this.$watch("tab",()=>this.updateQueryString()),(!this.tab||!t.includes(this.tab))&&(this.tab=t[w-1]),Livewire.hook("commit",({component:n,commit:d,succeed:r,fail:h,respond:u})=>{r(({snapshot:p,effect:i})=>{this.$nextTick(()=>{if(n.id!==g)return;let s=this.getTabs();s.includes(this.tab)||(this.tab=s[w-1]??this.tab)})})}),f||(this.boundResizeHandler=this.debouncedUpdateTabsWithinDropdown.bind(this),window.addEventListener("resize",this.boundResizeHandler),this.updateTabsWithinDropdown())},calculateAvailableWidth(t){let e=window.getComputedStyle(t);return Math.floor(t.clientWidth)-Math.ceil(parseFloat(e.paddingLeft))*2},calculateContainerGap(t){let e=window.getComputedStyle(t);return Math.ceil(parseFloat(e.columnGap))},calculateDropdownIconWidth(t){let e=t.querySelector(".fi-icon");return Math.ceil(e.clientWidth)},calculateTabItemGap(t){let e=window.getComputedStyle(t);return Math.ceil(parseFloat(e.columnGap)||8)},calculateTabItemPadding(t){let e=window.getComputedStyle(t);return Math.ceil(parseFloat(e.paddingLeft))+Math.ceil(parseFloat(e.paddingRight))},findOverflowIndex(t,e,n,d,r,h){let u=t.map(i=>Math.ceil(i.clientWidth)),p=t.map(i=>{let s=i.querySelector(".fi-tabs-item-label"),a=i.querySelector(".fi-badge"),o=Math.ceil(s.clientWidth),l=a?Math.ceil(a.clientWidth):0;return{label:o,badge:l,total:o+(l>0?d+l:0)}});for(let i=0;i<t.length;i++){let s=u.slice(0,i+1).reduce((b,y)=>b+y,0),a=i*n,o=p.slice(i+1),l=o.length>0,W=l?Math.max(...o.map(b=>b.total)):0,D=l?r+W+d+h+n:0;if(s+a+D>e)return i}return-1},get isDropdownButtonVisible(){return this.withinDropdownMounted?this.withinDropdownIndex===null?!1:this.getTabs().findIndex(e=>e===this.tab)<this.withinDropdownIndex:!0},getTabs(){return this.$refs.tabsData?JSON.parse(this.$refs.tabsData.value):[]},updateQueryString(){if(!m)return;let t=new URL(window.location.href);t.searchParams.set(c,this.tab),history.replaceState(null,document.title,t.toString())},debouncedUpdateTabsWithinDropdown(){clearTimeout(this.resizeDebounceTimer),this.resizeDebounceTimer=setTimeout(()=>this.updateTabsWithinDropdown(),150)},async updateTabsWithinDropdown(){this.withinDropdownIndex=null,this.withinDropdownMounted=!1,await this.$nextTick();let t=this.$el.querySelector(".fi-tabs"),e=t.querySelector(".fi-tabs-item:last-child"),n=Array.from(t.children).slice(0,-1),d=n.map(a=>a.style.display);n.forEach(a=>a.style.display=""),t.offsetHeight;let r=this.calculateAvailableWidth(t),h=this.calculateContainerGap(t),u=this.calculateDropdownIconWidth(e),p=this.calculateTabItemGap(n[0]),i=this.calculateTabItemPadding(n[0]),s=this.findOverflowIndex(n,r,h,p,i,u);n.forEach((a,o)=>a.style.display=d[o]),s!==-1&&(this.withinDropdownIndex=s),this.withinDropdownMounted=!0},destroy(){this.boundResizeHandler&&window.removeEventListener("resize",this.boundResizeHandler),clearTimeout(this.resizeDebounceTimer)}}}export{I as default};
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
function o({isSkippable:s,isStepPersistedInQueryString:i,key:r,startStep:h,stepQueryStringKey:n}){return{step:null,init(){this.$watch("step",()=>this.updateQueryString()),this.step=this.getSteps().at(h-1),this.autofocusFields()},async requestNextStep(){await this.$wire.callSchemaComponentMethod(r,"nextStep",{currentStepIndex:this.getStepIndex(this.step)})},goToNextStep(){let t=this.getStepIndex(this.step)+1;t>=this.getSteps().length||(this.step=this.getSteps()[t],this.autofocusFields(),this.scroll())},goToPreviousStep(){let t=this.getStepIndex(this.step)-1;t<0||(this.step=this.getSteps()[t],this.autofocusFields(),this.scroll())},scroll(){this.$nextTick(()=>{this.$refs.header?.children[this.getStepIndex(this.step)].scrollIntoView({behavior:"smooth",block:"start"})})},autofocusFields(){this.$nextTick(()=>this.$refs[`step-${this.step}`].querySelector("[autofocus]")?.focus())},getStepIndex(t){let e=this.getSteps().findIndex(p=>p===t);return e===-1?0:e},getSteps(){return JSON.parse(this.$refs.stepsData.value)},isFirstStep(){return this.getStepIndex(this.step)<=0},isLastStep(){return this.getStepIndex(this.step)+1>=this.getSteps().length},isStepAccessible(t){return s||this.getStepIndex(this.step)>this.getStepIndex(t)},updateQueryString(){if(!i)return;let t=new URL(window.location.href);t.searchParams.set(n,this.step),history.replaceState(null,document.title,t.toString())}}}export{o as default};
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
(()=>{var d=()=>({isSticky:!1,width:0,resizeObserver:null,boundUpdateWidth:null,init(){let t=this.$el.parentElement;t&&(this.updateWidth(),this.resizeObserver=new ResizeObserver(()=>this.updateWidth()),this.resizeObserver.observe(t),this.boundUpdateWidth=this.updateWidth.bind(this),window.addEventListener("resize",this.boundUpdateWidth))},enableSticky(){this.isSticky=this.$el.getBoundingClientRect().top>0},disableSticky(){this.isSticky=!1},updateWidth(){let t=this.$el.parentElement;if(!t)return;let e=getComputedStyle(this.$root.querySelector(".fi-ac"));this.width=t.offsetWidth+parseInt(e.marginInlineStart,10)*-1+parseInt(e.marginInlineEnd,10)*-1},destroy(){this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),this.boundUpdateWidth&&(window.removeEventListener("resize",this.boundUpdateWidth),this.boundUpdateWidth=null)}});var u=function(t,e,n){let i=t;if(e.startsWith("/")&&(n=!0,e=e.slice(1)),n)return e;for(;e.startsWith("../");)i=i.includes(".")?i.slice(0,i.lastIndexOf(".")):null,e=e.slice(3);return["",null,void 0].includes(i)?e:["",null,void 0].includes(e)?i:`${i}.${e}`},c=t=>{let e=Alpine.findClosest(t,n=>n.__livewire);if(!e)throw"Could not find Livewire component in DOM tree.";return e.__livewire};document.addEventListener("alpine:init",()=>{window.Alpine.data("filamentSchema",({livewireId:t})=>({handleFormValidationError(e){e.detail.livewireId===t&&this.$nextTick(()=>{let n=this.$el.querySelector("[data-validation-error]");if(!n)return;let i=n;for(;i;)i.dispatchEvent(new CustomEvent("expand")),i=i.parentNode;setTimeout(()=>n.closest("[data-field-wrapper]").scrollIntoView({behavior:"smooth",block:"start",inline:"start"}),200)})},isStateChanged(e,n){if(e===void 0)return!1;try{return JSON.stringify(e)!==JSON.stringify(n)}catch{return e!==n}}})),window.Alpine.data("filamentSchemaComponent",({path:t,containerPath:e,$wire:n})=>({$statePath:t,$get:(i,s)=>n.$get(u(e,i,s)),$set:(i,s,a,o=!1)=>n.$set(u(e,i,a),s,o),get $state(){return n.$get(t)}})),window.Alpine.data("filamentActionsSchemaComponent",d),Livewire.hook("commit",({component:t,commit:e,respond:n,succeed:i,fail:s})=>{i(({snapshot:a,effects:o})=>{o.dispatches?.forEach(r=>{if(!r.params?.awaitSchemaComponent)return;let l=Array.from(t.el.querySelectorAll(`[wire\\:partial="schema-component::${r.params.awaitSchemaComponent}"]`)).filter(h=>c(h)===t);if(l.length!==1){if(l.length>1)throw`Multiple schema components found with key [${r.params.awaitSchemaComponent}].`;window.addEventListener(`schema-component-${t.id}-${r.params.awaitSchemaComponent}-loaded`,()=>{window.dispatchEvent(new CustomEvent(r.name,{detail:r.params}))},{once:!0})}})})})});})();
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
||||||
function o({name:i,recordKey:s,state:a}){return{error:void 0,isLoading:!1,state:a,init(){Livewire.hook("commit",({component:e,commit:r,succeed:n,fail:h,respond:u})=>{n(({snapshot:f,effect:d})=>{this.$nextTick(()=>{if(this.isLoading||e.id!==this.$root.closest("[wire\\:id]")?.attributes["wire:id"].value)return;let t=this.getServerState();t===void 0||Alpine.raw(this.state)===t||(this.state=t)})})}),this.$watch("state",async()=>{let e=this.getServerState();if(e===void 0||Alpine.raw(this.state)===e)return;this.isLoading=!0;let r=await this.$wire.updateTableColumnState(i,s,this.state);this.error=r?.error??void 0,!this.error&&this.$refs.serverState&&(this.$refs.serverState.value=this.state?"1":"0"),this.isLoading=!1})},getServerState(){if(this.$refs.serverState)return[1,"1"].includes(this.$refs.serverState.value)}}}export{o as default};
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
||||||
function o({name:i,recordKey:s,state:a}){return{error:void 0,isLoading:!1,state:a,init(){Livewire.hook("commit",({component:e,commit:r,succeed:n,fail:d,respond:u})=>{n(({snapshot:f,effect:h})=>{this.$nextTick(()=>{if(this.isLoading||e.id!==this.$root.closest("[wire\\:id]")?.attributes["wire:id"].value)return;let t=this.getServerState();t===void 0||this.getNormalizedState()===t||(this.state=t)})})}),this.$watch("state",async()=>{let e=this.getServerState();if(e===void 0||this.getNormalizedState()===e)return;this.isLoading=!0;let r=await this.$wire.updateTableColumnState(i,s,this.state);this.error=r?.error??void 0,!this.error&&this.$refs.serverState&&(this.$refs.serverState.value=this.getNormalizedState()),this.isLoading=!1})},getServerState(){if(this.$refs.serverState)return[null,void 0].includes(this.$refs.serverState.value)?"":this.$refs.serverState.value.replaceAll('\\"','"')},getNormalizedState(){let e=Alpine.raw(this.state);return[null,void 0].includes(e)?"":e}}}export{o as default};
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
function o({name:i,recordKey:s,state:a}){return{error:void 0,isLoading:!1,state:a,init(){Livewire.hook("commit",({component:e,commit:r,succeed:n,fail:h,respond:u})=>{n(({snapshot:f,effect:d})=>{this.$nextTick(()=>{if(this.isLoading||e.id!==this.$root.closest("[wire\\:id]")?.attributes["wire:id"].value)return;let t=this.getServerState();t===void 0||Alpine.raw(this.state)===t||(this.state=t)})})}),this.$watch("state",async()=>{let e=this.getServerState();if(e===void 0||Alpine.raw(this.state)===e)return;this.isLoading=!0;let r=await this.$wire.updateTableColumnState(i,s,this.state);this.error=r?.error??void 0,!this.error&&this.$refs.serverState&&(this.$refs.serverState.value=this.state?"1":"0"),this.isLoading=!1})},getServerState(){if(this.$refs.serverState)return[1,"1"].includes(this.$refs.serverState.value)}}}export{o as default};
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -29,16 +29,15 @@ class UploadImageAdminTest extends DuskTestCase
|
||||||
->waitForLocation('/admin/media')
|
->waitForLocation('/admin/media')
|
||||||
->assertPathIs('/admin/media')
|
->assertPathIs('/admin/media')
|
||||||
->assertTitleContains('Media')
|
->assertTitleContains('Media')
|
||||||
->clickLink('New media')
|
->clickLink('Add')
|
||||||
->waitForText('Create Media')
|
->waitForText('Create Asset')
|
||||||
->type('#form\\.name', 'test image')
|
->type('#form\\.alt_text', 'test image')
|
||||||
->assertVisible('.filepond--drop-label')
|
->assertVisible('.filepond--drop-label')
|
||||||
->attach('.filepond--browser', $filePath)
|
->attach('.filepond--browser', $filePath)
|
||||||
->pause(7000)
|
->pause(7000)
|
||||||
->waitForText('Create')
|
->waitForText('Create')
|
||||||
->waitFor('#key-bindings-1:not([disabled])')
|
->waitFor('#key-bindings-1:not([disabled])')
|
||||||
->click('#key-bindings-1')
|
->click('#key-bindings-1'),
|
||||||
->assertSee('Collection name'),
|
|
||||||
1000 // Custom pause time for this test
|
1000 // Custom pause time for this test
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -27,17 +27,16 @@ class CreateEntryAdminTest extends DuskTestCase
|
||||||
->waitForLocation('/admin/media')
|
->waitForLocation('/admin/media')
|
||||||
->assertPathIs('/admin/media')
|
->assertPathIs('/admin/media')
|
||||||
->assertTitleContains('Media')
|
->assertTitleContains('Media')
|
||||||
->clickLink('New media')
|
->clickLink('Add')
|
||||||
->waitForText('Create Media')
|
->waitForText('Create Asset')
|
||||||
->type('#form\\.name', 'test image')
|
->type('#form\\.alt_text', 'test image')
|
||||||
->assertVisible('.filepond--drop-label')
|
->assertVisible('.filepond--drop-label')
|
||||||
->attach('.filepond--browser', $filePath)
|
->attach('.filepond--browser', $filePath)
|
||||||
->pause(7000)
|
->pause(7000)
|
||||||
->waitForText('Create')
|
->waitForText('Create Asset')
|
||||||
->waitFor('#key-bindings-1:not([disabled])')
|
->waitFor('#key-bindings-1:not([disabled])')
|
||||||
->click('#key-bindings-1')
|
->click('#key-bindings-1')
|
||||||
->assertSee('Collection name')
|
->pause(1000)
|
||||||
->pause(5000)
|
|
||||||
|
|
||||||
->visit('/admin/entries')
|
->visit('/admin/entries')
|
||||||
->waitForLocation('/admin/entries')
|
->waitForLocation('/admin/entries')
|
||||||
|
|
@ -54,19 +53,12 @@ class CreateEntryAdminTest extends DuskTestCase
|
||||||
->assertSee('Updated at')
|
->assertSee('Updated at')
|
||||||
->visit('/admin/entries/1/edit')
|
->visit('/admin/entries/1/edit')
|
||||||
->waitForText('Edit TEST ENTRY')
|
->waitForText('Edit TEST ENTRY')
|
||||||
->pause(2000)
|
|
||||||
->waitForText('Featured Image')
|
->waitForText('Featured Image')
|
||||||
->click('#featured-picker-button')
|
->click('#featured-picker-button')
|
||||||
|
->pause(5000)
|
||||||
->waitForText('Select an existing image')
|
->waitForText('Select an existing image')
|
||||||
->click('.fi-select-input-btn')
|
->pause(3000),
|
||||||
->pause(2000)
|
// TODO: find a way to select the image
|
||||||
->click('li:first-child')
|
|
||||||
->waitForText('Submit')
|
|
||||||
->clickAtXPath('//button[contains(., "Submit")]')
|
|
||||||
|
|
||||||
->waitForText('Edit TEST ENTRY')
|
|
||||||
->click('#key-bindings-1'),
|
|
||||||
// ->pause(20000),
|
|
||||||
1000 // Custom pause time for this test
|
1000 // Custom pause time for this test
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue