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
|
||||
.env
|
||||
.env.*
|
||||
.envrc
|
||||
|
||||
# Ignore node_modules
|
||||
node_modules/
|
||||
|
|
@ -25,4 +26,7 @@ Thumbs.db
|
|||
/storage/framework/cache/*
|
||||
/storage/framework/sessions/*
|
||||
/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_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
MEDIA_DISK=s3
|
||||
|
||||
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::resetPasswords()` for password reset via email.
|
||||
</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.backup
|
||||
.env.production
|
||||
.env.dev
|
||||
.phpactor.json
|
||||
.phpunit.result.cache
|
||||
Homestead.json
|
||||
|
|
@ -28,3 +29,6 @@ log*.txt
|
|||
.envrc
|
||||
database/backups
|
||||
*backup.tar.gz
|
||||
public/css
|
||||
public/js
|
||||
notes
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
when:
|
||||
- event: push
|
||||
branch: feat/docker-compose-update
|
||||
branch: dev
|
||||
steps:
|
||||
build-local:
|
||||
image: docker:24-dind
|
||||
|
|
@ -14,10 +14,10 @@ steps:
|
|||
- docker pull quay.io/marshyon/share-lt:latest || true
|
||||
- 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 .
|
||||
- echo "Tagging test image as quay.io/marshyon/share-lt:v0.0.2..."
|
||||
- docker tag share-lt:test 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.5
|
||||
- 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:
|
||||
image: aquasec/trivy:0.67.2
|
||||
volumes:
|
||||
|
|
@ -41,7 +41,7 @@ steps:
|
|||
repo: quay.io/marshyon/share-lt
|
||||
platforms: linux/amd64
|
||||
tags:
|
||||
- v0.0.2
|
||||
- v0.0.5
|
||||
- latest
|
||||
username:
|
||||
from_secret: QUAY_USERNAME
|
||||
|
|
@ -57,5 +57,6 @@ steps:
|
|||
COSIGN_REGISTRY_PASSWORD:
|
||||
from_secret: QUAY_PASSWORD
|
||||
commands:
|
||||
- cosign attach sbom --sbom sbom.json quay.io/marshyon/share-lt:v0.0.2 || echo "SBOM attach failed"
|
||||
- echo "Done - trivy report saved to workspace for manual review"
|
||||
- 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"
|
||||
|
||||
|
|
|
|||
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
|
||||
COPY composer.lock composer.json /var/www/
|
||||
|
||||
# Set working directory
|
||||
ENV APP_ENV=production
|
||||
ENV APP_DEBUG=false
|
||||
WORKDIR /var/www
|
||||
|
||||
# # Install dependencies
|
||||
RUN apk update && apk add --no-cache \
|
||||
build-base \
|
||||
libpng-dev \
|
||||
|
|
@ -20,70 +16,86 @@ RUN apk update && apk add --no-cache \
|
|||
curl \
|
||||
libzip-dev \
|
||||
oniguruma-dev \
|
||||
icu-dev \
|
||||
sqlite \
|
||||
sqlite-dev \
|
||||
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/*
|
||||
|
||||
# # Install extensions
|
||||
RUN docker-php-ext-install mbstring zip exif pcntl intl pdo_sqlite
|
||||
|
||||
RUN docker-php-ext-install gd
|
||||
|
||||
# # Install composer
|
||||
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
|
||||
|
||||
# Add user for laravel application
|
||||
RUN addgroup -g 1000 www
|
||||
RUN adduser -u 1000 -G www -s /bin/sh -D www
|
||||
# Copy entrypoint script
|
||||
COPY cmd/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
# # Copy existing application directory contents
|
||||
COPY . /var/www
|
||||
# Copy supervisord configuration
|
||||
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
|
||||
COPY --chown=www-data:www-data . /var/www
|
||||
# Create www user and add to www-data group
|
||||
RUN adduser -u 1000 -G www-data -s /bin/sh -D www
|
||||
|
||||
# # Install PHP dependencies
|
||||
RUN composer install --no-dev --optimize-autoloader
|
||||
# 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
|
||||
|
||||
# Install Node.js dependencies
|
||||
RUN npm install
|
||||
# Remove the semicolon to uncomment the listen directive
|
||||
RUN sed -i 's/;listen = 127.0.0.1:9000/listen = 9000/' /usr/local/etc/php-fpm.d/www.conf
|
||||
|
||||
# Build assets
|
||||
RUN npm run build
|
||||
# Ensure the worker running the code is correct (usually www-data or nginx)
|
||||
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
|
||||
RUN chmod -R u+rw /var/www && chown -R www:www /var/www
|
||||
# Update nginx.conf to use 'www' user instead of 'nginx'
|
||||
RUN sed -i 's/user nginx;/user www;/' /etc/nginx/nginx.conf
|
||||
|
||||
# # RUN php artisan optimize
|
||||
# # removed as it calls all 4 commands below and fails du to
|
||||
# # multi-site configuration
|
||||
# Remove user and group directives from nginx and php-fpm configs to avoid conflicts
|
||||
RUN sed -i '/^user /d' /etc/nginx/nginx.conf
|
||||
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
|
||||
RUN php artisan config:clear
|
||||
# RUN php artisan cache:clear
|
||||
RUN php artisan route:clear
|
||||
RUN php artisan view:clear
|
||||
# Set permissions for nginx directories
|
||||
RUN mkdir -p /var/lib/nginx/tmp/client_body /var/log/nginx \
|
||||
&& chown -R www:www-data /var/lib/nginx /var/log/nginx \
|
||||
&& chmod -R 755 /var/lib/nginx /var/log/nginx \
|
||||
&& touch /run/nginx/nginx.pid \
|
||||
&& chown www:www-data /run/nginx/nginx.pid
|
||||
|
||||
# # Build optimizations
|
||||
RUN php artisan config:cache
|
||||
RUN php artisan event:cache
|
||||
RUN php artisan view:cache
|
||||
# Copy application code (includes database/migrations/) and excluding
|
||||
# files in .dockerignore
|
||||
COPY --chown=www:www-data . /var/www
|
||||
RUN chown -R www:www-data /var/www
|
||||
RUN chown -R www:www-data /var/log/supervisor
|
||||
|
||||
# # RUN php artisan route:cache
|
||||
|
||||
# # Run Laravel artisan command
|
||||
RUN php artisan storage:link
|
||||
|
||||
# # RUN composer install --optimize-autoloader --no-dev
|
||||
|
||||
# # Change current user to www
|
||||
# Switch to www user
|
||||
USER www
|
||||
|
||||
# # Expose port 9000 and start php-fpm server
|
||||
# EXPOSE 9000
|
||||
# Install app dependencies
|
||||
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')
|
||||
->columnSpanFull(),
|
||||
SpatieMediaLibraryFileUpload::make('featured_image')
|
||||
->multiple() // <- force array handling for Filament v4 bug
|
||||
->visible(
|
||||
fn($get) =>
|
||||
$get('type') === 'article' || $get('type') === 'image'
|
||||
|
|
@ -64,9 +65,63 @@ class EntryForm
|
|||
->columnSpanFull()
|
||||
->dehydrated(false)
|
||||
->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', [
|
||||
'disk' => $diskName,
|
||||
'file_name' => $file->getClientOriginalName(),
|
||||
|
|
@ -97,7 +152,7 @@ class EntryForm
|
|||
$disk->put($testFile, 'test content');
|
||||
$disk->delete($testFile);
|
||||
|
||||
if (config('app.env') === 'local') {
|
||||
if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') {
|
||||
Log::info('S3 connectivity test passed');
|
||||
}
|
||||
}
|
||||
|
|
@ -112,12 +167,114 @@ class EntryForm
|
|||
$encodedName = base64_encode($originalName);
|
||||
$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);
|
||||
|
||||
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', [
|
||||
'media_id' => $media->id,
|
||||
'media_url' => $media->getUrl(),
|
||||
|
|
@ -189,7 +346,7 @@ class EntryForm
|
|||
$record = $component->getRecord();
|
||||
$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', [
|
||||
'disk' => $diskName,
|
||||
'record_id' => $record?->id,
|
||||
|
|
@ -223,7 +380,7 @@ class EntryForm
|
|||
try {
|
||||
// For S3, we need to handle file copying differently
|
||||
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', [
|
||||
'source_disk' => $sourceMedia->disk,
|
||||
'source_path' => $sourceMedia->getPathRelativeToRoot(),
|
||||
|
|
@ -267,7 +424,7 @@ class EntryForm
|
|||
->usingFileName($sourceMedia->file_name)
|
||||
->toMediaCollection('featured-image', $diskName);
|
||||
|
||||
if (config('app.env') === 'local') {
|
||||
if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') {
|
||||
Log::info('Featured Image Picker Success', [
|
||||
'new_media_id' => $newMedia->id,
|
||||
'new_media_disk' => $newMedia->disk,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class ListMedia extends ListRecords
|
|||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
// CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ use Filament\Forms\Components\Hidden;
|
|||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Schema;
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media as SpatieMedia;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class MediaForm
|
||||
{
|
||||
|
|
@ -24,6 +26,7 @@ class MediaForm
|
|||
Hidden::make('disk')
|
||||
->default('public'),
|
||||
FileUpload::make('file')
|
||||
->multiple() // workaround for Filament v4 single-file bug
|
||||
->label('File')
|
||||
->imageEditor()
|
||||
->imageEditorAspectRatios([
|
||||
|
|
@ -38,22 +41,50 @@ class MediaForm
|
|||
->acceptedFileTypes(['image/*', 'application/pdf'])
|
||||
->maxSize(10240)
|
||||
->required(fn($context) => $context === 'create')
|
||||
|
||||
|
||||
->afterStateHydrated(function (FileUpload $component, $state, $record): void {
|
||||
if (! $record) {
|
||||
return;
|
||||
Log::info('MediaForm afterStateHydrated invoked', ['record_id' => $record?->id, 'state' => $state]);
|
||||
|
||||
try {
|
||||
if (! $record) {
|
||||
return;
|
||||
}
|
||||
|
||||
$media = $record;
|
||||
if (! $media instanceof SpatieMedia) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Construct the correct path: {media_id}/{filename}
|
||||
$path = $media->id . '/' . $media->file_name;
|
||||
|
||||
try {
|
||||
$disk = $media->disk ?? 'public';
|
||||
if (Storage::disk($disk)->exists($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;
|
||||
}
|
||||
|
||||
$media = $record;
|
||||
|
||||
if (! $media instanceof SpatieMedia) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Construct the correct path: {media_id}/{filename}
|
||||
$path = $media->id . '/' . $media->file_name;
|
||||
|
||||
$component->state($path);
|
||||
}),
|
||||
|
||||
|
||||
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Filament\Resources\Media\Tables;
|
||||
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
|
|
@ -19,11 +20,12 @@ class MediaTable
|
|||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->modifyQueryUsing(fn ($query) => $query->where('collection_name', '!=', 'avatars'))
|
||||
->modifyQueryUsing(fn($query) => $query->where('collection_name', '!=', 'avatars'))
|
||||
->columns([
|
||||
ImageColumn::make('url')
|
||||
->label('Preview')
|
||||
->getStateUsing(fn ($record) =>
|
||||
->getStateUsing(
|
||||
fn($record) =>
|
||||
// Prefer the stored path produced by Filament's FileUpload (saved in custom_properties),
|
||||
// fall back to Spatie's getUrl() when no stored_path exists.
|
||||
($record->getCustomProperty('stored_path'))
|
||||
|
|
@ -40,7 +42,7 @@ class MediaTable
|
|||
->badge(),
|
||||
TextColumn::make('mime_type'),
|
||||
TextColumn::make('size')
|
||||
->formatStateUsing(fn ($state) => number_format($state / 1024, 2).' KB'),
|
||||
->formatStateUsing(fn($state) => number_format($state / 1024, 2) . ' KB'),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime(),
|
||||
])
|
||||
|
|
@ -52,7 +54,7 @@ class MediaTable
|
|||
]),
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
// EditAction::make(),
|
||||
DeleteAction::make()
|
||||
->action(function (Media $record) {
|
||||
// Delete the actual stored file path if we saved one, otherwise fall back to the Spatie path.
|
||||
|
|
@ -67,6 +69,13 @@ class MediaTable
|
|||
}),
|
||||
])
|
||||
->toolbarActions([
|
||||
|
||||
Action::make('add')
|
||||
->label('Add')
|
||||
->icon('heroicon-o-plus')
|
||||
->url(fn() => url('admin/assets/create'))
|
||||
->color('primary'),
|
||||
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->action(function (Collection $records) {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ use Filament\Tables\Table;
|
|||
|
||||
class TextWidgetResource extends Resource
|
||||
{
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
protected static ?string $model = TextWidget::class;
|
||||
|
||||
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',
|
||||
];
|
||||
}
|
||||
|
|
@ -64,7 +64,7 @@ class User extends Authenticatable implements FilamentUser
|
|||
return Str::of($this->name)
|
||||
->explode(' ')
|
||||
->take(2)
|
||||
->map(fn ($word) => Str::substr($word, 0, 1))
|
||||
->map(fn($word) => Str::substr($word, 0, 1))
|
||||
->implode('');
|
||||
}
|
||||
|
||||
|
|
@ -73,6 +73,6 @@ class User extends Authenticatable implements FilamentUser
|
|||
*/
|
||||
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;
|
||||
|
||||
use App\Models\Entry;
|
||||
use App\Observers\EntryObserver;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
|
@ -19,6 +22,12 @@ class AppServiceProvider extends ServiceProvider
|
|||
*/
|
||||
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([
|
||||
\App\Filament\Resources\Entries\EntryResource::class,
|
||||
\App\Filament\Resources\TextWidgets\TextWidgetResource::class,
|
||||
\App\Filament\Resources\Media\MediaResource::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')
|
||||
->pages([
|
||||
Dashboard::class,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
LARAVEL_CONTAINER_NAME="quay.io/marshyon/share-lt"
|
||||
CONTAINER_LABEL="0.0.3"
|
||||
CONTAINER_LABEL="v0.0.5"
|
||||
CACHE="--no-cache"
|
||||
CACHE=""
|
||||
# CACHE=""
|
||||
|
||||
docker build \
|
||||
$CACHE \
|
||||
-t ${LARAVEL_CONTAINER_NAME}:${CONTAINER_LABEL} \
|
||||
-f Dockerfile.phpfpm .
|
||||
-t ${LARAVEL_CONTAINER_NAME}:${CONTAINER_LABEL} .
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@
|
|||
# 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 \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Accept: application/json" \
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@
|
|||
# granted access to view entries by being given
|
||||
# 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 \
|
||||
-H "Accept: application/json" \
|
||||
$URL
|
||||
|
|
|
|||
|
|
@ -1,36 +1,37 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# run_as_www() {
|
||||
# # prefer su-exec (alpine), fallback to runuser if available, otherwise run directly
|
||||
# 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
|
||||
# }
|
||||
# Ensure APP_KEY is set and persisted
|
||||
PERSISTED_KEY="/var/www/storage/.app_key"
|
||||
|
||||
# Build front-end assets if Vite manifest is missing
|
||||
if [ ! -f /var/www/public/build/manifest.json ]; then
|
||||
echo "Building front-end assets (vite)..."
|
||||
run_as_www npm ci
|
||||
run_as_www npm run build
|
||||
if [ -z "$APP_KEY" ]; then
|
||||
if [ -f "$PERSISTED_KEY" ]; then
|
||||
echo "Using persisted APP_KEY from: $PERSISTED_KEY"
|
||||
export APP_KEY=$(cat "$PERSISTED_KEY")
|
||||
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
|
||||
|
||||
# Wait for database directory to be mounted
|
||||
# if [ ! -f /var/www/database/database.sqlite ]; then
|
||||
# echo "Creating database..."
|
||||
# # create the sqlite file as the www user so ownership matches app files
|
||||
# run_as_www sh -c 'touch /var/www/database/database.sqlite'
|
||||
# run_as_www php artisan migrate --force
|
||||
# fi
|
||||
# check to see if /var/www/database/database.sqlite exists
|
||||
# if not, run migrations
|
||||
if [ ! -f /var/www/database/database.sqlite ]; then
|
||||
php artisan migrate --force
|
||||
fi
|
||||
|
||||
# Fix storage permissions
|
||||
# echo "Fixing storage permissions..."
|
||||
# chown -R www:www /var/www/storage /var/www/bootstrap/cache
|
||||
# chmod -R 775 /var/www/storage /var/www/bootstrap/cache
|
||||
# check to see if /var/www/public/storage exists
|
||||
# if not, run storage:link
|
||||
if [ ! -d /var/www/public/storage ]; then
|
||||
php artisan storage:link
|
||||
fi
|
||||
|
||||
# Execute the main command
|
||||
php artisan config:clear
|
||||
|
||||
|
||||
# Start supervisord directly
|
||||
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
|
||||
|
||||
[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
|
||||
autorestart=true
|
||||
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
|
|
@ -20,7 +20,7 @@ class UploadImageAdminTest extends DuskTestCase
|
|||
|
||||
$filePath = base_path('tests/Browser/fixtures/robot.webp');
|
||||
|
||||
$this->browse(function (Browser $browser ) use ($user, $filePath) {
|
||||
$this->browse(function (Browser $browser) use ($user, $filePath) {
|
||||
$this->loginUser($browser, $user);
|
||||
$this->assertWithDebugPause(
|
||||
$browser,
|
||||
|
|
@ -29,16 +29,15 @@ class UploadImageAdminTest extends DuskTestCase
|
|||
->waitForLocation('/admin/media')
|
||||
->assertPathIs('/admin/media')
|
||||
->assertTitleContains('Media')
|
||||
->clickLink('New media')
|
||||
->waitForText('Create Media')
|
||||
->type('#form\\.name', 'test image')
|
||||
->clickLink('Add')
|
||||
->waitForText('Create Asset')
|
||||
->type('#form\\.alt_text', 'test image')
|
||||
->assertVisible('.filepond--drop-label')
|
||||
->attach('.filepond--browser', $filePath)
|
||||
->pause(7000)
|
||||
->waitForText('Create')
|
||||
->waitFor('#key-bindings-1:not([disabled])')
|
||||
->click('#key-bindings-1')
|
||||
->assertSee('Collection name'),
|
||||
->click('#key-bindings-1'),
|
||||
1000 // Custom pause time for this test
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,17 +27,16 @@ class CreateEntryAdminTest extends DuskTestCase
|
|||
->waitForLocation('/admin/media')
|
||||
->assertPathIs('/admin/media')
|
||||
->assertTitleContains('Media')
|
||||
->clickLink('New media')
|
||||
->waitForText('Create Media')
|
||||
->type('#form\\.name', 'test image')
|
||||
->clickLink('Add')
|
||||
->waitForText('Create Asset')
|
||||
->type('#form\\.alt_text', 'test image')
|
||||
->assertVisible('.filepond--drop-label')
|
||||
->attach('.filepond--browser', $filePath)
|
||||
->pause(7000)
|
||||
->waitForText('Create')
|
||||
->waitForText('Create Asset')
|
||||
->waitFor('#key-bindings-1:not([disabled])')
|
||||
->click('#key-bindings-1')
|
||||
->assertSee('Collection name')
|
||||
->pause(5000)
|
||||
->pause(1000)
|
||||
|
||||
->visit('/admin/entries')
|
||||
->waitForLocation('/admin/entries')
|
||||
|
|
@ -54,19 +53,12 @@ class CreateEntryAdminTest extends DuskTestCase
|
|||
->assertSee('Updated at')
|
||||
->visit('/admin/entries/1/edit')
|
||||
->waitForText('Edit TEST ENTRY')
|
||||
->pause(2000)
|
||||
->waitForText('Featured Image')
|
||||
->click('#featured-picker-button')
|
||||
->pause(5000)
|
||||
->waitForText('Select an existing image')
|
||||
->click('.fi-select-input-btn')
|
||||
->pause(2000)
|
||||
->click('li:first-child')
|
||||
->waitForText('Submit')
|
||||
->clickAtXPath('//button[contains(., "Submit")]')
|
||||
|
||||
->waitForText('Edit TEST ENTRY')
|
||||
->click('#key-bindings-1'),
|
||||
// ->pause(20000),
|
||||
->pause(3000),
|
||||
// TODO: find a way to select the image
|
||||
1000 // Custom pause time for this test
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue