Skip to content

Commit

Permalink
Merge pull request #174 from ConductionNL/feature/IBOC-107/nextcloud-…
Browse files Browse the repository at this point in the history
…menu

feature/IBOC-107/nextcloud-menu
  • Loading branch information
remko48 authored Jan 21, 2025
2 parents faea2c6 + 91c11a6 commit b84368e
Show file tree
Hide file tree
Showing 25 changed files with 392 additions and 238 deletions.
2 changes: 2 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
'organizations' => ['url' => '/api/organizations'],
'themes' => ['url' => '/api/themes'],
'pages' => ['url' => '/api/pages'],
'menus' => ['url' => '/api/menu'],
'attachments' => ['url' => '/api/attachments'],
'catalogi' => ['url' => '/api/catalogi'],
'listings' => ['url' => '/api/listings'],
Expand Down Expand Up @@ -41,6 +42,7 @@
['name' => 'search#theme', 'url' => '/api/search/themes/{themeId}', 'verb' => 'GET', 'requirements' => ['themeId' => '\d+']],
['name' => 'search#pages', 'url' => '/api/public/pages', 'verb' => 'GET'],
['name' => 'search#page', 'url' => '/api/public/pages/{pageSlug}', 'verb' => 'GET', 'requirements' => ['pageId' => '.+']],
['name' => 'search#menu', 'url' => '/api/public/menu', 'verb' => 'GET'],
// Object API routes
['name' => 'objects#index', 'url' => 'api/objects/{objectType}', 'verb' => 'GET'],
['name' => 'objects#create', 'url' => 'api/objects/{objectType}', 'verb' => 'POST'],
Expand Down
36 changes: 36 additions & 0 deletions lib/Controller/SearchController.php
Original file line number Diff line number Diff line change
Expand Up @@ -345,5 +345,41 @@ public function page(string $pageSlug): JSONResponse
return new JSONResponse($object);
}

/**
* Return all menus.
*
* @CORS
* @PublicPage
* @NoAdminRequired
* @NoCSRFRequired
*
* @return JSONResponse The Response containing all menus.
*/
public function menu(): JSONResponse
{
// Get all page objects with request parameters
$objects = $this->objectService->getResultArrayForRequest(objectType: 'menu', requestParams: $this->request->getParams());

// Format dates for each result
$formattedResults = array_map(function($object) {
// Format created_at if it exists
if (isset($object['created_at'])) {
$created = new \DateTime($object['created_at']);
$object['created_at'] = $created->format('Y-m-d\TH:i:s.u\Z');
}
// Format updated_at if it exists
if (isset($object['updated_at'])) {
$updated = new \DateTime($object['updated_at']);
$object['updated_at'] = $updated->format('Y-m-d\TH:i:s.u\Z');
}
return $object;
}, $objects['results']);

// Prepare the response data with formatted dates
$data = [
'data' => $formattedResults
];

return new JSONResponse($data);
}
}
4 changes: 2 additions & 2 deletions lib/Db/PageMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public function findAll(int $limit = null, int $offset = null, array $filters =
* @param array $object An array of Page data
* @return Page The newly created Page entity
*/
public function createFromArray(array $object): Page
public function createFromArray(array $object, array $extend = []): Page
{
$page = new Page();
$page->hydrate(object: $object);
Expand Down Expand Up @@ -135,7 +135,7 @@ public function createFromArray(array $object): Page
* @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, bool $updateVersion = false, bool $patch = false): Page
public function updateFromArray(int $id, array $object, array $extend, bool $updateVersion = false, bool $patch = false): Page
{
$page = $this->find($id);
// Fallback to create if the page does not exist
Expand Down
3 changes: 2 additions & 1 deletion lib/Service/ObjectService.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use OCP\App\IAppManager;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\AppFramework\Db\QBMapper;
use OCP\IURLGenerator;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
Expand Down Expand Up @@ -86,7 +87,7 @@ public function __construct(
* @throws NotFoundExceptionInterface|ContainerExceptionInterface If OpenRegister service is not available or if register/schema is not configured.
* @throws Exception
*/
private function getMapper(string $objectType): mixed
private function getMapper(string $objectType): QBMapper|\OCA\OpenRegister\Service\ObjectService
{
$objectTypeLower = strtolower($objectType);

Expand Down
2 changes: 1 addition & 1 deletion src/entities/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ export * from './publication/index.js'
export * from './theme/index.js'
export * from './publicationType/index.js'
export * from './page/index.js'
export * from './menu/index.js'
export * from './menu/index.js'
1 change: 1 addition & 0 deletions src/entities/listing/listing.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { TOrganization } from '../organization'
import { TListing } from './listing.types'
import { SafeParseReturnType, z } from 'zod'
Expand Down
1 change: 1 addition & 0 deletions src/entities/listing/listing.types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { TOrganization } from '../organization'

export type TListing = {
Expand Down
1 change: 0 additions & 1 deletion src/entities/menu/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from './menu.ts'
export * from './menu.types.ts'
export * from './menu.mock.ts'

14 changes: 7 additions & 7 deletions src/entities/menu/menu.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,33 +11,33 @@ export const mockMenuData = (): TMenu[] => [
uuid: '123e4567-e89b-12d3-a456-426614174000',
name: 'Main Menu',
position: 1,
items: '[{"id": 1, "title": "Home", "url": "/"}]',
items: [{ name: 'Home', description: 'Home', slug: '/', icon: 'home', link: '/' }],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
updatedAt: new Date().toISOString(),
},
// @ts-expect-error -- expected missing items
{ // partial data
id: '2',
uuid: '123e4567-e89b-12d3-a456-426614174001',
uuid: '123e4567-e89b-12d3-a456-426614174001',
name: 'Footer Menu',
position: 2,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
updatedAt: new Date().toISOString(),
},
{ // invalid data
id: '3',
uuid: '123e4567-e89b-12d3-a456-426614174002',
name: '',
position: -1,
items: '[]',
items: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
updatedAt: new Date().toISOString(),
},
]

/**
* Creates an array of Menu instances from provided data or default mock data
* @param data Optional array of menu data to convert to Menu instances
* @returns Array of Menu instances
* @return {TMenu[]} Array of Menu instances
*/
export const mockMenu = (data: TMenu[] = mockMenuData()): TMenu[] => data.map(item => new Menu(item))
12 changes: 6 additions & 6 deletions src/entities/menu/menu.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SafeParseReturnType, z } from 'zod'
import { TMenu } from './menu.types'
import { TMenu, TMenuItem } from './menu.types'

/**
* Menu class representing a navigation menu entity with validation
Expand All @@ -11,7 +11,7 @@ export class Menu implements TMenu {
public uuid: string
public name: string
public position: number
public items: string // JSON string containing menu items
public items: TMenuItem[] // Array of menu items
public createdAt: string
public updatedAt: string

Expand All @@ -26,29 +26,29 @@ export class Menu implements TMenu {
/* istanbul ignore next */ // Jest does not recognize the code coverage of these 2 methods
/**
* Hydrates the menu object with provided data
* @param data Menu data to populate the instance
* @param {TMenu} data Menu data to populate the instance
*/
private hydrate(data: TMenu) {
this.id = data?.id?.toString() || ''
this.uuid = data?.uuid || ''
this.name = data?.name || ''
this.position = data?.position || 0
this.items = data?.items || '[]' // Default to empty array in JSON string format
this.items = data?.items || [] // Default to empty array in JSON string format
this.createdAt = data?.createdAt || ''
this.updatedAt = data?.updatedAt || ''
}

/* istanbul ignore next */
/**
* Validates the menu data against a schema
* @return SafeParseReturnType containing validation result
* @return {SafeParseReturnType<TMenu, unknown>} SafeParseReturnType containing validation result
*/
public validate(): SafeParseReturnType<TMenu, unknown> {
// Schema validation for menu data
const schema = z.object({
name: z.string().min(1, 'naam is verplicht'),
position: z.number().min(0, 'positie moet 0 of hoger zijn'),
items: z.string().min(2, 'items zijn verplicht'), // At least '[]'
items: z.array(z.any()), // At least '[]'
})

const result = schema.safeParse({
Expand Down
31 changes: 24 additions & 7 deletions src/entities/menu/menu.types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
export type TMenuSubItem = {
name: string
slug: string
link: string
description?: string
icon?: string
}

export type TMenuItem = {
name: string
slug: string
link: string
description?: string
icon?: string
items?: TMenuSubItem[]
}

/**
* Type definition for a Menu object
* Represents the structure of a navigation menu with items and metadata
*/
export type TMenu = {
id: string // Unique identifier for the menu
uuid: string // UUID for the menu
name: string // Display name of the menu
position: number // Order/position of the menu in navigation
items: string // JSON string containing menu items and their structure
createdAt: string // Creation timestamp
updatedAt: string // Last update timestamp
id: string // Unique identifier for the menu
uuid: string // UUID for the menu
name: string // Display name of the menu
position: number // Order/position of the menu in navigation
items: TMenuItem[] // Array of menu items
createdAt: string // Creation timestamp
updatedAt: string // Last update timestamp
}
1 change: 0 additions & 1 deletion src/entities/page/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from './page.ts'
export * from './page.types.ts'
export * from './page.mock.ts'

6 changes: 6 additions & 0 deletions src/modals/Modals.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import { navigationStore, publicationStore } from './../store/store.js'
<EditThemeModal />
<PageForm v-if="navigationStore.modal === 'pageForm'" />
<AddPageContentsModal v-if="navigationStore.modal === 'addPageContents'" />
<EditMenuModal />
<DeleteMenuModal />
</div>
</template>

Expand Down Expand Up @@ -55,6 +57,8 @@ import AddThemeModal from './theme/AddThemeModal.vue'
import EditThemeModal from './theme/EditThemeModal.vue'
import PageForm from './page/PageForm.vue'
import AddPageContentsModal from './pageContents/AddPageContents.vue'
import EditMenuModal from './menu/EditMenuModal.vue'
import DeleteMenuModal from './menu/DeleteMenuModal.vue'
/**
* Component that contains all modals used in the application
Expand Down Expand Up @@ -84,6 +88,8 @@ export default {
EditThemeModal,
PageForm,
AddPageContentsModal,
EditMenuModal,
DeleteMenuModal,
},
}
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,23 @@ import { menuStore, navigationStore } from '../../store/store.js'
</script>

<template>
<NcDialog v-if="navigationStore.dialog === 'deleteMenu'"
<NcDialog v-if="navigationStore.modal === 'deleteMenu'"
name="Delete Menu"
size="normal"
:can-close="false">
<p v-if="success === null">
Do you want to permanently delete <b>{{ menuStore.menuItem?.id }}</b>? This action cannot be undone.
Weet je zeker dat je het menu <b>{{ menuStore.menuItem?.name }}</b> wilt verwijderen? Dit kan niet ongedaan worden gemaakt.
</p>

<NcNoteCard v-if="success" type="success">
<p>Menu successfully deleted</p>
<p>Menu succesvol verwijderd</p>
</NcNoteCard>
<NcNoteCard v-if="error" type="error">
<p>{{ error }}</p>
</NcNoteCard>

<template #actions>
<NcButton @click="closeDialog">
<NcButton @click="closeModal">
<template #icon>
<Cancel :size="20" />
</template>
Expand Down Expand Up @@ -55,7 +55,7 @@ import TrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue'
* Component for deleting menu items
*/
export default {
name: 'DeleteMenu',
name: 'DeleteMenuModal',
components: {
NcDialog,
NcButton,
Expand All @@ -77,8 +77,8 @@ export default {
/**
* Closes the delete dialog and resets state
*/
closeDialog() {
navigationStore.setDialog(false)
closeModal() {
navigationStore.setModal(false)
clearTimeout(this.closeModalTimeout)
this.success = null
this.loading = false
Expand All @@ -90,12 +90,10 @@ export default {
async deleteMenu() {
this.loading = true
menuStore.deleteMenu({
...menuStore.menuItem,
}).then(({ response }) => {
menuStore.deleteMenu(menuStore.menuItem.id).then(({ response }) => {
this.success = response.ok
this.error = false
response.ok && (this.closeModalTimeout = setTimeout(this.closeDialog, 2000))
response.ok && (this.closeModalTimeout = setTimeout(this.closeModal, 2000))
}).catch((error) => {
this.success = false
this.error = error.message || 'An error occurred while deleting the menu'
Expand Down
Loading

0 comments on commit b84368e

Please sign in to comment.