Changes
This commit is contained in:
@@ -8,6 +8,7 @@ use App\Helpers\ByteConverter;
|
||||
use App\Models\Category;
|
||||
use App\Models\CustomForm;
|
||||
use App\Models\Page;
|
||||
use App\Models\PageReferenceList;
|
||||
use App\Models\Post;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Builder;
|
||||
@@ -379,6 +380,13 @@ class ContentBuilderItem
|
||||
->searchable()
|
||||
->required(),
|
||||
])->label('Форма'),
|
||||
Builder\Block::make('pageResourceList')
|
||||
->schema([
|
||||
Select::make('resource')
|
||||
->options(PageReferenceList::query()->where('is_active', true)->pluck('title', 'slug'))
|
||||
->searchable()
|
||||
->required(),
|
||||
])->label('Ресурсы'),
|
||||
])
|
||||
->collapsed()
|
||||
->blockNumbers(false)
|
||||
|
||||
@@ -9,10 +9,13 @@ use App\Helpers\ByteConverter;
|
||||
use App\Models\Category;
|
||||
use App\Models\CustomForm;
|
||||
use App\Models\Page;
|
||||
use App\Models\PageReferenceList;
|
||||
use App\Models\Post;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Builder;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\RichEditor;
|
||||
use Filament\Forms\Components\Section;
|
||||
@@ -23,6 +26,8 @@ use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Components\Tab;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
@@ -79,6 +84,30 @@ class PostForm
|
||||
SpatieTagsInput::make('tags')->label('Тэги'),
|
||||
Forms\Components\TagsInput::make('authors')
|
||||
->label('Авторы')->placeholder('Добавить автора'),
|
||||
Section::make('Отложенная публикация')->schema([
|
||||
Grid::make(2)->schema([
|
||||
Toggle::make('publish_setting.publish_after')
|
||||
->label('Включить')
|
||||
->inline(false)
|
||||
->default(false)
|
||||
->live(),
|
||||
DateTimePicker::make('publish_setting.publish_at')
|
||||
->label('Дата публикации')
|
||||
->native()
|
||||
->displayFormat('d/m/Y')
|
||||
|
||||
->required(fn (Forms\Get $get) => $get('publish_setting.publish_after'))
|
||||
->disabled(fn (Forms\Get $get) => !$get('publish_setting.publish_after'))
|
||||
->minDate(Carbon::now()->subWeek())
|
||||
->maxDate(Carbon::now()->addMonth()),
|
||||
]),
|
||||
]),
|
||||
Section::make('Публикация в сервисах')->schema([
|
||||
Forms\Components\Grid::make()->schema([
|
||||
Toggle::make('publication.vk')->label('Публикация в VK')->default(true),
|
||||
Toggle::make('publication.telegram')->label('Публикация в Telegram')->default(true),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tabs\Tab::make('Содержание новости')
|
||||
->schema([
|
||||
@@ -89,7 +118,7 @@ class PostForm
|
||||
TextInput::make('content')
|
||||
->label('')
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(function (string $operation, string $state, Forms\Set $set, $get) {
|
||||
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set, $get) {
|
||||
}),
|
||||
]),
|
||||
Builder\Block::make('paragraph')->label('Текст')
|
||||
@@ -440,8 +469,16 @@ class PostForm
|
||||
->searchable()
|
||||
->required(),
|
||||
])->label('Форма'),
|
||||
Builder\Block::make('PageResourceList')
|
||||
->schema([
|
||||
Select::make('resource')
|
||||
->options(PageReferenceList::query()->where('is_active', true)->pluck('title', 'slug'))
|
||||
->searchable()
|
||||
->required(),
|
||||
])->label('Ресурсы'),
|
||||
])
|
||||
->collapsed()
|
||||
->required()
|
||||
->blockNumbers(false)
|
||||
->collapsible()
|
||||
->blockPickerColumns(3)
|
||||
@@ -468,7 +505,6 @@ class PostForm
|
||||
->directory('images'),
|
||||
]),
|
||||
]),
|
||||
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -125,8 +125,6 @@ class MainSliderResource extends Resource
|
||||
|
||||
Toggle::make('is_active')->default(true)->label('Активный слайдер')->inline(false),
|
||||
]),
|
||||
|
||||
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\PageReferenceListResource\Pages;
|
||||
use App\Filament\Resources\PageReferenceListResource\RelationManagers;
|
||||
use App\Models\Event;
|
||||
use App\Models\Page;
|
||||
use App\Models\PageReferenceList;
|
||||
use App\Models\Post;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\ColorPicker;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PageReferenceListResource extends Resource
|
||||
{
|
||||
protected static ?string $model = PageReferenceList::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\Section::make('')->schema([
|
||||
Tabs::make('Tabs')
|
||||
->tabs([
|
||||
Tabs\Tab::make('Основная информация')
|
||||
->schema([
|
||||
Forms\Components\Grid::make(2)->schema([
|
||||
TextInput::make('title')->label('Название ресурса')->required()
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set) {
|
||||
$set('slug', Str::slug($state));
|
||||
$set('seo.title', $state);
|
||||
}),
|
||||
TextInput::make('slug')->label('Slug')->unique(ignoreRecord: true)->readOnly()->required(),
|
||||
Toggle::make('is_active')->default(true)->label('Активный ресурс')->inline(false),
|
||||
])
|
||||
]),
|
||||
Tabs\Tab::make('Содержание новости')
|
||||
->schema([
|
||||
Repeater::make('content')->schema([
|
||||
Forms\Components\Section::make('Быстрая настройка ресурса')->schema([
|
||||
Forms\Components\Grid::make()->schema([
|
||||
Forms\Components\Select::make('model_select')
|
||||
->name('')
|
||||
->label('Выбор типа данных')
|
||||
->options([
|
||||
'Post' => 'Новость',
|
||||
'Page' => 'Страница',
|
||||
'Event' => 'Мероприятие',
|
||||
'Custom' => 'Кастомная ссылка',
|
||||
])
|
||||
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set, Forms\Get $get) {
|
||||
if ($get('model_select') === 'Custom') {
|
||||
$set('model', null);
|
||||
$set('title', null);
|
||||
$set('content', null);
|
||||
$set('link', null);
|
||||
};
|
||||
})->live(onBlur: true),
|
||||
|
||||
Forms\Components\Select::make('model')
|
||||
->label('Поиск данных')
|
||||
->name('')
|
||||
->live(onBlur: true)
|
||||
->searchable()
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(function (string $operation, string|null $state, Forms\Set $set, Forms\Get $get) {
|
||||
if ($get('model_select') === 'Post') {
|
||||
$post = Post::find($state);
|
||||
$set('title', $post->title);
|
||||
$relativeUrl = parse_url(route('client.post.show', $post->slug), PHP_URL_PATH);
|
||||
$set('link', $relativeUrl);
|
||||
};
|
||||
if ($get('model_select') === 'Page') {
|
||||
$page = Page::find($state);
|
||||
$set('title', $page->title);
|
||||
$set('link', $page->path);
|
||||
};
|
||||
if ($get('model_select') === 'Event') {
|
||||
$event = Event::find($state);
|
||||
$set('title', $event->title);
|
||||
$relativeUrl = parse_url(route('client.event.show', $event->slug), PHP_URL_PATH);
|
||||
$set('link', $relativeUrl);
|
||||
};
|
||||
|
||||
})
|
||||
->options(function (Forms\Get $get) {
|
||||
if ($get('model_select') === 'Post') {
|
||||
return Post::where('status', '=', 'published')->pluck('title', 'id');
|
||||
};
|
||||
if ($get('model_select') === 'Page') {
|
||||
return Page::where('title', '!=', null)->pluck('title', 'id');
|
||||
};
|
||||
if ($get('model_select') === 'Event') {
|
||||
return Event::all()->pluck('title', 'id');
|
||||
};
|
||||
if ($get('model_select') === 'Custom') {
|
||||
return [];
|
||||
};
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
TextInput::make('title')->label('Title')->required(),
|
||||
FileUpload::make('image')
|
||||
->label('Изображение')
|
||||
->image()
|
||||
->optimize('webp')
|
||||
->resize(50)
|
||||
->disk('public')
|
||||
->directory('images')
|
||||
->imageEditor(),
|
||||
Forms\Components\Grid::make(2)->schema([
|
||||
Forms\Components\TextInput::make('link')
|
||||
->label('Ссылка ресурса')
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('link_text')
|
||||
->default('Читать')
|
||||
->label('Текст кнопки')
|
||||
->required(),
|
||||
]),
|
||||
])->collapsed()->required(),
|
||||
]),
|
||||
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
//
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListPageReferenceLists::route('/'),
|
||||
'create' => Pages\CreatePageReferenceList::route('/create'),
|
||||
'edit' => Pages\EditPageReferenceList::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\PageReferenceListResource\Pages;
|
||||
|
||||
use App\Filament\Resources\PageReferenceListResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreatePageReferenceList extends CreateRecord
|
||||
{
|
||||
protected static string $resource = PageReferenceListResource::class;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\PageReferenceListResource\Pages;
|
||||
|
||||
use App\Filament\Resources\PageReferenceListResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditPageReferenceList extends EditRecord
|
||||
{
|
||||
protected static string $resource = PageReferenceListResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\PageReferenceListResource\Pages;
|
||||
|
||||
use App\Filament\Resources\PageReferenceListResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListPageReferenceLists extends ListRecords
|
||||
{
|
||||
protected static string $resource = PageReferenceListResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -103,7 +103,8 @@ class PostResource extends Resource implements HasShieldPermissions
|
||||
'update',
|
||||
'delete',
|
||||
'delete_any',
|
||||
'publish'
|
||||
'publish',
|
||||
'view_only_own_records'
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ namespace App\Filament\Resources\PostResource\Pages;
|
||||
|
||||
use App\Enums\PostStatus;
|
||||
use App\Filament\Resources\PostResource;
|
||||
use App\Jobs\CreateVkPost;
|
||||
use App\Models\Post;
|
||||
use App\Models\User;
|
||||
use App\Services\VK\VkService;
|
||||
use Carbon\Carbon;
|
||||
use Closure;
|
||||
use Filament\Actions;
|
||||
@@ -14,6 +16,7 @@ use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Notifications\Messages\BroadcastMessage;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use PhpParser\Node\Expr\AssignOp\Mod;
|
||||
use VK\Client\VKApiClient;
|
||||
@@ -28,12 +31,17 @@ class CreatePost extends CreateRecord
|
||||
protected static string $resource = PostResource::class;
|
||||
|
||||
protected array $seoData;
|
||||
protected array $publicationAgreements;
|
||||
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
$this->publicationAgreements = $data['publication'];
|
||||
unset($data['publication']);
|
||||
$this->seoData = $this->generateSeo($data);
|
||||
$data['preview_text'] = $this->setPreviewText($data);
|
||||
$data['publish_at'] = $this->setPublishDateTime($data['status']);
|
||||
$data['publish_at'] = $this->setPublishDateTime($data['publish_setting']);
|
||||
unset($data['publish_setting']);
|
||||
$data['search_data'] = $this->generateSearchData($data['content']);
|
||||
$data['reading_time'] = $this->calculateReadingTime($data['search_data']);
|
||||
return $data;
|
||||
@@ -43,6 +51,7 @@ class CreatePost extends CreateRecord
|
||||
{
|
||||
$this->record->seo()->create($this->seoData);
|
||||
$this->sendNotify($this->record, auth()->user());
|
||||
$this->postToSocialMedia($this->publicationAgreements, $this->record->content, $this->record->title, Carbon::parse($this->record->publish_at)->timestamp);
|
||||
}
|
||||
|
||||
private function generateSeo(array $data) : array
|
||||
@@ -62,7 +71,6 @@ class CreatePost extends CreateRecord
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
private function setPreviewText(array $data) : string
|
||||
{
|
||||
$rowData = $this->getBlockBySeoActiveState('paragraph', $data['content']);
|
||||
@@ -126,10 +134,10 @@ class CreatePost extends CreateRecord
|
||||
|
||||
])->sendToDatabase($recipient);
|
||||
}
|
||||
private function setPublishDateTime(PostStatus $status) : Carbon|null
|
||||
private function setPublishDateTime(array $data) : Carbon|null
|
||||
{
|
||||
if ($status !== PostStatus::PUBLISHED) {
|
||||
return null;
|
||||
if ($data['publish_after'] === true) {
|
||||
return Carbon::parse($data['publish_at']);
|
||||
}
|
||||
return Carbon::now();
|
||||
}
|
||||
@@ -243,6 +251,58 @@ class CreatePost extends CreateRecord
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function postToSocialMedia($settings, $content, $title, $publish_date) : void
|
||||
{
|
||||
if ($this->record->status === PostStatus::PUBLISHED) {
|
||||
if ($settings['vk']) {
|
||||
$text = "";
|
||||
foreach ($content as $block) {
|
||||
$text .= $this->generateContentToVK($block);
|
||||
}
|
||||
|
||||
$images = $this->generateImageLinksToVK($this->record->images);
|
||||
|
||||
$post_id = $this->record->id;
|
||||
|
||||
|
||||
dispatch(new CreateVkPost($title, $text, $images, $post_id, $publish_date));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function generateContentToVK($block) : string
|
||||
{
|
||||
$data = "";
|
||||
|
||||
switch ($block['type']) {
|
||||
case 'paragraph':
|
||||
// Удаляем все HTML-теги и заменяем закрывающие теги p и h2 на двойной отступ
|
||||
$content = preg_replace('/<\/(p|h2)>/', "\n\n", $block['data']['content']);
|
||||
$data .= strip_tags($content);
|
||||
break;
|
||||
|
||||
case 'heading':
|
||||
// Удаляем теги заголовка и добавляем двойной отступ
|
||||
$data .= $block['data']['content'] . "\n\n";
|
||||
break;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
private function generateImageLinksToVK($images)
|
||||
{
|
||||
|
||||
$imageUrls = array_map(function ($file) {
|
||||
return url(Storage::url($file)); // Добавляем домен
|
||||
}, $images);
|
||||
|
||||
|
||||
return $imageUrls; // Возвращаем массив с полными URL изображений
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ namespace App\Filament\Resources\PostResource\Pages;
|
||||
|
||||
use App\Enums\PostStatus;
|
||||
use App\Filament\Resources\PostResource;
|
||||
use App\Jobs\UpdateVkPost;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class EditPost extends EditRecord
|
||||
@@ -14,11 +16,14 @@ class EditPost extends EditRecord
|
||||
protected static string $resource = PostResource::class;
|
||||
|
||||
protected array $seoData;
|
||||
protected array $publicationAgreements;
|
||||
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
$this->seoData = $this->generateSeo($data);
|
||||
$this->publicationAgreements = $data['publication'];
|
||||
unset($data['publication']);
|
||||
$data['preview_text'] = $this->setPreviewText($data);
|
||||
$data['publish_at'] = $this->setPublishDateTime($data['status'], $this->record->publish_at);
|
||||
$data['search_data'] = $this->generateSearchData($data['content']);
|
||||
@@ -30,6 +35,7 @@ class EditPost extends EditRecord
|
||||
protected function afterSave(): void
|
||||
{
|
||||
$this->record->seo()->update($this->seoData);
|
||||
$this->postToSocialMedia($this->publicationAgreements, $this->record->content, $this->record->title, Carbon::parse($this->record->publish_at)->timestamp);
|
||||
}
|
||||
|
||||
private function setPreviewText(array $data) : string
|
||||
@@ -59,8 +65,6 @@ class EditPost extends EditRecord
|
||||
return $block;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function generateSeo(array $data) : array
|
||||
{
|
||||
$title = $data['title'];
|
||||
@@ -166,6 +170,59 @@ class EditPost extends EditRecord
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function generateContentToVK($block) : string
|
||||
{
|
||||
$data = "";
|
||||
|
||||
switch ($block['type']) {
|
||||
case 'paragraph':
|
||||
// Удаляем все HTML-теги и заменяем закрывающие теги p и h2 на двойной отступ
|
||||
$content = preg_replace('/<\/(p|h2)>/', "\n\n", $block['data']['content']);
|
||||
$data .= strip_tags($content);
|
||||
break;
|
||||
|
||||
case 'heading':
|
||||
// Удаляем теги заголовка и добавляем двойной отступ
|
||||
$data .= $block['data']['content'] . "\n\n";
|
||||
break;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
private function postToSocialMedia($settings, $content, $title, $publish_date) : void
|
||||
{
|
||||
if ($this->record->status === PostStatus::PUBLISHED) {
|
||||
if ($settings['vk']) {
|
||||
$text = "";
|
||||
foreach ($content as $block) {
|
||||
$text .= $this->generateContentToVK($block);
|
||||
}
|
||||
|
||||
$images = $this->generateImageLinksToVK($this->record->images);
|
||||
|
||||
$post_id = $this->record->id;
|
||||
|
||||
|
||||
dispatch(new UpdateVkPost($title, $text, $images, $post_id, $publish_date));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function generateImageLinksToVK($images)
|
||||
{
|
||||
|
||||
$imageUrls = array_map(function ($file) {
|
||||
return url(Storage::url($file)); // Добавляем домен
|
||||
}, $images);
|
||||
|
||||
|
||||
return $imageUrls; // Возвращаем массив с полными URL изображений
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -9,21 +9,26 @@ use Filament\Actions;
|
||||
use Filament\Resources\Components\Tab;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ListPosts extends ListRecords
|
||||
{
|
||||
public \Illuminate\Support\Collection $postsByStatuses;
|
||||
protected static string $resource = PostResource::class;
|
||||
|
||||
public function __construct()
|
||||
public function getPostsByStatuses(): \Illuminate\Support\Collection
|
||||
{
|
||||
$this->postsByStatuses = Post::select('status', DB::raw('count(*) as post_count'))
|
||||
return Post::select('status', DB::raw('count(*) as post_count'))
|
||||
->groupBy('status')
|
||||
->when($this->canViewOnlyOwnRecords(), function (Builder $query) {
|
||||
$query->where('user_id', auth()->id());
|
||||
})
|
||||
->pluck('post_count', 'status');
|
||||
}
|
||||
|
||||
protected static string $resource = PostResource::class;
|
||||
protected function canViewOnlyOwnRecords(): bool
|
||||
{
|
||||
return auth()->user()->can('view_only_own_records_post');
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
@@ -34,11 +39,21 @@ class ListPosts extends ListRecords
|
||||
|
||||
public function getTabs(): array
|
||||
{
|
||||
$postsByStatuses = $this->getPostsByStatuses();
|
||||
|
||||
return [
|
||||
'status' => Tab::make('Новости на рассмотрении')->modifyQueryUsing(function (Builder $query) {
|
||||
$query->where('status', '=', PostStatus::VERIFICATION->value);
|
||||
})->badge($this->postsByStatuses[PostStatus::VERIFICATION->value] ?? 0),
|
||||
'All' => Tab::make('Все новости'),
|
||||
if ($this->canViewOnlyOwnRecords()) {
|
||||
$query->where('user_id', auth()->id());
|
||||
}
|
||||
})->badge($postsByStatuses[PostStatus::VERIFICATION->value] ?? 0),
|
||||
|
||||
'All' => Tab::make('Все новости')->modifyQueryUsing(function (Builder $query) {
|
||||
if ($this->canViewOnlyOwnRecords()) {
|
||||
$query->where('user_id', auth()->id());
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Shield;
|
||||
|
||||
use BezhanSalleh\FilamentShield\Contracts\HasShieldPermissions;
|
||||
use BezhanSalleh\FilamentShield\Facades\FilamentShield;
|
||||
use BezhanSalleh\FilamentShield\FilamentShieldPlugin;
|
||||
use BezhanSalleh\FilamentShield\Forms\ShieldSelectAllToggle;
|
||||
use App\Filament\Resources\Shield\RoleResource\Pages;
|
||||
use BezhanSalleh\FilamentShield\Support\Utils;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Component;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class RoleResource extends Resource implements HasShieldPermissions
|
||||
{
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static function getPermissionPrefixes(): array
|
||||
{
|
||||
return [
|
||||
'view',
|
||||
'view_any',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
'delete_any',
|
||||
];
|
||||
}
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\Grid::make()
|
||||
->schema([
|
||||
Forms\Components\Section::make()
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
->label(__('filament-shield::filament-shield.field.name'))
|
||||
->unique(ignoreRecord: true)
|
||||
->required()
|
||||
->maxLength(255),
|
||||
|
||||
Forms\Components\TextInput::make('guard_name')
|
||||
->label(__('filament-shield::filament-shield.field.guard_name'))
|
||||
->default(Utils::getFilamentAuthGuard())
|
||||
->nullable()
|
||||
->maxLength(255),
|
||||
|
||||
ShieldSelectAllToggle::make('select_all')
|
||||
->onIcon('heroicon-s-shield-check')
|
||||
->offIcon('heroicon-s-shield-exclamation')
|
||||
->label(__('filament-shield::filament-shield.field.select_all.name'))
|
||||
->helperText(fn (): HtmlString => new HtmlString(__('filament-shield::filament-shield.field.select_all.message')))
|
||||
->dehydrated(fn ($state): bool => $state),
|
||||
|
||||
])
|
||||
->columns([
|
||||
'sm' => 2,
|
||||
'lg' => 3,
|
||||
]),
|
||||
]),
|
||||
Forms\Components\Tabs::make('Permissions')
|
||||
->contained()
|
||||
->tabs([
|
||||
static::getTabFormComponentForResources(),
|
||||
static::getTabFormComponentForPage(),
|
||||
static::getTabFormComponentForWidget(),
|
||||
static::getTabFormComponentForCustomPermissions(),
|
||||
])
|
||||
->columnSpan('full'),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->badge()
|
||||
->label(__('filament-shield::filament-shield.column.name'))
|
||||
->formatStateUsing(fn ($state): string => Str::headline($state))
|
||||
->colors(['primary'])
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('guard_name')
|
||||
->badge()
|
||||
->label(__('filament-shield::filament-shield.column.guard_name')),
|
||||
Tables\Columns\TextColumn::make('permissions_count')
|
||||
->badge()
|
||||
->label(__('filament-shield::filament-shield.column.permissions'))
|
||||
->counts('permissions')
|
||||
->colors(['success']),
|
||||
Tables\Columns\TextColumn::make('updated_at')
|
||||
->label(__('filament-shield::filament-shield.column.updated_at'))
|
||||
->dateTime(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\DeleteAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListRoles::route('/'),
|
||||
'create' => Pages\CreateRole::route('/create'),
|
||||
'view' => Pages\ViewRole::route('/{record}'),
|
||||
'edit' => Pages\EditRole::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getCluster(): ?string
|
||||
{
|
||||
return Utils::getResourceCluster() ?? static::$cluster;
|
||||
}
|
||||
|
||||
public static function getModel(): string
|
||||
{
|
||||
return Utils::getRoleModel();
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('filament-shield::filament-shield.resource.label.role');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('filament-shield::filament-shield.resource.label.roles');
|
||||
}
|
||||
|
||||
public static function shouldRegisterNavigation(): bool
|
||||
{
|
||||
return Utils::isResourceNavigationRegistered();
|
||||
}
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return Utils::isResourceNavigationGroupEnabled()
|
||||
? __('filament-shield::filament-shield.nav.group')
|
||||
: '';
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('filament-shield::filament-shield.nav.role.label');
|
||||
}
|
||||
|
||||
public static function getNavigationIcon(): string
|
||||
{
|
||||
return __('filament-shield::filament-shield.nav.role.icon');
|
||||
}
|
||||
|
||||
public static function getNavigationSort(): ?int
|
||||
{
|
||||
return Utils::getResourceNavigationSort();
|
||||
}
|
||||
|
||||
public static function getSlug(): string
|
||||
{
|
||||
return Utils::getResourceSlug();
|
||||
}
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return Utils::isResourceNavigationBadgeEnabled()
|
||||
? strval(static::getEloquentQuery()->count())
|
||||
: null;
|
||||
}
|
||||
|
||||
public static function isScopedToTenant(): bool
|
||||
{
|
||||
return Utils::isScopedToTenant();
|
||||
}
|
||||
|
||||
public static function canGloballySearch(): bool
|
||||
{
|
||||
return Utils::isResourceGloballySearchable() && count(static::getGloballySearchableAttributes()) && static::canViewAny();
|
||||
}
|
||||
|
||||
public static function getResourceEntitiesSchema(): ?array
|
||||
{
|
||||
return collect(FilamentShield::getResources())
|
||||
->sortKeys()
|
||||
->map(function ($entity) {
|
||||
$sectionLabel = strval(
|
||||
static::shield()->hasLocalizedPermissionLabels()
|
||||
? FilamentShield::getLocalizedResourceLabel($entity['fqcn'])
|
||||
: $entity['model']
|
||||
);
|
||||
|
||||
return Forms\Components\Section::make($sectionLabel)
|
||||
->description(fn () => new HtmlString('<span style="word-break: break-word;">' . Utils::showModelPath($entity['fqcn']) . '</span>'))
|
||||
->compact()
|
||||
->schema([
|
||||
static::getCheckBoxListComponentForResource($entity),
|
||||
])
|
||||
->columnSpan(static::shield()->getSectionColumnSpan())
|
||||
->collapsible();
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public static function getResourceTabBadgeCount(): ?int
|
||||
{
|
||||
return collect(FilamentShield::getResources())
|
||||
->map(fn ($resource) => count(static::getResourcePermissionOptions($resource)))
|
||||
->sum();
|
||||
}
|
||||
|
||||
public static function getResourcePermissionOptions(array $entity): array
|
||||
{
|
||||
return collect(Utils::getResourcePermissionPrefixes($entity['fqcn']))
|
||||
->flatMap(function ($permission) use ($entity) {
|
||||
$name = $permission . '_' . $entity['resource'];
|
||||
$label = static::shield()->hasLocalizedPermissionLabels()
|
||||
? FilamentShield::getLocalizedResourcePermissionLabel($permission)
|
||||
: $name;
|
||||
|
||||
return [
|
||||
$name => $label,
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public static function setPermissionStateForRecordPermissions(Component $component, string $operation, array $permissions, ?Model $record): void
|
||||
{
|
||||
|
||||
if (in_array($operation, ['edit', 'view'])) {
|
||||
|
||||
if (blank($record)) {
|
||||
return;
|
||||
}
|
||||
if ($component->isVisible() && count($permissions) > 0) {
|
||||
$component->state(
|
||||
collect($permissions)
|
||||
/** @phpstan-ignore-next-line */
|
||||
->filter(fn ($value, $key) => $record->checkPermissionTo($key))
|
||||
->keys()
|
||||
->toArray()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function getPageOptions(): array
|
||||
{
|
||||
return collect(FilamentShield::getPages())
|
||||
->flatMap(fn ($page) => [
|
||||
$page['permission'] => static::shield()->hasLocalizedPermissionLabels()
|
||||
? FilamentShield::getLocalizedPageLabel($page['class'])
|
||||
: $page['permission'],
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public static function getWidgetOptions(): array
|
||||
{
|
||||
return collect(FilamentShield::getWidgets())
|
||||
->flatMap(fn ($widget) => [
|
||||
$widget['permission'] => static::shield()->hasLocalizedPermissionLabels()
|
||||
? FilamentShield::getLocalizedWidgetLabel($widget['class'])
|
||||
: $widget['permission'],
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public static function getCustomPermissionOptions(): ?array
|
||||
{
|
||||
return FilamentShield::getCustomPermissions()
|
||||
->mapWithKeys(fn ($customPermission) => [
|
||||
$customPermission => static::shield()->hasLocalizedPermissionLabels() ? str($customPermission)->headline()->toString() : $customPermission,
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public static function getTabFormComponentForResources(): Component
|
||||
{
|
||||
return static::shield()->hasSimpleResourcePermissionView()
|
||||
? static::getTabFormComponentForSimpleResourcePermissionsView()
|
||||
: Forms\Components\Tabs\Tab::make('resources')
|
||||
->label(__('filament-shield::filament-shield.resources'))
|
||||
->visible(fn (): bool => (bool) Utils::isResourceEntityEnabled())
|
||||
->badge(static::getResourceTabBadgeCount())
|
||||
->schema([
|
||||
Forms\Components\Grid::make()
|
||||
->schema(static::getResourceEntitiesSchema())
|
||||
->columns(static::shield()->getGridColumns()),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getCheckBoxListComponentForResource(array $entity): Component
|
||||
{
|
||||
$permissionsArray = static::getResourcePermissionOptions($entity);
|
||||
|
||||
return static::getCheckboxListFormComponent($entity['resource'], $permissionsArray, false);
|
||||
}
|
||||
|
||||
public static function getTabFormComponentForPage(): Component
|
||||
{
|
||||
$options = static::getPageOptions();
|
||||
$count = count($options);
|
||||
|
||||
return Forms\Components\Tabs\Tab::make('pages')
|
||||
->label(__('filament-shield::filament-shield.pages'))
|
||||
->visible(fn (): bool => (bool) Utils::isPageEntityEnabled() && $count > 0)
|
||||
->badge($count)
|
||||
->schema([
|
||||
static::getCheckboxListFormComponent('pages_tab', $options),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getTabFormComponentForWidget(): Component
|
||||
{
|
||||
$options = static::getWidgetOptions();
|
||||
$count = count($options);
|
||||
|
||||
return Forms\Components\Tabs\Tab::make('widgets')
|
||||
->label(__('filament-shield::filament-shield.widgets'))
|
||||
->visible(fn (): bool => (bool) Utils::isWidgetEntityEnabled() && $count > 0)
|
||||
->badge($count)
|
||||
->schema([
|
||||
static::getCheckboxListFormComponent('widgets_tab', $options),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getTabFormComponentForCustomPermissions(): Component
|
||||
{
|
||||
$options = static::getCustomPermissionOptions();
|
||||
$count = count($options);
|
||||
|
||||
return Forms\Components\Tabs\Tab::make('custom')
|
||||
->label(__('filament-shield::filament-shield.custom'))
|
||||
->visible(fn (): bool => (bool) Utils::isCustomPermissionEntityEnabled() && $count > 0)
|
||||
->badge($count)
|
||||
->schema([
|
||||
static::getCheckboxListFormComponent('custom_permissions', $options),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getTabFormComponentForSimpleResourcePermissionsView(): Component
|
||||
{
|
||||
$options = FilamentShield::getAllResourcePermissions();
|
||||
$count = count($options);
|
||||
|
||||
return Forms\Components\Tabs\Tab::make('resources')
|
||||
->label(__('filament-shield::filament-shield.resources'))
|
||||
->visible(fn (): bool => (bool) Utils::isResourceEntityEnabled() && $count > 0)
|
||||
->badge($count)
|
||||
->schema([
|
||||
static::getCheckboxListFormComponent('resources_tab', $options),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getCheckboxListFormComponent(string $name, array $options, bool $searchable = true): Component
|
||||
{
|
||||
return Forms\Components\CheckboxList::make($name)
|
||||
->label('')
|
||||
->options(fn (): array => $options)
|
||||
->searchable($searchable)
|
||||
->afterStateHydrated(
|
||||
fn (Component $component, string $operation, ?Model $record) => static::setPermissionStateForRecordPermissions(
|
||||
component: $component,
|
||||
operation: $operation,
|
||||
permissions: $options,
|
||||
record: $record
|
||||
)
|
||||
)
|
||||
->dehydrated(fn ($state) => ! blank($state))
|
||||
->bulkToggleable()
|
||||
->gridDirection('row')
|
||||
->columns(static::shield()->getCheckboxListColumns())
|
||||
->columnSpan(static::shield()->getCheckboxListColumnSpan());
|
||||
}
|
||||
|
||||
public static function shield(): FilamentShieldPlugin
|
||||
{
|
||||
return FilamentShieldPlugin::get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Shield\RoleResource\Pages;
|
||||
|
||||
use App\Filament\Resources\Shield\RoleResource;
|
||||
use BezhanSalleh\FilamentShield\Support\Utils;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class CreateRole extends CreateRecord
|
||||
{
|
||||
protected static string $resource = RoleResource::class;
|
||||
|
||||
public Collection $permissions;
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
$this->permissions = collect($data)
|
||||
->filter(function ($permission, $key) {
|
||||
return ! in_array($key, ['name', 'guard_name', 'select_all']);
|
||||
})
|
||||
->values()
|
||||
->flatten()
|
||||
->unique();
|
||||
|
||||
return Arr::only($data, ['name', 'guard_name']);
|
||||
}
|
||||
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
$permissionModels = collect();
|
||||
$this->permissions->each(function ($permission) use ($permissionModels) {
|
||||
$permissionModels->push(Utils::getPermissionModel()::firstOrCreate([
|
||||
/** @phpstan-ignore-next-line */
|
||||
'name' => $permission,
|
||||
'guard_name' => $this->data['guard_name'],
|
||||
]));
|
||||
});
|
||||
|
||||
$this->record->syncPermissions($permissionModels);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Shield\RoleResource\Pages;
|
||||
|
||||
use App\Filament\Resources\Shield\RoleResource;
|
||||
use BezhanSalleh\FilamentShield\Support\Utils;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class EditRole extends EditRecord
|
||||
{
|
||||
protected static string $resource = RoleResource::class;
|
||||
|
||||
public Collection $permissions;
|
||||
|
||||
protected function getActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
$this->permissions = collect($data)
|
||||
->filter(function ($permission, $key) {
|
||||
return ! in_array($key, ['name', 'guard_name', 'select_all']);
|
||||
})
|
||||
->values()
|
||||
->flatten()
|
||||
->unique();
|
||||
|
||||
return Arr::only($data, ['name', 'guard_name']);
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
$permissionModels = collect();
|
||||
$this->permissions->each(function ($permission) use ($permissionModels) {
|
||||
$permissionModels->push(Utils::getPermissionModel()::firstOrCreate([
|
||||
'name' => $permission,
|
||||
'guard_name' => $this->data['guard_name'],
|
||||
]));
|
||||
});
|
||||
|
||||
$this->record->syncPermissions($permissionModels);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Shield\RoleResource\Pages;
|
||||
|
||||
use App\Filament\Resources\Shield\RoleResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListRoles extends ListRecords
|
||||
{
|
||||
protected static string $resource = RoleResource::class;
|
||||
|
||||
protected function getActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Shield\RoleResource\Pages;
|
||||
|
||||
use App\Filament\Resources\Shield\RoleResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewRole extends ViewRecord
|
||||
{
|
||||
protected static string $resource = RoleResource::class;
|
||||
|
||||
protected function getActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\EditAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -37,11 +37,24 @@ class UserResource extends Resource implements HasShieldPermissions
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\DateTimePicker::make('email_verified_at'),
|
||||
|
||||
Forms\Components\TextInput::make('password')
|
||||
->password()
|
||||
->required(fn (string $context): bool => $context === 'create')
|
||||
->dehydrated(fn ($state) => filled($state))
|
||||
->maxLength(255),
|
||||
|
||||
Forms\Components\Select::make('roles')
|
||||
->relationship('roles', 'name')
|
||||
->multiple()
|
||||
->preload()
|
||||
->searchable(),
|
||||
|
||||
Forms\Components\Select::make('permissions')
|
||||
->relationship('permissions', 'name')
|
||||
->multiple()
|
||||
->preload()
|
||||
->searchable()
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user