diff --git a/README.md b/README.md index 8bae66aef..306f7cac6 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ export function App() { ``` @@ -215,7 +215,7 @@ or place the following in the `` tag: ```html ``` diff --git a/package-lock.json b/package-lock.json index 2e5a25bdf..572ed212e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@seamapi/react", - "version": "4.3.1", + "version": "4.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@seamapi/react", - "version": "4.3.1", + "version": "4.5.0", "license": "MIT", "dependencies": { - "@seamapi/http": "^1.14.0", + "@seamapi/http": "^1.20.0", "@tanstack/react-query": "^5.27.5", "classnames": "^2.3.2", "luxon": "^3.3.0", @@ -26,7 +26,7 @@ "@rxfork/r2wc-react-to-web-component": "^2.4.0", "@seamapi/fake-devicedb": "^1.6.1", "@seamapi/fake-seam-connect": "^1.69.1", - "@seamapi/types": "^1.292.2", + "@seamapi/types": "^1.344.3", "@storybook/addon-designs": "^7.0.1", "@storybook/addon-essentials": "^7.0.2", "@storybook/addon-links": "^7.0.2", @@ -83,7 +83,7 @@ "npm": ">= 9.0.0" }, "peerDependencies": { - "@seamapi/types": "^1.26.2", + "@seamapi/types": "^1.344.3", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "react": "^18.0.0", @@ -5863,9 +5863,9 @@ } }, "node_modules/@seamapi/http": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@seamapi/http/-/http-1.14.0.tgz", - "integrity": "sha512-/b1tGZL5aYmoegnvw+H3OSEOwQjawbDHW8BTHZiZkCY8B0k/AhWSPIPXP0AHqLc3278UECFrv1UeOsLPlGrXkQ==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@seamapi/http/-/http-1.20.0.tgz", + "integrity": "sha512-5FP9yT4dJUQEbcUdcu3aUE6LbFyl6ZY0Xk9rtyl4rIwfmvTEy4b21dtzMxIjzBNR7zQue7NnPEizKjYlcANv/w==", "license": "MIT", "dependencies": { "@seamapi/url-search-params-serializer": "^1.1.0", @@ -5878,7 +5878,7 @@ "npm": ">= 9.0.0" }, "peerDependencies": { - "@seamapi/types": "^1.292.2" + "@seamapi/types": "^1.343.0" }, "peerDependenciesMeta": { "@seamapi/types": { @@ -5887,9 +5887,9 @@ } }, "node_modules/@seamapi/types": { - "version": "1.294.0", - "resolved": "https://registry.npmjs.org/@seamapi/types/-/types-1.294.0.tgz", - "integrity": "sha512-NcZemUYBNECpMXJnBgteRwPrri0jZSUPKPJTdeOM7X0ba3kWpuj3nvA5JESsLArFB7Tv7kUAVngIvY2vWrc7aw==", + "version": "1.344.3", + "resolved": "https://registry.npmjs.org/@seamapi/types/-/types-1.344.3.tgz", + "integrity": "sha512-YUw1MStsMwQU63H7S0qOQe3wJLt6ArRZQNlxxpIiz8gb+NtefZEmVdvEv3MaRT2YTfL6g6Lkie7s6Td1AB2YRg==", "devOptional": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 22ac0dc99..8dfcf3e24 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@seamapi/react", - "version": "4.3.1", + "version": "4.5.0", "description": "Seam Components.", "type": "module", "main": "index.js", @@ -109,7 +109,7 @@ "npm": ">= 9.0.0" }, "peerDependencies": { - "@seamapi/types": "^1.26.2", + "@seamapi/types": "^1.344.3", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "react": "^18.0.0", @@ -127,7 +127,7 @@ } }, "dependencies": { - "@seamapi/http": "^1.14.0", + "@seamapi/http": "^1.20.0", "@tanstack/react-query": "^5.27.5", "classnames": "^2.3.2", "luxon": "^3.3.0", @@ -144,7 +144,7 @@ "@rxfork/r2wc-react-to-web-component": "^2.4.0", "@seamapi/fake-devicedb": "^1.6.1", "@seamapi/fake-seam-connect": "^1.69.1", - "@seamapi/types": "^1.292.2", + "@seamapi/types": "^1.344.3", "@storybook/addon-designs": "^7.0.1", "@storybook/addon-essentials": "^7.0.2", "@storybook/addon-links": "^7.0.2", diff --git a/src/lib/seam/components/AccessCodeDetails/AccessCodeDevice.tsx b/src/lib/seam/components/AccessCodeDetails/AccessCodeDevice.tsx index eeb8e9425..db2330025 100644 --- a/src/lib/seam/components/AccessCodeDetails/AccessCodeDevice.tsx +++ b/src/lib/seam/components/AccessCodeDetails/AccessCodeDevice.tsx @@ -1,8 +1,11 @@ +import { useState } from 'react' + import { useDevice } from 'lib/seam/devices/use-device.js' import { isLockDevice, type LockDevice } from 'lib/seam/locks/lock-device.js' import { useToggleLock } from 'lib/seam/locks/use-toggle-lock.js' import { Button } from 'lib/ui/Button.js' import { DeviceImage } from 'lib/ui/device/DeviceImage.js' +import { Snackbar, type SnackbarVariant } from 'lib/ui/Snackbar/Snackbar.js' import { TextButton } from 'lib/ui/TextButton.js' export function AccessCodeDevice({ @@ -49,35 +52,64 @@ function Content(props: { onSelectDevice: (deviceId: string) => void }): JSX.Element { const { device, disableLockUnlock, onSelectDevice } = props - const toggleLock = useToggleLock() + const [snackbarVisible, setSnackbarVisible] = useState(false) + const [snackbarVariant, setSnackbarVariant] = + useState('success') + + const toggleLock = useToggleLock({ + onSuccess: () => { + setSnackbarVisible(true) + setSnackbarVariant('success') + }, + onError: () => { + setSnackbarVisible(true) + setSnackbarVariant('error') + }, + }) const toggleLockLabel = device.properties.locked ? t.unlock : t.lock return ( -
-
- -
-
-
{device.properties.name}
- { - onSelectDevice(device.device_id) - }} - > - {t.deviceDetails} - + <> + { + setSnackbarVisible(false) + }} + message={ + snackbarVariant === 'success' + ? t.successfullyUpdated + : t.failedToUpdate + } + autoDismiss + /> + +
+
+ +
+
+
{device.properties.name}
+ { + onSelectDevice(device.device_id) + }} + > + {t.deviceDetails} + +
+ {!disableLockUnlock && device.properties.online && ( + + )}
- {!disableLockUnlock && device.properties.online && ( - - )} -
+ ) } @@ -85,4 +117,6 @@ const t = { deviceDetails: 'Device details', unlock: 'Unlock', lock: 'Lock', + successfullyUpdated: 'Lock status has been successfully updated', + failedToUpdate: 'Failed to update lock status', } diff --git a/src/lib/seam/components/DeviceDetails/LockDeviceDetails.tsx b/src/lib/seam/components/DeviceDetails/LockDeviceDetails.tsx index 9e19e47d0..cfc741a6c 100644 --- a/src/lib/seam/components/DeviceDetails/LockDeviceDetails.tsx +++ b/src/lib/seam/components/DeviceDetails/LockDeviceDetails.tsx @@ -1,4 +1,5 @@ import classNames from 'classnames' +import { useState } from 'react' import { ChevronRightIcon } from 'lib/icons/ChevronRight.js' import { useAccessCodes } from 'lib/seam/access-codes/use-access-codes.js' @@ -16,6 +17,7 @@ import { DeviceImage } from 'lib/ui/device/DeviceImage.js' import { EditableDeviceName } from 'lib/ui/device/EditableDeviceName.js' import { OnlineStatus } from 'lib/ui/device/OnlineStatus.js' import { ContentHeader } from 'lib/ui/layout/ContentHeader.js' +import { Snackbar, type SnackbarVariant } from 'lib/ui/Snackbar/Snackbar.js' import { useToggle } from 'lib/ui/use-toggle.js' interface LockDeviceDetailsProps extends NestedSpecificDeviceDetailsProps { @@ -38,11 +40,25 @@ export function LockDeviceDetails({ onEditName, }: LockDeviceDetailsProps): JSX.Element | null { const [accessCodesOpen, toggleAccessCodesOpen] = useToggle() - const toggleLock = useToggleLock() const { accessCodes } = useAccessCodes({ device_id: device.device_id, }) + const [snackbarVisible, setSnackbarVisible] = useState(false) + const [snackbarVariant, setSnackbarVariant] = + useState('success') + + const toggleLock = useToggleLock({ + onSuccess: () => { + setSnackbarVisible(true) + setSnackbarVariant('success') + }, + onError: () => { + setSnackbarVisible(true) + setSnackbarVariant('error') + }, + }) + const lockStatus = device.properties.locked ? t.locked : t.unlocked const toggleLockLabel = device.properties.locked ? t.unlock : t.lock @@ -88,87 +104,103 @@ export function LockDeviceDetails({ ] return ( -
- -
-
-
-
- -
-
- {t.device} - -
- {t.status}:{' '} - - {device.properties.online && ( - <> - {t.power}:{' '} - - - )} - + <> + { + setSnackbarVisible(false) + }} + message={ + snackbarVariant === 'success' + ? t.successfullyUpdated + : t.failedToUpdate + } + autoDismiss + /> + +
+ +
+
+
+
+ +
+
+ {t.device} + +
+ {t.status}:{' '} + + {device.properties.online && ( + <> + {t.power}:{' '} + + + )} + +
+
- -
-
-
- - {accessCodeCount} {t.accessCodes} - - +
+
+ + {accessCodeCount} {t.accessCodes} + + +
-
-
- {device.properties.locked && device.properties.online && ( -
-
- {t.lockStatus} - {lockStatus} -
-
- {!disableLockUnlock && - device.capabilities_supported.includes('lock') && ( - - )} +
+ {device.properties.locked && device.properties.online && ( +
+
+ {t.lockStatus} + {lockStatus} +
+
+ {!disableLockUnlock && + device.capabilities_supported.includes('lock') && ( + + )} +
-
- )} + )} - +
+
-
-
+ ) } @@ -208,4 +240,6 @@ const t = { lockStatus: 'Lock status', status: 'Status', power: 'Power', + successfullyUpdated: 'Lock status has been successfully updated', + failedToUpdate: 'Failed to update lock status', } diff --git a/src/lib/seam/locks/use-toggle-lock.ts b/src/lib/seam/locks/use-toggle-lock.ts index 502c44e7a..8c4a87b15 100644 --- a/src/lib/seam/locks/use-toggle-lock.ts +++ b/src/lib/seam/locks/use-toggle-lock.ts @@ -12,8 +12,6 @@ import { import { NullSeamClientError, useSeamClient } from 'lib/seam/use-seam-client.js' -export type UseToggleLockParams = never - export type UseToggleLockData = undefined export type UseToggleLockMutationVariables = Pick & { @@ -29,7 +27,14 @@ type MutationError = | SeamActionAttemptFailedError | SeamActionAttemptTimeoutError -export function useToggleLock(): UseMutationResult< +interface UseToggleLockParams { + onError?: () => void + onSuccess?: () => void +} + +export function useToggleLock( + params: UseToggleLockParams = {} +): UseMutationResult< UseToggleLockData, MutationError, UseToggleLockMutationVariables @@ -92,6 +97,8 @@ export function useToggleLock(): UseMutationResult< ) }, onError: async (_error, variables) => { + params.onError?.() + await queryClient.invalidateQueries({ queryKey: ['devices', 'list'], }) @@ -99,5 +106,8 @@ export function useToggleLock(): UseMutationResult< queryKey: ['devices', 'get', { device_id: variables.device_id }], }) }, + onSuccess() { + params.onSuccess?.() + }, }) } diff --git a/src/lib/ui/Snackbar/Snackbar.tsx b/src/lib/ui/Snackbar/Snackbar.tsx index a9f7a339b..66a49201b 100644 --- a/src/lib/ui/Snackbar/Snackbar.tsx +++ b/src/lib/ui/Snackbar/Snackbar.tsx @@ -5,9 +5,9 @@ import { CheckGreenIcon } from 'lib/icons/CheckGreen.js' import { CloseWhiteIcon } from 'lib/icons/CloseWhite.js' import { ExclamationCircleIcon } from 'lib/icons/ExclamationCircle.js' -type SnackbarVariant = 'success' | 'error' +export type SnackbarVariant = 'success' | 'error' -interface SnackbarProps { +export interface SnackbarProps { message: string variant: SnackbarVariant visible: boolean