From 3864426c05ef59e379829f97d8caa2ea5ac38447 Mon Sep 17 00:00:00 2001 From: Chen Asraf Date: Thu, 9 Apr 2026 14:23:33 +0300 Subject: [PATCH] refactor: unify pref endpoints --- lib/Controller/PrefsController.php | 300 ++----- lib/ResponseDefinitions.php | 14 +- lib/Service/PrefsService.php | 83 ++ openapi.json | 1096 +++-------------------- src/api/prefs.ts | 207 +++-- tests/unit/Service/PrefsServiceTest.php | 7 +- 6 files changed, 410 insertions(+), 1297 deletions(-) diff --git a/lib/Controller/PrefsController.php b/lib/Controller/PrefsController.php index c0d0c34..2bdb35c 100644 --- a/lib/Controller/PrefsController.php +++ b/lib/Controller/PrefsController.php @@ -20,9 +20,8 @@ use OCP\IRequest; use OCP\IUserSession; /** - * @psalm-import-type PantryLastHouse from ResponseDefinitions - * @psalm-import-type PantryImageFolder from ResponseDefinitions - * @psalm-import-type PantryNotificationPrefs from ResponseDefinitions + * @psalm-import-type PantryUserPrefs from ResponseDefinitions + * @psalm-import-type PantryHousePrefs from ResponseDefinitions */ final class PrefsController extends OCSController { use TranslatesDomainExceptions; @@ -38,252 +37,90 @@ final class PrefsController extends OCSController { } /** - * Get the current user's last-used house id + * Get all user-level preferences (not scoped to a house) * - * @return DataResponse + * @return DataResponse * - * 200: Last house returned + * 200: Prefs returned */ - #[ApiRoute(verb: 'GET', url: '/api/prefs/last-house')] + #[ApiRoute(verb: 'GET', url: '/api/prefs')] #[NoAdminRequired] - public function getLastHouse(): DataResponse { + public function getUserPrefs(): DataResponse { return $this->runAction(function (): DataResponse { $uid = $this->requireUid(); - $houseId = $this->prefs->getLastHouseId($uid); + $prefs = $this->prefs->getAllUserPrefs($uid); // If the saved house is no longer accessible, forget it. + $houseId = $prefs['lastHouseId'] ?? null; if ($houseId !== null) { try { $this->auth->requireMember($houseId, $uid); } catch (ForbiddenException) { $this->prefs->setLastHouseId($uid, null); - $houseId = null; + $prefs['lastHouseId'] = null; } } - return new DataResponse(['houseId' => $houseId]); + return new DataResponse($prefs); }); } /** - * Set the current user's last-used house id + * Update user-level preferences * - * @param int|null $houseId House id, or null to clear. + * @param int|null $lastHouseId Last-used house id, or null to clear. * - * @return DataResponse + * @return DataResponse * - * 200: Last house updated + * 200: Prefs updated */ - #[ApiRoute(verb: 'PUT', url: '/api/prefs/last-house')] + #[ApiRoute(verb: 'PUT', url: '/api/prefs')] #[NoAdminRequired] - public function setLastHouse(?int $houseId = null): DataResponse { - return $this->runAction(function () use ($houseId): DataResponse { + public function setUserPrefs(?int $lastHouseId = null): DataResponse { + return $this->runAction(function () use ($lastHouseId): DataResponse { $uid = $this->requireUid(); - if ($houseId !== null) { - $this->auth->requireMember($houseId, $uid); + $patch = []; + if ($lastHouseId !== null) { + $this->auth->requireMember($lastHouseId, $uid); + $patch['lastHouseId'] = $lastHouseId; + } else { + // Explicit null means clear + $patch['lastHouseId'] = null; } - $this->prefs->setLastHouseId($uid, $houseId); - return new DataResponse(['houseId' => $houseId]); + $this->prefs->setUserPrefs($uid, $patch); + return new DataResponse($this->prefs->getAllUserPrefs($uid)); }); } /** - * Get the user's preferred image upload folder for a house + * Get all per-house preferences for the current user * * @param int $houseId House id. * - * @return DataResponse - * - * 200: Folder returned - */ - #[ApiRoute(verb: 'GET', url: '/api/houses/{houseId}/prefs/image-folder')] - #[NoAdminRequired] - public function getImageFolder(int $houseId): DataResponse { - return $this->runAction(function () use ($houseId): DataResponse { - $uid = $this->requireUid(); - $this->auth->requireMember($houseId, $uid); - return new DataResponse(['folder' => $this->prefs->getImageFolder($uid, $houseId)]); - }); - } - - /** - * Set the user's preferred image upload folder for a house - * - * @param int $houseId House id. - * @param string $folder Absolute path within the user's files. - * - * @return DataResponse - * - * 200: Folder updated - */ - #[ApiRoute(verb: 'PUT', url: '/api/houses/{houseId}/prefs/image-folder')] - #[NoAdminRequired] - public function setImageFolder(int $houseId, string $folder): DataResponse { - return $this->runAction(function () use ($houseId, $folder): DataResponse { - $uid = $this->requireUid(); - $this->auth->requireMember($houseId, $uid); - $stored = $this->prefs->setImageFolder($uid, $houseId, $folder); - return new DataResponse(['folder' => $stored]); - }); - } - - /** - * Get photo sort preference for a house - * - * @param int $houseId House id. - * - * @return DataResponse - * - * 200: Sort preference returned - */ - #[ApiRoute(verb: 'GET', url: '/api/houses/{houseId}/prefs/photo-sort')] - #[NoAdminRequired] - public function getPhotoSort(int $houseId): DataResponse { - return $this->runAction(function () use ($houseId): DataResponse { - $uid = $this->requireUid(); - $this->auth->requireMember($houseId, $uid); - return new DataResponse([ - 'sort' => $this->prefs->getPhotoSort($uid, $houseId), - 'foldersFirst' => $this->prefs->getPhotoFoldersFirst($uid, $houseId), - ]); - }); - } - - /** - * Set photo sort preference for a house - * - * @param int $houseId House id. - * @param string|null $sort Sort mode. - * @param bool|null $foldersFirst Whether folders appear first. - * - * @return DataResponse - * - * 200: Sort preference updated - */ - #[ApiRoute(verb: 'PUT', url: '/api/houses/{houseId}/prefs/photo-sort')] - #[NoAdminRequired] - public function setPhotoSort(int $houseId, ?string $sort = null, ?bool $foldersFirst = null): DataResponse { - return $this->runAction(function () use ($houseId, $sort, $foldersFirst): DataResponse { - $uid = $this->requireUid(); - $this->auth->requireMember($houseId, $uid); - if ($sort !== null) { - $this->prefs->setPhotoSort($uid, $houseId, $sort); - } - if ($foldersFirst !== null) { - $this->prefs->setPhotoFoldersFirst($uid, $houseId, $foldersFirst); - } - return new DataResponse([ - 'sort' => $this->prefs->getPhotoSort($uid, $houseId), - 'foldersFirst' => $this->prefs->getPhotoFoldersFirst($uid, $houseId), - ]); - }); - } - - /** - * Get note sort preference for a house - * - * @param int $houseId House id. - * - * @return DataResponse - * - * 200: Sort preference returned - */ - #[ApiRoute(verb: 'GET', url: '/api/houses/{houseId}/prefs/note-sort')] - #[NoAdminRequired] - public function getNoteSort(int $houseId): DataResponse { - return $this->runAction(function () use ($houseId): DataResponse { - $uid = $this->requireUid(); - $this->auth->requireMember($houseId, $uid); - return new DataResponse([ - 'sort' => $this->prefs->getNoteSort($uid, $houseId), - ]); - }); - } - - /** - * Set note sort preference for a house - * - * @param int $houseId House id. - * @param string $sort Sort mode. - * - * @return DataResponse - * - * 200: Sort preference updated - */ - #[ApiRoute(verb: 'PUT', url: '/api/houses/{houseId}/prefs/note-sort')] - #[NoAdminRequired] - public function setNoteSort(int $houseId, string $sort): DataResponse { - return $this->runAction(function () use ($houseId, $sort): DataResponse { - $uid = $this->requireUid(); - $this->auth->requireMember($houseId, $uid); - $stored = $this->prefs->setNoteSort($uid, $houseId, $sort); - return new DataResponse(['sort' => $stored]); - }); - } - - /** - * Get checklist item sort preference for a house - * - * @param int $houseId House id. - * - * @return DataResponse - * - * 200: Sort preference returned - */ - #[ApiRoute(verb: 'GET', url: '/api/houses/{houseId}/prefs/checklist-item-sort')] - #[NoAdminRequired] - public function getChecklistItemSort(int $houseId): DataResponse { - return $this->runAction(function () use ($houseId): DataResponse { - $uid = $this->requireUid(); - $this->auth->requireMember($houseId, $uid); - return new DataResponse([ - 'sort' => $this->prefs->getChecklistItemSort($uid, $houseId), - ]); - }); - } - - /** - * Set checklist item sort preference for a house - * - * @param int $houseId House id. - * @param string $sort Sort mode. - * - * @return DataResponse - * - * 200: Sort preference updated - */ - #[ApiRoute(verb: 'PUT', url: '/api/houses/{houseId}/prefs/checklist-item-sort')] - #[NoAdminRequired] - public function setChecklistItemSort(int $houseId, string $sort): DataResponse { - return $this->runAction(function () use ($houseId, $sort): DataResponse { - $uid = $this->requireUid(); - $this->auth->requireMember($houseId, $uid); - $stored = $this->prefs->setChecklistItemSort($uid, $houseId, $sort); - return new DataResponse(['sort' => $stored]); - }); - } - - /** - * Get notification preferences for a house - * - * @param int $houseId House id. - * - * @return DataResponse + * @return DataResponse * * 200: Prefs returned */ - #[ApiRoute(verb: 'GET', url: '/api/houses/{houseId}/prefs/notifications')] + #[ApiRoute(verb: 'GET', url: '/api/houses/{houseId}/prefs')] #[NoAdminRequired] - public function getNotificationPrefs(int $houseId): DataResponse { + public function getHousePrefs(int $houseId): DataResponse { return $this->runAction(function () use ($houseId): DataResponse { $uid = $this->requireUid(); $this->auth->requireMember($houseId, $uid); - return new DataResponse($this->prefs->getNotificationPrefs($uid, $houseId)); + return new DataResponse($this->prefs->getAllHousePrefs($uid, $houseId)); }); } /** - * Update notification preferences for a house + * Update per-house preferences for the current user + * + * Only the fields present in the request body are updated; omitted fields + * are left unchanged. * * @param int $houseId House id. + * @param string|null $imageFolder Image upload folder path. + * @param string|null $photoSort Photo sort mode. + * @param bool|null $photoFoldersFirst Whether folders appear first in photo board. + * @param string|null $noteSort Note sort mode. + * @param string|null $checklistItemSort Checklist item sort mode. * @param bool|null $notifyPhoto Photo upload notifications. * @param bool|null $notifyNoteCreate Note creation notifications. * @param bool|null $notifyNoteEdit Note edit notifications. @@ -291,35 +128,44 @@ final class PrefsController extends OCSController { * @param bool|null $notifyItemRecur Recurring item reappeared notifications. * @param bool|null $notifyItemDone Item completed notifications. * - * @return DataResponse + * @return DataResponse * * 200: Prefs updated */ - #[ApiRoute(verb: 'PUT', url: '/api/houses/{houseId}/prefs/notifications')] + #[ApiRoute(verb: 'PUT', url: '/api/houses/{houseId}/prefs')] #[NoAdminRequired] - public function setNotificationPrefs(int $houseId, ?bool $notifyPhoto = null, ?bool $notifyNoteCreate = null, ?bool $notifyNoteEdit = null, ?bool $notifyItemAdd = null, ?bool $notifyItemRecur = null, ?bool $notifyItemDone = null): DataResponse { - return $this->runAction(function () use ($houseId, $notifyPhoto, $notifyNoteCreate, $notifyNoteEdit, $notifyItemAdd, $notifyItemRecur, $notifyItemDone): DataResponse { + public function setHousePrefs( + int $houseId, + ?string $imageFolder = null, + ?string $photoSort = null, + ?bool $photoFoldersFirst = null, + ?string $noteSort = null, + ?string $checklistItemSort = null, + ?bool $notifyPhoto = null, + ?bool $notifyNoteCreate = null, + ?bool $notifyNoteEdit = null, + ?bool $notifyItemAdd = null, + ?bool $notifyItemRecur = null, + ?bool $notifyItemDone = null, + ): DataResponse { + return $this->runAction(function () use ($houseId, $imageFolder, $photoSort, $photoFoldersFirst, $noteSort, $checklistItemSort, $notifyPhoto, $notifyNoteCreate, $notifyNoteEdit, $notifyItemAdd, $notifyItemRecur, $notifyItemDone): DataResponse { $uid = $this->requireUid(); $this->auth->requireMember($houseId, $uid); - if ($notifyPhoto !== null) { - $this->prefs->setNotificationPref($uid, $houseId, 'notify_photo', $notifyPhoto); - } - if ($notifyNoteCreate !== null) { - $this->prefs->setNotificationPref($uid, $houseId, 'notify_note_create', $notifyNoteCreate); - } - if ($notifyNoteEdit !== null) { - $this->prefs->setNotificationPref($uid, $houseId, 'notify_note_edit', $notifyNoteEdit); - } - if ($notifyItemAdd !== null) { - $this->prefs->setNotificationPref($uid, $houseId, 'notify_item_add', $notifyItemAdd); - } - if ($notifyItemRecur !== null) { - $this->prefs->setNotificationPref($uid, $houseId, 'notify_item_recur', $notifyItemRecur); - } - if ($notifyItemDone !== null) { - $this->prefs->setNotificationPref($uid, $houseId, 'notify_item_done', $notifyItemDone); - } - return new DataResponse($this->prefs->getNotificationPrefs($uid, $houseId)); + $patch = array_filter([ + 'imageFolder' => $imageFolder, + 'photoSort' => $photoSort, + 'photoFoldersFirst' => $photoFoldersFirst, + 'noteSort' => $noteSort, + 'checklistItemSort' => $checklistItemSort, + 'notifyPhoto' => $notifyPhoto, + 'notifyNoteCreate' => $notifyNoteCreate, + 'notifyNoteEdit' => $notifyNoteEdit, + 'notifyItemAdd' => $notifyItemAdd, + 'notifyItemRecur' => $notifyItemRecur, + 'notifyItemDone' => $notifyItemDone, + ], fn ($v) => $v !== null); + $this->prefs->setHousePrefs($uid, $houseId, $patch); + return new DataResponse($this->prefs->getAllHousePrefs($uid, $houseId)); }); } diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 4dcccc5..ec59eea 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -70,11 +70,17 @@ namespace OCA\Pantry; * * @psalm-type PantrySuccess = array{success: true} * - * @psalm-type PantryLastHouse = array{houseId: int|null} + * @psalm-type PantryUserPrefs = array{ + * lastHouseId: int|null, + * firstDayOfWeek: int, + * } * - * @psalm-type PantryImageFolder = array{folder: string} - * - * @psalm-type PantryNotificationPrefs = array{ + * @psalm-type PantryHousePrefs = array{ + * imageFolder: string, + * photoSort: string, + * photoFoldersFirst: bool, + * noteSort: string, + * checklistItemSort: string, * notifyPhoto: bool, * notifyNoteCreate: bool, * notifyNoteEdit: bool, diff --git a/lib/Service/PrefsService.php b/lib/Service/PrefsService.php index 2f23256..08c2a3d 100644 --- a/lib/Service/PrefsService.php +++ b/lib/Service/PrefsService.php @@ -9,6 +9,7 @@ namespace OCA\Pantry\Service; use OCA\Pantry\AppInfo\Application; use OCP\IConfig; +use OCP\IL10N; class PrefsService { private const KEY_LAST_HOUSE = 'last_house_id'; @@ -17,9 +18,18 @@ class PrefsService { public function __construct( private IConfig $config, + private IL10N $l, ) { } + public function getFirstDayOfWeek(string $uid): int { + $value = $this->config->getUserValue($uid, 'core', 'first_day_of_week', ''); + if ($value === '') { + return (int)$this->l->l('firstday', null); + } + return (int)$value; + } + public function getLastHouseId(string $uid): ?int { $value = $this->config->getUserValue($uid, Application::APP_ID, self::KEY_LAST_HOUSE, ''); if ($value === '') { @@ -52,6 +62,28 @@ class PrefsService { return $normalized; } + // ----- Unified user prefs ----- + + /** + * @return array + */ + public function getAllUserPrefs(string $uid): array { + return [ + 'lastHouseId' => $this->getLastHouseId($uid), + 'firstDayOfWeek' => $this->getFirstDayOfWeek($uid), + ]; + } + + /** + * @param array $patch + */ + public function setUserPrefs(string $uid, array $patch): void { + if (array_key_exists('lastHouseId', $patch)) { + $v = $patch['lastHouseId']; + $this->setLastHouseId($uid, is_int($v) ? $v : null); + } + } + // ----- Sort preferences ----- private const KEY_PHOTO_SORT = 'photo_sort'; @@ -164,6 +196,57 @@ class PrefsService { ]; } + // ----- Unified house prefs ----- + + /** + * @return array + */ + public function getAllHousePrefs(string $uid, int $houseId): array { + return [ + 'imageFolder' => $this->getImageFolder($uid, $houseId), + 'photoSort' => $this->getPhotoSort($uid, $houseId), + 'photoFoldersFirst' => $this->getPhotoFoldersFirst($uid, $houseId), + 'noteSort' => $this->getNoteSort($uid, $houseId), + 'checklistItemSort' => $this->getChecklistItemSort($uid, $houseId), + ...$this->getNotificationPrefs($uid, $houseId), + ]; + } + + /** + * @param array $patch + */ + public function setHousePrefs(string $uid, int $houseId, array $patch): void { + if (array_key_exists('imageFolder', $patch) && is_string($patch['imageFolder'])) { + $this->setImageFolder($uid, $houseId, $patch['imageFolder']); + } + if (array_key_exists('photoSort', $patch) && is_string($patch['photoSort'])) { + $this->setPhotoSort($uid, $houseId, $patch['photoSort']); + } + if (array_key_exists('photoFoldersFirst', $patch) && is_bool($patch['photoFoldersFirst'])) { + $this->setPhotoFoldersFirst($uid, $houseId, $patch['photoFoldersFirst']); + } + if (array_key_exists('noteSort', $patch) && is_string($patch['noteSort'])) { + $this->setNoteSort($uid, $houseId, $patch['noteSort']); + } + if (array_key_exists('checklistItemSort', $patch) && is_string($patch['checklistItemSort'])) { + $this->setChecklistItemSort($uid, $houseId, $patch['checklistItemSort']); + } + // Notification prefs + $notifKeys = [ + 'notifyPhoto' => 'notify_photo', + 'notifyNoteCreate' => 'notify_note_create', + 'notifyNoteEdit' => 'notify_note_edit', + 'notifyItemAdd' => 'notify_item_add', + 'notifyItemRecur' => 'notify_item_recur', + 'notifyItemDone' => 'notify_item_done', + ]; + foreach ($notifKeys as $camel => $dbKey) { + if (array_key_exists($camel, $patch) && is_bool($patch[$camel])) { + $this->setNotificationPref($uid, $houseId, $dbKey, $patch[$camel]); + } + } + } + private function normalizeFolder(string $folder): string { $trimmed = trim($folder); if ($trimmed === '') { diff --git a/openapi.json b/openapi.json index 62c84ba..3bdd77b 100644 --- a/openapi.json +++ b/openapi.json @@ -103,27 +103,54 @@ } } }, - "ImageFolder": { + "HousePrefs": { "type": "object", "required": [ - "folder" + "imageFolder", + "photoSort", + "photoFoldersFirst", + "noteSort", + "checklistItemSort", + "notifyPhoto", + "notifyNoteCreate", + "notifyNoteEdit", + "notifyItemAdd", + "notifyItemRecur", + "notifyItemDone" ], "properties": { - "folder": { + "imageFolder": { "type": "string" - } - } - }, - "LastHouse": { - "type": "object", - "required": [ - "houseId" - ], - "properties": { - "houseId": { - "type": "integer", - "format": "int64", - "nullable": true + }, + "photoSort": { + "type": "string" + }, + "photoFoldersFirst": { + "type": "boolean" + }, + "noteSort": { + "type": "string" + }, + "checklistItemSort": { + "type": "string" + }, + "notifyPhoto": { + "type": "boolean" + }, + "notifyNoteCreate": { + "type": "boolean" + }, + "notifyNoteEdit": { + "type": "boolean" + }, + "notifyItemAdd": { + "type": "boolean" + }, + "notifyItemRecur": { + "type": "boolean" + }, + "notifyItemDone": { + "type": "boolean" } } }, @@ -344,37 +371,6 @@ } } }, - "NotificationPrefs": { - "type": "object", - "required": [ - "notifyPhoto", - "notifyNoteCreate", - "notifyNoteEdit", - "notifyItemAdd", - "notifyItemRecur", - "notifyItemDone" - ], - "properties": { - "notifyPhoto": { - "type": "boolean" - }, - "notifyNoteCreate": { - "type": "boolean" - }, - "notifyNoteEdit": { - "type": "boolean" - }, - "notifyItemAdd": { - "type": "boolean" - }, - "notifyItemRecur": { - "type": "boolean" - }, - "notifyItemDone": { - "type": "boolean" - } - } - }, "OCSMeta": { "type": "object", "required": [ @@ -500,6 +496,24 @@ ] } } + }, + "UserPrefs": { + "type": "object", + "required": [ + "lastHouseId", + "firstDayOfWeek" + ], + "properties": { + "lastHouseId": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "firstDayOfWeek": { + "type": "integer", + "format": "int64" + } + } } } }, @@ -6063,10 +6077,10 @@ } } }, - "/ocs/v2.php/apps/pantry/api/prefs/last-house": { + "/ocs/v2.php/apps/pantry/api/prefs": { "get": { - "operationId": "prefs-get-last-house", - "summary": "Get the current user's last-used house id", + "operationId": "prefs-get-user-prefs", + "summary": "Get all user-level preferences (not scoped to a house)", "tags": [ "prefs" ], @@ -6092,7 +6106,7 @@ ], "responses": { "200": { - "description": "Last house returned", + "description": "Prefs returned", "content": { "application/json": { "schema": { @@ -6112,7 +6126,7 @@ "$ref": "#/components/schemas/OCSMeta" }, "data": { - "$ref": "#/components/schemas/LastHouse" + "$ref": "#/components/schemas/UserPrefs" } } } @@ -6152,8 +6166,8 @@ } }, "put": { - "operationId": "prefs-set-last-house", - "summary": "Set the current user's last-used house id", + "operationId": "prefs-set-user-prefs", + "summary": "Update user-level preferences", "tags": [ "prefs" ], @@ -6172,12 +6186,12 @@ "schema": { "type": "object", "properties": { - "houseId": { + "lastHouseId": { "type": "integer", "format": "int64", "nullable": true, "default": null, - "description": "House id, or null to clear." + "description": "Last-used house id, or null to clear." } } } @@ -6198,7 +6212,7 @@ ], "responses": { "200": { - "description": "Last house updated", + "description": "Prefs updated", "content": { "application/json": { "schema": { @@ -6218,7 +6232,7 @@ "$ref": "#/components/schemas/OCSMeta" }, "data": { - "$ref": "#/components/schemas/LastHouse" + "$ref": "#/components/schemas/UserPrefs" } } } @@ -6258,931 +6272,10 @@ } } }, - "/ocs/v2.php/apps/pantry/api/houses/{houseId}/prefs/image-folder": { + "/ocs/v2.php/apps/pantry/api/houses/{houseId}/prefs": { "get": { - "operationId": "prefs-get-image-folder", - "summary": "Get the user's preferred image upload folder for a house", - "tags": [ - "prefs" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "houseId", - "in": "path", - "description": "House id.", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "OCS-APIRequest", - "in": "header", - "description": "Required to be true for the API request to pass", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "Folder returned", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": { - "$ref": "#/components/schemas/ImageFolder" - } - } - } - } - } - } - } - }, - "401": { - "description": "Current user is not logged in", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - }, - "put": { - "operationId": "prefs-set-image-folder", - "summary": "Set the user's preferred image upload folder for a house", - "tags": [ - "prefs" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "folder" - ], - "properties": { - "folder": { - "type": "string", - "description": "Absolute path within the user's files." - } - } - } - } - } - }, - "parameters": [ - { - "name": "houseId", - "in": "path", - "description": "House id.", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "OCS-APIRequest", - "in": "header", - "description": "Required to be true for the API request to pass", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "Folder updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": { - "$ref": "#/components/schemas/ImageFolder" - } - } - } - } - } - } - } - }, - "401": { - "description": "Current user is not logged in", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/apps/pantry/api/houses/{houseId}/prefs/photo-sort": { - "get": { - "operationId": "prefs-get-photo-sort", - "summary": "Get photo sort preference for a house", - "tags": [ - "prefs" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "houseId", - "in": "path", - "description": "House id.", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "OCS-APIRequest", - "in": "header", - "description": "Required to be true for the API request to pass", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "Sort preference returned", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": { - "type": "object", - "required": [ - "sort", - "foldersFirst" - ], - "properties": { - "sort": { - "type": "string" - }, - "foldersFirst": { - "type": "boolean" - } - } - } - } - } - } - } - } - } - }, - "401": { - "description": "Current user is not logged in", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - }, - "put": { - "operationId": "prefs-set-photo-sort", - "summary": "Set photo sort preference for a house", - "tags": [ - "prefs" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "sort": { - "type": "string", - "nullable": true, - "default": null, - "description": "Sort mode." - }, - "foldersFirst": { - "type": "boolean", - "nullable": true, - "default": null, - "description": "Whether folders appear first." - } - } - } - } - } - }, - "parameters": [ - { - "name": "houseId", - "in": "path", - "description": "House id.", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "OCS-APIRequest", - "in": "header", - "description": "Required to be true for the API request to pass", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "Sort preference updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": { - "type": "object", - "required": [ - "sort", - "foldersFirst" - ], - "properties": { - "sort": { - "type": "string" - }, - "foldersFirst": { - "type": "boolean" - } - } - } - } - } - } - } - } - } - }, - "401": { - "description": "Current user is not logged in", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/apps/pantry/api/houses/{houseId}/prefs/note-sort": { - "get": { - "operationId": "prefs-get-note-sort", - "summary": "Get note sort preference for a house", - "tags": [ - "prefs" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "houseId", - "in": "path", - "description": "House id.", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "OCS-APIRequest", - "in": "header", - "description": "Required to be true for the API request to pass", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "Sort preference returned", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": { - "type": "object", - "required": [ - "sort" - ], - "properties": { - "sort": { - "type": "string" - } - } - } - } - } - } - } - } - } - }, - "401": { - "description": "Current user is not logged in", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - }, - "put": { - "operationId": "prefs-set-note-sort", - "summary": "Set note sort preference for a house", - "tags": [ - "prefs" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "sort" - ], - "properties": { - "sort": { - "type": "string", - "description": "Sort mode." - } - } - } - } - } - }, - "parameters": [ - { - "name": "houseId", - "in": "path", - "description": "House id.", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "OCS-APIRequest", - "in": "header", - "description": "Required to be true for the API request to pass", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "Sort preference updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": { - "type": "object", - "required": [ - "sort" - ], - "properties": { - "sort": { - "type": "string" - } - } - } - } - } - } - } - } - } - }, - "401": { - "description": "Current user is not logged in", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/apps/pantry/api/houses/{houseId}/prefs/checklist-item-sort": { - "get": { - "operationId": "prefs-get-checklist-item-sort", - "summary": "Get checklist item sort preference for a house", - "tags": [ - "prefs" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "houseId", - "in": "path", - "description": "House id.", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "OCS-APIRequest", - "in": "header", - "description": "Required to be true for the API request to pass", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "Sort preference returned", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": { - "type": "object", - "required": [ - "sort" - ], - "properties": { - "sort": { - "type": "string" - } - } - } - } - } - } - } - } - } - }, - "401": { - "description": "Current user is not logged in", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - }, - "put": { - "operationId": "prefs-set-checklist-item-sort", - "summary": "Set checklist item sort preference for a house", - "tags": [ - "prefs" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "sort" - ], - "properties": { - "sort": { - "type": "string", - "description": "Sort mode." - } - } - } - } - } - }, - "parameters": [ - { - "name": "houseId", - "in": "path", - "description": "House id.", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "OCS-APIRequest", - "in": "header", - "description": "Required to be true for the API request to pass", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "Sort preference updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": { - "type": "object", - "required": [ - "sort" - ], - "properties": { - "sort": { - "type": "string" - } - } - } - } - } - } - } - } - } - }, - "401": { - "description": "Current user is not logged in", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/apps/pantry/api/houses/{houseId}/prefs/notifications": { - "get": { - "operationId": "prefs-get-notification-prefs", - "summary": "Get notification preferences for a house", + "operationId": "prefs-get-house-prefs", + "summary": "Get all per-house preferences for the current user", "tags": [ "prefs" ], @@ -7238,7 +6331,7 @@ "$ref": "#/components/schemas/OCSMeta" }, "data": { - "$ref": "#/components/schemas/NotificationPrefs" + "$ref": "#/components/schemas/HousePrefs" } } } @@ -7278,8 +6371,9 @@ } }, "put": { - "operationId": "prefs-set-notification-prefs", - "summary": "Update notification preferences for a house", + "operationId": "prefs-set-house-prefs", + "summary": "Update per-house preferences for the current user", + "description": "Only the fields present in the request body are updated; omitted fields are left unchanged.", "tags": [ "prefs" ], @@ -7298,6 +6392,36 @@ "schema": { "type": "object", "properties": { + "imageFolder": { + "type": "string", + "nullable": true, + "default": null, + "description": "Image upload folder path." + }, + "photoSort": { + "type": "string", + "nullable": true, + "default": null, + "description": "Photo sort mode." + }, + "photoFoldersFirst": { + "type": "boolean", + "nullable": true, + "default": null, + "description": "Whether folders appear first in photo board." + }, + "noteSort": { + "type": "string", + "nullable": true, + "default": null, + "description": "Note sort mode." + }, + "checklistItemSort": { + "type": "string", + "nullable": true, + "default": null, + "description": "Checklist item sort mode." + }, "notifyPhoto": { "type": "boolean", "nullable": true, @@ -7383,7 +6507,7 @@ "$ref": "#/components/schemas/OCSMeta" }, "data": { - "$ref": "#/components/schemas/NotificationPrefs" + "$ref": "#/components/schemas/HousePrefs" } } } diff --git a/src/api/prefs.ts b/src/api/prefs.ts index 2551a82..0b9d4c9 100644 --- a/src/api/prefs.ts +++ b/src/api/prefs.ts @@ -1,77 +1,44 @@ import { ocs } from '@/axios' +// ----- User-level prefs (not per-house) ----- + +export interface UserPrefs { + lastHouseId: number | null + /** 0 = Sunday, 1 = Monday, …, 6 = Saturday. Read-only from server. */ + firstDayOfWeek: number +} + +export async function getUserPrefs(): Promise { + const resp = await ocs.get('/prefs') + return resp.data ?? { lastHouseId: null, firstDayOfWeek: 1 } +} + +export async function setUserPrefs(patch: Partial): Promise { + const resp = await ocs.put('/prefs', patch) + return resp.data ?? { lastHouseId: null, firstDayOfWeek: 1 } +} + +// Convenience wrappers (used widely, keep the simple API) export async function getLastHouse(): Promise { - const resp = await ocs.get<{ houseId: number | null }>('/prefs/last-house') - return resp.data?.houseId ?? null + const prefs = await getUserPrefs() + return prefs.lastHouseId } export async function setLastHouse(houseId: number | null): Promise { - await ocs.put('/prefs/last-house', { houseId }) + await setUserPrefs({ lastHouseId: houseId }) } -export async function getImageFolder(houseId: number): Promise { - const resp = await ocs.get<{ folder: string }>(`/houses/${houseId}/prefs/image-folder`) - return resp.data?.folder ?? '/Pantry' -} - -export async function setImageFolder(houseId: number, folder: string): Promise { - const resp = await ocs.put<{ folder: string }>(`/houses/${houseId}/prefs/image-folder`, { - folder, - }) - return resp.data?.folder ?? folder -} +// ----- Per-house prefs ----- export type PhotoSort = 'custom' | 'newest' | 'oldest' | 'description_asc' | 'description_desc' export type NoteSort = 'custom' | 'newest' | 'oldest' | 'title_asc' | 'title_desc' +export type ChecklistItemSort = 'custom' | 'newest' | 'oldest' | 'name_asc' | 'name_desc' export interface PhotoSortPrefs { sort: PhotoSort foldersFirst: boolean } -export async function getPhotoSort(houseId: number): Promise { - const resp = await ocs.get(`/houses/${houseId}/prefs/photo-sort`) - return resp.data ?? { sort: 'custom', foldersFirst: true } -} - -export async function setPhotoSort( - houseId: number, - prefs: Partial, -): Promise { - const resp = await ocs.put(`/houses/${houseId}/prefs/photo-sort`, prefs) - return resp.data ?? { sort: 'custom', foldersFirst: true } -} - -export async function getNoteSort(houseId: number): Promise<{ sort: NoteSort }> { - const resp = await ocs.get<{ sort: NoteSort }>(`/houses/${houseId}/prefs/note-sort`) - return resp.data ?? { sort: 'custom' } -} - -export async function setNoteSort(houseId: number, sort: NoteSort): Promise<{ sort: NoteSort }> { - const resp = await ocs.put<{ sort: NoteSort }>(`/houses/${houseId}/prefs/note-sort`, { sort }) - return resp.data ?? { sort } -} - -export type ChecklistItemSort = 'custom' | 'newest' | 'oldest' | 'name_asc' | 'name_desc' - -export async function getChecklistItemSort(houseId: number): Promise<{ sort: ChecklistItemSort }> { - const resp = await ocs.get<{ sort: ChecklistItemSort }>( - `/houses/${houseId}/prefs/checklist-item-sort`, - ) - return resp.data ?? { sort: 'custom' } -} - -export async function setChecklistItemSort( - houseId: number, - sort: ChecklistItemSort, -): Promise<{ sort: ChecklistItemSort }> { - const resp = await ocs.put<{ sort: ChecklistItemSort }>( - `/houses/${houseId}/prefs/checklist-item-sort`, - { sort }, - ) - return resp.data ?? { sort } -} - export interface NotificationPrefs { notifyPhoto: boolean notifyNoteCreate: boolean @@ -81,33 +48,115 @@ export interface NotificationPrefs { notifyItemDone: boolean } +export interface HousePrefs extends NotificationPrefs { + imageFolder: string + photoSort: PhotoSort + photoFoldersFirst: boolean + noteSort: NoteSort + checklistItemSort: ChecklistItemSort +} + +const housePrefsDefaults: HousePrefs = { + imageFolder: '/Pantry', + photoSort: 'custom', + photoFoldersFirst: true, + noteSort: 'custom', + checklistItemSort: 'custom', + notifyPhoto: true, + notifyNoteCreate: true, + notifyNoteEdit: true, + notifyItemAdd: true, + notifyItemRecur: true, + notifyItemDone: true, +} + +export async function getHousePrefs(houseId: number): Promise { + const resp = await ocs.get(`/houses/${houseId}/prefs`) + return { ...housePrefsDefaults, ...resp.data } +} + +export async function setHousePrefs( + houseId: number, + patch: Partial, +): Promise { + const resp = await ocs.put(`/houses/${houseId}/prefs`, patch) + return { ...housePrefsDefaults, ...resp.data } +} + +// ----- Convenience wrappers for individual prefs ----- + +export async function getPhotoSort(houseId: number): Promise { + const p = await getHousePrefs(houseId) + return { sort: p.photoSort, foldersFirst: p.photoFoldersFirst } +} + +export async function setPhotoSort( + houseId: number, + prefs: Partial, +): Promise { + const patch: Partial = {} + if (prefs.sort !== undefined) patch.photoSort = prefs.sort + if (prefs.foldersFirst !== undefined) patch.photoFoldersFirst = prefs.foldersFirst + const p = await setHousePrefs(houseId, patch) + return { sort: p.photoSort, foldersFirst: p.photoFoldersFirst } +} + +export async function getNoteSort(houseId: number): Promise<{ sort: NoteSort }> { + const p = await getHousePrefs(houseId) + return { sort: p.noteSort } +} + +export async function setNoteSort(houseId: number, sort: NoteSort): Promise<{ sort: NoteSort }> { + const p = await setHousePrefs(houseId, { noteSort: sort }) + return { sort: p.noteSort } +} + +export async function getChecklistItemSort(houseId: number): Promise<{ sort: ChecklistItemSort }> { + const p = await getHousePrefs(houseId) + return { sort: p.checklistItemSort } +} + +export async function setChecklistItemSort( + houseId: number, + sort: ChecklistItemSort, +): Promise<{ sort: ChecklistItemSort }> { + const p = await setHousePrefs(houseId, { checklistItemSort: sort }) + return { sort: p.checklistItemSort } +} + +export async function getImageFolder(houseId: number): Promise { + const p = await getHousePrefs(houseId) + return p.imageFolder +} + +export async function setImageFolder(houseId: number, folder: string): Promise { + const p = await setHousePrefs(houseId, { imageFolder: folder }) + return p.imageFolder +} + export async function getNotificationPrefs(houseId: number): Promise { - const resp = await ocs.get(`/houses/${houseId}/prefs/notifications`) - return ( - resp.data ?? { - notifyPhoto: true, - notifyNoteCreate: true, - notifyNoteEdit: true, - notifyItemAdd: true, - notifyItemRecur: true, - notifyItemDone: true, - } - ) + const p = await getHousePrefs(houseId) + return { + notifyPhoto: p.notifyPhoto, + notifyNoteCreate: p.notifyNoteCreate, + notifyNoteEdit: p.notifyNoteEdit, + notifyItemAdd: p.notifyItemAdd, + notifyItemRecur: p.notifyItemRecur, + notifyItemDone: p.notifyItemDone, + } } export async function setNotificationPrefs( houseId: number, prefs: Partial, ): Promise { - const resp = await ocs.put(`/houses/${houseId}/prefs/notifications`, prefs) - return ( - resp.data ?? { - notifyPhoto: true, - notifyNoteCreate: true, - notifyNoteEdit: true, - notifyItemAdd: true, - notifyItemRecur: true, - notifyItemDone: true, - } - ) + const p = await setHousePrefs(houseId, prefs) + return { + notifyPhoto: p.notifyPhoto, + notifyNoteCreate: p.notifyNoteCreate, + notifyNoteEdit: p.notifyNoteEdit, + notifyItemAdd: p.notifyItemAdd, + notifyItemRecur: p.notifyItemRecur, + notifyItemDone: p.notifyItemDone, + } } diff --git a/tests/unit/Service/PrefsServiceTest.php b/tests/unit/Service/PrefsServiceTest.php index 03a3123..aa1c2ba 100644 --- a/tests/unit/Service/PrefsServiceTest.php +++ b/tests/unit/Service/PrefsServiceTest.php @@ -10,17 +10,22 @@ namespace OCA\Pantry\Tests\Unit\Service; use OCA\Pantry\AppInfo\Application; use OCA\Pantry\Service\PrefsService; use OCP\IConfig; +use OCP\IL10N; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class PrefsServiceTest extends TestCase { /** @var IConfig&MockObject */ private IConfig $config; + /** @var IL10N&MockObject */ + private IL10N $l; private PrefsService $svc; protected function setUp(): void { $this->config = $this->createMock(IConfig::class); - $this->svc = new PrefsService($this->config); + $this->l = $this->createMock(IL10N::class); + $this->l->method('l')->willReturn('1'); // Monday fallback + $this->svc = new PrefsService($this->config, $this->l); } // ----- Notification preferences -----