Skip to content

Commit 2194802

Browse files
committed
Add firm metadata and a publication gate for library imports.
Items can store firm path, library area, committee, and project tags. Manifest imports land as draft or pending and stay out of search until a gatekeeper publishes them.
1 parent 7350539 commit 2194802

23 files changed

Lines changed: 1337 additions & 36 deletions

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
All notable changes to `:package_name` will be documented in this file.
44

5+
## Unreleased
6+
7+
* Library items can store firm path, library area, committee, and project tags
8+
* Manifest import lands each row as draft or pending and never auto-publishes
9+
* Search and library browse exclude draft, pending, and rejected items until a gatekeeper publishes them
10+
* Gatekeeper queue, review command, and publish / reject events
11+
512
## v1.4.1 - 2026-09-07
613

714
Fix User column searchable() callback for Filament 5.7.8 — pass custom search via named query: argument so Filament does not treat the closure as isSearchable.

‎README.md‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,14 @@ public function boot(): void
248248
}
249249
```
250250

251+
### `LibraryItemPublished`
252+
253+
[`Tapp\FilamentLibrary\Events\LibraryItemPublished`](src/Events/LibraryItemPublished.php) is fired when a gatekeeper publishes an item. Listen here to update a search index. Imports do not fire this event.
254+
255+
### `LibraryItemRejected`
256+
257+
[`Tapp\FilamentLibrary\Events\LibraryItemRejected`](src/Events/LibraryItemRejected.php) is fired when a gatekeeper rejects an item. The item stays out of search.
258+
251259
### `LibraryFileRestored`
252260

253261
[`Tapp\FilamentLibrary\Events\LibraryFileRestored`](src/Events/LibraryFileRestored.php) is fired after a soft-deleted `LibraryItem` of type `file` is restored (for example via Filament's Restore action).
@@ -271,6 +279,23 @@ public function boot(): void
271279
}
272280
```
273281

282+
## Publication and firm metadata
283+
284+
Library items can carry the path to the real file and the metadata that arrives with a manifest:
285+
286+
- `firm_path` — path to the real file (Copy Path reads this)
287+
- `library_area` — folder or Library area
288+
- `committee` — committee that owns the item
289+
- `project_tags` — project tags from the manifest
290+
291+
Imports do not publish themselves. `php artisan filament-library:import {manifest.csv} --user={id}` creates each row as `pending` (or `--status=draft`). A `published` value in the file is ignored. Draft, pending, and rejected items are excluded from Search All and from the main Library and Public Library lists. `LibraryItem::query()->searchable()` is the same rule for Ask or any other search.
292+
293+
A gatekeeper publishes or rejects an item with `php artisan filament-library:review {id} publish` or `reject`, or from the Gatekeeper Queue when the library admin check passes. Publishing dispatches `LibraryItemPublished`. Rejecting dispatches `LibraryItemRejected` and keeps the item out of search. `return` sends a pending item back to draft, which is also excluded from search.
294+
295+
Ordinary creates stay published so existing libraries keep working. Set `publication.new_items_require_approval` to `true` when new items should wait for a gatekeeper too. Set `publication.committees` to the allowed committee names (for example `['Design', 'Technical']`) when imports should reject any other committee. Leave it empty to accept any committee name.
296+
297+
Manifest columns: `name`, `firm_path`, `library_area` (or `folder`), `committee`, `project_tags` (split on `|`, `;`, or `,`), `type` (`file`, `folder`, or `link`), `url`, and `status` (`draft` or `pending`).
298+
274299
## Testing
275300

276301
```bash

‎config/filament-library.php‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,25 @@
226226
| Configure text-based file previews for markdown and JSON exports.
227227
|
228228
*/
229+
/*
230+
|--------------------------------------------------------------------------
231+
| Publication
232+
|--------------------------------------------------------------------------
233+
|
234+
| Imports land as draft or pending and stay out of search until a
235+
| gatekeeper publishes them. Rejected items stay out of search.
236+
|
237+
| Existing items and ordinary creates stay published unless
238+
| new_items_require_approval is turned on. committees restricts the
239+
| committee values an import will accept. Leave it empty to allow any
240+
| committee name. Example: ['Design', 'Technical'].
241+
|
242+
*/
243+
'publication' => [
244+
'new_items_require_approval' => env('FILAMENT_LIBRARY_NEW_ITEMS_REQUIRE_APPROVAL', false),
245+
'committees' => [],
246+
],
247+
229248
'preview' => [
230249
'text_max_bytes' => 2 * 1024 * 1024,
231250
'markdown_extensions' => ['md', 'markdown', 'mdown'],
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*/
12+
public function up(): void
13+
{
14+
Schema::table('library_items', function (Blueprint $table) {
15+
$table->string('firm_path', 1024)->nullable();
16+
$table->string('library_area')->nullable();
17+
$table->string('committee')->nullable();
18+
$table->json('project_tags')->nullable();
19+
$table->string('publication_status')->default('published')->index();
20+
$table->timestamp('published_at')->nullable();
21+
$table->foreignId('published_by')->nullable()->constrained('users')->nullOnDelete();
22+
$table->timestamp('rejected_at')->nullable();
23+
$table->foreignId('rejected_by')->nullable()->constrained('users')->nullOnDelete();
24+
$table->text('rejection_reason')->nullable();
25+
});
26+
}
27+
28+
/**
29+
* Reverse the migrations.
30+
*/
31+
public function down(): void
32+
{
33+
Schema::table('library_items', function (Blueprint $table) {
34+
$table->dropConstrainedForeignId('published_by');
35+
$table->dropConstrainedForeignId('rejected_by');
36+
$table->dropColumn([
37+
'firm_path',
38+
'library_area',
39+
'committee',
40+
'project_tags',
41+
'publication_status',
42+
'published_at',
43+
'rejected_at',
44+
'rejection_reason',
45+
]);
46+
});
47+
}
48+
};
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
namespace Tapp\FilamentLibrary\Commands;
4+
5+
use Illuminate\Console\Command;
6+
use Illuminate\Support\Facades\DB;
7+
use Tapp\FilamentLibrary\Enums\LibraryPublicationStatus;
8+
use Tapp\FilamentLibrary\Services\LibraryImporter;
9+
10+
class ImportLibraryCommand extends Command
11+
{
12+
public $signature = 'filament-library:import
13+
{path : CSV manifest path}
14+
{--user= : User id recorded as the creator}
15+
{--status=pending : draft or pending. Published values are ignored}';
16+
17+
public $description = 'Import a library manifest as draft or pending items';
18+
19+
public function handle(LibraryImporter $importer): int
20+
{
21+
$userId = (int) $this->option('user');
22+
23+
if ($userId < 1 || ! DB::table('users')->where('id', $userId)->exists()) {
24+
$this->error('Pass a valid --user id. Imports are recorded against that user.');
25+
26+
return self::FAILURE;
27+
}
28+
29+
$status = LibraryPublicationStatus::tryFrom(strtolower((string) $this->option('status')));
30+
31+
if (! in_array($status, [LibraryPublicationStatus::Draft, LibraryPublicationStatus::Pending], true)) {
32+
$this->error('Status must be draft or pending. Imports are not published automatically.');
33+
34+
return self::FAILURE;
35+
}
36+
37+
$result = $importer->importCsv((string) $this->argument('path'), $userId, $status);
38+
39+
foreach ($result->errors as $error) {
40+
$this->error($error);
41+
}
42+
43+
$this->info("Imported {$result->importedCount()} library item(s) as {$status->value}.");
44+
45+
if (! $result->succeeded()) {
46+
return self::FAILURE;
47+
}
48+
49+
return self::SUCCESS;
50+
}
51+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
namespace Tapp\FilamentLibrary\Commands;
4+
5+
use Illuminate\Console\Command;
6+
use Illuminate\Support\Facades\DB;
7+
use Tapp\FilamentLibrary\FilamentLibraryPlugin;
8+
use Tapp\FilamentLibrary\Models\LibraryItem;
9+
10+
class ReviewLibraryItemCommand extends Command
11+
{
12+
public $signature = 'filament-library:review
13+
{id : Library item id}
14+
{action : publish, reject, or return}
15+
{--reason= : Note stored when rejecting}
16+
{--user= : Gatekeeper user id}';
17+
18+
public $description = 'Publish, reject, or return a library item';
19+
20+
public function handle(): int
21+
{
22+
$model = FilamentLibraryPlugin::libraryItemModelClass();
23+
$item = $model::query()->find($this->argument('id'));
24+
25+
if (! $item instanceof LibraryItem) {
26+
$this->error('Library item was not found.');
27+
28+
return self::FAILURE;
29+
}
30+
31+
$gatekeeperId = $this->gatekeeperId();
32+
33+
if ($gatekeeperId === false) {
34+
return self::FAILURE;
35+
}
36+
37+
$action = strtolower((string) $this->argument('action'));
38+
$reason = $this->option('reason');
39+
$reason = is_string($reason) && trim($reason) !== '' ? trim($reason) : null;
40+
41+
match ($action) {
42+
'publish' => $item->publish($gatekeeperId),
43+
'reject' => $item->reject($gatekeeperId, $reason),
44+
'return' => $item->returnToDraft($gatekeeperId),
45+
default => null,
46+
};
47+
48+
if (! in_array($action, ['publish', 'reject', 'return'], true)) {
49+
$this->error('Action must be publish, reject, or return.');
50+
51+
return self::FAILURE;
52+
}
53+
54+
$item->refresh();
55+
$this->info("Library item {$item->getKey()} is now {$item->publication_status->value}.");
56+
57+
return self::SUCCESS;
58+
}
59+
60+
protected function gatekeeperId(): int | false | null
61+
{
62+
$userId = $this->option('user');
63+
64+
if ($userId === null || $userId === '') {
65+
return null;
66+
}
67+
68+
$userId = (int) $userId;
69+
70+
if ($userId < 1 || ! DB::table('users')->where('id', $userId)->exists()) {
71+
$this->error('Gatekeeper user was not found.');
72+
73+
return false;
74+
}
75+
76+
return $userId;
77+
}
78+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace Tapp\FilamentLibrary\Enums;
4+
5+
enum LibraryPublicationStatus: string
6+
{
7+
case Draft = 'draft';
8+
case Pending = 'pending';
9+
case Published = 'published';
10+
case Rejected = 'rejected';
11+
12+
public function isSearchable(): bool
13+
{
14+
return $this === self::Published;
15+
}
16+
17+
public function label(): string
18+
{
19+
return match ($this) {
20+
self::Draft => 'Draft',
21+
self::Pending => 'Pending',
22+
self::Published => 'Published',
23+
self::Rejected => 'Rejected',
24+
};
25+
}
26+
27+
/**
28+
* @return array<string, string>
29+
*/
30+
public static function options(): array
31+
{
32+
$options = [];
33+
34+
foreach (self::cases() as $status) {
35+
$options[$status->value] = $status->label();
36+
}
37+
38+
return $options;
39+
}
40+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Tapp\FilamentLibrary\Events;
4+
5+
use Illuminate\Foundation\Events\Dispatchable;
6+
use Illuminate\Queue\SerializesModels;
7+
use Tapp\FilamentLibrary\Models\LibraryItem;
8+
9+
/**
10+
* Fired when a gatekeeper publishes a library item.
11+
* Host applications may listen to update a search index. Drafts are not published by import.
12+
*/
13+
class LibraryItemPublished
14+
{
15+
use Dispatchable;
16+
use SerializesModels;
17+
18+
public function __construct(
19+
public LibraryItem $libraryItem,
20+
) {}
21+
}

‎src/Events/LibraryItemRejected.php‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Tapp\FilamentLibrary\Events;
4+
5+
use Illuminate\Foundation\Events\Dispatchable;
6+
use Illuminate\Queue\SerializesModels;
7+
use Tapp\FilamentLibrary\Models\LibraryItem;
8+
9+
/**
10+
* Fired when a gatekeeper rejects a library item.
11+
* Rejected items stay out of search. Host applications may listen to drop an index entry.
12+
*/
13+
class LibraryItemRejected
14+
{
15+
use Dispatchable;
16+
use SerializesModels;
17+
18+
public function __construct(
19+
public LibraryItem $libraryItem,
20+
public ?string $reason = null,
21+
) {}
22+
}

0 commit comments

Comments
 (0)