diff --git a/lib/Controller/PrefsController.php b/lib/Controller/PrefsController.php index 2bdb35c..eee687e 100644 --- a/lib/Controller/PrefsController.php +++ b/lib/Controller/PrefsController.php @@ -66,7 +66,11 @@ final class PrefsController extends OCSController { /** * Update user-level preferences * + * Only the fields present in the request body are updated; omitted fields + * are left unchanged. + * * @param int|null $lastHouseId Last-used house id, or null to clear. + * @param bool|null $tapRowToComplete Whether clicking anywhere on a checklist row marks it done. * * @return DataResponse * @@ -74,16 +78,16 @@ final class PrefsController extends OCSController { */ #[ApiRoute(verb: 'PUT', url: '/api/prefs')] #[NoAdminRequired] - public function setUserPrefs(?int $lastHouseId = null): DataResponse { - return $this->runAction(function () use ($lastHouseId): DataResponse { + public function setUserPrefs(?int $lastHouseId = null, ?bool $tapRowToComplete = null): DataResponse { + return $this->runAction(function () use ($lastHouseId, $tapRowToComplete): DataResponse { $uid = $this->requireUid(); $patch = []; if ($lastHouseId !== null) { $this->auth->requireMember($lastHouseId, $uid); $patch['lastHouseId'] = $lastHouseId; - } else { - // Explicit null means clear - $patch['lastHouseId'] = null; + } + if ($tapRowToComplete !== null) { + $patch['tapRowToComplete'] = $tapRowToComplete; } $this->prefs->setUserPrefs($uid, $patch); return new DataResponse($this->prefs->getAllUserPrefs($uid)); diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 212eb41..ea3a291 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -84,6 +84,7 @@ namespace OCA\Pantry; * @psalm-type PantryUserPrefs = array{ * lastHouseId: int|null, * firstDayOfWeek: int, + * tapRowToComplete: bool, * } * * @psalm-type PantryHousePrefs = array{ diff --git a/lib/Service/PrefsService.php b/lib/Service/PrefsService.php index 32a4b99..304fa16 100644 --- a/lib/Service/PrefsService.php +++ b/lib/Service/PrefsService.php @@ -14,6 +14,7 @@ use OCP\IL10N; class PrefsService { private const KEY_LAST_HOUSE = 'last_house_id'; private const KEY_IMAGE_FOLDER = 'image_folder'; + private const KEY_TAP_ROW_TO_COMPLETE = 'tap_row_to_complete'; public const DEFAULT_IMAGE_FOLDER = '/Pantry'; public function __construct( @@ -62,6 +63,25 @@ class PrefsService { return $normalized; } + public function getTapRowToComplete(string $uid): bool { + // Off by default — taps only register on the checkbox itself. + return $this->config->getUserValue( + $uid, + Application::APP_ID, + self::KEY_TAP_ROW_TO_COMPLETE, + '0', + ) === '1'; + } + + public function setTapRowToComplete(string $uid, bool $value): void { + $this->config->setUserValue( + $uid, + Application::APP_ID, + self::KEY_TAP_ROW_TO_COMPLETE, + $value ? '1' : '0', + ); + } + // ----- Unified user prefs ----- /** @@ -71,6 +91,7 @@ class PrefsService { return [ 'lastHouseId' => $this->getLastHouseId($uid), 'firstDayOfWeek' => $this->getFirstDayOfWeek($uid), + 'tapRowToComplete' => $this->getTapRowToComplete($uid), ]; } @@ -82,6 +103,9 @@ class PrefsService { $v = $patch['lastHouseId']; $this->setLastHouseId($uid, is_int($v) ? $v : null); } + if (array_key_exists('tapRowToComplete', $patch) && is_bool($patch['tapRowToComplete'])) { + $this->setTapRowToComplete($uid, $patch['tapRowToComplete']); + } } // ----- Sort preferences ----- diff --git a/openapi.json b/openapi.json index 0683f0c..6e545b6 100644 --- a/openapi.json +++ b/openapi.json @@ -658,7 +658,8 @@ "type": "object", "required": [ "lastHouseId", - "firstDayOfWeek" + "firstDayOfWeek", + "tapRowToComplete" ], "properties": { "lastHouseId": { @@ -669,6 +670,9 @@ "firstDayOfWeek": { "type": "integer", "format": "int64" + }, + "tapRowToComplete": { + "type": "boolean" } } } @@ -6952,6 +6956,7 @@ "put": { "operationId": "prefs-set-user-prefs", "summary": "Update user-level preferences", + "description": "Only the fields present in the request body are updated; omitted fields are left unchanged.", "tags": [ "prefs" ], @@ -6976,6 +6981,12 @@ "nullable": true, "default": null, "description": "Last-used house id, or null to clear." + }, + "tapRowToComplete": { + "type": "boolean", + "nullable": true, + "default": null, + "description": "Whether clicking anywhere on a checklist row marks it done." } } } diff --git a/src/api/prefs.ts b/src/api/prefs.ts index ff13e4a..e5382d9 100644 --- a/src/api/prefs.ts +++ b/src/api/prefs.ts @@ -6,6 +6,14 @@ export interface UserPrefs { lastHouseId: number | null /** 0 = Sunday, 1 = Monday, …, 6 = Saturday. Read-only from server. */ firstDayOfWeek: number + /** When true, clicking anywhere on a checklist row marks the item done. */ + tapRowToComplete: boolean +} + +const userPrefsDefaults: UserPrefs = { + lastHouseId: null, + firstDayOfWeek: 1, + tapRowToComplete: false, } let userPrefsInflight: Promise | null = null @@ -15,7 +23,7 @@ export function getUserPrefs(): Promise { userPrefsInflight = ocs .get('/prefs') - .then((resp) => resp.data ?? { lastHouseId: null, firstDayOfWeek: 1 }) + .then((resp) => ({ ...userPrefsDefaults, ...resp.data })) .finally(() => { userPrefsInflight = null }) @@ -25,7 +33,7 @@ export function getUserPrefs(): Promise { export async function setUserPrefs(patch: Partial): Promise { const resp = await ocs.put('/prefs', patch) - return resp.data ?? { lastHouseId: null, firstDayOfWeek: 1 } + return { ...userPrefsDefaults, ...resp.data } } // Convenience wrappers (used widely, keep the simple API) @@ -38,6 +46,16 @@ export async function setLastHouse(houseId: number | null): Promise { await setUserPrefs({ lastHouseId: houseId }) } +export async function getTapRowToComplete(): Promise { + const prefs = await getUserPrefs() + return prefs.tapRowToComplete +} + +export async function setTapRowToComplete(value: boolean): Promise { + const prefs = await setUserPrefs({ tapRowToComplete: value }) + return prefs.tapRowToComplete +} + // ----- Per-house prefs ----- export type PhotoSort = 'custom' | 'newest' | 'oldest' | 'description_asc' | 'description_desc' diff --git a/src/components/AccountSettingsDialog/AccountSettingsDialog.test.ts b/src/components/AccountSettingsDialog/AccountSettingsDialog.test.ts index 041079a..911754b 100644 --- a/src/components/AccountSettingsDialog/AccountSettingsDialog.test.ts +++ b/src/components/AccountSettingsDialog/AccountSettingsDialog.test.ts @@ -11,6 +11,8 @@ vi.mock('@/api/prefs', () => ({ setImageFolder: vi.fn(), getNotificationPrefs: vi.fn(), setNotificationPrefs: vi.fn(), + getTapRowToComplete: vi.fn(), + setTapRowToComplete: vi.fn(), })) // Mock Nextcloud Vue components that pull in CSS @@ -24,7 +26,7 @@ vi.mock('@nextcloud/vue/components/NcAppSettingsDialog', () => ({ vi.mock('@nextcloud/vue/components/NcAppSettingsSection', () => ({ default: { name: 'NcAppSettingsSection', - template: '
', + template: '
', props: ['id', 'name'], }, })) @@ -66,7 +68,7 @@ const NcAppSettingsDialogStub = { } const NcAppSettingsSectionStub = { - template: '
', + template: '
', props: ['id', 'name'], } @@ -225,7 +227,8 @@ describe('AccountSettingsDialog', () => { const wrapper = mountComponent({ open: true, houseId: 1 }) await flushPromises() - const checkboxes = wrapper.findAll('.nc-checkbox') + const notifSection = wrapper.find('#pantry-notifications') + const checkboxes = notifSection.findAll('.nc-checkbox') expect(checkboxes).toHaveLength(6) }) @@ -242,7 +245,8 @@ describe('AccountSettingsDialog', () => { const wrapper = mountComponent({ open: true, houseId: 3 }) await flushPromises() - const checkbox = wrapper.find('.nc-checkbox input') + const notifSection = wrapper.find('#pantry-notifications') + const checkbox = notifSection.find('.nc-checkbox input') await checkbox.setValue(false) await flushPromises() diff --git a/src/components/AccountSettingsDialog/AccountSettingsDialog.vue b/src/components/AccountSettingsDialog/AccountSettingsDialog.vue index a81cf3e..1c164f7 100644 --- a/src/components/AccountSettingsDialog/AccountSettingsDialog.vue +++ b/src/components/AccountSettingsDialog/AccountSettingsDialog.vue @@ -31,6 +31,20 @@ + + + +