Compare commits

...

30 Commits

Author SHA1 Message Date
philipp lang bbdc9e9dfb Fix tests
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2024-07-01 12:08:59 +02:00
philipp lang 7e39a4da84 Fix tests
continuous-integration/drone/push Build is failing Details
2024-06-30 23:50:01 +02:00
philipp lang ae450ef9ae Add table export
continuous-integration/drone/push Build is failing Details
2024-06-30 23:36:44 +02:00
philipp lang 2425ff6638 Add adapter test 2024-06-30 21:04:54 +02:00
philipp lang 2586669c96 Delete skeleton dir when container starts 2024-06-30 21:04:20 +02:00
philipp lang af124f897b Fix Form export
continuous-integration/drone/push Build is passing Details
2024-06-30 18:51:14 +02:00
philipp lang c78c1cfee7 Make Export not nullable
continuous-integration/drone/push Build is failing Details
2024-06-30 18:10:53 +02:00
philipp lang bbd3237330 Add fields for form export
continuous-integration/drone/push Build is passing Details
2024-06-29 19:09:00 +02:00
philipp lang fcc01d26ac Lint
continuous-integration/drone/push Build is passing Details
2024-06-29 18:54:56 +02:00
philipp lang 93d30d10eb Add export to form
continuous-integration/drone/push Build is failing Details
2024-06-29 18:02:23 +02:00
philipp lang c7fc76466e Add fileshare to groups
continuous-integration/drone/push Build is passing Details
2024-06-29 14:36:35 +02:00
philipp lang 6aaeb75b2a Add remote selector
continuous-integration/drone/push Build is passing Details
2024-06-29 13:13:11 +02:00
philipp lang 8c3dc2211b Add loading component 2024-06-29 11:27:08 +02:00
philipp lang d06ae7acb5 Lint
continuous-integration/drone/push Build is passing Details
2024-06-29 11:23:33 +02:00
philipp lang 63c62dc958 Fix: Return Basename instead of Filename 2024-06-29 10:34:40 +02:00
philipp lang 71903936a3 Add api for listing subdirectories
continuous-integration/drone/push Build is failing Details
2024-06-28 23:56:34 +02:00
philipp lang 0ad6b40393 Add tests 2024-06-28 22:26:38 +02:00
philipp lang f04cf7a1b2 Install webdav filesystem 2024-06-28 22:26:09 +02:00
philipp lang 94cc0147bc Add fileshare api 2024-06-28 15:51:40 +02:00
philipp lang 73633d7628 Move component assertion 2024-06-28 15:51:40 +02:00
philipp lang 5482d47f0c Rename fileshare 2024-06-28 15:51:40 +02:00
philipp lang e1c3240290 Add update for fileshares 2024-06-28 15:51:40 +02:00
philipp lang e9e0be83cc Add: Store connection frontend 2024-06-28 15:51:40 +02:00
philipp lang 32e7f8c41d Rename test files 2024-06-28 15:51:40 +02:00
philipp lang 4cfa70e534 Lint 2024-06-28 15:51:40 +02:00
philipp lang 45597558e3 Add owncloud domain to drone 2024-06-28 15:51:40 +02:00
philipp lang c99a5a0369 Add owncloud application to drone 2024-06-28 15:51:40 +02:00
philipp lang 258093f2c4 Add fileshare storage 2024-06-28 15:51:40 +02:00
philipp lang f16b58dbc6 Add type information to index 2024-06-28 15:51:40 +02:00
philipp lang 8609a648fd Add fileshare index 2024-06-28 15:51:40 +02:00
73 changed files with 2811 additions and 890 deletions

View File

@ -21,6 +21,16 @@ steps:
commands: commands:
- while ! mysqladmin ping -h db -u db -pdb --silent; do sleep 1; done - while ! mysqladmin ping -h db -u db -pdb --silent; do sleep 1; done
- name: ocdb_healthcheck
image: mysql:oracle
commands:
- while ! mysqladmin ping -h ownclouddb -u owncloud -powncloud --silent; do sleep 1; done
- name: oc_healthcheck
image: zoomyboy/adrema-base:latest
commands:
- while ! curl --silent 'http://owncloudserver:8080/ocs/v1.php/cloud/capabilities?format=json' -u admin:admin | grep '"status":"ok"'; do sleep 1; done
- name: node - name: node
image: node:18.13.0-slim image: node:18.13.0-slim
commands: commands:
@ -58,6 +68,7 @@ steps:
XELATEX_BIN: /usr/bin/xelatex XELATEX_BIN: /usr/bin/xelatex
SCOUT_DRIVER: database SCOUT_DRIVER: database
MEILI_MASTER_KEY: abc MEILI_MASTER_KEY: abc
TEST_OWNCLOUD_DOMAIN: http://owncloudserver:8080
- name: docker_app_push - name: docker_app_push
image: plugins/docker image: plugins/docker
@ -160,6 +171,29 @@ services:
image: getmeili/meilisearch:v1.6 image: getmeili/meilisearch:v1.6
commands: commands:
- meilisearch --master-key="abc" - meilisearch --master-key="abc"
- name: ownclouddb
image: mariadb:10.11
environment:
MYSQL_ROOT_PASSWORD: owncloud
MYSQL_USER: owncloud
MYSQL_PASSWORD: owncloud
MYSQL_DATABASE: owncloud
MARIADB_AUTO_UPGRADE: 1
- name: owncloudserver
image: owncloud/server:10.10.0
environment:
OWNCLOUD_DOMAIN: http://owncloudserver:8080
OWNCLOUD_TRUSTED_DOMAINS: owncloudserver
OWNCLOUD_DB_TYPE: mysql
OWNCLOUD_DB_NAME: owncloud
OWNCLOUD_DB_USERNAME: owncloud
OWNCLOUD_DB_PASSWORD: owncloud
OWNCLOUD_DB_HOST: ownclouddb
OWNCLOUD_ADMIN_USERNAME: admin
OWNCLOUD_ADMIN_PASSWORD: admin
OWNCLOUD_MYSQL_UTF8MB4: true
OWNCLOUD_REDIS_ENABLED: false
OWNCLOUD_REDIS_HOST: false
trigger: trigger:
event: event:

1
.gitignore vendored
View File

@ -16,6 +16,7 @@ Homestead.json
.phpunit.result.cache .phpunit.result.cache
/storage/temp/ /storage/temp/
/storage/debugbar/ /storage/debugbar/
/tests/Fileshare/oc_tmp/*
# User data files # User data files
/data/ /data/

6
.gitmodules vendored
View File

@ -14,3 +14,9 @@
path = packages/medialibrary-helper path = packages/medialibrary-helper
url = https://git.zoomyboy.de/zoomyboy/medialibrary-helper.git url = https://git.zoomyboy.de/zoomyboy/medialibrary-helper.git
branch = version2 branch = version2
[submodule "packages/flysystem-webdav"]
path = packages/flysystem-webdav
url = https://github.com/zoomyboy/flysystem-webdav.git
[submodule "packages/table-document"]
path = packages/table-document
url = https://git.zoomyboy.de/zoomyboy/table-document.git

View File

@ -25,7 +25,7 @@ class ContributionFactory
]; ];
/** /**
* @return Collection<int, array{title: mixed, class: mixed}> * @return Collection<int, array{title: string, class: class-string<ContributionDocument>}>
*/ */
public function compilerSelect(): Collection public function compilerSelect(): Collection
{ {

View File

@ -8,9 +8,6 @@ use Sabre\CardDAV\Backend\AbstractBackend;
use Sabre\DAV\PropPatch; use Sabre\DAV\PropPatch;
use Sabre\VObject\Component\VCard; use Sabre\VObject\Component\VCard;
/**
* @template M as array{lastmodified: int, etag: string, uri: string, id: int, size: int}
*/
class AddressBookBackend extends AbstractBackend class AddressBookBackend extends AbstractBackend
{ {
/** /**
@ -115,7 +112,7 @@ class AddressBookBackend extends AbstractBackend
* *
* @param mixed $addressbookId * @param mixed $addressbookId
* *
* @return array<int, M> * @return array<int, AddressBookCard>
*/ */
public function getCards($addressbookId): array public function getCards($addressbookId): array
{ {
@ -133,7 +130,7 @@ class AddressBookBackend extends AbstractBackend
* @param mixed $addressBookId * @param mixed $addressBookId
* @param string $cardUri * @param string $cardUri
* *
* @return M * @return AddressBookCard|bool
*/ */
public function getCard($addressBookId, $cardUri) public function getCard($addressBookId, $cardUri)
{ {
@ -248,7 +245,7 @@ class AddressBookBackend extends AbstractBackend
} }
/** /**
* @return M * @return AddressBookCard
*/ */
private function cardMeta(Member $member): array private function cardMeta(Member $member): array
{ {

View File

@ -0,0 +1,21 @@
<?php
namespace App\Fileshare\Actions;
use App\Fileshare\Models\Fileshare;
use App\Fileshare\Resources\FileshareResource;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Lorisleiva\Actions\Concerns\AsAction;
class FileshareApiIndexAction
{
use AsAction;
public function handle(): AnonymousResourceCollection
{
session()->put('menu', 'setting');
session()->put('title', 'Datei-Verbindungen');
return FileshareResource::collection(Fileshare::paginate(15));
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Fileshare\Actions;
use App\Fileshare\Models\Fileshare;
use App\Fileshare\Resources\FileshareResource;
use Inertia\Inertia;
use Inertia\Response;
use Lorisleiva\Actions\Concerns\AsAction;
class FileshareIndexAction
{
use AsAction;
public function handle(): Response
{
session()->put('menu', 'setting');
session()->put('title', 'Datei-Verbindungen');
return Inertia::render('fileshare/Index', [
'data' => FileshareResource::collection(Fileshare::paginate(15)),
]);
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Fileshare\Actions;
use App\Fileshare\Models\Fileshare;
use App\Lib\Events\Succeeded;
use Illuminate\Validation\ValidationException;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class FileshareStoreAction
{
use AsAction;
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'type' => 'required|string|max:255|exclude',
'config' => 'array|exclude',
];
}
public function asController(ActionRequest $request): void
{
$type = $request->input('type')::from($request->input('config'));
if (!$type->check()) {
throw ValidationException::withMessages(['type' => 'Verbindung fehlgeschlagen']);
}
Fileshare::create([
...$request->validated(),
'type' => $type,
]);
Succeeded::message('Verbindung erstellt.')->dispatch();
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Fileshare\Actions;
use App\Fileshare\Models\Fileshare;
use App\Lib\Events\Succeeded;
use Illuminate\Validation\ValidationException;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class FileshareUpdateAction
{
use AsAction;
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'type' => 'required|string|max:255|exclude',
'config' => 'array|exclude',
];
}
public function handle(ActionRequest $request, Fileshare $fileshare): void
{
$type = $request->input('type')::from($request->input('config'));
if (!$type->check()) {
throw ValidationException::withMessages(['type' => 'Verbindung fehlgeschlagen']);
}
$fileshare->update([
...$request->validated(),
'type' => $type,
]);
Succeeded::message('Verbindung bearbeitet.')->dispatch();
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Fileshare\Actions;
use App\Fileshare\Data\ResourceData;
use App\Fileshare\Models\Fileshare;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
use Spatie\LaravelData\DataCollection;
class ListFilesAction
{
use AsAction;
/**
* @return DataCollection<int, ResourceData>
*/
public function handle(ActionRequest $request, Fileshare $fileshare): DataCollection
{
return ResourceData::collection($fileshare->type->getSubDirectories($request->input('parent')))->wrap('data');
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace App\Fileshare\ConnectionTypes;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Collection;
use Spatie\LaravelData\Data;
abstract class ConnectionType extends Data
{
abstract public function check(): bool;
/**
* @return array<string, mixed>
*/
abstract public static function defaults(): array;
abstract public static function title(): string;
abstract public function getFilesystem(): FilesystemAdapter;
/**
* @return array<int, array{label: string, key: string, type: string}>
*/
abstract public static function fields(): array;
/**
* @return array<int, mixed>
*/
public static function forSelect(): array
{
return self::types()
->map(fn ($file) => ['id' => $file, 'name' => $file::title(), 'defaults' => $file::defaults(), 'fields' => $file::fields()])
->toArray();
}
/**
* @return array<int, string>
*/
public function getSubDirectories(?string $parent): array
{
$filesystem = $this->getFilesystem();
return $filesystem->directories($parent ?: '/');
}
/**
* @return Collection<int, class-string<ConnectionType>>
*/
private static function types(): Collection
{
return collect(glob(base_path('app/Fileshare/ConnectionTypes/*')))
->map(fn ($file) => 'App\\Fileshare\\ConnectionTypes\\' . pathinfo($file, PATHINFO_FILENAME))
->filter(fn ($file) => $file !== static::class)
->values();
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace App\Fileshare\ConnectionTypes;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use League\Flysystem\Filesystem;
use League\Flysystem\WebDAV\WebDAVAdapter;
use Sabre\DAV\Client;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
#[MapInputName(SnakeCaseMapper::class)]
#[MapOutputName(SnakeCaseMapper::class)]
class OwncloudConnection extends ConnectionType
{
public function __construct(
public string $user,
public string $password,
public string $baseUrl,
) {
}
public function check(): bool
{
try {
$response = Http::withoutVerifying()->withBasicAuth($this->user, $this->password)->acceptJson()->get($this->baseUrl . '/ocs/v1.php/cloud/capabilities?format=json');
return $response->ok();
} catch (ConnectionException $e) {
return false;
}
}
/**
* @inheritdoc
*/
public static function defaults(): array
{
return [
'user' => '',
'password' => '',
'base_url' => '',
];
}
public static function title(): string
{
return 'Owncloud';
}
/**
* @inheritdoc
*/
public static function fields(): array
{
return [
['label' => 'URL', 'key' => 'base_url', 'type' => 'text'],
['label' => 'Benutzer', 'key' => 'user', 'type' => 'text'],
['label' => 'Passwort', 'key' => 'password', 'type' => 'password'],
];
}
public function getFilesystem(): FilesystemAdapter
{
$adapter = new WebDAVAdapter(new Client([
'baseUri' => $this->baseUrl . '/remote.php/dav/files/' . $this->user,
'userName' => $this->user,
'password' => $this->password,
]), '/remote.php/dav/files/' . $this->user);
return new FilesystemAdapter(new Filesystem($adapter), $adapter);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Fileshare\Data;
use App\Fileshare\Models\Fileshare;
use Illuminate\Filesystem\FilesystemAdapter;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
#[MapInputName(SnakeCaseMapper::class)]
#[MapOutputName(SnakeCaseMapper::class)]
class FileshareResourceData extends Data
{
public function __construct(public int $connectionId, public string $resource)
{
}
public function getConnection(): Fileshare
{
return Fileshare::find($this->connectionId);
}
public function getStorage(): FilesystemAdapter
{
return $this->getConnection()->type->getFilesystem();
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Fileshare\Data;
use Spatie\LaravelData\Data;
class ResourceData extends Data
{
public function __construct(public string $name, public string $path, public string $parent)
{
}
public static function fromString(string $path): self
{
$dir = '/' . trim($path, '\\/');
return self::from([
'path' => $dir,
'name' => pathinfo($dir, PATHINFO_BASENAME),
'parent' => pathinfo($dir, PATHINFO_DIRNAME),
]);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Fileshare;
use App\Fileshare\Actions\FileshareIndexAction;
use App\Setting\Contracts\Indexable;
use App\Setting\LocalSettings;
class FileshareSettings extends LocalSettings implements Indexable
{
public static function group(): string
{
return 'fileshare';
}
public static function slug(): string
{
return 'fileshare';
}
public static function indexAction(): string
{
return FileshareIndexAction::class;
}
public static function title(): string
{
return 'Datei-Verbindungen';
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Fileshare\Models;
use App\Fileshare\ConnectionTypes\ConnectionType;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Fileshare extends Model
{
use HasFactory;
public $guarded = [];
public $casts = [
'type' => ConnectionType::class,
];
}

View File

@ -0,0 +1,56 @@
<?php
namespace App\Fileshare\Resources;
use App\Fileshare\ConnectionTypes\ConnectionType;
use App\Fileshare\Models\Fileshare;
use App\Lib\HasMeta;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin Fileshare
*/
class FileshareResource extends JsonResource
{
use HasMeta;
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array<string, mixed>
*/
public function toArray($request)
{
return [
'name' => $this->name,
'is_active' => $this->type->check(),
'type' => get_class($this->type),
'config' => $this->type->toArray(),
'id' => $this->id,
'type_human' => $this->type::title(),
'links' => [
'update' => route('fileshare.update', ['fileshare' => $this->getModel()]),
]
];
}
/**
* @return array<string, mixed>
*/
public static function meta(): array
{
return [
'default' => [
'name' => '',
'type' => null,
'config' => null,
],
'types' => ConnectionType::forSelect(),
'links' => [
'store' => route('fileshare.store'),
]
];
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Form;
use App\Form\Models\Participant;
use App\Group;
use Illuminate\Support\Collection;
use Lorisleiva\Actions\Concerns\AsAction;
use Zoomyboy\TableDocument\SheetData;
use Zoomyboy\TableDocument\TableDocumentData;
class ExportSyncAction
{
use AsAction;
public Form $form;
public function handle(Form $form): void
{
$this->form = $form;
if (!$form->export->root) {
return;
}
$storage = $form->export->root->getStorage();
$storage->put($form->export->root->resource . '/Anmeldungen ' . $form->name . '.xlsx', file_get_contents($this->allSheet($this->form->participants)->compile($this->tempPath())));
if ($form->export->toGroupField) {
foreach ($form->participants->groupBy(fn ($participant) => $participant->data[$form->export->toGroupField]) as $groupId => $participants) {
$group = Group::find($groupId);
if (!$group?->fileshare) {
continue;
}
$group->fileshare->getStorage()->put($group->fileshare->resource . '/Anmeldungen ' . $form->name . '.xlsx', file_get_contents($this->allSheet($participants)->compile($this->tempPath())));
}
}
}
public function asJob(int $formId): void
{
$this->handle(Form::find($formId));
}
/**
* @param Collection<int, Participant> $participants
*/
private function allSheet(Collection $participants): TableDocumentData
{
$document = TableDocumentData::from(['title' => 'Anmeldungen für ' . $this->form->name, 'sheets' => []]);
$headers = $this->form->getFields()->map(fn ($field) => $field->name)->toArray();
$document->addSheet(SheetData::from([
'header' => $headers,
'data' => $participants
->map(fn ($participant) => $this->form->getFields()->map(fn ($field) => $participant->getFields()->find($field)->presentRaw())->toArray())
->toArray(),
'name' => 'Alle',
]));
if ($this->form->export->groupBy) {
$groups = $participants->groupBy(fn ($participant) => $participant->getFields()->findByKey($this->form->export->groupBy)->presentRaw());
foreach ($groups as $name => $participants) {
$document->addSheet(SheetData::from([
'header' => $headers,
'data' => $participants
->map(fn ($participant) => $this->form->getFields()->map(fn ($field) => $participant->getFields()->find($field)->presentRaw())->toArray())
->toArray(),
'name' => $name,
]));
}
$document->addSheet(SheetData::from([
'header' => ['Wert', 'Anzahl'],
'data' => $groups->map(fn ($participants, $name) => [$name, (string) count($participants)])->toArray(),
'name' => 'Statistik',
]));
}
return $document;
}
private function tempPath(): string
{
return sys_get_temp_dir() . '/' . str()->uuid()->toString();
}
}

View File

@ -34,6 +34,7 @@ class FormStoreAction
'mailattachments' => 'present|array|exclude', 'mailattachments' => 'present|array|exclude',
'is_active' => 'boolean', 'is_active' => 'boolean',
'is_private' => 'boolean', 'is_private' => 'boolean',
'export' => 'nullable|array',
]; ];
} }

View File

@ -33,6 +33,7 @@ class FormUpdateAction
'mail_bottom' => 'array', 'mail_bottom' => 'array',
'is_active' => 'boolean', 'is_active' => 'boolean',
'is_private' => 'boolean', 'is_private' => 'boolean',
'export' => 'nullable|array',
]; ];
} }

View File

@ -32,6 +32,7 @@ class RegisterAction
$form->getFields()->each(fn ($field) => $field->afterRegistration($form, $participant, $input)); $form->getFields()->each(fn ($field) => $field->afterRegistration($form, $participant, $input));
$participant->sendConfirmationMail(); $participant->sendConfirmationMail();
ExportSyncAction::dispatch($form->id);
return $participant; return $participant;
} }

View File

@ -10,7 +10,7 @@ use Spatie\LaravelData\Support\DataProperty;
class FieldCollectionCast implements Cast class FieldCollectionCast implements Cast
{ {
/** /**
* @param array<int, array<string, mixed>> $value * @param array<int, array<string, string>> $value
* @param array<string, mixed> $context * @param array<string, mixed> $context
* @return FieldCollection * @return FieldCollection
*/ */

View File

@ -0,0 +1,19 @@
<?php
namespace App\Form\Data;
use App\Fileshare\Data\FileshareResourceData;
use App\Form\Fields\Field;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
#[MapInputName(SnakeCaseMapper::class)]
#[MapOutputName(SnakeCaseMapper::class)]
class ExportData extends Data
{
public function __construct(public ?FileshareResourceData $root = null, public ?string $groupBy = null, public ?string $toGroupField = null)
{
}
}

View File

@ -79,6 +79,9 @@ abstract class Field extends Data
->toArray(); ->toArray();
} }
/**
* @return class-string<Field>
*/
public static function classFromType(string $type): ?string public static function classFromType(string $type): ?string
{ {
/** @var class-string<Field> */ /** @var class-string<Field> */
@ -101,10 +104,7 @@ abstract class Field extends Data
]; ];
} }
/** public function presentRaw(): string
* @return mixed
*/
public function presentRaw()
{ {
return $this->getPresenter()->present($this->value); return $this->getPresenter()->present($this->value);
} }

View File

@ -2,6 +2,7 @@
namespace App\Form\Models; namespace App\Form\Models;
use App\Form\Data\ExportData;
use App\Form\Data\FieldCollection; use App\Form\Data\FieldCollection;
use App\Form\Data\FormConfigData; use App\Form\Data\FormConfigData;
use Cviebrock\EloquentSluggable\Sluggable; use Cviebrock\EloquentSluggable\Sluggable;
@ -33,6 +34,7 @@ class Form extends Model implements HasMedia
'mail_bottom' => 'json', 'mail_bottom' => 'json',
'is_active' => 'boolean', 'is_active' => 'boolean',
'is_private' => 'boolean', 'is_private' => 'boolean',
'export' => ExportData::class,
]; ];
/** @var array<int, string> */ /** @var array<int, string> */

View File

@ -2,6 +2,7 @@
namespace App\Form\Resources; namespace App\Form\Resources;
use App\Form\Data\ExportData;
use App\Form\Enums\NamiType; use App\Form\Enums\NamiType;
use App\Form\Enums\SpecialType; use App\Form\Enums\SpecialType;
use App\Form\Fields\Field; use App\Form\Fields\Field;
@ -47,6 +48,7 @@ class FormResource extends JsonResource
'is_active' => $this->is_active, 'is_active' => $this->is_active,
'is_private' => $this->is_private, 'is_private' => $this->is_private,
'has_nami_field' => $this->getFields()->hasNamiField(), 'has_nami_field' => $this->getFields()->hasNamiField(),
'export' => $this->export,
'links' => [ 'links' => [
'participant_index' => route('form.participant.index', ['form' => $this->getModel(), 'parent' => null]), 'participant_index' => route('form.participant.index', ['form' => $this->getModel(), 'parent' => null]),
'participant_root_index' => route('form.participant.index', ['form' => $this->getModel(), 'parent' => -1]), 'participant_root_index' => route('form.participant.index', ['form' => $this->getModel(), 'parent' => -1]),
@ -92,6 +94,7 @@ class FormResource extends JsonResource
'header_image' => null, 'header_image' => null,
'mailattachments' => [], 'mailattachments' => [],
'id' => null, 'id' => null,
'export' => ExportData::from([]),
], ],
'section_default' => [ 'section_default' => [
'name' => '', 'name' => '',

View File

@ -2,6 +2,7 @@
namespace App; namespace App;
use App\Fileshare\Data\FileshareResourceData;
use App\Group\Enums\Level; use App\Group\Enums\Level;
use App\Nami\HasNamiField; use App\Nami\HasNamiField;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
@ -14,11 +15,12 @@ class Group extends Model
use HasFactory; use HasFactory;
use HasNamiField; use HasNamiField;
public $fillable = ['nami_id', 'name', 'inner_name', 'level', 'parent_id']; public $fillable = ['nami_id', 'name', 'inner_name', 'level', 'parent_id', 'fileshare'];
public $timestamps = false; public $timestamps = false;
public $casts = [ public $casts = [
'level' => Level::class 'level' => Level::class,
'fileshare' => FileshareResourceData::class,
]; ];
/** /**

View File

@ -22,6 +22,9 @@ class GroupBulkstoreAction
'*.id' => 'required|integer|exists:groups,id', '*.id' => 'required|integer|exists:groups,id',
'*.inner_name' => 'required|string|max:255', '*.inner_name' => 'required|string|max:255',
'*.level' => ['required', 'string', Rule::in(Level::values())], '*.level' => ['required', 'string', Rule::in(Level::values())],
'*.fileshare' => 'present|nullable',
'*.fileshare.connection_id' => 'nullable|numeric|exists:fileshares,id',
'*.fileshare.resource' => 'nullable|string',
]; ];
} }
@ -31,7 +34,7 @@ class GroupBulkstoreAction
public function handle(array $groups): void public function handle(array $groups): void
{ {
foreach ($groups as $payload) { foreach ($groups as $payload) {
Group::find($payload['id'])->update(['level' => $payload['level'], 'inner_name' => $payload['inner_name']]); Group::find($payload['id'])->update(['level' => $payload['level'], 'inner_name' => $payload['inner_name'], 'fileshare' => $payload['fileshare']]);
} }
} }

View File

@ -30,6 +30,7 @@ class GroupResource extends JsonResource
'id' => $this->id, 'id' => $this->id,
'level' => $this->level?->value, 'level' => $this->level?->value,
'children_count' => $this->children_count, 'children_count' => $this->children_count,
'fileshare' => $this->fileshare,
'links' => [ 'links' => [
'children' => route('api.group', ['group' => $this->id]), 'children' => route('api.group', ['group' => $this->id]),
] ]

View File

@ -2,6 +2,7 @@
namespace App\Setting; namespace App\Setting;
use App\Fileshare\FileshareSettings;
use App\Form\FormSettings; use App\Form\FormSettings;
use App\Invoice\InvoiceSettings; use App\Invoice\InvoiceSettings;
use App\Mailgateway\MailgatewaySettings; use App\Mailgateway\MailgatewaySettings;
@ -32,5 +33,6 @@ class SettingServiceProvider extends ServiceProvider
app(SettingFactory::class)->register(MailgatewaySettings::class); app(SettingFactory::class)->register(MailgatewaySettings::class);
app(SettingFactory::class)->register(NamiSettings::class); app(SettingFactory::class)->register(NamiSettings::class);
app(SettingFactory::class)->register(FormSettings::class); app(SettingFactory::class)->register(FormSettings::class);
app(SettingFactory::class)->register(FileshareSettings::class);
} }
} }

View File

@ -14,6 +14,20 @@
"symlink": true "symlink": true
} }
}, },
{
"type": "path",
"url": "./packages/table-document",
"options": {
"symlink": true
}
},
{
"type": "path",
"url": "./packages/flysystem-webdav",
"options": {
"symlink": true
}
},
{ {
"type": "path", "type": "path",
"url": "./packages/tex", "url": "./packages/tex",
@ -58,10 +72,12 @@
"spatie/laravel-settings": "^2.2", "spatie/laravel-settings": "^2.2",
"worksome/request-factories": "^2.5", "worksome/request-factories": "^2.5",
"zoomyboy/laravel-nami": "dev-master", "zoomyboy/laravel-nami": "dev-master",
"zoomyboy/medialibrary-helper": "dev-master as 1.0",
"league/flysystem-webdav": "dev-master as 3.28.0",
"zoomyboy/osm": "1.0.3", "zoomyboy/osm": "1.0.3",
"zoomyboy/phone": "^1.0", "zoomyboy/phone": "^1.0",
"zoomyboy/tex": "dev-main as 1.0", "zoomyboy/table-document": "dev-master as 1.0",
"zoomyboy/medialibrary-helper": "dev-master as 1.0" "zoomyboy/tex": "dev-main as 1.0"
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.9.1", "fakerphp/faker": "^1.9.1",

1899
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,43 @@
<?php
namespace Database\Factories\Fileshare\Models;
use App\Fileshare\ConnectionTypes\ConnectionType;
use App\Fileshare\Models\Fileshare;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Fileshare>
*/
class FileshareFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var class-string<Fileshare>
*/
protected $model = Fileshare::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition()
{
return [
'type' => '{}',
'name' => '',
];
}
public function type(ConnectionType $type): self
{
return $this->state(['type' => $type]);
}
public function name(string $name): self
{
return $this->state(['name' => $name]);
}
}

View File

@ -2,6 +2,7 @@
namespace Database\Factories\Form\Models; namespace Database\Factories\Form\Models;
use App\Form\Data\ExportData;
use App\Form\Models\Form; use App\Form\Models\Form;
use Database\Factories\Traits\FakesMedia; use Database\Factories\Traits\FakesMedia;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
@ -19,6 +20,7 @@ use Tests\RequestFactories\EditorRequestFactory;
* @method self registrationUntil(string|null $date) * @method self registrationUntil(string|null $date)
* @method self isActive(bool $isActive) * @method self isActive(bool $isActive)
* @method self isPrivate(bool $isPrivate) * @method self isPrivate(bool $isPrivate)
* @method self export(ExportData $data)
*/ */
class FormFactory extends Factory class FormFactory extends Factory
{ {
@ -51,6 +53,7 @@ class FormFactory extends Factory
'mail_bottom' => EditorRequestFactory::new()->create(), 'mail_bottom' => EditorRequestFactory::new()->create(),
'is_active' => true, 'is_active' => true,
'is_private' => false, 'is_private' => false,
'export' => ExportData::from([]),
]; ];
} }

View File

@ -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.
*
* @return void
*/
public function up()
{
Schema::create('fileshares', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->json('type');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('fileshares');
}
};

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('groups', function (Blueprint $table) {
$table->json('fileshare')->after('inner_name')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('groups', function (Blueprint $table) {
$table->dropColumn('fileshare');
});
}
};

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('forms', function (Blueprint $table) {
$table->json('export')->after('config')->nullable();
});
DB::table('forms')->update([
'export' => json_encode(['root' => null, 'group_by' => null, 'to_group_field' => null])
]);
Schema::table('forms', function (Blueprint $table) {
$table->json('export')->nullable(false)->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('forms', function (Blueprint $table) {
$table->dropColumn('export');
});
}
};

@ -0,0 +1 @@
Subproject commit c892034c6b6ebe9c70164a352ae78d1634463c4c

@ -1 +1 @@
Subproject commit b8164cd3d204412cd3be95cbf29b9fcc3d23a77d Subproject commit 84103d40521d77f936635a7f992cf1ae4b01dafe

@ -0,0 +1 @@
Subproject commit c1d0221dcd2b4200b3ff17747e31f451fcc749f0

View File

@ -25,6 +25,7 @@ parameters:
MailgatewayCustomField: 'array{name: string, label: string, type: string, storeValidator: string, updateValidator: string, default: string}' MailgatewayCustomField: 'array{name: string, label: string, type: string, storeValidator: string, updateValidator: string, default: string}'
MailgatewayParsedCustomField: 'array{name: string, label: string, type: string, storeValidator: string, updateValidator: string, default: string, is_required: bool}' MailgatewayParsedCustomField: 'array{name: string, label: string, type: string, storeValidator: string, updateValidator: string, default: string, is_required: bool}'
SluggableConfig: 'array<string, array{source: array<int, string>}>' SluggableConfig: 'array<string, array{source: array<int, string>}>'
AddressBookCard: 'array{lastmodified: int, etag: string, uri: string, id: int, size: int}'
ignoreErrors: ignoreErrors:
- -
@ -32,11 +33,6 @@ parameters:
count: 1 count: 1
path: app/Activity.php path: app/Activity.php
-
message: "#^Method App\\\\Dav\\\\AddressBookBackend\\:\\:getCard\\(\\) should return M of array\\{lastmodified\\: int, etag\\: string, uri\\: string, id\\: int, size\\: int\\} but returns false\\.$#"
count: 1
path: app/Dav/AddressBookBackend.php
- -
message: "#^Method App\\\\Dav\\\\AddressBookBackend\\:\\:getMultipleCards\\(\\) has parameter \\$uris with no value type specified in iterable type array\\.$#" message: "#^Method App\\\\Dav\\\\AddressBookBackend\\:\\:getMultipleCards\\(\\) has parameter \\$uris with no value type specified in iterable type array\\.$#"
count: 1 count: 1
@ -162,17 +158,6 @@ parameters:
count: 1 count: 1
path: database/factories/NationalityFactory.php path: database/factories/NationalityFactory.php
-
message: "#^Call to an undefined method Phake\\\\Proxies\\\\StubberProxy.*#"
-
message: "#^Call to an undefined method Phake\\\\Proxies\\\\VerifierProxy.*#"
-
message: "#^PHPDoc tag @param has invalid value \\(\\<class\\-string\\> \\$class\\)\\: Unexpected token \"\\<\", expected type at offset 18$#"
count: 1
path: tests/TestCase.php
- -
message: "#^Parameter \\#1 \\$api of class App\\\\Initialize\\\\InitializeGroups constructor expects Zoomyboy\\\\LaravelNami\\\\Api, PHPUnit\\\\Framework\\\\MockObject\\\\Stub given\\.$#" message: "#^Parameter \\#1 \\$api of class App\\\\Initialize\\\\InitializeGroups constructor expects Zoomyboy\\\\LaravelNami\\\\Api, PHPUnit\\\\Framework\\\\MockObject\\\\Stub given\\.$#"
count: 6 count: 6
@ -463,16 +448,6 @@ parameters:
count: 1 count: 1
path: tests/Feature/Member/DavTest.php path: tests/Feature/Member/DavTest.php
-
message: "#^Return type of call to method Illuminate\\\\Support\\\\Collection\\<int,class\\-string\\<App\\\\Contribution\\\\Documents\\\\ContributionDocument\\>\\>\\:\\:map\\(\\) contains unresolvable type\\.$#"
count: 1
path: app/Contribution/ContributionFactory.php
-
message: "#^Return type of call to method Illuminate\\\\Support\\\\Collection\\<int,class\\-string\\<App\\\\Setting\\\\LocalSettings\\>\\>\\:\\:map\\(\\) contains unresolvable type\\.$#"
count: 1
path: app/Setting/SettingFactory.php
- -
message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$email\\.$#" message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$email\\.$#"
count: 2 count: 2
@ -542,3 +517,53 @@ parameters:
message: "#^Unable to resolve the template type TValue in call to function collect$#" message: "#^Unable to resolve the template type TValue in call to function collect$#"
count: 1 count: 1
path: app/Form/Fields/NamiField.php path: app/Form/Fields/NamiField.php
-
message: "#^Method App\\\\Fileshare\\\\ConnectionTypes\\\\ConnectionType\\:\\:types\\(\\) should return Illuminate\\\\Support\\\\Collection\\<int, class\\-string\\<App\\\\Fileshare\\\\ConnectionTypes\\\\ConnectionType\\>\\> but returns Illuminate\\\\Support\\\\Collection\\<int, string\\>\\.$#"
count: 1
path: app/Fileshare/ConnectionTypes/ConnectionType.php
-
message: "#^Call to an undefined method Phake\\\\Proxies\\\\StubberProxy\\:\\:check\\(\\)\\.$#"
count: 1
path: tests/Feature/Mailgateway/IndexTest.php
-
message: "#^Call to an undefined method Phake\\\\Proxies\\\\StubberProxy\\:\\:setCredentials\\(\\)\\.$#"
count: 1
path: tests/Feature/Mailgateway/IndexTest.php
-
message: "#^Call to an undefined method Phake\\\\Proxies\\\\StubberProxy\\:\\:check\\(\\)\\.$#"
count: 2
path: tests/Feature/Mailgateway/MailmanTypeTest.php
-
message: "#^Call to an undefined method Phake\\\\Proxies\\\\StubberProxy\\:\\:setCredentials\\(\\)\\.$#"
count: 2
path: tests/Feature/Mailgateway/MailmanTypeTest.php
-
message: "#^Call to an undefined method Phake\\\\Proxies\\\\StubberProxy\\:\\:setOwner\\(\\)\\.$#"
count: 1
path: tests/Feature/Mailgateway/MailmanTypeTest.php
-
message: "#^Call to an undefined method Phake\\\\Proxies\\\\StubberProxy\\:\\:setOwner\\(\\)\\.$#"
count: 2
path: tests/Feature/Mailgateway/StoreTest.php
-
message: "#^Call to an undefined method Phake\\\\Proxies\\\\StubberProxy\\:\\:setParams\\(\\)\\.$#"
count: 3
path: tests/Feature/Mailgateway/StoreTest.php
-
message: "#^Call to an undefined method Phake\\\\Proxies\\\\StubberProxy\\:\\:works\\(\\)\\.$#"
count: 3
path: tests/Feature/Mailgateway/StoreTest.php
-
message: "#^Call to an undefined method Phake\\\\Proxies\\\\VerifierProxy\\:\\:handle\\(\\)\\.$#"
count: 2
path: tests/Feature/Member/NamiPutMemberActionTest.php

View File

@ -10,6 +10,9 @@
<testsuite name="Feature"> <testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory> <directory suffix="Test.php">./tests/Feature</directory>
</testsuite> </testsuite>
<testsuite name="Fileshare">
<directory suffix="Test.php">./tests/Fileshare</directory>
</testsuite>
<testsuite name="NamiUnit"> <testsuite name="NamiUnit">
<directory suffix="Test.php">./packages/laravel-nami/tests/Unit</directory> <directory suffix="Test.php">./packages/laravel-nami/tests/Unit</directory>
</testsuite> </testsuite>

View File

@ -0,0 +1 @@
<svg height="512" viewBox="0 0 48 48" width="512" xmlns="http://www.w3.org/2000/svg"><g data-name="Open Folder"><path d="M45 18h-4v-4a2.996 2.996 0 0 0-3-3H22.75a3 3 0 0 1-2.33-1.11l-1.96-2.41A3.985 3.985 0 0 0 15.36 6H4a2.996 2.996 0 0 0-3 3v29.57a3.367 3.367 0 0 0 1.01 2.42A3.367 3.367 0 0 0 4.43 42h33.66a3.441 3.441 0 0 0 3.3-2.47l5.53-18.97A2.003 2.003 0 0 0 45 18z" fill="rgba(currentColor, 0.5)"/><path d="M44.999 18H16.572a3.43 3.43 0 0 0-3.292 2.47L7.72 39.531A3.429 3.429 0 0 1 4.429 42h33.663a3.43 3.43 0 0 0 3.294-2.47l5.533-18.97a2 2 0 0 0-1.92-2.56z" fill="currentColor" style="filter: brightness(150%)"/></g></svg>

After

Width:  |  Height:  |  Size: 631 B

View File

@ -0,0 +1,8 @@
<template>
<div class="flex flex-col items-center justify-center h-full">
<ui-spinner class="border-primary-400 w-32 h-32"></ui-spinner>
<div class="text-3xl mt-10">Lade </div>
</div>
</template>
<script setup></script>

View File

@ -1,9 +1,7 @@
<template> <template>
<div class="fixed z-40 top-0 left-0 w-full h-full flex items-center justify-center p-6"> <div class="fixed z-40 top-0 left-0 w-full h-full flex items-center justify-center p-6">
<div <div class="relative rounded-lg p-8 bg-zinc-800 shadow-2xl shadow-black border border-zinc-700 border-solid w-full max-h-full flex flex-col overflow-auto"
class="relative rounded-lg p-8 bg-zinc-800 shadow-2xl shadow-black border border-zinc-700 border-solid w-full max-h-full flex flex-col overflow-auto" :class="full ? 'h-full' : innerWidth">
:class="full ? 'h-full' : innerWidth"
>
<div class="absolute top-0 right-0 mt-6 mr-6 flex space-x-6"> <div class="absolute top-0 right-0 mt-6 mr-6 flex space-x-6">
<slot name="actions"></slot> <slot name="actions"></slot>
<a href="#" @click.prevent="$emit('close')"> <a href="#" @click.prevent="$emit('close')">
@ -17,10 +15,7 @@
<slot></slot> <slot></slot>
</div> </div>
<template #fallback> <template #fallback>
<div class="flex flex-col items-center justify-center h-full"> <ui-loading></ui-loading>
<ui-spinner class="border-primary-400 w-32 h-32"></ui-spinner>
<div class="text-3xl mt-10">Lade </div>
</div>
</template> </template>
</suspense> </suspense>
</div> </div>

View File

@ -0,0 +1,53 @@
<template>
<ui-popup v-if="selecting !== false" heading="Resource auswählen" @close="selecting = false">
<ui-remote-selector :value="selecting" @input="set"></ui-remote-selector>
</ui-popup>
<label class="flex flex-col group" :for="id" :class="sizeClass(size)">
<f-label v-if="label" :required="false" :value="label"></f-label>
<div class="relative flex-none flex">
<div class="w-full flex flex-col justify-center" :class="[fieldHeight, fieldAppearance, paddingX]" @click.prevent="selecting = modelValue === null ? null : {...modelValue}">
<div v-if="modelValue !== null" v-text="modelValue.resource"></div>
<div v-else>Datei auswählen</div>
</div>
<f-hint v-if="hint" :value="hint"></f-hint>
</div>
</label>
</template>
<script setup>
import useFieldSize from '../../composables/useFieldSize';
import {ref} from 'vue';
const emit = defineEmits(['update:modelValue']);
const {fieldHeight, fieldAppearance, paddingX, sizeClass} = useFieldSize();
const selecting = ref(false);
function set(resource) {
emit('update:modelValue', resource);
selecting.value = false;
}
const props = defineProps({
size: {
type: String,
default: () => 'base',
},
id: {
type: String,
required: true,
},
hint: {
type: String,
default: () => '',
},
modelValue: {
validator: (v) => typeof v === 'object' || v === null,
required: true,
},
label: {
type: String,
default: () => '',
},
});
</script>

View File

@ -0,0 +1,90 @@
<template>
<div>
<f-select id="connection" v-model="innerConnection" label="Verbindung" name="connection" class="mt-2" :options="data"></f-select>
<div v-if="innerConnection" class="mt-4">
<div class="flex space-x-3 items-center bg-zinc-700 rounded-lg mt-3 py-1 px-2">
<ui-sprite class="w-4 h-4 text-primary-700" src="open-folder"></ui-sprite>
<div class="text-sm grow" v-text="structure.parent"></div>
<ui-icon-button icon="undo" @click="emit('input', null)">löschen</ui-icon-button>
<ui-icon-button icon="undo" @click="updateFiles(getParentDir(structure.parent))">Zurück</ui-icon-button>
</div>
<a
v-for="(file, index) in structure.files"
:key="index"
href="#"
class="flex space-x-3 items-center mt-1 transition duration-200 hover:bg-zinc-600 py-1 px-2 rounded"
@click.prevent="updateFiles(file.path)"
>
<ui-sprite class="w-8 h-8 text-primary-700" src="open-folder"></ui-sprite>
<span class="grow" :value="file.name">
{{ file.name }}
</span>
<button class="btn btn-primary btn-sm" @click.self.prevent.stop="select(file)">Auswählen</button>
<ui-sprite class="w-3 h-3 -rotate-90 text-primary-400" src="chevron"></ui-sprite>
</a>
</div>
</div>
</template>
<script setup>
import {ref, watch} from 'vue';
import {useApiIndex} from '../../composables/useApiIndex';
const {reload, data, axios} = useApiIndex('/api/fileshare');
const emit = defineEmits(['input']);
const props = defineProps({
value: {
validator: (v) => typeof v === 'object' || v === null,
required: true,
},
});
const innerConnection = ref(props.value === null ? null : props.value.connection_id);
const structure = ref({
parent: props.value === null ? '/' : getParentDir(props.value.resource),
files: [],
});
function select(file) {
emit('input', {
connection_id: innerConnection.value,
resource: file.path,
});
}
function getParentDir(dir) {
if (!dir) {
return '/';
}
return '/' + dir.split('/').slice(1, -1).join('/');
}
watch(innerConnection, () => updateFiles('/'));
async function updateFiles(parentDir) {
console.log(innerConnection);
if (innerConnection.value === null) {
structure.value = {
parent: '/',
files: [],
};
return;
}
const response = await axios.post(`/api/fileshare/${innerConnection.value}/files`, {
parent: parentDir,
});
structure.value = {
parent: parentDir,
files: response.data.data,
};
}
await reload();
updateFiles(structure.value.parent);
</script>

View File

@ -0,0 +1,94 @@
<template>
<page-layout>
<template #toolbar>
<page-toolbar-button color="primary" icon="plus" @click.prevent="create">Neue Verbindung</page-toolbar-button>
</template>
<ui-popup v-if="single !== null" :heading="single.id ? 'Verbindung bearbeiten' : 'Neue Verbindung'" @close="cancel">
<form @submit.prevent="submit">
<section class="grid grid-cols-2 gap-3 mt-6">
<f-text id="name" v-model="single.name" name="name" label="Bezeichnung" required></f-text>
<f-select
id="type"
:model-value="single.type"
label="Typ"
name="type"
:options="meta.types"
required
@update:model-value="
single = {
...single,
type: $event,
config: {...getType($event).defaults},
}
"
></f-select>
<template v-for="(field, index) in getType(single.type).fields">
<f-text
v-if="field.type === 'text' || field.type === 'password' || field.type === 'email'"
:id="field.key"
:key="index"
v-model="single.config[field.key]"
:label="field.label"
:type="field.type"
:name="field.key"
required
></f-text>
</template>
</section>
<section class="flex mt-4 space-x-2">
<ui-button type="submit" class="btn-danger">Speichern</ui-button>
<ui-button class="btn-primary" @click.prevent="single = null">Abbrechen</ui-button>
</section>
</form>
</ui-popup>
<setting-layout>
<div class="w-full h-full pb-6">
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm hidden md:table">
<thead>
<th>Bezeichnung</th>
<th>Typ</th>
<th>Prüfung</th>
<th>Aktion</th>
</thead>
<tr v-for="(connection, index) in data" :key="index">
<td v-text="connection.name"></td>
<td v-text="connection.type_human"></td>
<td>
<ui-boolean-display
:value="connection.is_active"
long-label="Verbindungsstatus"
:label="connection.is_active ? 'Verbindung erfolgreich' : 'Verbindung fehlgeschlagen'"
></ui-boolean-display>
</td>
<td>
<a v-tooltip="`Bearbeiten`" href="#" class="inline-flex btn btn-warning btn-sm" @click.prevent="edit(connection)"><ui-sprite src="pencil"></ui-sprite></a>
</td>
</tr>
</table>
<div class="px-6">
<ui-pagination class="mt-4" :value="meta" :only="['data']"></ui-pagination>
</div>
</div>
</setting-layout>
</page-layout>
</template>
<script setup>
import {useApiIndex} from '../../composables/useApiIndex.js';
import SettingLayout from '../setting/Layout.vue';
const {meta, data, reload, create, edit, cancel, single, submit} = useApiIndex('/api/fileshare', 'fileshare');
function getType(type) {
if (!type) {
return {
fields: [],
};
}
return meta.value.types.find((t) => t.id === type);
}
reload();
</script>

View File

@ -61,10 +61,10 @@
v-model="single.excerpt" v-model="single.excerpt"
hint="Gebe hier eine kurze Beschreibung für die Veranstaltungs-Übersicht ein (Maximal 130 Zeichen)." hint="Gebe hier eine kurze Beschreibung für die Veranstaltungs-Übersicht ein (Maximal 130 Zeichen)."
label="Auszug" label="Auszug"
rows="5" :rows="5"
required required
></f-textarea> ></f-textarea>
<f-editor id="description" v-model="single.description" name="description" label="Beschreibung" rows="10" required></f-editor> <f-editor id="description" v-model="single.description" name="description" label="Beschreibung" :rows="10" required></f-editor>
</div> </div>
<div v-if="active === 1"> <div v-if="active === 1">
<ui-note class="mt-2"> Sobald sich der erste Teilnehmer für die Veranstaltung angemeldet hat, kann dieses Formular nicht mehr geändert werden. </ui-note> <ui-note class="mt-2"> Sobald sich der erste Teilnehmer für die Veranstaltung angemeldet hat, kann dieses Formular nicht mehr geändert werden. </ui-note>
@ -79,12 +79,12 @@
</ui-note> </ui-note>
<div> <div>
<ui-tabs v-model="activeMailTab" :entries="mailTabs"></ui-tabs> <ui-tabs v-model="activeMailTab" :entries="mailTabs"></ui-tabs>
<f-editor v-if="activeMailTab === 0" id="mail_top" v-model="single.mail_top" name="mail_top" label="E-Mail-Teil 1" rows="8" conditions required> <f-editor v-if="activeMailTab === 0" id="mail_top" v-model="single.mail_top" name="mail_top" label="E-Mail-Teil 1" :rows="8" conditions required>
<template #conditions="{data, resolve}"> <template #conditions="{data, resolve}">
<conditions :single="single" :value="data" @save="resolve"> </conditions> <conditions :single="single" :value="data" @save="resolve"> </conditions>
</template> </template>
</f-editor> </f-editor>
<f-editor v-if="activeMailTab === 1" id="mail_bottom" v-model="single.mail_bottom" name="mail_bottom" label="E-Mail-Teil 2" rows="8" conditions required> <f-editor v-if="activeMailTab === 1" id="mail_bottom" v-model="single.mail_bottom" name="mail_bottom" label="E-Mail-Teil 2" :rows="8" conditions required>
<template #conditions="{data, resolve}"> <template #conditions="{data, resolve}">
<conditions :single="single" :value="data" @save="resolve"> </conditions> <conditions :single="single" :value="data" @save="resolve"> </conditions>
</template> </template>
@ -108,6 +108,13 @@
</template> </template>
</f-multiplefiles> </f-multiplefiles>
</div> </div>
<div v-if="active === 3">
<div class="grid gap-3">
<ui-remote-resource id="export" v-model="single.export.root" label="Haupt-Ordner"></ui-remote-resource>
<f-select id="group_by" v-model="single.export.group_by" :options="allFields" label="Gruppieren nach" name="group_by"></f-select>
<f-select id="to_group_field" v-model="single.export.to_group_field" :options="allFields" label="Nach Gruppe schreiben" name="to_group_field"></f-select>
</div>
</div>
</div> </div>
<template #actions> <template #actions>
<a href="#" @click.prevent="submit"> <a href="#" @click.prevent="submit">
@ -122,8 +129,8 @@
<page-filter breakpoint="xl"> <page-filter breakpoint="xl">
<f-text id="search" :model-value="getFilter('search')" label="Suchen …" size="sm" @update:model-value="setFilter('search', $event)"></f-text> <f-text id="search" :model-value="getFilter('search')" label="Suchen …" size="sm" @update:model-value="setFilter('search', $event)"></f-text>
<f-switch id="past" :model-value="getFilter('past')" label="vergangene zeigen" size="sm" @update:model-value="setFilter('past', $event)"></f-switch> <f-switch id="past" :model-value="getFilter('past')" label="vergangene zeigen" name="past" size="sm" @update:model-value="setFilter('past', $event)"></f-switch>
<f-switch id="inactive" :model-value="getFilter('inactive')" label="inaktive zeigen" size="sm" @update:model-value="setFilter('inactive', $event)"></f-switch> <f-switch id="inactive" :model-value="getFilter('inactive')" label="inaktive zeigen" name="inactive" size="sm" @update:model-value="setFilter('inactive', $event)"></f-switch>
</page-filter> </page-filter>
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm"> <table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm">
@ -166,7 +173,7 @@
</template> </template>
<script setup> <script setup>
import {ref, inject} from 'vue'; import {ref, inject, computed} from 'vue';
import {indexProps, useIndex} from '../../composables/useInertiaApiIndex.js'; import {indexProps, useIndex} from '../../composables/useInertiaApiIndex.js';
import FormBuilder from '../formtemplate/FormBuilder.vue'; import FormBuilder from '../formtemplate/FormBuilder.vue';
import Participants from './Participants.vue'; import Participants from './Participants.vue';
@ -187,6 +194,21 @@ const fileSettingPopup = ref(null);
const tabs = [{title: 'Allgemeines'}, {title: 'Formular'}, {title: 'Bestätigungs-E-Mail'}, {title: 'Export'}]; const tabs = [{title: 'Allgemeines'}, {title: 'Formular'}, {title: 'Bestätigungs-E-Mail'}, {title: 'Export'}];
const mailTabs = [{title: 'vor Daten'}, {title: 'nach Daten'}]; const mailTabs = [{title: 'vor Daten'}, {title: 'nach Daten'}];
const allFields = computed(() => {
if (!single.value) {
return [];
}
var result = [];
single.value.config.sections.forEach((section) => {
section.fields.forEach((field) => {
result.push({id: field.key, name: field.name});
});
});
return result;
});
function setTemplate(template) { function setTemplate(template) {
active.value = 0; active.value = 0;
single.value.config = template.config; single.value.config = template.config;

View File

@ -19,6 +19,7 @@
<th>NaMi-Name</th> <th>NaMi-Name</th>
<th>Interner Name</th> <th>Interner Name</th>
<th>Ebene</th> <th>Ebene</th>
<th>Remote</th>
</thead> </thead>
<tr v-for="child in editing.children" :key="child.id"> <tr v-for="child in editing.children" :key="child.id">
<td> <td>
@ -30,6 +31,9 @@
<td> <td>
<f-select :id="`level-${child.id}`" v-model="child.level" label="" size="sm" :name="`level-${child.id}`" :options="meta.levels"></f-select> <f-select :id="`level-${child.id}`" v-model="child.level" label="" size="sm" :name="`level-${child.id}`" :options="meta.levels"></f-select>
</td> </td>
<td>
<ui-remote-resource :id="`fileshare-${child.id}`" v-model="child.fileshare" size="sm" label=""></ui-remote-resource>
</td>
</tr> </tr>
</table> </table>
</div> </div>
@ -108,7 +112,8 @@ async function edit(parent) {
async function store() { async function store() {
await axios.post(meta.value.links.bulkstore, [editing.value.parent, ...editing.value.children]); await axios.post(meta.value.links.bulkstore, [editing.value.parent, ...editing.value.children]);
children[editing.value.parent.id] = (await axios.get(editing.value.parent.links.children)).data.data; await toggle(editing.value.parent);
await toggle(editing.value.parent);
editing.value = null; editing.value = null;
} }
</script> </script>

View File

@ -19,6 +19,10 @@ use App\Invoice\Actions\InvoiceStoreAction;
use App\Course\Actions\CourseUpdateAction; use App\Course\Actions\CourseUpdateAction;
use App\Dashboard\Actions\IndexAction as DashboardIndexAction; use App\Dashboard\Actions\IndexAction as DashboardIndexAction;
use App\Efz\ShowEfzDocumentAction; use App\Efz\ShowEfzDocumentAction;
use App\Fileshare\Actions\FileshareApiIndexAction;
use App\Fileshare\Actions\FileshareStoreAction;
use App\Fileshare\Actions\FileshareUpdateAction;
use App\Fileshare\Actions\ListFilesAction;
use App\Form\Actions\ExportAction as ActionsExportAction; use App\Form\Actions\ExportAction as ActionsExportAction;
use App\Form\Actions\FormDestroyAction; use App\Form\Actions\FormDestroyAction;
use App\Form\Actions\FormIndexAction; use App\Form\Actions\FormIndexAction;
@ -166,4 +170,10 @@ Route::group(['middleware' => 'auth:web'], function (): void {
Route::get('/form/{form}/participants/{parent?}', ParticipantIndexAction::class)->name('form.participant.index'); Route::get('/form/{form}/participants/{parent?}', ParticipantIndexAction::class)->name('form.participant.index');
Route::post('/form/{form}/is-dirty', IsDirtyAction::class)->name('form.is-dirty'); Route::post('/form/{form}/is-dirty', IsDirtyAction::class)->name('form.is-dirty');
Route::delete('/participant/{participant}', ParticipantDestroyAction::class)->name('participant.destroy'); Route::delete('/participant/{participant}', ParticipantDestroyAction::class)->name('participant.destroy');
// ------------------------------------ fileshare -----------------------------------
Route::post('/fileshare', FileshareStoreAction::class)->name('fileshare.store');
Route::patch('/fileshare/{fileshare}', FileshareUpdateAction::class)->name('fileshare.update');
Route::get('/api/fileshare', FileshareApiIndexAction::class)->name('api.fileshare.index');
Route::post('/api/fileshare/{fileshare}/files', ListFilesAction::class)->name('api.fileshare.files');
}); });

View File

@ -7,7 +7,6 @@ use App\Subactivity;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Tests\EndToEndTestCase;
use Tests\Feature\Form\FormtemplateSectionRequest; use Tests\Feature\Form\FormtemplateSectionRequest;
use Tests\RequestFactories\EditorRequestFactory; use Tests\RequestFactories\EditorRequestFactory;

View File

@ -2,6 +2,8 @@
namespace Tests\EndToEnd\Form; namespace Tests\EndToEnd\Form;
use App\Fileshare\Data\FileshareResourceData;
use App\Form\Data\ExportData;
use App\Form\FormSettings; use App\Form\FormSettings;
use App\Form\Models\Form; use App\Form\Models\Form;
use App\Form\Models\Formtemplate; use App\Form\Models\Formtemplate;
@ -63,6 +65,7 @@ class FormIndexActionTest extends FormTestCase
->assertInertiaPath('data.meta.default.is_active', true) ->assertInertiaPath('data.meta.default.is_active', true)
->assertInertiaPath('data.meta.default.is_private', false) ->assertInertiaPath('data.meta.default.is_private', false)
->assertInertiaPath('data.meta.default.mailattachments', []) ->assertInertiaPath('data.meta.default.mailattachments', [])
->assertInertiaPath('data.meta.default.export', ['root' => null, 'group_by' => null, 'to_group_field' => null])
->assertInertiaPath('data.meta.default.config', null) ->assertInertiaPath('data.meta.default.config', null)
->assertInertiaPath('data.meta.base_url', url('')) ->assertInertiaPath('data.meta.base_url', url(''))
->assertInertiaPath('data.meta.namiTypes.0', ['id' => 'Vorname', 'name' => 'Vorname']) ->assertInertiaPath('data.meta.namiTypes.0', ['id' => 'Vorname', 'name' => 'Vorname'])
@ -70,6 +73,22 @@ class FormIndexActionTest extends FormTestCase
->assertInertiaPath('data.meta.section_default.name', ''); ->assertInertiaPath('data.meta.section_default.name', '');
} }
public function testItDisplaysExport(): void
{
$this->login()->loginNami()->withoutExceptionHandling();
Form::factory()
->name('lala')
->export(ExportData::from(['root' => FileshareResourceData::from(['connection_id' => 2, 'resource' => '/dir']), 'group_by' => 'lala', 'to_group_field' => 'abc']))
->create();
sleep(1);
$this->get(route('form.index'))
->assertInertiaPath('data.data.0.export.group_by', 'lala')
->assertInertiaPath('data.data.0.export.root.connection_id', 2)
->assertInertiaPath('data.data.0.export.root.resource', '/dir')
->assertInertiaPath('data.data.0.export.to_group_field', 'abc');
}
public function testItHandlesFullTextSearch(): void public function testItHandlesFullTextSearch(): void
{ {
$this->withoutExceptionHandling()->login()->loginNami(); $this->withoutExceptionHandling()->login()->loginNami();

View File

@ -25,9 +25,8 @@ class IndexTest extends EndToEndTestCase
]); ]);
sleep(1); sleep(1);
$response = $this->get('/member'); $response = $this->get('/member')->assertComponent('member/VIndex');
$this->assertComponent('member/VIndex', $response);
$this->assertInertiaHas('::firstname::', $response, 'data.data.0.firstname'); $this->assertInertiaHas('::firstname::', $response, 'data.data.0.firstname');
$this->assertInertiaHas(false, $response, 'data.data.0.has_nami'); $this->assertInertiaHas(false, $response, 'data.data.0.has_nami');
$this->assertInertiaHas('Kölner Str 3, 33333 Hilden', $response, 'data.data.0.full_address'); $this->assertInertiaHas('Kölner Str 3, 33333 Hilden', $response, 'data.data.0.full_address');

View File

@ -15,9 +15,7 @@ class ForgotPasswordTest extends TestCase
public function testItShowsResetForm(): void public function testItShowsResetForm(): void
{ {
$this->withoutExceptionHandling(); $this->withoutExceptionHandling();
$response = $this->get('/password/reset'); $this->get('/password/reset')->assertComponent('authentication/PasswordReset');
$this->assertComponent('authentication/PasswordReset', $response);
} }
public function testItRequiresAnEmailAddress(): void public function testItRequiresAnEmailAddress(): void

View File

@ -2,6 +2,8 @@
namespace Tests\Feature\Form; namespace Tests\Feature\Form;
use App\Form\Actions\ExportAction;
use App\Form\Actions\ExportSyncAction;
use App\Form\Enums\NamiType; use App\Form\Enums\NamiType;
use App\Form\Enums\SpecialType; use App\Form\Enums\SpecialType;
use App\Form\Mails\ConfirmRegistrationMail; use App\Form\Mails\ConfirmRegistrationMail;

View File

@ -2,6 +2,7 @@
namespace Tests\Feature\Form; namespace Tests\Feature\Form;
use App\Form\Data\ExportData;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Tests\RequestFactories\EditorRequestFactory; use Tests\RequestFactories\EditorRequestFactory;
@ -19,6 +20,7 @@ use Worksome\RequestFactories\RequestFactory;
* @method self registrationUntil(string|null $date) * @method self registrationUntil(string|null $date)
* @method self isActive(bool $isActive) * @method self isActive(bool $isActive)
* @method self isPrivate(bool $isPrivate) * @method self isPrivate(bool $isPrivate)
* @method self export(ExportData $export)
*/ */
class FormRequest extends RequestFactory class FormRequest extends RequestFactory
{ {
@ -46,6 +48,7 @@ class FormRequest extends RequestFactory
'mail_bottom' => EditorRequestFactory::new()->create(), 'mail_bottom' => EditorRequestFactory::new()->create(),
'header_image' => $this->getHeaderImagePayload(str()->uuid() . '.jpg'), 'header_image' => $this->getHeaderImagePayload(str()->uuid() . '.jpg'),
'mailattachments' => [], 'mailattachments' => [],
'export' => ExportData::from([])->toArray(),
]; ];
} }

View File

@ -2,6 +2,8 @@
namespace Tests\Feature\Form; namespace Tests\Feature\Form;
use App\Fileshare\Data\FileshareResourceData;
use App\Form\Data\ExportData;
use App\Form\Enums\NamiType; use App\Form\Enums\NamiType;
use App\Form\Models\Form; use App\Form\Models\Form;
use App\Lib\Events\Succeeded; use App\Lib\Events\Succeeded;
@ -66,6 +68,16 @@ class FormStoreActionTest extends FormTestCase
]); ]);
} }
public function testItStoresExport(): void
{
$this->login()->loginNami()->withoutExceptionHandling();
$this->postJson(route('form.store'), FormRequest::new()->export(ExportData::from(['root' => FileshareResourceData::from(['connection_id' => 2, 'resource' => '/dir']), 'group_by' => 'lala', 'to_group_field' => 'abc']))->create())->assertOk();
$form = Form::first();
$this->assertEquals(2, $form->export->root->connectionId);
}
public function validationDataProvider(): Generator public function validationDataProvider(): Generator
{ {
yield [FormRequest::new()->name(''), ['name' => 'Name ist erforderlich.']]; yield [FormRequest::new()->name(''), ['name' => 'Name ist erforderlich.']];

View File

@ -2,6 +2,8 @@
namespace Tests\Feature\Form; namespace Tests\Feature\Form;
use App\Fileshare\Data\FileshareResourceData;
use App\Form\Data\ExportData;
use App\Form\Models\Form; use App\Form\Models\Form;
use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
@ -39,6 +41,16 @@ class FormUpdateActionTest extends FormTestCase
$this->assertFrontendCacheCleared(); $this->assertFrontendCacheCleared();
} }
public function testItUpdatesExport(): void
{
$this->login()->loginNami()->withoutExceptionHandling();
$form = Form::factory()->create();
$this->patchJson(route('form.update', ['form' => $form]), FormRequest::new()->export(ExportData::from(['root' => FileshareResourceData::from(['connection_id' => 2, 'resource' => '/dir']), 'group_by' => 'lala', 'to_group_field' => 'abc']))->create());
$this->assertEquals(2, $form->fresh()->export->root->connectionId);
}
public function testItUpdatesActiveColumnsWhenFieldRemoved(): void public function testItUpdatesActiveColumnsWhenFieldRemoved(): void
{ {
$this->login()->loginNami()->withoutExceptionHandling(); $this->login()->loginNami()->withoutExceptionHandling();

View File

@ -2,6 +2,8 @@
namespace Tests\Feature\Group; namespace Tests\Feature\Group;
use App\Fileshare\ConnectionTypes\OwncloudConnection;
use App\Fileshare\Models\Fileshare;
use App\Group; use App\Group;
use App\Group\Enums\Level; use App\Group\Enums\Level;
use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\DatabaseTransactions;
@ -18,13 +20,37 @@ class BulkstoreTest extends TestCase
$group = Group::factory()->for(Group::first(), 'parent')->create(['inner_name' => 'Gruppe', 'level' => Level::REGION]); $group = Group::factory()->for(Group::first(), 'parent')->create(['inner_name' => 'Gruppe', 'level' => Level::REGION]);
$this->postJson(route('group.bulkstore'), [ $this->postJson(route('group.bulkstore'), [
['id' => $group->id, 'inner_name' => 'Abc', 'level' => Level::FEDERATION->value] ['id' => $group->id, 'inner_name' => 'Abc', 'level' => Level::FEDERATION->value, 'fileshare' => null]
])->assertOk(); ])->assertOk();
$this->assertNull($group->fresh()->fileshare);
$this->assertDatabaseHas('groups', [ $this->assertDatabaseHas('groups', [
'id' => $group->id, 'id' => $group->id,
'inner_name' => 'Abc', 'inner_name' => 'Abc',
'level' => 'Diözese', 'level' => 'Diözese',
'fileshare' => null,
]); ]);
} }
public function testItStoresFileconnection(): void
{
$this->login()->loginNami()->withoutExceptionHandling();
$connection = Fileshare::factory()
->type(OwncloudConnection::from(['user' => 'badenpowell', 'password' => 'secret', 'base_url' => env('TEST_OWNCLOUD_DOMAIN')]))
->name('lokaler Server')
->create();
$group = Group::factory()->for(Group::first(), 'parent')->create(['inner_name' => 'Gruppe', 'level' => Level::REGION]);
$this->postJson(route('group.bulkstore'), [
['id' => $group->id, 'inner_name' => 'Abc', 'level' => Level::FEDERATION->value, 'fileshare' => [
'connection_id' => $connection->id,
'resource' => '/abc',
]]
])->assertOk();
$this->assertEquals($connection->id, $group->fresh()->fileshare->connectionId);
$this->assertEquals('/abc', $group->fresh()->fileshare->resource);
}
} }

View File

@ -2,6 +2,8 @@
namespace Tests\Feature\Group; namespace Tests\Feature\Group;
use App\Fileshare\ConnectionTypes\OwncloudConnection;
use App\Fileshare\Models\Fileshare;
use App\Group; use App\Group;
use App\Group\Enums\Level; use App\Group\Enums\Level;
use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\DatabaseTransactions;
@ -73,4 +75,21 @@ class IndexTest extends TestCase
$this->get('/api/group/' . Group::first()->id)->assertJsonPath('data.0.id', $group->id); $this->get('/api/group/' . Group::first()->id)->assertJsonPath('data.0.id', $group->id);
} }
public function testItDisplaysFileshare(): void
{
$this->login()->loginNami()->withoutExceptionHandling();
$connection = Fileshare::factory()
->type(OwncloudConnection::from(['user' => 'badenpowell', 'password' => 'secret', 'base_url' => env('TEST_OWNCLOUD_DOMAIN')]))
->name('lokaler Server')
->create();
Group::factory()->for(Group::first(), 'parent')->create(['level' => null, 'fileshare' => [
'connection_id' => $connection->id,
'resource' => '/abc',
]]);
$this->get('/api/group/' . Group::first()->id)->assertJsonPath('data.0.fileshare.resource', '/abc');
}
} }

5
tests/Fileshare/08-skeleton.sh Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
rm -R /var/www/owncloud/core/skeleton/*
true

View File

@ -0,0 +1,31 @@
<?php
namespace Tests\Fileshare;
use App\Fileshare\ConnectionTypes\OwncloudConnection;
use App\Fileshare\Models\Fileshare;
use Tests\FileshareTestCase;
class AdapterTest extends FileshareTestCase
{
public function testItGetsFilesInRoot(): void
{
$this->withoutExceptionHandling()->login()->loginNami()->withOwncloudUser('badenpowell', 'secret')
->withDirs('badenpowell', []);
$storage = OwncloudConnection::from(['user' => 'badenpowell', 'password' => 'secret', 'base_url' => env('TEST_OWNCLOUD_DOMAIN')])->getFilesystem();
$storage->put('/test.pdf', '');
$this->assertEquals(['test.pdf'], $storage->files('/'));
}
public function testItGetsFilesInSubdirectory(): void
{
$this->withoutExceptionHandling()->login()->loginNami()->withOwncloudUser('badenpowell', 'secret')
->withDirs('badenpowell', ['/pictures']);
$storage = OwncloudConnection::from(['user' => 'badenpowell', 'password' => 'secret', 'base_url' => env('TEST_OWNCLOUD_DOMAIN')])->getFilesystem();
$storage->put('/pictures/test.pdf', '');
$this->assertEquals([], $storage->files('/'));
$this->assertEquals(['pictures/test.pdf'], $storage->files('/pictures'));
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace Tests\Fileshare;
use App\Fileshare\ConnectionTypes\OwncloudConnection;
use App\Fileshare\Data\FileshareResourceData;
use App\Fileshare\Models\Fileshare;
use App\Form\Actions\ExportSyncAction;
use App\Form\Data\ExportData;
use App\Form\Models\Form;
use App\Form\Models\Participant;
use App\Group;
use Tests\FileshareTestCase;
use Tests\Lib\CreatesFormFields;
class ExportSyncActionTest extends FileshareTestCase
{
use CreatesFormFields;
public function testItDoesntUploadFileWhenNoExportGiven(): void
{
$form = Form::factory()->fields([
$this->textField('vorname'),
$this->textField('nachname'),
])->create();
ExportSyncAction::run($form);
$this->assertTrue(true);
}
public function testItUploadsRootFile(): void
{
$this->withoutExceptionHandling()->withOwncloudUser('badenpowell', 'secret')->withDirs('badenpowell', ['/abc']);
$connection = Fileshare::factory()
->type(OwncloudConnection::from(['user' => 'badenpowell', 'password' => 'secret', 'base_url' => env('TEST_OWNCLOUD_DOMAIN')]))
->create();
$form = Form::factory()->name('Formular')->fields([
$this->textField('vorname'),
$this->textField('nachname'),
])->export(ExportData::from(['root' => FileshareResourceData::from(['connection_id' => $connection->id, 'resource' => '/abc'])]))->create();
Participant::factory()->for($form)->data(['firstname' => 'AAA', 'lastname' => 'BBB'])->create();
ExportSyncAction::run($form);
$this->assertEquals(['abc/Anmeldungen Formular.xlsx'], $connection->type->getFilesystem()->files('/abc'));
$this->assertTrue(true);
}
public function testItUploadsGroupFile(): void
{
$this->withoutExceptionHandling()->withOwncloudUser('badenpowell', 'secret')->withDirs('badenpowell', ['/abc', '/stamm']);
$connection = Fileshare::factory()
->type(OwncloudConnection::from(['user' => 'badenpowell', 'password' => 'secret', 'base_url' => env('TEST_OWNCLOUD_DOMAIN')]))
->create();
$group = Group::factory()->create(['fileshare' => FileshareResourceData::from(['connection_id' => $connection->id, 'resource' => '/stamm'])]);
$form = Form::factory()->name('Formular')->fields([
$this->textField('vorname')->name('Vorname'),
$this->textField('nachname')->name('Nachname'),
$this->groupField('stamm')->name('Stamm'),
])->export(ExportData::from(['to_group_field' => 'stamm', 'group_by' => 'vorname', 'root' => FileshareResourceData::from(['connection_id' => $connection->id, 'resource' => '/abc'])]))->create();
Participant::factory()->for($form)->data(['vorname' => 'AAA', 'nachname' => 'BBB', 'stamm' => $group->id])->create();
Participant::factory()->for($form)->data(['vorname' => 'CCC', 'nachname' => 'DDD', 'stamm' => null])->create();
ExportSyncAction::run($form);
$this->assertEquals(['stamm/Anmeldungen Formular.xlsx'], $connection->type->getFilesystem()->files('/stamm'));
$this->assertTrue(true);
}
}

View File

@ -0,0 +1,80 @@
<?php
namespace Tests\Fileshare;
use App\Fileshare\ConnectionTypes\OwncloudConnection;
use App\Fileshare\Models\Fileshare;
use Tests\FileshareTestCase;
class FileshareFilesActionTest extends FileshareTestCase
{
public function testItGetsFilesForAConnection(): void
{
$this->withoutExceptionHandling()->login()->loginNami()->withOwncloudUser('badenpowell', 'secret')
->withDirs('badenpowell', ['/pictures', '/lala']);
$connection = Fileshare::factory()
->type(OwncloudConnection::from(['user' => 'badenpowell', 'password' => 'secret', 'base_url' => env('TEST_OWNCLOUD_DOMAIN')]))
->create();
$this->postJson(route('api.fileshare.files', ['fileshare' => $connection]), [
'parent' => null,
])
->assertJsonCount(2, 'data')
->assertJsonPath('data.0.name', 'lala')
->assertJsonPath('data.0.path', '/lala')
->assertJsonPath('data.0.parent', '/')
->assertJsonPath('data.1.name', 'pictures')
->assertJsonPath('data.1.path', '/pictures')
->assertJsonPath('data.1.parent', '/');
}
public function testItGetsSubdirectories(): void
{
$this->withoutExceptionHandling()->login()->loginNami()->withOwncloudUser('badenpowell', 'secret')
->withDirs('badenpowell', ['/pictures', '/lala', '/lala/dd', '/lala/ff']);
$connection = Fileshare::factory()
->type(OwncloudConnection::from(['user' => 'badenpowell', 'password' => 'secret', 'base_url' => env('TEST_OWNCLOUD_DOMAIN')]))
->create();
$this->postJson(route('api.fileshare.files', ['fileshare' => $connection]), ['parent' => '/pictures'])->assertJsonCount(0, 'data');
$this->postJson(route('api.fileshare.files', ['fileshare' => $connection]), ['parent' => '/lala'])
->assertJsonCount(2, 'data')
->assertJsonPath('data.0.name', 'dd')
->assertJsonPath('data.0.path', '/lala/dd')
->assertJsonPath('data.0.parent', '/lala')
->assertJsonPath('data.1.name', 'ff')
->assertJsonPath('data.1.path', '/lala/ff')
->assertJsonPath('data.1.parent', '/lala');
}
public function testItGetsSubdirectoriesOfSubdirectory(): void
{
$this->withoutExceptionHandling()->login()->loginNami()->withOwncloudUser('badenpowell', 'secret')
->withDirs('badenpowell', ['/lala', '/lala/dd', '/lala/dd/ee']);
$connection = Fileshare::factory()
->type(OwncloudConnection::from(['user' => 'badenpowell', 'password' => 'secret', 'base_url' => env('TEST_OWNCLOUD_DOMAIN')]))
->create();
$this->postJson(route('api.fileshare.files', ['fileshare' => $connection]), ['parent' => '/lala/dd'])
->assertJsonCount(1, 'data')
->assertJsonPath('data.0.name', 'ee')
->assertJsonPath('data.0.path', '/lala/dd/ee')
->assertJsonPath('data.0.parent', '/lala/dd');
}
public function testItGetsFilesWithDot(): void
{
$this->withoutExceptionHandling()->login()->loginNami()->withOwncloudUser('badenpowell', 'secret')
->withDirs('badenpowell', ['/1. aa']);
$connection = Fileshare::factory()
->type(OwncloudConnection::from(['user' => 'badenpowell', 'password' => 'secret', 'base_url' => env('TEST_OWNCLOUD_DOMAIN')]))
->create();
$this->postJson(route('api.fileshare.files', ['fileshare' => $connection]), ['parent' => '/'])
->assertJsonPath('data.0.name', '1. aa');
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Tests\Fileshare;
use App\Fileshare\ConnectionTypes\OwncloudConnection;
use App\Fileshare\Models\Fileshare;
use Tests\FileshareTestCase;
class FileshareIndexActionTest extends FileshareTestCase
{
public function testItListsOwncloudConnectionsThatAreActive(): void
{
$this->withoutExceptionHandling()->login()->loginNami()->withOwncloudUser('badenpowell', 'secret');
$connection = Fileshare::factory()
->type(OwncloudConnection::from(['user' => 'badenpowell', 'password' => 'secret', 'base_url' => env('TEST_OWNCLOUD_DOMAIN')]))
->name('lokaler Server')
->create();
$this->get('/api/fileshare')
->assertJsonPath('data.0.name', 'lokaler Server')
->assertJsonPath('data.0.type', OwncloudConnection::class)
->assertJsonPath('data.0.config.user', 'badenpowell')
->assertJsonPath('data.0.config.password', 'secret')
->assertJsonPath('data.0.config.base_url', env('TEST_OWNCLOUD_DOMAIN'))
->assertJsonPath('data.0.id', $connection->id)
->assertJsonPath('data.0.is_active', true)
->assertJsonPath('data.0.type_human', 'Owncloud')
->assertJsonPath('data.0.links.update', route('fileshare.update', ['fileshare' => $connection]))
->assertJsonPath('meta.default.name', '')
->assertJsonPath('meta.links.store', route('fileshare.store'))
->assertJsonPath('meta.types.0.id', OwncloudConnection::class)
->assertJsonPath('meta.types.0.name', 'Owncloud')
->assertJsonPath('meta.types.0.defaults.base_url', '')
->assertJsonPath('meta.types.0.fields.1', ['label' => 'Benutzer', 'key' => 'user', 'type' => 'text']);
}
public function testItRendersComponent(): void
{
$this->withoutExceptionHandling()->login()->loginNami();
$this->get('/setting/fileshare')->assertComponent('fileshare/Index');
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace Tests\Fileshare;
use App\Fileshare\ConnectionTypes\OwncloudConnection;
use App\Fileshare\Models\Fileshare;
use Tests\FileshareTestCase;
class FileshareStoreActionTest extends FileshareTestCase
{
public function testItStoresAConnection(): void
{
$this->withoutExceptionHandling()->login()->loginNami()->withOwncloudUser('badenpowell', 'secret');
$this->post(route('fileshare.store'), [
'name' => 'Lala',
'type' => OwncloudConnection::class,
'config' => [
'user' => 'badenpowell',
'password' => 'secret',
'base_url' => env('TEST_OWNCLOUD_DOMAIN'),
]
])->assertOk();
$connection = Fileshare::firstOrFail();
$this->assertEquals('badenpowell', $connection->type->user);
$this->assertEquals('secret', $connection->type->password);
$this->assertEquals(env('TEST_OWNCLOUD_DOMAIN'), $connection->type->baseUrl);
$this->assertEquals('Lala', $connection->name);
}
public function testItChecksConnection(): void
{
$this->withExceptionHandling()->login()->loginNami();
$this->postJson(route('fileshare.store'), [
'name' => 'Lala',
'type' => OwncloudConnection::class,
'config' => [
'user' => 'badenpowell',
'password' => 'secret',
'base_url' => env('TEST_OWNCLOUD_DOMAIN'),
]
])->assertJsonValidationErrors(['type' => 'Verbindung fehlgeschlagen']);
}
public function testItNeedsName(): void
{
$this->withExceptionHandling()->login()->loginNami();
$this->postJson(route('fileshare.store'), [
'name' => '',
'type' => OwncloudConnection::class,
'config' => [
'user' => 'badenpowell',
'password' => 'secret',
'base_url' => env('TEST_OWNCLOUD_DOMAIN'),
]
])->assertJsonValidationErrors(['name' => 'Name ist erforderlich.']);
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace Tests\Fileshare;
use App\Fileshare\ConnectionTypes\OwncloudConnection;
use App\Fileshare\Models\Fileshare;
use Tests\FileshareTestCase;
class FileshareUpdateActionTest extends FileshareTestCase
{
public function testItStoresAConnection(): void
{
$this->withoutExceptionHandling()->login()->loginNami()->withOwncloudUser('badenpowell', 'secret');
$connection = Fileshare::factory()
->type(OwncloudConnection::from(['user' => 'test', 'password' => 'test', 'base_url' => env('TEST_OWNCLOUD_DOMAIN')]))
->name('lokaler Server')
->create();
$this->patch(route('fileshare.update', ['fileshare' => $connection]), [
'name' => 'Lala',
'type' => OwncloudConnection::class,
'config' => [
'user' => 'badenpowell',
'password' => 'secret',
'base_url' => env('TEST_OWNCLOUD_DOMAIN'),
]
])->assertOk();
$connection = Fileshare::firstOrFail();
$this->assertEquals('badenpowell', $connection->type->user);
$this->assertEquals('secret', $connection->type->password);
$this->assertEquals(env('TEST_OWNCLOUD_DOMAIN'), $connection->type->baseUrl);
$this->assertEquals('Lala', $connection->name);
}
public function testItChecksConnection(): void
{
$this->login()->loginNami()->withOwncloudUser('test', 'test');
$connection = Fileshare::factory()
->type(OwncloudConnection::from(['user' => 'test', 'password' => 'test', 'base_url' => env('TEST_OWNCLOUD_DOMAIN')]))
->name('lokaler Server')
->create();
$this->patchJson(route('fileshare.update', ['fileshare' => $connection]), [
'name' => 'Lala',
'type' => OwncloudConnection::class,
'config' => [
'user' => 'badenpowell',
'password' => 'secret',
'base_url' => env('TEST_OWNCLOUD_DOMAIN'),
]
])->assertJsonValidationErrors(['type' => 'Verbindung fehlgeschlagen']);
}
}

View File

@ -0,0 +1,47 @@
version: '3'
services:
owncloud:
image: owncloud/server:10.10.0
ports:
- 5566:8080
depends_on:
- mariadb
environment:
- OWNCLOUD_DOMAIN=http://localhost:5566
- OWNCLOUD_TRUSTED_DOMAINS=
- OWNCLOUD_DB_TYPE=mysql
- OWNCLOUD_DB_NAME=owncloud
- OWNCLOUD_DB_USERNAME=owncloud
- OWNCLOUD_DB_PASSWORD=owncloud
- OWNCLOUD_DB_HOST=mariadb
- OWNCLOUD_ADMIN_USERNAME=admin
- OWNCLOUD_ADMIN_PASSWORD=admin
- OWNCLOUD_MYSQL_UTF8MB4=true
- OWNCLOUD_REDIS_ENABLED=false
- OWNCLOUD_REDIS_HOST=false
healthcheck:
test: ['CMD', '/usr/bin/healthcheck']
interval: 30s
timeout: 10s
retries: 5
volumes:
- ./oc_tmp/files:/mnt/data
- ./08-skeleton.sh:/etc/entrypoint.d/08-skeleton.sh
mariadb:
image: mariadb:10.11
environment:
- MYSQL_ROOT_PASSWORD=owncloud
- MYSQL_USER=owncloud
- MYSQL_PASSWORD=owncloud
- MYSQL_DATABASE=owncloud
- MARIADB_AUTO_UPGRADE=1
command: ['--max-allowed-packet=128M', '--innodb-log-file-size=64M']
healthcheck:
test: ['CMD', 'mysqladmin', 'ping', '-u', 'root', '--password=owncloud']
interval: 10s
timeout: 5s
retries: 5
volumes:
- ./oc_tmp/db:/var/lib/mysql

View File

@ -0,0 +1,85 @@
<?php
namespace Tests;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use League\Flysystem\DirectoryAttributes;
use League\Flysystem\Filesystem;
use League\Flysystem\WebDAV\WebDAVAdapter;
use Sabre\DAV\Client;
use Throwable;
abstract class FileshareTestCase extends TestCase
{
use DatabaseTransactions;
protected string $adminUser = 'admin';
protected string $adminPassword = 'admin';
/**
* @var array<string, string>
*/
protected array $passwords = [];
public function setUp(): void
{
parent::setUp();
foreach ($this->http()->get('/ocs/v1.php/cloud/users?format=json')->json('ocs.data.users') as $user) {
if ($user === $this->adminUser) {
continue;
}
$this->http()->delete('/ocs/v1.php/cloud/users/' . $user);
}
}
public function withOwncloudUser(string $username, string $password): self
{
$this->passwords[$username] = $password;
$this->http()->asForm()->post('/ocs/v1.php/cloud/users?format=json', ['password' => $password, 'userid' => $username]);
return $this;
}
private function http(): PendingRequest
{
return Http::withOptions(['base_uri' => env('TEST_OWNCLOUD_DOMAIN')])->withBasicAuth($this->adminUser, $this->adminPassword)->acceptJson();
}
/**
* @param array<int, string> $dirs
*/
protected function withDirs(string $username, array $dirs): self
{
$adapter = $this->adapter($username);
foreach ($adapter->directories('/') as $directory) {
$adapter->deleteDirectory($directory);
}
foreach ($adapter->files('/') as $file) {
$adapter->delete($file);
}
foreach ($dirs as $dir) {
$adapter->makeDirectory($dir);
}
return $this;
}
private function adapter(string $username): FilesystemAdapter
{
$adapter = new WebDAVAdapter(new Client([
'baseUri' => env('TEST_OWNCLOUD_DOMAIN') . '/remote.php/dav/files/' . $username,
'userName' => $username,
'password' => $this->passwords[$username],
]), '/remote.php/dav/files/' . $username);
return new FilesystemAdapter(new Filesystem($adapter), $adapter);
}
}

View File

@ -22,11 +22,6 @@ trait TestsInertia
$this->assertInertiaDeepNest($bindings, $viewData); $this->assertInertiaDeepNest($bindings, $viewData);
} }
public function assertComponent(string $component, TestResponse $response): void
{
PHPUnit::assertEquals($component, $response->viewData('page')['component']);
}
/** /**
* @param mixed $should * @param mixed $should
* @param mixed $is * @param mixed $is

View File

@ -34,7 +34,7 @@ abstract class TestCase extends BaseTestCase
$this->initInertiaTestcase(); $this->initInertiaTestcase();
} }
public function loginNami(int $mglnr = 12345, string $password = 'password', int|Group $groupId = 55): self public function loginNami(int $mglnr = 12345, string $password = 'password', int|Group $groupId = 55): static
{ {
Auth::success($mglnr, $password); Auth::success($mglnr, $password);
$group = is_int($groupId) $group = is_int($groupId)
@ -57,7 +57,7 @@ abstract class TestCase extends BaseTestCase
return $this; return $this;
} }
public function login(): self public function login(): static
{ {
$this->be($user = User::factory()->create()); $this->be($user = User::factory()->create());
$this->me = $user; $this->me = $user;
@ -93,7 +93,8 @@ abstract class TestCase extends BaseTestCase
} }
/** /**
* @param <class-string> $class * @template M of object
* @param class-string<M> $class
*/ */
public function stubIo(string $class, callable $mocker): self public function stubIo(string $class, callable $mocker): self
{ {
@ -133,6 +134,13 @@ abstract class TestCase extends BaseTestCase
return $this; return $this;
}); });
TestResponse::macro('assertComponent', function (string $component) {
/** @var TestResponse */
$response = $this;
Assert::assertEquals($component, data_get($response->viewData('page'), 'component'));
return $this;
});
TestResponse::macro('assertPdfPageCount', function (int $count) { TestResponse::macro('assertPdfPageCount', function (int $count) {
/** @var TestResponse */ /** @var TestResponse */
$response = $this; $response = $this;

View File

@ -10,6 +10,7 @@ use Symfony\Component\HttpFoundation\File\File;
* @method self assertPdfPageCount(int $count) * @method self assertPdfPageCount(int $count)
* @method self assertPdfName(string $filename) * @method self assertPdfName(string $filename)
* @method self assertHasJsonPath(string $path) * @method self assertHasJsonPath(string $path)
* @method self assertComponent(string $component)
* @method File getFile() * @method File getFile()
*/ */
class TestResponse class TestResponse