diff --git a/assets/app.js b/assets/app.js index f614f3a..f0d9b3d 100644 --- a/assets/app.js +++ b/assets/app.js @@ -5,7 +5,4 @@ import './bootstrap.js'; * This file will be included onto the page via the importmap() Twig function, * which should already be in your base.html.twig. */ -import './styles/app.css'; -import API_CONFIG from "./config.js"; - -window.API_CONFIG = API_CONFIG; \ No newline at end of file +import './styles/app.css'; \ No newline at end of file diff --git a/assets/bim.js b/assets/bim.js index 69638b2..97e6e4b 100644 --- a/assets/bim.js +++ b/assets/bim.js @@ -77,8 +77,8 @@ BIM.loadIfc = async function (buffer, name) { } const fragments = this.components.get(OBC.FragmentsManager); - const fragmentIfcLoader = this.components.get(OBC.IfcLoader); + const classifier = this.components.get(OBC.Classifier); // NOTE: loads web-ifc WASM from https://unpkg.com/web-ifc@0.0.53/ await fragmentIfcLoader.setup(); @@ -107,14 +107,20 @@ BIM.loadIfc = async function (buffer, name) { model.name = name; this.world.scene.three.add(model); + // To actually add the model to the scene... for (const fragment of model.items) { this.world.meshes.add(fragment.mesh); } + classifier.byEntity(model); + const entities = classifier.list.entities; + // Useful? this.fragments = fragments; this.model = model; + console.log(entities); + return model; } @@ -147,6 +153,7 @@ BIM.setupHighligther = async function (model) { li.innerHTML = ` `; diff --git a/assets/controllers.json b/assets/controllers.json index a1c6e90..29ea244 100644 --- a/assets/controllers.json +++ b/assets/controllers.json @@ -1,4 +1,15 @@ { - "controllers": [], + "controllers": { + "@symfony/ux-turbo": { + "turbo-core": { + "enabled": true, + "fetch": "eager" + }, + "mercure-turbo-stream": { + "enabled": false, + "fetch": "eager" + } + } + }, "entrypoints": [] } diff --git a/assets/controllers/form_controller.js b/assets/controllers/form_controller.js index 8ce5344..7c9e670 100644 --- a/assets/controllers/form_controller.js +++ b/assets/controllers/form_controller.js @@ -11,8 +11,6 @@ export default class extends Controller { 'buildingForm' ]; - API_BASE = window.API_CONFIG.dev; - async submit(event) { event.preventDefault(); @@ -22,10 +20,7 @@ export default class extends Controller { 'POST' ); - const res = await this.send( - `${this.API_BASE}/api/buildings`, - options - ); + const res = await this.send('/project/create', options); if (res.id) { this.buildingTarget.setAttribute('data-id', res.id); diff --git a/composer.json b/composer.json index 35a8db5..4e24b91 100644 --- a/composer.json +++ b/composer.json @@ -30,6 +30,7 @@ "symfony/serializer": "7.1.*", "symfony/stimulus-bundle": "^2.23", "symfony/twig-bundle": "7.1.*", + "symfony/ux-turbo": "^2.23", "symfony/validator": "7.1.*", "symfony/yaml": "7.1.*" }, diff --git a/composer.lock b/composer.lock index de73800..3c37242 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4e98bff9162121f1c4fb1d257c7b7411", + "content-hash": "87b4a80041b3e1a8c74c5e68788219c9", "packages": [ { "name": "api-platform/doctrine-common", @@ -7024,6 +7024,104 @@ ], "time": "2024-09-25T14:20:29+00:00" }, + { + "name": "symfony/ux-turbo", + "version": "v2.23.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/ux-turbo.git", + "reference": "db96cf04d70a8c820671ce55530e8bf641ada33f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/ux-turbo/zipball/db96cf04d70a8c820671ce55530e8bf641ada33f", + "reference": "db96cf04d70a8c820671ce55530e8bf641ada33f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/stimulus-bundle": "^2.9.1" + }, + "conflict": { + "symfony/flex": "<1.13" + }, + "require-dev": { + "dbrekelmans/bdi": "dev-main", + "doctrine/doctrine-bundle": "^2.4.3", + "doctrine/orm": "^2.8 | 3.0", + "phpstan/phpstan": "^1.10", + "symfony/asset-mapper": "^6.4|^7.0", + "symfony/debug-bundle": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/form": "^5.4|^6.0|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/mercure-bundle": "^0.3.7", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/panther": "^2.1", + "symfony/phpunit-bridge": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|6.3.*|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/security-core": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/twig-bundle": "^6.4|^7.0", + "symfony/ux-twig-component": "^2.21", + "symfony/web-profiler-bundle": "^5.4|^6.0|^7.0" + }, + "type": "symfony-bundle", + "extra": { + "thanks": { + "url": "https://github.com/symfony/ux", + "name": "symfony/ux" + } + }, + "autoload": { + "psr-4": { + "Symfony\\UX\\Turbo\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Hotwire Turbo integration for Symfony", + "homepage": "https://symfony.com", + "keywords": [ + "hotwire", + "javascript", + "mercure", + "symfony-ux", + "turbo", + "turbo-stream" + ], + "support": { + "source": "https://github.com/symfony/ux-turbo/tree/v2.23.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-06T08:47:30+00:00" + }, { "name": "symfony/validator", "version": "v7.1.11", @@ -9886,7 +9984,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -9894,6 +9992,6 @@ "ext-ctype": "*", "ext-iconv": "*" }, - "platform-dev": {}, + "platform-dev": [], "plugin-api-version": "2.6.0" } diff --git a/config/bundles.php b/config/bundles.php index 0844371..3b06470 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -11,4 +11,5 @@ return [ Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true], + Symfony\UX\Turbo\TurboBundle::class => ['all' => true], ]; diff --git a/importmap.php b/importmap.php index 0964693..67f630c 100644 --- a/importmap.php +++ b/importmap.php @@ -332,14 +332,17 @@ return [ '@thatopen/components' => [ 'version' => '2.4.5', ], - 'three' => [ - 'version' => '0.160.1', - ], '@thatopen/components-front' => [ - 'version' => '2.4.4', + 'version' => '2.4.5', ], 'bootstrap-icons/font/bootstrap-icons.min.css' => [ 'version' => '1.11.3', 'type' => 'css', - ], + ], + 'three' => [ + 'version' => '0.160.1', + ], + '@hotwired/turbo' => [ + 'version' => '7.3.0', + ], ]; diff --git a/migrations/Version20250402061353.php b/migrations/Version20250402061353.php new file mode 100644 index 0000000..99c13c9 --- /dev/null +++ b/migrations/Version20250402061353.php @@ -0,0 +1,47 @@ +addSql(<<<'SQL' + ALTER TABLE project DROP FOREIGN KEY FK_2FB3D0EEA76ED395 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE project DROP FOREIGN KEY FK_2FB3D0EE4D2A7E12 + SQL); + $this->addSql(<<<'SQL' + DROP TABLE project + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql(<<<'SQL' + CREATE TABLE project (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, building_id INT NOT NULL, ifc VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, INDEX IDX_2FB3D0EEA76ED395 (user_id), INDEX IDX_2FB3D0EE4D2A7E12 (building_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB COMMENT = '' + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE project ADD CONSTRAINT FK_2FB3D0EEA76ED395 FOREIGN KEY (user_id) REFERENCES user (id) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE project ADD CONSTRAINT FK_2FB3D0EE4D2A7E12 FOREIGN KEY (building_id) REFERENCES architettura (id) + SQL); + } +} diff --git a/migrations/Version20250402061726.php b/migrations/Version20250402061726.php new file mode 100644 index 0000000..d6b31f7 --- /dev/null +++ b/migrations/Version20250402061726.php @@ -0,0 +1,47 @@ +addSql(<<<'SQL' + CREATE TABLE project (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, building_id INT NOT NULL, ifc VARCHAR(255) DEFAULT NULL, created_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', last_modified DATETIME DEFAULT NULL COMMENT '(DC2Type:datetime_immutable)', INDEX IDX_2FB3D0EEA76ED395 (user_id), INDEX IDX_2FB3D0EE4D2A7E12 (building_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE project ADD CONSTRAINT FK_2FB3D0EEA76ED395 FOREIGN KEY (user_id) REFERENCES user (id) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE project ADD CONSTRAINT FK_2FB3D0EE4D2A7E12 FOREIGN KEY (building_id) REFERENCES architettura (id) + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql(<<<'SQL' + ALTER TABLE project DROP FOREIGN KEY FK_2FB3D0EEA76ED395 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE project DROP FOREIGN KEY FK_2FB3D0EE4D2A7E12 + SQL); + $this->addSql(<<<'SQL' + DROP TABLE project + SQL); + } +} diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php new file mode 100644 index 0000000..10152ff --- /dev/null +++ b/src/Controller/ProjectController.php @@ -0,0 +1,70 @@ +render('project/index.html.twig', [ + 'controller_name' => 'ProjectController', + ]); + } + + #[Route('/projects', name: 'app_projects')] + public function projects(#[CurrentUser] User $user, EntityManagerInterface $em): Response + { + $repo = $em->getRepository(Project::class); + $projects = $repo->findBy(['user' => $user]); + + return $this->render('project/index.html.twig', [ + 'controller_name' => 'ProjectController', + 'projects' => $projects, + ]); + } + + #[Route('/project/create', name: 'app_project_create', methods: ['POST'])] + public function create( + EntityManagerInterface $em, + Request $request, + #[CurrentUser()] + User $user + ): JsonResponse + { + $data = $request->getPayload(); + $datetime = new \DateTimeImmutable(); + + $building = new Building(); + $building->setName($data->get('name')); + $em->persist($building); + $em->flush(); + + $project = new Project(); + $project->setUser($user); + $project->setBuilding($building); + $project->setCreatedAt($datetime); + $project->setLastModified($datetime); + $em->persist($project); + $em->flush(); + + $user->addProject($project); + + // TODO Does the JSON Building object include its ID? + return $this->json([ + 'id' => $building->getId(), + 'name' => $building->getName()] + ); + } +} diff --git a/src/Entity/Building.php b/src/Entity/Building.php index 3f904d7..9b2280b 100644 --- a/src/Entity/Building.php +++ b/src/Entity/Building.php @@ -7,6 +7,8 @@ use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\GetCollection; use App\Repository\BuildingRepository; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Attribute\Groups; @@ -33,6 +35,17 @@ class Building #[Groups(['building:list', 'building:item'])] private ?string $name = null; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: Project::class, mappedBy: 'building')] + private Collection $projects; + + public function __construct() + { + $this->projects = new ArrayCollection(); + } + public function getId(): ?int { return $this->id; @@ -56,4 +69,34 @@ class Building return $this; } + + /** + * @return Collection + */ + public function getProjects(): Collection + { + return $this->projects; + } + + public function addProject(Project $project): static + { + if (!$this->projects->contains($project)) { + $this->projects->add($project); + $project->setBuilding($this); + } + + return $this; + } + + public function removeProject(Project $project): static + { + if ($this->projects->removeElement($project)) { + // set the owning side to null (unless already changed) + if ($project->getBuilding() === $this) { + $project->setBuilding(null); + } + } + + return $this; + } } diff --git a/src/Entity/Project.php b/src/Entity/Project.php index 38fc9fe..83206cd 100644 --- a/src/Entity/Project.php +++ b/src/Entity/Project.php @@ -2,26 +2,12 @@ namespace App\Entity; -use App\Repository\ProjectRepository; use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\Post; -use ApiPlatform\Metadata\GetCollection; -use Symfony\Component\Serializer\Attribute\Groups; -use Doctrine\DBAL\Types\Types; +use App\Repository\ProjectRepository; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity(repositoryClass: ProjectRepository::class)] -#[ORM\Table(name: 'progetto')] -#[ApiResource( - operations: [ - new Get(normalizationContext: ['groups' => 'project:item']), - new GetCollection(normalizationContext: ['groups' => 'project:list']), - new Post(security: "is_granted('ROLE_USER')"), - ], - order: ['name' => 'DESC'], - paginationEnabled: false, -)] +#[ApiResource] class Project { #[ORM\Id] @@ -29,51 +15,48 @@ class Project #[ORM\Column] private ?int $id = null; - #[ORM\Column(type: Types::BIGINT, name: 'id_utente')] - #[ORM\OneToOne(User::class, )] - private ?string $userid = null; + #[ORM\ManyToOne(inversedBy: 'projects')] + #[ORM\JoinColumn(nullable: false)] + private ?User $user = null; - #[ORM\Column(type: Types::BIGINT, name: 'id_architettura')] - private ?string $buildingid = null; + #[ORM\ManyToOne(inversedBy: 'projects')] + #[ORM\JoinColumn(nullable: false)] + private ?Building $building = null; #[ORM\Column(length: 255, nullable: true)] - /** - * @var string $ifc Path to IFC file for this project - */ private ?string $ifc = null; + #[ORM\Column] + private ?\DateTimeImmutable $createdAt = null; + + #[ORM\Column(nullable: true)] + private ?\DateTimeImmutable $lastModified = null; + public function getId(): ?int { return $this->id; } - public function setId(string $id): static + public function getUser(): ?User { - $this->id = $id; + return $this->user; + } + + public function setUser(?User $user): static + { + $this->user = $user; return $this; } - public function getUserid(): ?string + public function getBuilding(): ?Building { - return $this->userid; + return $this->building; } - public function setUserid(string $userid): static + public function setBuilding(?Building $building): static { - $this->userid = $userid; - - return $this; - } - - public function getBuildingid(): ?string - { - return $this->buildingid; - } - - public function setBuildingid(string $buildingid): static - { - $this->buildingid = $buildingid; + $this->building = $building; return $this; } @@ -89,4 +72,28 @@ class Project return $this; } + + public function getCreatedAt(): ?\DateTimeImmutable + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeImmutable $createdAt): static + { + $this->createdAt = $createdAt; + + return $this; + } + + public function getLastModified(): ?\DateTimeImmutable + { + return $this->lastModified; + } + + public function setLastModified(?\DateTimeImmutable $lastModified): static + { + $this->lastModified = $lastModified; + + return $this; + } } diff --git a/src/Entity/User.php b/src/Entity/User.php index a7d9250..ee85fce 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -3,6 +3,8 @@ namespace App\Entity; use App\Repository\UserRepository; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -40,6 +42,17 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\Column] private ?string $email = null; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: Project::class, mappedBy: 'user')] + private Collection $projects; + + public function __construct() + { + $this->projects = new ArrayCollection(); + } + public function getId(): ?int { return $this->id; @@ -149,4 +162,34 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface // If you store any temporary, sensitive data on the user, clear it here // $this->plainPassword = null; } + + /** + * @return Collection + */ + public function getProjects(): Collection + { + return $this->projects; + } + + public function addProject(Project $project): static + { + if (!$this->projects->contains($project)) { + $this->projects->add($project); + $project->setUser($this); + } + + return $this; + } + + public function removeProject(Project $project): static + { + if ($this->projects->removeElement($project)) { + // set the owning side to null (unless already changed) + if ($project->getUser() === $this) { + $project->setUser(null); + } + } + + return $this; + } } diff --git a/symfony.lock b/symfony.lock index bb525ac..f3e8a53 100644 --- a/symfony.lock +++ b/symfony.lock @@ -211,6 +211,15 @@ "ref": "0df5844274d871b37fc3816c57a768ffc60a43a5" } }, + "symfony/ux-turbo": { + "version": "2.23", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.19", + "ref": "9dd2778a116b6e5e01e5e1582d03d5a9e82630de" + } + }, "symfony/validator": { "version": "7.1", "recipe": { diff --git a/templates/project/index.html.twig b/templates/project/index.html.twig new file mode 100644 index 0000000..505310d --- /dev/null +++ b/templates/project/index.html.twig @@ -0,0 +1,41 @@ +{% extends 'base.html.twig' %} + +{% block title %}Progetti | WebArchi{% endblock %} + +{% block body %} +{% include 'partials/navbar.html.twig' %} +
+

Progetti

+ + + + {% for project in projects %} + + + + + + + + {% endfor %} +
ArchitetturaFile IFCData creazioneUltima modificaAzioni
{{ project.building.name }}{{ project.ifc }}{{ project.createdAt.format('Y-m-d') }}{{ project.lastModified.format('Y-m-d H:i:s') }} +
+ + + +
+
+
+{% endblock %} diff --git a/templates/user/index.html.twig b/templates/user/index.html.twig index e39a86c..a813fbf 100644 --- a/templates/user/index.html.twig +++ b/templates/user/index.html.twig @@ -19,7 +19,7 @@

{{ app.user.useridentifier }}

- + {{ app.user.email ?? 'no email' }}

@@ -40,7 +40,7 @@

- + Permessi

@@ -48,12 +48,8 @@
{% if 'ROLE_ADMIN' in app.user.roles %}

- Administrators can create and edit users of the ArCOA data entry system, - including changing passwords and user roles, and disabling accounts. -

-

- They can perform all actions on all records and vocabularies - (create, view, edit, delete). + Gli amministratori possono creare e modificare utenti in WebArchi, + cambiare password e ruoli utente, e disabilitare account.

{% elseif 'ROLE_REVISOR' in app.user.roles %}

Revisors can perform all actions (create, view, edit, delete) on all records and vocabs.

diff --git a/tests/Controller/ProjectControllerTest.php b/tests/Controller/ProjectControllerTest.php new file mode 100644 index 0000000..c324d57 --- /dev/null +++ b/tests/Controller/ProjectControllerTest.php @@ -0,0 +1,16 @@ +request('GET', '/project'); + + self::assertResponseIsSuccessful(); + } +}