diff --git a/css/main.css b/css/main.css index 754ed28eee..290da6e181 100755 --- a/css/main.css +++ b/css/main.css @@ -1,10 +1,11 @@ :root { - --OC-margin-10: 10px; - --OC-margin-20: 20px; - --OC-margin-50: 50px; + --OR-margin-10: 10px; + --OR-margin-20: 20px; + --OR-margin-50: 50px; } /* Pages */ + .pageHeader { margin-block-start: var(--app-navigation-padding); margin-inline-start: calc( @@ -15,13 +16,16 @@ } /* Lists */ + .listHeader { + display: flex; + position: sticky; top: 0; z-index: 1000; background-color: var(--color-main-background); border-bottom: 1px solid var(--color-border); - display: flex; + flex-direction: row; align-items: center; } @@ -37,15 +41,11 @@ } /* Detail pages */ -.detailHeader { - display: flex; - justify-content: space-between; -} .detailContainer { - margin-block-start: var(--OC-margin-20); - margin-inline-start: var(--OC-margin-20); - margin-inline-end: var(--OC-margin-20); + margin-block-start: var(--OR-margin-20); + margin-inline-start: var(--OR-margin-20); + margin-inline-end: var(--OR-margin-20); } .tabContainer > * ul > li { @@ -83,7 +83,7 @@ } .tabContainer > * div[role="tabpanel"] { - margin-block-start: var(--OC-margin-10); + margin-block-start: var(--OR-margin-10); } .tabPanel { @@ -112,7 +112,7 @@ /* Modals */ .modalContent { - margin: var(--OC-margin-50); + margin: var(--OR-margin-50); text-align: center; } @@ -136,6 +136,7 @@ } /* File drag and drop */ + .filesListDragDropNotice { width: 100%; min-height: 113px; diff --git a/lib/Controller/RegistersController.php b/lib/Controller/RegistersController.php index 58270c1efe..f1066b32c9 100644 --- a/lib/Controller/RegistersController.php +++ b/lib/Controller/RegistersController.php @@ -4,8 +4,8 @@ use OCA\OpenRegister\Service\ObjectService; use OCA\OpenRegister\Service\SearchService; -use OCA\OpenRegister\Db\Source; -use OCA\OpenRegister\Db\SourceMapper; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\RegisterMapper; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\JSONResponse; @@ -15,7 +15,7 @@ class RegistersController extends Controller { /** - * Constructor for the SourcesController + * Constructor for the RegistersController * * @param string $appName The name of the app * @param IRequest $request The request object @@ -25,7 +25,7 @@ public function __construct( $appName, IRequest $request, private readonly IAppConfig $config, - private readonly SourceMapper $sourceMapper + private readonly RegisterMapper $registerMapper ) { parent::__construct($appName, $request); @@ -51,56 +51,56 @@ public function page(): TemplateResponse } /** - * Retrieves a list of all sources + * Retrieves a list of all registers * - * This method returns a JSON response containing an array of all sources in the system. + * This method returns a JSON response containing an array of all registers in the system. * * @NoAdminRequired * @NoCSRFRequired * - * @return JSONResponse A JSON response containing the list of sources + * @return JSONResponse A JSON response containing the list of registers */ public function index(ObjectService $objectService, SearchService $searchService): JSONResponse { $filters = $this->request->getParams(); - $fieldsToSearch = ['name', 'description']; + $fieldsToSearch = ['title', 'description']; $searchParams = $searchService->createMySQLSearchParams(filters: $filters); $searchConditions = $searchService->createMySQLSearchConditions(filters: $filters, fieldsToSearch: $fieldsToSearch); $filters = $searchService->unsetSpecialQueryParams(filters: $filters); - return new JSONResponse(['results' => $this->sourceMapper->findAll(limit: null, offset: null, filters: $filters, searchConditions: $searchConditions, searchParams: $searchParams)]); + return new JSONResponse(['results' => $this->registerMapper->findAll(limit: null, offset: null, filters: $filters, searchConditions: $searchConditions, searchParams: $searchParams)]); } /** - * Retrieves a single source by its ID + * Retrieves a single register by its ID * - * This method returns a JSON response containing the details of a specific source. + * This method returns a JSON response containing the details of a specific register. * * @NoAdminRequired * @NoCSRFRequired * - * @param string $id The ID of the source to retrieve - * @return JSONResponse A JSON response containing the source details + * @param string $id The ID of the register to retrieve + * @return JSONResponse A JSON response containing the register details */ public function show(string $id): JSONResponse { try { - return new JSONResponse($this->sourceMapper->find(id: (int) $id)); + return new JSONResponse($this->registerMapper->find(id: (int) $id)); } catch (DoesNotExistException $exception) { return new JSONResponse(data: ['error' => 'Not Found'], statusCode: 404); } } /** - * Creates a new source + * Creates a new register * - * This method creates a new source based on POST data. + * This method creates a new register based on POST data. * * @NoAdminRequired * @NoCSRFRequired * - * @return JSONResponse A JSON response containing the created source + * @return JSONResponse A JSON response containing the created register */ public function create(): JSONResponse { @@ -116,19 +116,19 @@ public function create(): JSONResponse unset($data['id']); } - return new JSONResponse($this->sourceMapper->createFromArray(object: $data)); + return new JSONResponse($this->registerMapper->createFromArray(object: $data)); } /** - * Updates an existing source + * Updates an existing register * - * This method updates an existing source based on its ID. + * This method updates an existing register based on its ID. * * @NoAdminRequired * @NoCSRFRequired * - * @param string $id The ID of the source to update - * @return JSONResponse A JSON response containing the updated source details + * @param string $id The ID of the register to update + * @return JSONResponse A JSON response containing the updated register details */ public function update(int $id): JSONResponse { @@ -142,23 +142,23 @@ public function update(int $id): JSONResponse if (isset($data['id'])) { unset($data['id']); } - return new JSONResponse($this->sourceMapper->updateFromArray(id: (int) $id, object: $data)); + return new JSONResponse($this->registerMapper->updateFromArray(id: (int) $id, object: $data)); } /** - * Deletes a source + * Deletes a register * - * This method deletes a source based on its ID. + * This method deletes a register based on its ID. * * @NoAdminRequired * @NoCSRFRequired * - * @param string $id The ID of the source to delete + * @param string $id The ID of the register to delete * @return JSONResponse An empty JSON response */ public function destroy(int $id): JSONResponse { - $this->sourceMapper->delete($this->sourceMapper->find((int) $id)); + $this->registerMapper->delete($this->registerMapper->find((int) $id)); return new JSONResponse([]); } diff --git a/lib/Controller/SchemasController.php b/lib/Controller/SchemasController.php index 127e717724..7f7badff3c 100644 --- a/lib/Controller/SchemasController.php +++ b/lib/Controller/SchemasController.php @@ -63,7 +63,7 @@ public function page(): TemplateResponse public function index(ObjectService $objectService, SearchService $searchService): JSONResponse { $filters = $this->request->getParams(); - $fieldsToSearch = ['name', 'description']; + $fieldsToSearch = ['title', 'description']; $searchParams = $searchService->createMySQLSearchParams(filters: $filters); $searchConditions = $searchService->createMySQLSearchConditions(filters: $filters, fieldsToSearch: $fieldsToSearch); diff --git a/lib/Controller/SourcesController.php b/lib/Controller/SourcesController.php index 4b10ae7492..bc98c2d219 100644 --- a/lib/Controller/SourcesController.php +++ b/lib/Controller/SourcesController.php @@ -63,7 +63,7 @@ public function page(): TemplateResponse public function index(ObjectService $objectService, SearchService $searchService): JSONResponse { $filters = $this->request->getParams(); - $fieldsToSearch = ['name', 'description']; + $fieldsToSearch = ['title', 'description']; $searchParams = $searchService->createMySQLSearchParams(filters: $filters); $searchConditions = $searchService->createMySQLSearchConditions(filters: $filters, fieldsToSearch: $fieldsToSearch); diff --git a/lib/Db/Register.php b/lib/Db/Register.php index 7e4a99dfcc..4d289fc1a7 100644 --- a/lib/Db/Register.php +++ b/lib/Db/Register.php @@ -8,41 +8,58 @@ class Register extends Entity implements JsonSerializable { - protected ?string $id = null; protected ?string $title = null; protected ?string $description = null; - protected ?array $schemas = null; + protected ?array $schemas = []; protected ?string $source = null; protected ?string $tablePrefix = null; protected ?DateTime $updated = null; protected ?DateTime $created = null; public function __construct() { - $this->addType('id', 'string'); - $this->addType('title', 'title'); - $this->addType('description', 'string'); - $this->addType('schemas', 'array'); - $this->addType('source', 'string'); - $this->addType('tablePrefix', 'string'); - $this->addType('updated', 'datetime'); - $this->addType('created', 'datetime'); + $this->addType(fieldName: 'title', type: 'string'); + $this->addType(fieldName: 'description', type: 'string'); + $this->addType(fieldName: 'schemas', type: 'json'); + $this->addType(fieldName: 'source', type: 'string'); + $this->addType(fieldName: 'tablePrefix', type: 'string'); + $this->addType(fieldName:'updated', type: 'datetime'); + $this->addType(fieldName:'created', type: 'datetime'); + } + + public function getJsonFields(): array + { + return array_keys( + array_filter($this->getFieldTypes(), function ($field) { + return $field === 'json'; + }) + ); } public function hydrate(array $object): self { + $jsonFields = $this->getJsonFields(); + + if(isset($object['metadata']) === false) { + $object['metadata'] = []; + } + 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) { - // Error handling can be added here } } return $this; } + public function jsonSerialize(): array { return [ @@ -52,8 +69,8 @@ public function jsonSerialize(): array 'schemas' => $this->schemas, 'source' => $this->source, 'tablePrefix' => $this->tablePrefix, - 'updated' => $this->updated, - 'created' => $this->created + 'updated' => isset($this->updated) ? $this->updated->format('c') : null, + 'created' => isset($this->created) ? $this->created->format('c') : null ]; } } \ No newline at end of file diff --git a/lib/Db/Schema.php b/lib/Db/Schema.php index fae5121c08..351d94b09f 100644 --- a/lib/Db/Schema.php +++ b/lib/Db/Schema.php @@ -8,7 +8,6 @@ class Schema extends Entity implements JsonSerializable { - protected ?string $title = null; protected ?string $version = null; protected ?string $description = null; protected ?string $summary = null; @@ -20,13 +19,13 @@ class Schema extends Entity implements JsonSerializable protected ?DateTime $created = null; public function __construct() { - $this->addType(fieldName: 'archive', type: 'json'); $this->addType(fieldName: 'title', type: 'string'); $this->addType(fieldName: 'version', type: 'string'); $this->addType(fieldName: 'description', type: 'string'); $this->addType(fieldName: 'summary', type: 'string'); $this->addType(fieldName: 'required', type: 'json'); $this->addType(fieldName: 'properties', type: 'json'); + $this->addType(fieldName: 'archive', type: 'json'); $this->addType(fieldName: 'source', type: 'string'); $this->addType(fieldName:'updated', type: 'datetime'); $this->addType(fieldName:'created', type: 'datetime'); @@ -45,6 +44,10 @@ public function hydrate(array $object): self { $jsonFields = $this->getJsonFields(); + if(isset($object['metadata']) === false) { + $object['metadata'] = []; + } + foreach($object as $key => $value) { if (in_array($key, $jsonFields) === true && $value === []) { $value = null; @@ -61,6 +64,7 @@ public function hydrate(array $object): self return $this; } + public function jsonSerialize(): array { $properties = []; diff --git a/lib/Db/Source.php b/lib/Db/Source.php index 2bd9693585..1a8b9289e8 100644 --- a/lib/Db/Source.php +++ b/lib/Db/Source.php @@ -8,8 +8,7 @@ class Source extends Entity implements JsonSerializable { - protected ?string $id = null; - protected ?string $name = null; + protected ?string $title = null; protected ?string $description = null; protected ?string $databaseUrl = null; protected ?string $type = null; @@ -17,30 +16,46 @@ class Source extends Entity implements JsonSerializable protected ?DateTime $created = null; public function __construct() { - $this->addType('id', 'string'); - $this->addType('title', 'string'); - $this->addType('description', 'string'); - $this->addType('databaseUrl', 'string'); - $this->addType('type', 'string'); - $this->addType('updated', 'datetime'); - $this->addType('created', 'datetime'); + $this->addType(fieldName: 'title', type: 'string'); + $this->addType(fieldName: 'description', type: 'string'); + $this->addType(fieldName: 'databaseUrl', type: 'string'); + $this->addType(fieldName: 'type', type: 'string'); + $this->addType(fieldName: 'updated', type: 'datetime'); + $this->addType(fieldName: 'created', type: 'datetime'); + } + + public function getJsonFields(): array + { + return array_keys( + array_filter($this->getFieldTypes(), function ($field) { + return $field === 'json'; + }) + ); } public function hydrate(array $object): self { + $jsonFields = $this->getJsonFields(); + + if(isset($object['metadata']) === false) { + $object['metadata'] = []; + } + 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) { - // Error handling can be added here } } return $this; } - public function jsonSerialize(): array { return [ @@ -49,8 +64,8 @@ public function jsonSerialize(): array 'description' => $this->description, 'databaseUrl' => $this->databaseUrl, 'type' => $this->type, - 'updated' => $this->updated, - 'created' => $this->created + 'updated' => isset($this->updated) ? $this->updated->format('c') : null, + 'created' => isset($this->created) ? $this->created->format('c') : null ]; } } \ No newline at end of file diff --git a/lib/Migration/Version1Date20240924200009.php b/lib/Migration/Version1Date20240924200009.php index 0d950fdb47..f13c802c22 100755 --- a/lib/Migration/Version1Date20240924200009.php +++ b/lib/Migration/Version1Date20240924200009.php @@ -45,8 +45,8 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $table->addColumn('description', Types::TEXT, ['notnull' => false]); $table->addColumn('database_url', Types::STRING, ['notnull' => true, 'length' => 255]); $table->addColumn('type', Types::STRING, ['notnull' => true, 'length' => 64]); - $table->addColumn('updated', Types::DATETIME, ['notnull' => true]); - $table->addColumn('created', Types::DATETIME, ['notnull' => true]); + $table->addColumn('updated', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); + $table->addColumn('created', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); $table->setPrimaryKey(['id']); $table->addIndex(['title'], 'register_sources_title_index'); @@ -64,8 +64,8 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $table->addColumn('properties', Types::JSON, ['notnull' => false]); $table->addColumn('archive', Types::JSON, ['notnull' => false]); $table->addColumn('source', Types::STRING, ['notnull' => true, 'length' => 64]); - $table->addColumn('updated', Types::DATETIME, ['notnull' => true]); - $table->addColumn('created', Types::DATETIME, ['notnull' => true]); + $table->addColumn('updated', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); + $table->addColumn('created', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); $table->setPrimaryKey(['id']); $table->addIndex(['title'], 'register_schemas_title_index'); @@ -80,8 +80,8 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $table->addColumn('schemas', Types::JSON, ['notnull' => false]); $table->addColumn('source', Types::STRING, ['notnull' => true, 'length' => 64]); $table->addColumn('table_prefix', Types::STRING, ['notnull' => true, 'length' => 64]); - $table->addColumn('updated', Types::DATETIME, ['notnull' => true]); - $table->addColumn('created', Types::DATETIME, ['notnull' => true]); + $table->addColumn('updated', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); + $table->addColumn('created', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); $table->setPrimaryKey(['id']); $table->addIndex(['title'], 'registers_title_index'); diff --git a/src/entities/register/register.mock.ts b/src/entities/register/register.mock.ts index 94a976b75b..3e7bb13c26 100644 --- a/src/entities/register/register.mock.ts +++ b/src/entities/register/register.mock.ts @@ -4,19 +4,25 @@ import { TRegister } from './register.types' export const mockRegisterData = (): TRegister[] => [ { id: '1234a1e5-b54d-43ad-abd1-4b5bff5fcd3f', - name: 'Character Register', + title: 'Character Register', description: 'Stores character data for the game', schemas: ['5678a1e5-b54d-43ad-abd1-4b5bff5fcd3f'], + source: '1', databaseId: 'db1-a1e5-b54d-43ad-abd1-4b5bff5fcd3f', tablePrefix: 'character_', + created: new Date().toISOString(), + updated: new Date().toISOString(), }, { id: '5678a1e5-b54d-43ad-abd1-4b5bff5fcd3f', - name: 'Item Register', + title: 'Item Register', description: 'Stores item data for the game', schemas: ['9012a1e5-b54d-43ad-abd1-4b5bff5fcd3f'], + source: '1', databaseId: 'db2-a1e5-b54d-43ad-abd1-4b5bff5fcd3f', tablePrefix: 'item_', + created: new Date().toISOString(), + updated: new Date().toISOString(), }, ] diff --git a/src/entities/register/register.spec.ts b/src/entities/register/register.spec.ts index f2b9cc92db..6c774b43db 100644 --- a/src/entities/register/register.spec.ts +++ b/src/entities/register/register.spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { Register } from './register' import { mockRegisterData } from './register.mock' @@ -11,32 +12,24 @@ describe('Register Entity', () => { }) it('should create a Register entity with partial data', () => { - const partialData = { - name: 'Partial Register', - description: 'A register with partial data', - schemas: [] as any[], // Explicitly typing schemas as any[] - databaseId: 'db1-a1e5-b54d-43ad-abd1-4b5bff5fcd3f', - } - const register = new Register(partialData) + const register = new Register(mockRegisterData()[0]) expect(register).toBeInstanceOf(Register) expect(register.id).toBe('') - expect(register.name).toBe(partialData.name) + expect(register.title).toBe(mockRegisterData()[0].title) expect(register.tablePrefix).toBe('') expect(register.validate().success).toBe(true) }) it('should fail validation with invalid data', () => { - const invalidData = { - name: '', - description: 'Invalid register', - schemas: [] as any[], // Explicitly type the schemas property - databaseId: '', - } - const register = new Register(invalidData) + const register = new Register(mockRegisterData()[1]) expect(register).toBeInstanceOf(Register) expect(register.validate().success).toBe(false) + expect(register.validate().error?.issues).toContainEqual(expect.objectContaining({ + path: ['name'], + message: 'String must contain at least 1 character(s)', + })) }) it('should correctly combine database and register prefixes', () => { diff --git a/src/entities/register/register.ts b/src/entities/register/register.ts index 749bf05ede..6d29555b25 100644 --- a/src/entities/register/register.ts +++ b/src/entities/register/register.ts @@ -4,27 +4,34 @@ import { TRegister } from './register.types' export class Register implements TRegister { public id: string - public name: string + public title: string public description: string public schemas: string[] + public source: string public databaseId: string public tablePrefix: string + public updated: string + public created: string constructor(register: TRegister) { this.id = register.id || '' - this.name = register.name + this.title = register.title this.description = register.description this.schemas = register.schemas || [] + this.source = register.source || '' this.databaseId = register.databaseId this.tablePrefix = register.tablePrefix || '' + this.updated = register.updated || '' + this.created = register.created || '' } public validate(): SafeParseReturnType { const schema = z.object({ id: z.string().min(1), - name: z.string().min(1), + title: z.string().min(1), description: z.string(), schemas: z.array(z.string()), + source: z.string(), databaseId: z.string().min(1), tablePrefix: z.string(), }) diff --git a/src/entities/register/register.types.ts b/src/entities/register/register.types.ts index 9982f231ec..1016d45c68 100644 --- a/src/entities/register/register.types.ts +++ b/src/entities/register/register.types.ts @@ -1,8 +1,11 @@ export type TRegister = { id?: string - name: string + title: string description: string schemas: string[] // Array of Schema UUIDs + source: string // Reference to the Source entity databaseId: string // Reference to the Database entity tablePrefix?: string + updated?: string + created: string } diff --git a/src/entities/source/source.mock.ts b/src/entities/source/source.mock.ts index 7b0b098363..a9e1f95689 100644 --- a/src/entities/source/source.mock.ts +++ b/src/entities/source/source.mock.ts @@ -3,12 +3,14 @@ import { TSource } from './source.types' export const mockSourceData = (): TSource[] => [ { - id: '5678a1e5-b54d-43ad-abd1-4b5bff5fcd3f', - name: 'Main PostgreSQL Database', + id: 1, + title: 'Main PostgreSQL Database', description: 'Primary database for user data', databaseUrl: 'postgresql://user:password@localhost:5432/maindb', + type: 'postgresql', + updated: new Date().toISOString(), + created: new Date().toISOString(), }, - // ... existing code ... ] export const mockSource = (data: TSource[] = mockSourceData()): TSource[] => data.map(item => new Source(item)) diff --git a/src/entities/source/source.ts b/src/entities/source/source.ts index c5f2ca462e..ffa715ebc2 100644 --- a/src/entities/source/source.ts +++ b/src/entities/source/source.ts @@ -3,24 +3,31 @@ import { TSource } from './source.types' export class Source implements TSource { - public id: string - public name: string + public id: string | number + public title: string public description: string public databaseUrl: string + public type: string + public updated: string + public created: string constructor(source: TSource) { this.id = source.id || '' - this.name = source.name || '' + this.title = source.title || '' this.description = source.description || '' this.databaseUrl = source.databaseUrl || '' + this.type = source.type || '' + this.updated = source.updated || '' + this.created = source.created || '' } public validate(): SafeParseReturnType { const schema = z.object({ - id: z.string().min(1), - name: z.string().min(1), + id: z.union([z.string(), z.number()]), + title: z.string().min(1), description: z.string(), databaseUrl: z.string().url(), + type: z.string(), }) return schema.safeParse(this) diff --git a/src/entities/source/source.types.ts b/src/entities/source/source.types.ts index 7879efb130..05e55d1d65 100644 --- a/src/entities/source/source.types.ts +++ b/src/entities/source/source.types.ts @@ -1,6 +1,9 @@ export type TSource = { - id?: string - name: string - description: string - databaseUrl: string + id?: string | number; + title: string; + description: string; + databaseUrl: string; + type: string; + updated: string; + created: string; } diff --git a/src/modals/register/EditRegister.vue b/src/modals/register/EditRegister.vue index b7a642d4da..676babcfe9 100644 --- a/src/modals/register/EditRegister.vue +++ b/src/modals/register/EditRegister.vue @@ -1,5 +1,5 @@ diff --git a/src/views/source/SourceDetails.vue b/src/views/source/SourceDetails.vue index c788f139db..3ea2748a5f 100644 --- a/src/views/source/SourceDetails.vue +++ b/src/views/source/SourceDetails.vue @@ -1,5 +1,5 @@ diff --git a/src/views/source/SourcesList.vue b/src/views/source/SourcesList.vue index 6a1d0ad025..f9acb2d227 100644 --- a/src/views/source/SourcesList.vue +++ b/src/views/source/SourcesList.vue @@ -33,7 +33,7 @@ import { sourceStore, navigationStore, searchStore } from '../../store/store.js'