diff --git a/api/migrations/Version20250306152729.php b/api/migrations/Version20250306152729.php new file mode 100644 index 00000000..fd915fce --- /dev/null +++ b/api/migrations/Version20250306152729.php @@ -0,0 +1,37 @@ +addSql('CREATE TABLE book (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, isbn VARCHAR(255) DEFAULT NULL, title VARCHAR(255) NOT NULL, description TEXT NOT NULL, author VARCHAR(255) NOT NULL, publication_date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE TABLE review (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, rating SMALLINT NOT NULL, body TEXT NOT NULL, author VARCHAR(255) NOT NULL, publication_date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, book_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_794381C616A2B381 ON review (book_id)'); + $this->addSql('ALTER TABLE review ADD CONSTRAINT FK_794381C616A2B381 FOREIGN KEY (book_id) REFERENCES book (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE review DROP CONSTRAINT FK_794381C616A2B381'); + $this->addSql('DROP TABLE book'); + $this->addSql('DROP TABLE review'); + } +} diff --git a/api/src/Entity/Book.php b/api/src/Entity/Book.php new file mode 100644 index 00000000..e51d68a8 --- /dev/null +++ b/api/src/Entity/Book.php @@ -0,0 +1,77 @@ + 'ASC', + 'isbn' => 'ASC', + 'title' => 'ASC', + 'author' => 'ASC', + 'publicationDate' => 'DESC' +])] +#[ApiFilter(SearchFilter::class, properties: [ + 'id' => 'exact', + 'title' => 'ipartial', + 'author' => 'ipartial' +])] +#[ApiFilter(DateFilter::class, properties: ['publicationDate'])] +class Book +{ + /** The ID of this book */ + #[ORM\Id] + #[ORM\Column] + #[ORM\GeneratedValue] + private ?int $id = null; + + /** The ISBN of this book (or null if doesn't have one) */ + #[ORM\Column(nullable: true)] + public ?string $isbn = null; + + /** The title of this book */ + #[ORM\Column] + #[Assert\NotBlank] + #[ApiProperty(iris: ['http://schema.org/name'])] + public string $title = ''; + + /** The description of this book */ + #[ORM\Column(type: 'text')] + #[Assert\NotBlank] + public string $description = ''; + + /** The author of this book */ + #[ORM\Column] + #[Assert\NotBlank] + public string $author = ''; + + /** The publication date of this book */ + #[ORM\Column] + #[Assert\NotNull] + public ?\DateTimeImmutable $publicationDate = null; + + /** @var Review[] Available reviews for this book */ + #[ORM\OneToMany(mappedBy: 'book', targetEntity: Review::class, cascade: ['persist', 'remove'])] + public iterable $reviews; + + public function __construct() + { + $this->reviews = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } +} diff --git a/api/src/Entity/Review.php b/api/src/Entity/Review.php new file mode 100644 index 00000000..dc7de3f2 --- /dev/null +++ b/api/src/Entity/Review.php @@ -0,0 +1,69 @@ + 'ASC', + 'rating' => 'ASC', + 'author' => 'ASC', + 'publicationDate' => 'DESC' +])] +#[ApiFilter(SearchFilter::class, properties: [ + 'id' => 'exact', + 'body' => 'ipartial', + 'author' => 'ipartial' +])] +#[ApiFilter(NumericFilter::class, properties: ['rating'])] +#[ApiFilter(DateFilter::class, properties: ['publicationDate'])] +class Review +{ + /** The ID of this review */ + #[ORM\Id] + #[ORM\Column] + #[ORM\GeneratedValue] + private ?int $id = null; + + /** The rating of this review (between 0 and 5) */ + #[ORM\Column(type: 'smallint')] + #[Assert\Range(min: 0, max: 5)] + public int $rating = 0; + + /** The body of this review */ + #[ORM\Column(type: 'text')] + #[Assert\NotBlank] + public string $body = ''; + + /** The author of this review */ + #[ORM\Column] + #[Assert\NotBlank] + public string $author = ''; + + /** The publication date of this review */ + #[ORM\Column] + #[Assert\NotNull] + #[ApiProperty(iris: ['http://schema.org/name'])] + public ?\DateTimeImmutable $publicationDate = null; + + /** The book this review is about */ + #[ORM\ManyToOne(inversedBy: 'reviews')] + #[Assert\NotNull] + public ?Book $book = null; + + public function getId(): ?int + { + return $this->id; + } +} diff --git a/src/stories/Custom.stories.tsx b/src/stories/Custom.stories.tsx deleted file mode 100644 index a68b6281..00000000 --- a/src/stories/Custom.stories.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import { HydraAdmin } from '../hydra'; -import ResourceGuesser from '../core/ResourceGuesser'; -import ListGuesser from '../list/ListGuesser'; -import ShowGuesser from '../show/ShowGuesser'; -import FieldGuesser from '../field/FieldGuesser'; -import EditGuesser from '../edit/EditGuesser'; -import InputGuesser from '../input/InputGuesser'; -import CreateGuesser from '../create/CreateGuesser'; - -export default { - title: 'Admin/Custom', - parameters: { - layout: 'fullscreen', - }, -}; - -const GreetingList = () => ( - - - -); - -const GreetingShow = () => ( - - - -); - -const GreetingEdit = () => ( - - - -); - -const GreetingCreate = () => ( - - - -); - -export const Custom = () => ( - - - -); diff --git a/src/stories/custom/AdvancedCustomization.stories.tsx b/src/stories/custom/AdvancedCustomization.stories.tsx new file mode 100644 index 00000000..c3c90b84 --- /dev/null +++ b/src/stories/custom/AdvancedCustomization.stories.tsx @@ -0,0 +1,247 @@ +import AutoStoriesIcon from '@mui/icons-material/AutoStories'; +import ReviewsIcon from '@mui/icons-material/Reviews'; +import { Rating, Stack } from '@mui/material'; +import React from 'react'; +import type { InputProps } from 'react-admin'; +import { + AutocompleteInput, + Create, + Datagrid, + DateField, + Edit, + Labeled, + Layout, + List, + NumberField, + ReferenceArrayField, + ReferenceField, + ReferenceInput, + Show, + SimpleForm, + SimpleList, + SimpleShowLayout, + TabbedShowLayout, + TextField, + TextInput, + WithRecord, + WrapperField, + defaultDarkTheme, + defaultLightTheme, + required, + useInput, +} from 'react-admin'; +import ResourceGuesser from '../../core/ResourceGuesser'; +import FieldGuesser from '../../field/FieldGuesser'; +import { HydraAdmin } from '../../hydra'; +import InputGuesser from '../../input/InputGuesser'; + +export default { + title: 'Admin/Custom/AdvancedCustomization', + parameters: { + layout: 'fullscreen', + }, +}; + +const BookCreate = () => ( + + + + + + + + + + + +); + +const BookEdit = () => ( + + + + + + + + + + + +); + +const BookShow = () => ( + + + + + + + + + + + + + + + review.author + .split(' ') + .map((name: string) => name[0]) + .join('') + } + // eslint-disable-next-line react/no-unstable-nested-components + tertiaryText={(review) => ( + + )} + /> + + + + +); + +const BookList = () => ( + + + + + + + + + +); + +const RatingInput = (props: InputProps) => { + const { field } = useInput(props); + return ( + { + field.onChange(value); + }} + /> + ); +}; + +const filterToBookQuery = (searchText: string) => ({ + title: `%${searchText}%`, +}); + +const ReviewCreate = () => ( + + + + + + + + + + + + +); + +const ReviewEdit = () => ( + + + + + + + + + + + + + + + +); + +const ReviewShow = () => ( + + + + + + + ( + + )} + /> + + + + +); + +const ReviewList = () => ( + + + + + + ( + + )} + /> + + + + +); + +export const AdvancedCustomization = () => ( + + + + +); diff --git a/src/stories/custom/UsingGuessers.stories.tsx b/src/stories/custom/UsingGuessers.stories.tsx new file mode 100644 index 00000000..79a59bee --- /dev/null +++ b/src/stories/custom/UsingGuessers.stories.tsx @@ -0,0 +1,119 @@ +import AutoStoriesIcon from '@mui/icons-material/AutoStories'; +import ReviewsIcon from '@mui/icons-material/Reviews'; +import React from 'react'; +import { HydraAdmin } from '../../hydra'; +import ResourceGuesser from '../../core/ResourceGuesser'; +import ListGuesser from '../../list/ListGuesser'; +import ShowGuesser from '../../show/ShowGuesser'; +import FieldGuesser from '../../field/FieldGuesser'; +import EditGuesser from '../../edit/EditGuesser'; +import InputGuesser from '../../input/InputGuesser'; +import CreateGuesser from '../../create/CreateGuesser'; + +export default { + title: 'Admin/Custom/UsingGuessers', + parameters: { + layout: 'fullscreen', + }, +}; + +const BookCreate = () => ( + + + + + + + +); + +const BookEdit = () => ( + + + + + + + +); + +const BookShow = () => ( + + + + + + + + +); + +const BookList = () => ( + + + + + + + +); + +const ReviewCreate = () => ( + + + + + + + +); + +const ReviewEdit = () => ( + + + + + + + +); + +const ReviewShow = () => ( + + + + + + + +); + +const ReviewList = () => ( + + + + + + +); + +export const UsingGuessers = () => ( + + + + +);