Skip to content

Commit

Permalink
New File Upload Input (#136)
Browse files Browse the repository at this point in the history
* wip

* update ui

* add all options to file input

* implement useFileDialog for Settings

* wip frontend

* wip

* show icons for uploads

* update configure file test

* use watcheffect

* change test payload

* upload route

* first version of working upload

* remove unused import

* first version of downloadable files in submissions view

* output download urls in submission export

* wip file type validation

* validate file on drop

* update packages

* add file upload progress to form button

* fix focusing D9Input in ClickInteraction

* change style and wording of block settings

* update packages

* fix file upload handline

* hide file upload input if max files are reached

* show validation on update event

* update translations

* change valid email translation

* update translation keys

* update lang keys

* wip language

* add validation to set files

* update translations

* update vue-tsc

* update button test snapshot

* fix has uploads getter

* delete uploads when form session is deleted

* display 404 when file not found

* fix purging of submissions

* fix webhook caller with file uploads

* update vue

* fix navigator

* fix wrong key in navigation button

* fix progress indicator

* add json encoding to debug mode when saving form response
  • Loading branch information
PhilReinking authored Aug 15, 2024
1 parent 6fc8372 commit 8c269fa
Show file tree
Hide file tree
Showing 63 changed files with 1,764 additions and 693 deletions.
1 change: 1 addition & 0 deletions app/Enums/FormBlockInteractionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ enum FormBlockInteractionType: string
case textarea = 'textarea';
case button = 'button';
case consent = 'consent';
case file = 'file';

case range = 'range';

Expand Down
2 changes: 2 additions & 0 deletions app/Enums/FormBlockType.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ enum FormBlockType: string
case checkbox = 'checkbox';
case radio = 'radio';

case file = 'input-file';

case long = 'input-long';
case short = 'input-short';
case email = 'input-email';
Expand Down
16 changes: 11 additions & 5 deletions app/Http/Controllers/Api/FormSubmitController.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,22 @@ public function __invoke(Request $request, Form $form)
{
$request->validate([
'token' => 'required|string',
'payload' => 'array',
'is_uploading' => 'boolean',
'payload' => 'array|nullable',
]);

$session = $form->formSessions()
->where('token', $request->input('token'))
->firstOrFail()
->submit($request->input('payload'));
->firstOrFail();

event(new FormSessionCompletedEvent($session));
if (!is_null($request->payload)) {
$session->submit($request->input('payload'));
}

return response()->json($session, 200);
if (!$request->input('is_uploading', false)) {
event(new FormSessionCompletedEvent($session));
}

return response()->json($session->setHidden(['form']), 200);
}
}
38 changes: 38 additions & 0 deletions app/Http/Controllers/Api/FormUploadController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Form;
use App\Models\FormBlockInteraction;
use Illuminate\Http\Request;

class FormUploadController extends Controller
{
public function __invoke(Request $request, Form $form)
{
$request->validate([
'token' => 'required|string',
'actionId' => 'required|string',
'file' => 'file',
]);

$interaction = FormBlockInteraction::withUuid($request->input('actionId'))
->firstOrFail();

// Validate that action belongs to the form
if ($interaction->formBlock->form->id !== $form->id) {
abort(404, 'Action not found');
}

$session = $form->formSessions()
->where('token', $request->input('token'))
->firstOrFail();

$sessionResponse = $session->formSessionResponses->where('form_block_interaction_id', $interaction->id)->first();

$upload = $sessionResponse->saveUpload($request->file('file'));

return response()->json($upload, 201);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public function __invoke(Form $form)
{
$this->authorize('update', $form);

$form->formSessions()->delete();
$form->formSessions->each(fn ($session) => $session->delete());

return response()->json(null, 204);
}
Expand Down
21 changes: 21 additions & 0 deletions app/Http/Controllers/FormUploadsDownloadController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\FormSessionUpload;
use Illuminate\Support\Facades\Storage;

class FormUploadsDownloadController extends Controller
{
public function __invoke(Request $request, $upload)
{
$upload = FormSessionUpload::whereUuid($upload)->firstOrFail();

if (!Storage::fileExists($upload->path)) {
abort(404);
}

return Storage::download($upload->path, $upload->name);
}
}
1 change: 1 addition & 0 deletions app/Http/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class Kernel extends HttpKernel
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'check-user-setup' => CheckUserSetup::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
];

/**
Expand Down
21 changes: 19 additions & 2 deletions app/Http/Resources/FormSessionResponseResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@ public function toArray($request)
'message' => strip_tags($this->formBlock->message),
'name' => $this->formBlock->title ?? $this->formBlock->uuid,
'value' => $this->formatValue($this->value),
'original' => $this->value,
'original' => $this->formBlock->type === FormBlockType::file ? $this->appendFiles() : $this->value,
'type' => $this->formBlock->type,
];
} catch (\Exception $e) {
return [
'name' => '',
'value' => '',
'original' => '',
'message' => '',
'type' => '',
];
}
}
Expand All @@ -50,13 +52,28 @@ protected function formatValue($value)
if ($this->formBlock->type === FormBlockType::consent) {
$accepted = $value['accepted'] ? 'yes' : 'no';

return $value['consent'].': '.$accepted;
return $value['consent'] . ': ' . $accepted;
}

if ($this->formBlock->type === FormBlockType::rating || $this->formBlock->type === FormBlockType::scale) {
return $value;
}

if ($this->formBlock->type === FormBlockType::file) {
return $this->formSessionUploads->map(fn ($upload) => $upload->downloadUrl)->join(', ');
}

return 'Unsupported value type';
}

protected function appendFiles()
{
return $this->formSessionUploads->map(function ($upload) {
return [
'uuid' => $upload->uuid,
'name' => $upload->name,
'url' => $upload->downloadUrl,
];
});
}
}
3 changes: 3 additions & 0 deletions app/Models/FormBlock.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ public function getInteractionType(): ?FormBlockInteractionType
case FormBlockType::long:
return FormBlockInteractionType::textarea;

case FormBlockType::file:
return FormBlockInteractionType::file;

case FormBlockType::checkbox:
case FormBlockType::radio:
return FormBlockInteractionType::button;
Expand Down
17 changes: 17 additions & 0 deletions app/Models/FormSession.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Storage;

class FormSession extends Model
{
Expand All @@ -26,6 +27,17 @@ class FormSession extends Model
'is_completed',
];

protected static function booted(): void
{
static::deleting(function (FormSession $session) {
$session->responses->each(function (FormSessionResponse $response) {
$response->formSessionUploads->each(function (FormSessionUpload $upload) {
Storage::delete($upload->path);
});
});
});
}

public function form()
{
return $this->belongsTo(Form::class, 'form_id', 'id');
Expand All @@ -36,6 +48,11 @@ public function webhooks()
return $this->hasMany(FormSessionWebhook::class);
}

public function responses()
{
return $this->hasMany(FormSessionResponse::class);
}

public static function getByTokenAndForm(string $token, Form $form)
{
return self::where('token', $token)->where('form_id', $form->id)->first();
Expand Down
22 changes: 20 additions & 2 deletions app/Models/FormSessionResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Str;

class FormSessionResponse extends Model
{
Expand Down Expand Up @@ -38,17 +40,33 @@ public function formSession()
return $this->belongsTo(FormSession::class, 'form_session_id');
}

public function formSessionUploads()
{
return $this->hasMany(FormSessionUpload::class);
}

public function setValueAttribute($new)
{
$this->attributes['value'] = encrypt($new);
$this->attributes['value'] = config('app.debug') ? json_encode($new) : encrypt($new);
}

public function getValueAttribute()
{
try {
return decrypt($this->attributes['value']);
return config('app.debug') ? json_decode($this->attributes['value'], true) : decrypt($this->attributes['value']);
} catch (\Throwable $th) {
return $this->attributes['value'];
}
}

public function saveUpload(UploadedFile $file)
{
return $this->formSessionUploads()->create([
'uuid' => Str::uuid(),
'name' => $file->getClientOriginalName(),
'path' => $file->store(implode('/', ['uploads', $this->id])),
'type' => $file->getClientMimeType(),
'size' => $file->getSize(),
]);
}
}
29 changes: 29 additions & 0 deletions app/Models/FormSessionUpload.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace App\Models;

use Illuminate\Support\Facades\URL;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class FormSessionUpload extends Model
{
use HasFactory;

protected $guarded = [];

protected $appends = ['download_url'];

public function formSessionResponse()
{
return $this->belongsTo(FormSessionResponse::class);
}

public function downloadUrl(): Attribute
{
return Attribute::make(
get: fn (mixed $value, array $attributes) => URL::temporarySignedRoute('forms.submission-uploads.download', now()->addDays(7), $attributes['uuid'])
);
}
}
6 changes: 5 additions & 1 deletion app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ public function register()
$this->app->bind(
HttpClientInterface::class,
function ($app) {
return new NoPrivateNetworkHttpClient(HttpClient::create());
return new NoPrivateNetworkHttpClient(HttpClient::create([
'headers' => [
'user-agent' => 'Input-App/1.0',
],
]));
}
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?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('form_session_uploads', function (Blueprint $table) {
$table->id();
$table->uuid('uuid');
$table->string('name');
$table->string('path');
$table->string('type');
$table->string('size');
$table->unsignedBigInteger('form_session_response_id');
$table->timestamps();
});

Schema::table('form_session_uploads', function (Blueprint $table) {
if (DB::getDriverName() !== 'sqlite') {
$table->foreign('form_session_response_id')
->references('id')->on('form_session_responses')
->onDelete('CASCADE');
}
});
}
};
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ services:
MINIO_ROOT_USER: "sail"
MINIO_ROOT_PASSWORD: "password"
volumes:
- "./storage/minio:/data/export"
- "sail-minio:/data/minio"
networks:
- sail
command: minio server /data/export --console-address ":8900"
command: minio server /data/minio --console-address ":8900"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
retries: 3
Expand Down
Loading

0 comments on commit 8c269fa

Please sign in to comment.