File manager improvements (#936)

* add separate button for "save & close"

* make language selection for editor work

* fix download url

* add info banner for .pelicanignore files

* small cleanup

* fix import

* Move File Lang

* add `ctrl+shift+s` for save & close

* fix keybind

* cleanup and fix default value for edit

* remove unnecessary File::get & trait

* More EditorLanguages not matching their names

* mdx has its own highlighter

---------

Co-authored-by: notCharles <charles@pelican.dev>
Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
This commit is contained in:
Boy132 2025-01-26 14:29:53 +01:00 committed by GitHub
parent 401026efa1
commit 71f3abe464
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 173 additions and 57 deletions

View File

@ -36,6 +36,7 @@ enum EditorLanguages: string implements HasLabel
case java = 'java';
case javascript = 'javascript';
case julia = 'julia';
case json = 'json';
case kotlin = 'kotlin';
case less = 'less';
case lexon = 'lexon';
@ -89,7 +90,49 @@ enum EditorLanguages: string implements HasLabel
case wgsl = 'wgsl';
case xml = 'xml';
case yaml = 'yaml';
case json = 'json';
public static function fromWithAlias(string $match): self
{
return match ($match) {
'h' => self::c,
'cc', 'hpp' => self::cpp,
'cs' => self::csharp,
'class' => self::java,
'htm' => self::html,
'js', 'mjs', 'cjs' => self::javascript,
'kt', 'kts' => self::kotlin,
'md' => self::markdown,
'm' => self::objectivec,
'pl', 'pm' => self::perl,
'php3', 'php4', 'php5', 'phtml' => self::php,
'py', 'pyc', 'pyo', 'pyi' => self::python,
'rdata', 'rds' => self::r,
'rb', 'erb' => self::ruby,
'sc' => self::scala,
'sh', 'zsh' => self::shell,
'ts', 'tsx' => self::typescript,
'yml' => self::yaml,
default => self::tryFrom($match) ?? self::plaintext,
};
}
public function getLabel(): string
{

View File

@ -56,6 +56,7 @@ class FileResource extends Resource
return [
'edit' => Pages\EditFiles::route('/edit/{path}'),
'search' => Pages\SearchFiles::route('/search/{searchTerm}'), // TODO: find better way?
'download' => Pages\DownloadFiles::route('/download/{path}'),
'index' => Pages\ListFiles::route('/{path?}'),
];
}

View File

@ -0,0 +1,64 @@
<?php
namespace App\Filament\Server\Resources\FileResource\Pages;
use App\Facades\Activity;
use App\Filament\Server\Resources\FileResource;
use App\Models\Permission;
use App\Models\Server;
use App\Services\Nodes\NodeJWTService;
use Carbon\CarbonImmutable;
use Filament\Facades\Filament;
use Filament\Panel;
use Filament\Resources\Pages\Page;
use Filament\Resources\Pages\PageRegistration;
use Illuminate\Routing\Route;
use Illuminate\Support\Facades\Route as RouteFacade;
use Livewire\Attributes\Locked;
class DownloadFiles extends Page
{
protected static string $resource = FileResource::class;
#[Locked]
public string $path;
public function mount(string $path, NodeJWTService $service): void
{
$this->authorizeAccess();
/** @var Server $server */
$server = Filament::getTenant();
$token = $service
->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
->setUser(auth()->user())
->setClaims([
'file_path' => rawurldecode($path),
'server_uuid' => $server->uuid,
])
->handle($server->node, auth()->user()->id . $server->uuid);
Activity::event('server:file.download')
->property('file', $path)
->log();
redirect()->away($server->node->getConnectionAddress() . '/download/file?token=' . $token->toString());
}
protected function authorizeAccess(): void
{
abort_unless(auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, Filament::getTenant()), 403);
}
public static function route(string $path): PageRegistration
{
return new PageRegistration(
page: static::class,
route: fn (Panel $panel): Route => RouteFacade::get($path, static::class)
->middleware(static::getRouteMiddleware($panel))
->withoutMiddleware(static::getWithoutRouteMiddleware($panel))
->where('path', '.*'),
);
}
}

View File

@ -6,7 +6,7 @@ use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
use App\Enums\EditorLanguages;
use App\Facades\Activity;
use App\Filament\Server\Resources\FileResource;
use App\Models\File;
use App\Livewire\AlertBanner;
use App\Models\Permission;
use App\Models\Server;
use App\Repositories\Daemon\DaemonFileRepository;
@ -18,7 +18,6 @@ use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Notifications\Notification;
use Filament\Pages\Concerns\HasUnsavedDataChangesAlert;
use Filament\Pages\Concerns\InteractsWithFormActions;
use Filament\Panel;
use Filament\Resources\Pages\Page;
@ -34,7 +33,6 @@ use Livewire\Attributes\Locked;
*/
class EditFiles extends Page
{
use HasUnsavedDataChangesAlert;
use InteractsWithFormActions;
use InteractsWithForms;
@ -54,27 +52,34 @@ class EditFiles extends Page
/** @var Server $server */
$server = Filament::getTenant();
File::get($server, dirname($this->path))->orderByDesc('is_directory')->orderBy('name');
return $form
->schema([
Select::make('lang')
->live()
->label('')
->placeholder('File Language')
->options(EditorLanguages::class)
->hidden() //TODO Fix Dis
->default(function () {
$ext = pathinfo($this->path, PATHINFO_EXTENSION);
if ($ext === 'yml') {
return 'yaml';
}
return $ext;
}),
Section::make('Editing: ' . $this->path)
->footerActions([
Action::make('save_and_close')
->label('Save & Close')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->icon('tabler-device-floppy')
->keyBindings('mod+shift+s')
->action(function (DaemonFileRepository $fileRepository) use ($server) {
$data = $this->form->getState();
$fileRepository
->setServer($server)
->putContent($this->path, $data['editor'] ?? '');
Activity::event('server:file.write')
->property('file', $this->path)
->log();
Notification::make()
->success()
->title('File saved')
->body(fn () => $this->path)
->send();
$this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
}),
Action::make('save')
->label('Save')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
@ -93,12 +98,9 @@ class EditFiles extends Page
Notification::make()
->success()
->duration(5000) // 5 seconds
->title('Saved File')
->title('File saved')
->body(fn () => $this->path)
->send();
$this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
}),
Action::make('cancel')
->label('Cancel')
@ -108,10 +110,17 @@ class EditFiles extends Page
])
->footerActionsAlignment(Alignment::End)
->schema([
Select::make('lang')
->label('Syntax Highlighting')
->live()
->options(EditorLanguages::class)
->selectablePlaceholder(false)
->afterStateUpdated(fn ($state) => $this->dispatch('setLanguage', lang: $state))
->default(fn () => EditorLanguages::fromWithAlias(pathinfo($this->path, PATHINFO_EXTENSION))),
MonacoEditor::make('editor')
->label('')
->placeholderText('')
->formatStateUsing(function (DaemonFileRepository $fileRepository) use ($server) {
->default(function (DaemonFileRepository $fileRepository) use ($server) {
try {
return $fileRepository
->setServer($server)
@ -120,7 +129,7 @@ class EditFiles extends Page
abort(404, $this->path . ' not found.');
}
})
->language(fn (Get $get) => $get('lang') ?? 'plaintext')
->language(fn (Get $get) => $get('lang'))
->view('filament.plugins.monaco-editor'),
]),
]);
@ -133,6 +142,15 @@ class EditFiles extends Page
$this->path = $path;
$this->form->fill();
if (str($path)->endsWith('.pelicanignore')) {
AlertBanner::make()
->title('You\'re editing a <code>.pelicanignore</code> file!')
->body('Any files or directories listed in here will be excluded from backups. Wildcards are supported by using an asterisk (<code>*</code>).<br>You can negate a prior rule by prepending an exclamation point (<code>!</code>).')
->info()
->closable()
->send();
}
}
protected function authorizeAccess(): void

View File

@ -10,10 +10,8 @@ use App\Models\File;
use App\Models\Permission;
use App\Models\Server;
use App\Repositories\Daemon\DaemonFileRepository;
use App\Services\Nodes\NodeJWTService;
use App\Filament\Components\Tables\Columns\BytesColumn;
use App\Filament\Components\Tables\Columns\DateTimeColumn;
use Carbon\CarbonImmutable;
use Filament\Actions\Action as HeaderAction;
use Filament\Facades\Filament;
use Filament\Forms\Components\CheckboxList;
@ -21,6 +19,7 @@ use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Get;
use Filament\Notifications\Notification;
@ -175,22 +174,7 @@ class ListFiles extends ListRecords
->label('Download')
->icon('tabler-download')
->visible(fn (File $file) => $file->is_file)
->action(function (File $file, NodeJWTService $service) use ($server) {
$token = $service
->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
->setUser(auth()->user())
->setClaims([
'file_path' => rawurldecode(join_paths($this->path, $file->name)),
'server_uuid' => $server->uuid,
])
->handle($server->node, auth()->user()->id . $server->uuid);
Activity::event('server:file.download')
->property('file', join_paths($this->path, $file->name))
->log();
return redirect()->away(sprintf('%s/download/file?token=%s', $server->node->getConnectionAddress(), $token->toString())); // TODO: download works, but breaks modals
}),
->url(fn () => DownloadFiles::getUrl(['path' => $this->path]), true),
Action::make('move')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->label('Move')
@ -448,15 +432,16 @@ class ListFiles extends ListRecords
->label('File Name')
->required(),
Select::make('lang')
->label('Syntax Highlighting')
->live()
->hidden() //TODO: Make file language selection work
->label('Language')
->placeholder('File Language')
->options(EditorLanguages::class),
->options(EditorLanguages::class)
->selectablePlaceholder(false)
->afterStateUpdated(fn ($state) => $this->dispatch('setLanguage', lang: $state))
->default(EditorLanguages::plaintext->value),
MonacoEditor::make('editor')
->label('')
->view('filament.plugins.monaco-editor')
->language(fn (Get $get) => $get('lang')),
->language(fn (Get $get) => $get('lang') ?? 'plaintext'),
]),
HeaderAction::make('new_folder')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
@ -504,13 +489,12 @@ class ListFiles extends ListRecords
}
return redirect(ListFiles::getUrl(['path' => $this->path]));
})
->form([
Tabs::make()
->contained(false)
->schema([
Tabs\Tab::make('Upload Files')
Tab::make('Upload Files')
->live()
->schema([
FileUpload::make('files')
@ -520,7 +504,7 @@ class ListFiles extends ListRecords
->preserveFilenames()
->multiple(),
]),
Tabs\Tab::make('Upload From URL')
Tab::make('Upload From URL')
->live()
->disabled(fn (Get $get) => count($get('files')) > 0)
->schema([

View File

@ -6,7 +6,7 @@ return [
'show-full-screen-toggle' => true,
'show-placeholder' => true,
'placeholder-text' => 'Your code here...',
'show-loader' => true,
'show-loader' => false,
'font-size' => '16px',
'line-numbers-min-chars' => true,
'automatic-layout' => true,

View File

@ -1,3 +1,11 @@
@script
<script>
$wire.on('setLanguage', ({ lang }) => {
monaco.editor.setModelLanguage(document.getElementById('{{ $getId() }}').editor.getModel(), lang);
});
</script>
@endscript
<x-dynamic-component :component="$getFieldWrapperView()" :field="$field" class="overflow-hidden">
<div x-data="{
@ -12,7 +20,7 @@
monacoFontSize: '{{ $getFontSize() }}',
lineNumbersMinChars: {{ $getLineNumbersMinChars() }},
automaticLayout: {{ (int) $getAutomaticLayout() }},
monacoId: $id('monaco-editor'),
monacoId: '{{ $getId() }}',
toggleFullScreenMode() {
this.fullScreenModeEnabled = !this.fullScreenModeEnabled;

View File

@ -6,6 +6,4 @@
>
{{ $this->form }}
</x-filament-panels::form>
<x-filament-panels::page.unsaved-data-changes-alert />
</x-filament-panels::page>