Open a TaskStream board for a project that has none yet
The project form gets «Crear en TaskStream» while the project has no board, and `taskstream: true` on a PATCH does the same through the API. A refusal leaves the edit unsaved; a project with a board keeps it, and `taskstream_key` without the flag is still ignored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,11 +13,11 @@ use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
/**
|
||||
* Opens a board in TaskStream for a project that is about to be created
|
||||
* here, and points the project at it.
|
||||
* Opens a board in TaskStream for a project, new or one without a board
|
||||
* yet, and points the project at it.
|
||||
*
|
||||
* It runs before the project is saved: if TaskStream says no, nothing is
|
||||
* created here either, and the caller gets the reason to fix and retry.
|
||||
* saved here either, and the caller gets the reason to fix and retry.
|
||||
* The board is public, like the project it tracks is meant to be.
|
||||
*/
|
||||
class OpenInTaskStream
|
||||
|
||||
@@ -4,15 +4,22 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Resources\Projects\Pages;
|
||||
|
||||
use App\Actions\Projects\OpenInTaskStream;
|
||||
use App\Filament\Concerns\WarnsOfConcurrentEditing;
|
||||
use App\Filament\Resources\Projects\ProjectResource;
|
||||
use App\Filament\Support\DuplicateAction;
|
||||
use App\Filament\Support\PreviewAction;
|
||||
use App\Filament\Support\TranslateAction;
|
||||
use App\Services\Pireon\TokenStore;
|
||||
use App\Services\TaskStream\TaskStreamClient;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
class EditProject extends EditRecord
|
||||
{
|
||||
@@ -32,9 +39,72 @@ class EditProject extends EditRecord
|
||||
->url(fn () => app(TaskStreamClient::class)->projectUrl((string) $this->record->taskstream_key))
|
||||
->openUrlInNewTab()
|
||||
->visible(fn () => filled($this->record->taskstream_key)),
|
||||
$this->createInTaskStream(),
|
||||
DuplicateAction::make(),
|
||||
TranslateAction::make(),
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* For a project made without a board: opens one now, as the person
|
||||
* signed in, and saves only the link (whatever is being edited in the
|
||||
* form stays as it is, unsaved).
|
||||
*/
|
||||
protected function createInTaskStream(): Action
|
||||
{
|
||||
return Action::make('createInTaskStream')
|
||||
->label(__('panel.projects.taskstream.create'))
|
||||
->icon('heroicon-o-plus')
|
||||
->color('gray')
|
||||
->visible(fn () => blank($this->record->taskstream_key))
|
||||
->authorize('update')
|
||||
->modalHeading(__('panel.projects.taskstream.create_heading'))
|
||||
->modalDescription(__('panel.projects.taskstream.section_hint'))
|
||||
->modalSubmitActionLabel(__('panel.projects.taskstream.create'))
|
||||
->schema([
|
||||
TextInput::make('key')
|
||||
->label(__('panel.projects.taskstream.key'))
|
||||
->helperText(__('panel.projects.taskstream.key_hint'))
|
||||
->placeholder(fn () => app(OpenInTaskStream::class)
|
||||
->keyFrom((string) $this->record->getTranslation('slug', (string) config('locales.default'), false)))
|
||||
->maxLength(10)
|
||||
->regex('/^[A-Za-z][A-Za-z0-9]{1,9}$/'),
|
||||
])
|
||||
->action(function (array $data, Action $action): void {
|
||||
$project = $this->record;
|
||||
|
||||
try {
|
||||
app(OpenInTaskStream::class)->handle(
|
||||
$project,
|
||||
Auth::user(),
|
||||
app(TokenStore::class)->accessToken(),
|
||||
filled($data['key'] ?? null) ? $data['key'] : null,
|
||||
);
|
||||
} catch (ValidationException|HttpException $e) {
|
||||
// The modal stays open, to change the key and try again.
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title($e instanceof ValidationException ? collect($e->errors())->flatten()->first() : $e->getMessage())
|
||||
->send();
|
||||
|
||||
$action->halt();
|
||||
}
|
||||
|
||||
$project->save();
|
||||
|
||||
$url = app(TaskStreamClient::class)->projectUrl((string) $project->taskstream_key);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title(__('panel.projects.taskstream.created', ['url' => $url]))
|
||||
->actions([
|
||||
Action::make('taskstream')
|
||||
->label(__('panel.projects.taskstream.open'))
|
||||
->url($url, shouldOpenInNewTab: true),
|
||||
])
|
||||
->persistent()
|
||||
->send();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,11 +72,22 @@ class ProjectController extends Controller
|
||||
return ProjectResource::make($project->load(self::WITH));
|
||||
}
|
||||
|
||||
public function update(ProjectRequest $request, Project $project): ProjectResource
|
||||
/**
|
||||
* `taskstream: true` opens a board for a project that has none yet, as on
|
||||
* creation (and nothing is saved if that fails); one that has a board
|
||||
* keeps it.
|
||||
*/
|
||||
public function update(ProjectRequest $request, Project $project, OpenInTaskStream $openInTaskStream): ProjectResource
|
||||
{
|
||||
$this->authorize('update', $project);
|
||||
|
||||
return ProjectResource::make($this->persist($this->fill($project, $request), $request));
|
||||
$project = $this->fill($project, $request);
|
||||
|
||||
if ($request->boolean('taskstream') && $project->taskstream_project_id === null) {
|
||||
$openInTaskStream->handle($project, $request->user(), $request->bearerToken(), $request->validated('taskstream_key'));
|
||||
}
|
||||
|
||||
return ProjectResource::make($this->persist($project, $request));
|
||||
}
|
||||
|
||||
public function destroy(Project $project): JsonResponse
|
||||
|
||||
@@ -20,8 +20,9 @@ use Illuminate\Validation\Validator;
|
||||
* Relations travel as ids and are replaced whole when sent: `technology_ids`,
|
||||
* `service_ids`, and `members` with each one's role on the project.
|
||||
*
|
||||
* `taskstream` (on creation only) opens a public board in TaskStream for the
|
||||
* project, keyed `taskstream_key` or one derived from the slug.
|
||||
* `taskstream` opens a public board in TaskStream for the project, keyed
|
||||
* `taskstream_key` or one derived from the slug; on an update, only when the
|
||||
* project has none yet.
|
||||
*/
|
||||
class ProjectRequest extends FormRequest
|
||||
{
|
||||
@@ -78,10 +79,10 @@ class ProjectRequest extends FormRequest
|
||||
'members.*.id' => ['required', 'integer', 'distinct', Rule::exists('members', 'id')->whereNull('deleted_at')],
|
||||
'members.*.role' => ['nullable', 'string', 'max:120'],
|
||||
|
||||
// Only when creating; a client PATCHing back what it read sends
|
||||
// the key it got, which is ignored rather than refused.
|
||||
'taskstream' => [$creating ? 'sometimes' : 'exclude', 'boolean'],
|
||||
'taskstream_key' => [$creating ? 'sometimes' : 'exclude', 'nullable', 'string', 'regex:/^[A-Za-z][A-Za-z0-9]{1,9}$/'],
|
||||
// The key is only read along with `taskstream`: a client PATCHing
|
||||
// back what it read sends the key it got, which changes nothing.
|
||||
'taskstream' => ['sometimes', 'boolean'],
|
||||
'taskstream_key' => ['sometimes', 'nullable', 'string', 'regex:/^[A-Za-z][A-Za-z0-9]{1,9}$/'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user