From fc43d0ed4d14bd70cbdbb3c35cfab4d7b3685186 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Fri, 29 Nov 2024 15:16:49 +0100 Subject: [PATCH] Add pages to open catalogi --- appinfo/routes.php | 5 +- lib/Controller/PagesController | 145 +++++++++++++ lib/Controller/SettingsController.php | 7 +- lib/Db/Page.php | 112 ++++++++++ lib/Db/PageMapper.php | 150 ++++++++++++++ lib/Migration/Version6Date20241129151236.php | 74 +++++++ lib/Service/ObjectService.php | 4 + src/entities/index.js | 1 + src/entities/page/index.js | 4 + src/entities/page/page.mock.ts | 43 ++++ src/entities/page/page.spec.ts | 47 +++++ src/entities/page/page.ts | 74 +++++++ src/entities/page/page.types.ts | 13 ++ src/navigation/MainMenu.vue | 7 + src/store/modules/page.spec.ts | 53 +++++ src/store/modules/page.ts | 160 +++++++++++++++ src/store/store.js | 3 + src/views/Views.vue | 3 + src/views/pages/PageDetail.vue | 202 +++++++++++++++++++ src/views/pages/PageIndex.vue | 52 +++++ src/views/pages/PageList.vue | 202 +++++++++++++++++++ 21 files changed, 1358 insertions(+), 3 deletions(-) create mode 100644 lib/Controller/PagesController create mode 100644 lib/Db/Page.php create mode 100644 lib/Db/PageMapper.php create mode 100644 lib/Migration/Version6Date20241129151236.php create mode 100644 src/entities/page/index.js create mode 100644 src/entities/page/page.mock.ts create mode 100644 src/entities/page/page.spec.ts create mode 100644 src/entities/page/page.ts create mode 100644 src/entities/page/page.types.ts create mode 100644 src/store/modules/page.spec.ts create mode 100644 src/store/modules/page.ts create mode 100644 src/views/pages/PageDetail.vue create mode 100644 src/views/pages/PageIndex.vue create mode 100644 src/views/pages/PageList.vue diff --git a/appinfo/routes.php b/appinfo/routes.php index 427b3af4..311f82ae 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -6,6 +6,7 @@ 'publications' => ['url' => '/api/publications'], 'organizations' => ['url' => '/api/organizations'], 'themes' => ['url' => '/api/themes'], + 'pages' => ['url' => '/api/pages'], 'attachments' => ['url' => '/api/attachments'], 'catalogi' => ['url' => '/api/catalogi'], 'listings' => ['url' => '/api/listings'], @@ -37,6 +38,8 @@ ['name' => 'search#publication', 'url' => '/api/search/publications/{publicationId}', 'verb' => 'GET', 'requirements' => ['publicationId' => '[^/]+']], ['name' => 'search#attachments', 'url' => '/api/search/publications/{publicationId}/attachments', 'verb' => 'GET', 'requirements' => ['publicationId' => '[^/]+']], ['name' => 'search#themes', 'url' => '/api/search/themes', 'verb' => 'GET'], - ['name' => 'search#theme', 'url' => '/api/search/themes/{themeId}', 'verb' => 'GET', 'requirements' => ['themeId' => '\d+']] + ['name' => 'search#theme', 'url' => '/api/search/themes/{themeId}', 'verb' => 'GET', 'requirements' => ['themeId' => '\d+']], + ['name' => 'search#pages', 'url' => '/api/search/pages', 'verb' => 'GET'], + ['name' => 'search#page', 'url' => '/api/search/pages/{pageSlug}', 'verb' => 'GET', 'requirements' => ['pageId' => '[^/]+']] ] ]; diff --git a/lib/Controller/PagesController b/lib/Controller/PagesController new file mode 100644 index 00000000..d10ef7d5 --- /dev/null +++ b/lib/Controller/PagesController @@ -0,0 +1,145 @@ +request->getParams(); + + // Fetch page objects based on filters and order + $data = $this->objectService->getResultArrayForRequest('page', $requestParams); + + // Return JSON response + return new JSONResponse($data); + } + + /** + * Retrieve a specific page by its ID. + * + * @param string|int $id The ID of the page to retrieve + * @return JSONResponse JSON response containing the requested page + * + * @NoAdminRequired + * @NoCSRFRequired + */ + public function show(string|int $id): JSONResponse + { + // Fetch the page object by its ID + $object = $this->objectService->getObject('page', $id); + + // Return the page as a JSON response + return new JSONResponse($object); + } + + /** + * Create a new page. + * + * @return JSONResponse The response containing the created page object. + * + * @NoAdminRequired + * @NoCSRFRequired + */ + public function create(): JSONResponse + { + // Get all parameters from the request + $data = $this->request->getParams(); + + // Remove the 'id' field if it exists, as we're creating a new object + unset($data['id']); + + // Save the new page object + $object = $this->objectService->saveObject('page', $data); + + // Return the created object as a JSON response + return new JSONResponse($object); + } + + /** + * Update an existing page. + * + * @param string|int $id The ID of the page to update. + * @return JSONResponse The response containing the updated page object. + * + * @NoAdminRequired + * @NoCSRFRequired + */ + public function update(string|int $id): JSONResponse + { + // Get all parameters from the request + $data = $this->request->getParams(); + + // Ensure the ID in the data matches the ID in the URL + $data['id'] = $id; + + // Save the updated page object + $object = $this->objectService->saveObject('page', $data); + + // Return the updated object as a JSON response + return new JSONResponse($object); + } + + /** + * Delete a page. + * + * @param string|int $id The ID of the page to delete. + * @return JSONResponse The response indicating the result of the deletion. + * + * @NoAdminRequired + * @NoCSRFRequired + */ + public function destroy(string|int $id): JSONResponse + { + // Delete the page object + $result = $this->objectService->deleteObject('page', $id); + + // Return the result as a JSON response + return new JSONResponse(['success' => $result], $result === true ? '200' : '404'); + } +} diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index 8da08328..5d975be3 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -45,7 +45,7 @@ public function index(): JSONResponse { // Initialize the data array $data = []; - $data['objectTypes'] = ['attachment', 'catalog', 'listing', 'publicationtype', 'organization', 'publication', 'theme']; + $data['objectTypes'] = ['attachment', 'catalog', 'listing', 'publicationtype', 'organization', 'publication', 'theme', 'page']; $data['openRegisters'] = false; $data['availableRegisters'] = []; @@ -78,7 +78,10 @@ public function index(): JSONResponse 'publication_register' => '', 'theme_source' => 'internal', 'theme_schema' => '', - 'theme_register' => '' + 'theme_register' => '', + 'page_source' => 'internal', + 'page_schema' => '', + 'page_register' => '', ]; // Get the current values for the object types from the configuration diff --git a/lib/Db/Page.php b/lib/Db/Page.php new file mode 100644 index 00000000..30f6020f --- /dev/null +++ b/lib/Db/Page.php @@ -0,0 +1,112 @@ +addType(fieldName: 'uuid', type: 'string'); + $this->addType(fieldName: 'name', type: 'string'); + $this->addType(fieldName: 'slug', type: 'string'); + $this->addType(fieldName: 'contents', type: 'json'); + $this->addType(fieldName: 'created', type: 'datetime'); + $this->addType(fieldName: 'updated', type: 'datetime'); + } + + /** + * Get array of JSON field names + * + * @return array List of field names that are JSON type + */ + public function getJsonFields(): array + { + return array_keys( + array_filter($this->getFieldTypes(), function ($field) { + return $field === 'json'; + }) + ); + } + + /** + * Hydrate the entity from an array of data + * + * @param array $object Data to hydrate from + * @return self + */ + public function hydrate(array $object): self + { + $jsonFields = $this->getJsonFields(); + + // Remove any fields that start with an underscore + // These are typically internal fields that shouldn't be updated directly + foreach ($object as $key => $value) { + if (str_starts_with($key, '_')) { + unset($object[$key]); + } + } + + foreach ($object as $key => $value) { + if (in_array($key, $jsonFields) === true && $value === []) { + $value = null; + } + + $method = 'set'.ucfirst($key); + + try { + $this->$method($value); + } catch (\Exception $exception) { + } + } + + return $this; + } + + /** + * Serialize the entity to JSON + * + * @return array Serialized data + */ + public function jsonSerialize(): array + { + $array = [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'name' => $this->name, + 'slug' => $this->slug, + 'contents' => $this->contents, + 'created_at' => $this->created?->format('c'), + 'updated_at' => $this->updated?->format('c') + ]; + + $jsonFields = $this->getJsonFields(); + + foreach ($array as $key => $value) { + if (in_array($key, $jsonFields) === true && $value === null) { + $array[$key] = []; + } + } + + return $array; + } +} diff --git a/lib/Db/PageMapper.php b/lib/Db/PageMapper.php new file mode 100644 index 00000000..03bcb026 --- /dev/null +++ b/lib/Db/PageMapper.php @@ -0,0 +1,150 @@ +db->getQueryBuilder(); + + $qb->select('*') + ->from('ocat_pages') + ->where($qb->expr()->orX( + $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)), + $qb->expr()->eq('uuid', $qb->createNamedParameter($id, IQueryBuilder::PARAM_STR)) + )); + + try { + return $this->findEntity($qb); + } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { + return null; + } + } + + /** + * Find multiple Pages by their IDs or UUIDs + * + * @param array $ids An array of IDs or UUIDs + * @return array An array of found Page entities + */ + public function findMultiple(array $ids): array + { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from('ocat_pages') + ->where($qb->expr()->orX( + $qb->expr()->in('id', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY)), + $qb->expr()->in('uuid', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_STR_ARRAY)) + )); + + return $this->findEntities(query: $qb); + } + + /** + * Find all Pages with optional limit and offset + * + * @param int|null $limit Maximum number of results to return + * @param int|null $offset Number of results to skip + * @return array An array of all found Page entities + */ + public function findAll(int $limit = null, int $offset = null, array $filters = [], array $sort = [], ?string $search = null): array + { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from('ocat_pages') + ->setMaxResults($limit) + ->setFirstResult($offset); + + return $this->findEntities(query: $qb); + } + + /** + * Create a new Page from an array of data + * + * @param array $object An array of Page data + * @return Page The newly created Page entity + */ + public function createFromArray(array $object): Page + { + $page = new Page(); + $page->hydrate(object: $object); + + // Set uuid if not provided + if ($page->getUuid() === null) { + $page->setUuid(Uuid::v4()); + } + + // Generate slug from name if not provided + if ($page->getSlug() === null && $page->getName() !== null) { + // Convert to lowercase and replace spaces with dashes + $slug = strtolower($page->getName()); + $slug = preg_replace('/[^a-z0-9-]/', '-', $slug); + $slug = preg_replace('/-+/', '-', $slug); + $slug = trim($slug, '-'); + $page->setSlug($slug); + } + + return $this->insert(entity: $page); + } + + /** + * Update an existing Page from an array of data + * + * @param int $id The ID of the Page to update + * @param array $object An array of updated Page data + * @return Page The updated Page entity + * @throws DoesNotExistException If the entity is not found + * @throws MultipleObjectsReturnedException|\OCP\DB\Exception If multiple entities are found + */ + public function updateFromArray(int $id, array $object): Page + { + $page = $this->find($id); + // Fallback to create if the page does not exist + if ($page === null) { + $object['uuid'] = $id; + return $this->createFromArray($object); + } + + $page->hydrate($object); + + return $this->update($page); + } +} diff --git a/lib/Migration/Version6Date20241129151236.php b/lib/Migration/Version6Date20241129151236.php new file mode 100644 index 00000000..16192cb8 --- /dev/null +++ b/lib/Migration/Version6Date20241129151236.php @@ -0,0 +1,74 @@ +hasTable(tableName: 'ocat_pages') === false) { + $table = $schema->createTable(tableName: 'ocat_pages'); + + // Primary key and identifier columns + $table->addColumn(name: 'id', typeName: Types::BIGINT, options: ['autoincrement' => true, 'notnull' => true, 'length' => 4]); + $table->addColumn(name: 'uuid', typeName: Types::STRING, options: ['notnull' => true, 'length' => 255]); + $table->addColumn(name: 'version', typeName: Types::STRING, options: ['notnull' => true, 'length' => 255, 'default' => '0.0.1']); + + // Meta columns + $table->addColumn(name: 'name', typeName: Types::STRING, options: ['notnull' => true, 'length' => 255]); + $table->addColumn(name: 'slug', typeName: Types::STRING, options: ['notnull' => true, 'length' => 255]); + $table->addColumn(name: 'contents', typeName: Types::JSON, options: ['notnull' => false]); + $table->addColumn(name: 'updated', typeName: Types::DATETIME, options: ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); + $table->addColumn(name: 'created', typeName: Types::DATETIME, options: ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); + + // Keys and indexes + $table->setPrimaryKey(columnNames: ['id']); + $table->addIndex(['uuid'], 'ocat_pages_uuid_index'); + $table->addIndex(['slug'], 'ocat_pages_slug_index'); + } + + return $schema; + } + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + } +} diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index e0befbbf..e223d22f 100644 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -20,6 +20,7 @@ use OCP\IAppConfig; // Import mappers use OCA\OpenCatalogi\Db\AttachmentMapper; +use OCA\OpenCatalogi\Db\PageMapper; use OCA\OpenCatalogi\Db\CatalogMapper; use OCA\OpenCatalogi\Db\ListingMapper; use OCA\OpenCatalogi\Db\PublicationTypeMapper; @@ -47,6 +48,7 @@ class ObjectService * @param OrganizationMapper $organizationMapper Mapper for organizations * @param PublicationMapper $publicationMapper Mapper for publications * @param ThemeMapper $themeMapper Mapper for themes + * @param PageMapper $pageMapper Mapper for pages * @param ContainerInterface $container Container for dependency injection * @param IAppManager $appManager App manager interface * @param IAppConfig $config App configuration interface @@ -59,6 +61,7 @@ public function __construct( private OrganizationMapper $organizationMapper, private PublicationMapper $publicationMapper, private ThemeMapper $themeMapper, + private PageMapper $pageMapper, private ContainerInterface $container, private readonly IAppManager $appManager, private readonly IAppConfig $config, @@ -112,6 +115,7 @@ private function getMapper(string $objectType): mixed 'organization' => $this->organizationMapper, 'publication' => $this->publicationMapper, 'theme' => $this->themeMapper, + 'page' => $this->pageMapper, default => throw new InvalidArgumentException("Unknown object type: $objectType"), }; } diff --git a/src/entities/index.js b/src/entities/index.js index 2f4bb056..b5726d33 100644 --- a/src/entities/index.js +++ b/src/entities/index.js @@ -7,3 +7,4 @@ export * from './organization/index.js' export * from './publication/index.js' export * from './theme/index.js' export * from './publicationType/index.js' +export * from './page/index.js' \ No newline at end of file diff --git a/src/entities/page/index.js b/src/entities/page/index.js new file mode 100644 index 00000000..b496e408 --- /dev/null +++ b/src/entities/page/index.js @@ -0,0 +1,4 @@ +export * from './page.ts' +export * from './page.types.ts' +export * from './page.mock.ts' + diff --git a/src/entities/page/page.mock.ts b/src/entities/page/page.mock.ts new file mode 100644 index 00000000..90e5e8f5 --- /dev/null +++ b/src/entities/page/page.mock.ts @@ -0,0 +1,43 @@ +import { Page } from './page' +import { TPage } from './page.types' + +/** + * Mock data function that returns an array of page data objects + * Used for testing and development purposes + */ +export const mockPageData = (): TPage[] => [ + { // full data + id: '1', + uuid: '123e4567-e89b-12d3-a456-426614174000', + name: 'Test Page', + slug: 'test-page', + contents: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }, + // @ts-expect-error -- expected missing contents + { // partial data + id: '2', + uuid: '123e4567-e89b-12d3-a456-426614174001', + name: 'Another Page', + slug: 'another-page', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }, + { // invalid data + id: '3', + uuid: '123e4567-e89b-12d3-a456-426614174002', + name: '', + slug: '', + contents: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }, +] + +/** + * Creates an array of Page instances from provided data or default mock data + * @param data Optional array of page data to convert to Page instances + * @returns Array of Page instances + */ +export const mockPage = (data: TPage[] = mockPageData()): TPage[] => data.map(item => new Page(item)) diff --git a/src/entities/page/page.spec.ts b/src/entities/page/page.spec.ts new file mode 100644 index 00000000..fa342192 --- /dev/null +++ b/src/entities/page/page.spec.ts @@ -0,0 +1,47 @@ +/* eslint-disable no-console */ +import { Page } from './page' +import { mockPage } from './page.mock' + +describe('Page Store', () => { + it('create Page entity with full data', () => { + const page = new Page(mockPage()[0]) + + expect(page).toBeInstanceOf(Page) + expect(page).toEqual(mockPage()[0]) + expect(page.uuid).toBe(mockPage()[0].uuid) + expect(page.name).toBe(mockPage()[0].name) + expect(page.slug).toBe(mockPage()[0].slug) + expect(page.contents).toEqual(mockPage()[0].contents) + expect(page.createdAt).toBe(mockPage()[0].createdAt) + expect(page.updatedAt).toBe(mockPage()[0].updatedAt) + + expect(page.validate().success).toBe(true) + }) + + it('create Page entity with partial data', () => { + const page = new Page(mockPage()[1]) + + expect(page).toBeInstanceOf(Page) + expect(page.id).toBe(mockPage()[1].id) + expect(page.uuid).toBe(mockPage()[1].uuid) + expect(page.name).toBe(mockPage()[1].name) + expect(page.slug).toBe(mockPage()[1].slug) + expect(page.contents).toBeNull() + expect(page.createdAt).toBe(mockPage()[1].createdAt) + expect(page.updatedAt).toBe(mockPage()[1].updatedAt) + + expect(page.validate().success).toBe(true) + }) + + it('create Page entity with falsy data', () => { + const page = new Page(mockPage()[2]) + + expect(page).toBeInstanceOf(Page) + expect(page).toEqual(mockPage()[2]) + expect(page.uuid).toBe(mockPage()[2].uuid) + expect(page.name).toBe('') + expect(page.contents).toBeNull() + + expect(page.validate().success).toBe(false) + }) +}) diff --git a/src/entities/page/page.ts b/src/entities/page/page.ts new file mode 100644 index 00000000..978eaf07 --- /dev/null +++ b/src/entities/page/page.ts @@ -0,0 +1,74 @@ +import { SafeParseReturnType, z } from 'zod' +import { TPage } from './page.types' + +/** + * Page class representing a page entity with validation + * Implements the TPage interface for type safety + */ +export class Page implements TPage { + + public id: string + public uuid: string + public name: string + public slug: string + public contents: Array + public createdAt: string + public updatedAt: string + + /** + * Creates a new Page instance + * @param data Initial page data conforming to TPage interface + */ + constructor(data: TPage) { + this.hydrate(data) + } + + /* istanbul ignore next */ // Jest does not recognize the code coverage of these 2 methods + /** + * Hydrates the page object with provided data + * @param data Page data to populate the instance + */ + private hydrate(data: TPage) { + this.id = data?.id?.toString() || '' + this.uuid = data?.uuid || '' + this.name = data?.name || '' + this.slug = data?.slug || '' + this.contents = data?.contents?.map(contents => ({ + title: contents?.title || '', + summary: contents?.summary || '', + description: contents?.description || '', + image: contents?.image || '' + })) || [] + this.createdAt = data?.createdAt || '' + this.updatedAt = data?.updatedAt || '' + } + + /* istanbul ignore next */ + /** + * Validates the page data against a schema + * @returns SafeParseReturnType containing validation result + */ + public validate(): SafeParseReturnType { + // Schema validation for page data + const schema = z.object({ + uuid: z.string().min(1, 'is verplicht'), + name: z.string().min(1, 'is verplicht'), + slug: z.string().min(1, 'is verplicht'), + contents: z.array(z.object({ + title: z.string().min(1, 'is verplicht'), + summary: z.string().min(1, 'is verplicht'), + description: z.string(), + image: z.string() + })), + createdAt: z.string(), + updatedAt: z.string() + }) + + const result = schema.safeParse({ + ...this, + }) + + return result + } + +} diff --git a/src/entities/page/page.types.ts b/src/entities/page/page.types.ts new file mode 100644 index 00000000..2aea7d70 --- /dev/null +++ b/src/entities/page/page.types.ts @@ -0,0 +1,13 @@ +/** + * Type definition for a Page object + * Represents the structure of a page with content and metadata + */ +export type TPage = { + id: string // Unique identifier for the page + uuid: string // Unique identifier for the page + name: string // Title/heading of the page + contents: Array // Main content/body of the page - can contain any type of content + slug: string // URL-friendly version of the title + createdAt: string // Creation timestamp + updatedAt: string // Last update timestamp +} diff --git a/src/navigation/MainMenu.vue b/src/navigation/MainMenu.vue index 0b590dcf..896e5742 100644 --- a/src/navigation/MainMenu.vue +++ b/src/navigation/MainMenu.vue @@ -53,6 +53,11 @@ import { navigationStore, catalogiStore, publicationStore } from '../store/store + + + @@ -92,6 +97,7 @@ import Finance from 'vue-material-design-icons/Finance.vue' import BookOpenVariantOutline from 'vue-material-design-icons/BookOpenVariantOutline.vue' import OfficeBuildingOutline from 'vue-material-design-icons/OfficeBuildingOutline.vue' import ShapeOutline from 'vue-material-design-icons/ShapeOutline.vue' +import Web from 'vue-material-design-icons/Web.vue' export default { name: 'MainMenu', @@ -114,6 +120,7 @@ export default { BookOpenVariantOutline, OfficeBuildingOutline, ShapeOutline, + Web, }, data() { return { diff --git a/src/store/modules/page.spec.ts b/src/store/modules/page.spec.ts new file mode 100644 index 00000000..be1dbb7a --- /dev/null +++ b/src/store/modules/page.spec.ts @@ -0,0 +1,53 @@ +/* eslint-disable no-console */ +import { setActivePinia, createPinia } from 'pinia' + +import { usePageStore } from './page.js' +import { mockPage, Page } from '../../entities/index.js' + +describe('Page Store', () => { + beforeEach(() => { + setActivePinia(createPinia()) + }) + + it('sets page item correctly', () => { + const store = usePageStore() + + store.setPageItem(mockPage()[0]) + + expect(store.pageItem).toBeInstanceOf(Page) + expect(store.pageItem).toEqual(mockPage()[0]) + expect(store.pageItem.validate().success).toBe(true) + + store.setPageItem(mockPage()[1]) + + expect(store.pageItem).toBeInstanceOf(Page) + expect(store.pageItem).toEqual(mockPage()[1]) + expect(store.pageItem.validate().success).toBe(true) + + store.setPageItem(mockPage()[2]) + + expect(store.pageItem).toBeInstanceOf(Page) + expect(store.pageItem).toEqual(mockPage()[2]) + expect(store.pageItem.validate().success).toBe(false) + }) + + it('sets page list correctly', () => { + const store = usePageStore() + + store.setPageList(mockPage()) + + expect(store.pageList).toHaveLength(mockPage().length) + + expect(store.pageList[0]).toBeInstanceOf(Page) + expect(store.pageList[0]).toEqual(mockPage()[0]) + expect(store.pageList[0].validate().success).toBe(true) + + expect(store.pageList[1]).toBeInstanceOf(Page) + expect(store.pageList[1]).toEqual(mockPage()[1]) + expect(store.pageList[1].validate().success).toBe(true) + + expect(store.pageList[2]).toBeInstanceOf(Page) + expect(store.pageList[2]).toEqual(mockPage()[2]) + expect(store.pageList[2].validate().success).toBe(false) + }) +}) diff --git a/src/store/modules/page.ts b/src/store/modules/page.ts new file mode 100644 index 00000000..0d132a7c --- /dev/null +++ b/src/store/modules/page.ts @@ -0,0 +1,160 @@ +/* eslint-disable no-console */ +import { Page, TPage } from '../../entities/index.js' +import { defineStore } from 'pinia' + +const apiEndpoint = '/index.php/apps/opencatalogi/api/pages' + +interface Options { + /** + * Do not save the received item to the store, this can be enabled if API calls get run in a loop + */ + doNotSetStore?: boolean +} + +interface PageStoreState { + pageItem: Page; + pageList: Page[]; +} + +export const usePageStore = defineStore('page', { + state: () => ({ + pageItem: null, + pageList: [], + } as PageStoreState), + actions: { + setPageItem(pageItem: Page | TPage) { + this.pageItem = pageItem && new Page(pageItem as TPage) + console.log('Active page item set to ' + pageItem && pageItem?.id) + }, + setPageList(pageList: Page[] | TPage[]) { + this.pageList = pageList.map( + (pageItem) => new Page(pageItem as TPage), + ) + console.log('Page list set to ' + pageList.length + ' items') + }, + /* istanbul ignore next */ // ignore this for Jest until moved into a service + async refreshPageList(search: string = null) { + // @todo this might belong in a service? + let endpoint = apiEndpoint + if (search !== null && search !== '') { + endpoint = endpoint + '?_search=' + search + } + const response = await fetch( + endpoint, { + method: 'GET', + }, + ) + const rawData = (await response.json()).results + const data = rawData.map((pageItem: TPage) => new Page(pageItem)) + + this.setPageList(data) + + return { response, data } + }, + /* istanbul ignore next */ + async getAllPages(options: Options = {}) { + const response = await fetch( + `${apiEndpoint}`, + { method: 'get' }, + ) + + const rawData = await response.json() + + const data = rawData.results.map((pageItem: TPage) => new Page(pageItem)) + + options.doNotSetStore !== true && this.setPageList(data) + + return { response, data } + }, + /* istanbul ignore next */ + async getOnePage(id: number, options: Options = {}) { + if (!id) { + throw Error('Passed id is falsy') + } + + const response = await fetch( + `${apiEndpoint}/${id}`, + { method: 'get' }, + ) + + const data = new Page(await response.json()) + + options.doNotSetStore !== true && this.setPageItem(data) + + return { response, data } + }, + /* istanbul ignore next */ + async addPage(item: Page) { + if (!(item instanceof Page)) { + throw Error('Please pass a Page item from the Page class') + } + + const validateResult = item.validate() + if (!validateResult.success) { + throw Error(validateResult.error.issues[0].message) + } + + const response = await fetch(apiEndpoint, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(validateResult.data), + }, + ) + + const data = new Page(await response.json()) + + this.refreshPageList() + this.setPageItem(data) + + return { response, data } + }, + /* istanbul ignore next */ + async editPage(pageItem: Page) { + if (!(pageItem instanceof Page)) { + throw Error('Please pass a Page item from the Page class') + } + + const validateResult = pageItem.validate() + if (!validateResult.success) { + throw Error(validateResult.error.issues[0].message) + } + + const response = await fetch( + `${apiEndpoint}/${pageItem.id}`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(validateResult.data), + }, + ) + + const data = new Page(await response.json()) + + this.refreshPageList() + this.setPageItem(data) + + return { response, data } + }, + /* istanbul ignore next */ + async deletePage(id: number) { + if (!id) { + throw Error('Passed id is falsy') + } + + const response = await fetch( + `${apiEndpoint}/${id}`, + { method: 'DELETE' }, + ) + + this.refreshPageList() + this.setPageItem(null) + + return { response } + }, + }, +}) diff --git a/src/store/store.js b/src/store/store.js index 1eddc424..0f1b9e7d 100644 --- a/src/store/store.js +++ b/src/store/store.js @@ -14,6 +14,7 @@ import { useOrganizationStore } from './modules/organization' import { usePublicationStore } from './modules/publication' import { useSearchStore } from './modules/search' import { useThemeStore } from './modules/theme' +import { usePageStore } from './modules/page' const navigationStore = useNavigationStore(pinia) const searchStore = useSearchStore(pinia) @@ -24,6 +25,7 @@ const publicationStore = usePublicationStore(pinia) const organizationStore = useOrganizationStore(pinia) const themeStore = useThemeStore(pinia) const configurationStore = useConfigurationStore(pinia) +const pageStore = usePageStore(pinia) export { // generic @@ -37,4 +39,5 @@ export { organizationStore, themeStore, configurationStore, + pageStore, } diff --git a/src/views/Views.vue b/src/views/Views.vue index 8ba8cae6..ca810743 100644 --- a/src/views/Views.vue +++ b/src/views/Views.vue @@ -9,6 +9,7 @@ import { navigationStore } from '../store/store.js' + @@ -23,6 +24,7 @@ import { NcAppContent } from '@nextcloud/vue' import Catalogi from './catalogi/CatalogiIndex.vue' import Organizations from './organizations/OrganizationIndex.vue' import Themes from './themes/ThemeIndex.vue' +import Pages from './pages/PageIndex.vue' import Dashboard from './dashboard/DashboardIndex.vue' import Directory from './directory/DirectoryIndex.vue' import PublicationType from './publicationType/PublicationTypeIndex.vue' @@ -35,6 +37,7 @@ export default { Catalogi, Organizations, Themes, + Pages, Dashboard, Directory, PublicationType, diff --git a/src/views/pages/PageDetail.vue b/src/views/pages/PageDetail.vue new file mode 100644 index 00000000..e01d6930 --- /dev/null +++ b/src/views/pages/PageDetail.vue @@ -0,0 +1,202 @@ + + + + + + + diff --git a/src/views/pages/PageIndex.vue b/src/views/pages/PageIndex.vue new file mode 100644 index 00000000..8d1f20cf --- /dev/null +++ b/src/views/pages/PageIndex.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/src/views/pages/PageList.vue b/src/views/pages/PageList.vue new file mode 100644 index 00000000..49115cfe --- /dev/null +++ b/src/views/pages/PageList.vue @@ -0,0 +1,202 @@ + + + + +