Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New File Upload Input #136

Merged
merged 44 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
7368014
wip
PhilReinking Feb 18, 2024
cf1b3bd
update ui
PhilReinking Feb 18, 2024
dde4de9
add all options to file input
PhilReinking Feb 18, 2024
3b144d5
implement useFileDialog for Settings
PhilReinking Feb 18, 2024
fc3293e
wip frontend
PhilReinking Feb 20, 2024
e2fd239
wip
PhilReinking Feb 20, 2024
248e436
show icons for uploads
PhilReinking Feb 21, 2024
f5a0757
update configure file test
PhilReinking Feb 21, 2024
3f14249
use watcheffect
PhilReinking Feb 22, 2024
633c37b
change test payload
PhilReinking Feb 22, 2024
eea0845
upload route
PhilReinking Feb 22, 2024
2bf5da5
first version of working upload
PhilReinking Feb 22, 2024
83c4164
remove unused import
PhilReinking Feb 22, 2024
b4382b6
first version of downloadable files in submissions view
PhilReinking Mar 7, 2024
40d5282
output download urls in submission export
PhilReinking Mar 7, 2024
5587b20
wip file type validation
PhilReinking Mar 19, 2024
8b149eb
validate file on drop
PhilReinking Mar 28, 2024
e3a4f05
update packages
PhilReinking Jun 19, 2024
331574a
add file upload progress to form button
PhilReinking Jul 1, 2024
bbc8d8e
fix focusing D9Input in ClickInteraction
PhilReinking Jul 4, 2024
e890c68
change style and wording of block settings
PhilReinking Jul 4, 2024
aa642d2
update packages
PhilReinking Jul 4, 2024
60a3007
fix file upload handline
PhilReinking Jul 4, 2024
cd6f63c
hide file upload input if max files are reached
PhilReinking Jul 4, 2024
9db9420
show validation on update event
PhilReinking Jul 4, 2024
174208f
update translations
PhilReinking Jul 4, 2024
9d80c3a
change valid email translation
PhilReinking Jul 4, 2024
480b6be
update translation keys
PhilReinking Jul 4, 2024
3c18f91
update lang keys
PhilReinking Jul 4, 2024
d0224d5
wip language
PhilReinking Jul 5, 2024
c32fae1
add validation to set files
PhilReinking Jul 11, 2024
5fdc180
update translations
PhilReinking Jul 12, 2024
b7ab073
update vue-tsc
PhilReinking Jul 12, 2024
907a813
update button test snapshot
PhilReinking Jul 12, 2024
e7bffd1
fix has uploads getter
PhilReinking Jul 12, 2024
be1c28c
delete uploads when form session is deleted
PhilReinking Jul 19, 2024
f6981d9
display 404 when file not found
PhilReinking Jul 19, 2024
fce2e09
fix purging of submissions
PhilReinking Aug 2, 2024
84d355f
fix webhook caller with file uploads
PhilReinking Aug 2, 2024
fbebe40
update vue
PhilReinking Aug 15, 2024
83cd3d1
fix navigator
PhilReinking Aug 15, 2024
1d132d8
fix wrong key in navigation button
PhilReinking Aug 15, 2024
ec2a3fc
fix progress indicator
PhilReinking Aug 15, 2024
cae77b9
add json encoding to debug mode when saving form response
PhilReinking Aug 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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