From af88e0f56c8a0a00ed1b87d19a03eac2507d2148 Mon Sep 17 00:00:00 2001 From: William Wong Date: Mon, 16 Oct 2023 08:03:36 +0000 Subject: [PATCH 01/34] Fix types of RovingTabIndex --- .../RovingTabIndex/RovingTabIndexComposer.tsx | 39 +++++++++---------- .../RovingTabIndex/private/Context.ts | 2 +- .../providers/RovingTabIndex/useItemRef.ts | 4 +- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/packages/component/src/providers/RovingTabIndex/RovingTabIndexComposer.tsx b/packages/component/src/providers/RovingTabIndex/RovingTabIndexComposer.tsx index 28c06db473..56d41d625b 100644 --- a/packages/component/src/providers/RovingTabIndex/RovingTabIndexComposer.tsx +++ b/packages/component/src/providers/RovingTabIndex/RovingTabIndexComposer.tsx @@ -5,17 +5,21 @@ import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import RovingTabIndexContext from './private/Context'; -import type { FC, MutableRefObject, PropsWithChildren } from 'react'; +import type { MutableRefObject, PropsWithChildren } from 'react'; import type { RovingTabIndexContextType } from './private/Context'; -type ItemRef = MutableRefObject; +type ItemRef = MutableRefObject; type RovingTabIndexContextProps = PropsWithChildren<{ onEscapeKey?: () => void; orientation?: 'horizontal' | 'vertical'; }>; -const RovingTabIndexComposer: FC = ({ children, onEscapeKey, orientation }) => { +function isDisabled(element: HTMLElement) { + return element.ariaDisabled === 'true' || element.hasAttribute('disabled'); +} + +const RovingTabIndexComposer = ({ children, onEscapeKey, orientation }: RovingTabIndexContextProps) => { const activeItemIndexRef = useRef(0); const itemRefsRef = useRef([]); @@ -23,7 +27,7 @@ const RovingTabIndexComposer: FC = ({ children, onEs const { current: activeItemIndex } = activeItemIndexRef; itemRefsRef.current.forEach(({ current }, index) => { - current?.setAttribute('tabindex', activeItemIndex === index ? '0' : '-1'); + current?.setAttribute('tabindex', activeItemIndex === index && !isDisabled(current) ? '0' : '-1'); }); }, [activeItemIndexRef]); @@ -55,7 +59,7 @@ const RovingTabIndexComposer: FC = ({ children, onEs ); const handleFocus = useCallback( - event => { + (event: Event) => { const { target } = event; const index = itemRefsRef.current.findIndex(({ current }) => current === target); @@ -86,10 +90,10 @@ const RovingTabIndexComposer: FC = ({ children, onEs const nextIndex = itemIndices.indexOf(value) + 1; if (nextIndex >= itemIndices.length) { - return itemIndices[0]; + return itemIndices[0] as number; } - return itemIndices[+nextIndex]; + return itemIndices[+nextIndex] as number; }); break; @@ -109,10 +113,10 @@ const RovingTabIndexComposer: FC = ({ children, onEs const nextIndex = itemIndices.indexOf(value) - 1; if (nextIndex < 0) { - return itemIndices[itemIndices.length - 1]; + return itemIndices[itemIndices.length - 1] as number; } - return itemIndices[+nextIndex]; + return itemIndices[+nextIndex] as number; }); break; @@ -144,20 +148,20 @@ const RovingTabIndexComposer: FC = ({ children, onEs [setActiveItemIndex, onEscapeKey, orientation] ); - const itemEffector = useCallback( + const itemEffector = useCallback( (ref, index) => { const { current } = ref; itemRefsRef.current[+index] = ref; - current.addEventListener('focus', handleFocus); - current.addEventListener('keydown', handleKeyDown); + current?.addEventListener('focus', handleFocus); + current?.addEventListener('keydown', handleKeyDown); - current.setAttribute('tabindex', activeItemIndexRef.current === index ? '0' : '-1'); + current?.setAttribute('tabindex', activeItemIndexRef.current === index && !isDisabled(current) ? '0' : '-1'); return () => { - current.removeEventListener('focus', handleFocus); - current.removeEventListener('keydown', handleKeyDown); + current?.removeEventListener('focus', handleFocus); + current?.removeEventListener('keydown', handleKeyDown); delete itemRefsRef.current[+index]; }; @@ -181,11 +185,6 @@ const RovingTabIndexComposer: FC = ({ children, onEs return {children}; }; -RovingTabIndexComposer.defaultProps = { - onEscapeKey: undefined, - orientation: 'horizontal' -}; - RovingTabIndexComposer.propTypes = { onEscapeKey: PropTypes.func, orientation: PropTypes.oneOf(['horizontal', 'vertical']) diff --git a/packages/component/src/providers/RovingTabIndex/private/Context.ts b/packages/component/src/providers/RovingTabIndex/private/Context.ts index cd117fd15e..4db7839857 100644 --- a/packages/component/src/providers/RovingTabIndex/private/Context.ts +++ b/packages/component/src/providers/RovingTabIndex/private/Context.ts @@ -3,7 +3,7 @@ import { createContext } from 'react'; import type { MutableRefObject } from 'react'; type RovingTabIndexContextType = { - itemEffector: (ref: MutableRefObject, index: number) => () => void; + itemEffector: (ref: MutableRefObject, index: number) => () => void; }; const RovingTabIndexContext = createContext({ diff --git a/packages/component/src/providers/RovingTabIndex/useItemRef.ts b/packages/component/src/providers/RovingTabIndex/useItemRef.ts index 96f6f76e06..05d3efbc27 100644 --- a/packages/component/src/providers/RovingTabIndex/useItemRef.ts +++ b/packages/component/src/providers/RovingTabIndex/useItemRef.ts @@ -3,8 +3,8 @@ import useContext from './private/useContext'; import type { MutableRefObject } from 'react'; -export default function useItemRef(itemIndex: number): MutableRefObject { - const ref = useRef(); +export default function useItemRef(itemIndex: number): MutableRefObject { + const ref = useRef(null); const { itemEffector } = useContext(); From d1f6c29a93bed020c58bb71c6017eacf58178789 Mon Sep 17 00:00:00 2001 From: William Wong Date: Mon, 16 Oct 2023 08:04:42 +0000 Subject: [PATCH 02/34] Add strings for CSAT --- packages/api/src/localization/en-US.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/api/src/localization/en-US.json b/packages/api/src/localization/en-US.json index 940557559b..bbc73c06e2 100644 --- a/packages/api/src/localization/en-US.json +++ b/packages/api/src/localization/en-US.json @@ -66,6 +66,18 @@ "CONNECTIVITY_STATUS_ALT_SLOW_CONNECTION": "Taking longer than usual to connect.", "CONNECTIVITY_STATUS_ALT": "Connectivity Status: $1", "_CONNECTIVITY_STATUS_ALT.comment": "This is for screen reader. $1 will be one of \"CONNECTIVITY_STATUS_ALT_\"*.", + "CSAT_RATING_FEW_ALT": "$1 stars", + "_CSAT_RATING_FEW_ALT.comment": "This is for customer satisfactory UI which shows 5 stars for rating. $1 is the number of stars. This is for plural rule of \"few\".", + "CSAT_RATING_MANY_ALT": "$1 stars", + "_CSAT_RATING_MANY_ALT.comment": "This is for customer satisfactory UI which shows 5 stars for rating. $1 is the number of stars. This is for plural rule of \"many\".", + "CSAT_RATING_ONE_ALT": "$1 star", + "_CSAT_RATING_ONE_ALT.comment": "This is for customer satisfactory UI which shows 5 stars for rating. $1 is the number of stars. This is for plural rule of \"one\".", + "CSAT_RATING_OTHER_ALT": "$1 stars", + "_CSAT_RATING_OTHER_ALT.comment": "This is for customer satisfactory UI which shows 5 stars for rating. $1 is the number of stars. This is for plural rule of \"other\".", + "CSAT_RATING_TWO_ALT": "$1 stars", + "_CSAT_RATING_TWO_ALT.comment": "This is for customer satisfactory UI which shows 5 stars for rating. $1 is the number of stars. This is for plural rule of \"two\".", + "CSAT_SUBMIT_BUTTON_TEXT": "Submit", + "CSAT_SUBMITTED_TEXT": "Submitted", "FILE_CONTENT_ALT": "'$1'", "FILE_CONTENT_DOWNLOADABLE_ALT": "Download file '$1'", "FILE_CONTENT_DOWNLOADABLE_WITH_SIZE_ALT": "Download file '$1' of size $2", From d3063a8c3c8ef27e2243c2f406b25084622cd2e4 Mon Sep 17 00:00:00 2001 From: William Wong Date: Mon, 16 Oct 2023 08:04:55 +0000 Subject: [PATCH 03/34] Use bundler for module resolution --- packages/component/src/tsconfig.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/component/src/tsconfig.json b/packages/component/src/tsconfig.json index a8e108c83c..4b2e272e19 100644 --- a/packages/component/src/tsconfig.json +++ b/packages/component/src/tsconfig.json @@ -7,8 +7,8 @@ "downlevelIteration": true, "emitDeclarationOnly": true, "jsx": "react", - "module": "NodeNext", - "moduleResolution": "NodeNext", + "module": "ESNext", + "moduleResolution": "Bundler", "preserveWatchOutput": true, "pretty": true, "skipLibCheck": true, From 166e8d5399fc08e7c9e698aee1687dfc1017d2bd Mon Sep 17 00:00:00 2001 From: William Wong Date: Mon, 16 Oct 2023 08:49:09 +0000 Subject: [PATCH 04/34] Disable defaultProps --- .eslintrc.react.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc.react.yml b/.eslintrc.react.yml index 7c0a66f1d1..0f48d0a735 100644 --- a/.eslintrc.react.yml +++ b/.eslintrc.react.yml @@ -81,7 +81,7 @@ rules: react/no-will-update-set-state: error react/prefer-es6-class: error react/prefer-read-only-props: error - react/require-default-props: error + react/require-default-props: off # defaultProps is being deprecated react/self-closing-comp: error react/sort-default-props: error react/sort-prop-types: error From 03be8723bbf8b26811252449225c717c06503b8e Mon Sep 17 00:00:00 2001 From: William Wong Date: Mon, 16 Oct 2023 08:51:59 +0000 Subject: [PATCH 05/34] Fix Entity type --- packages/core/src/types/WebChatActivity.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/core/src/types/WebChatActivity.ts b/packages/core/src/types/WebChatActivity.ts index 9bd2cab842..facbc4a65b 100644 --- a/packages/core/src/types/WebChatActivity.ts +++ b/packages/core/src/types/WebChatActivity.ts @@ -109,12 +109,10 @@ type ClientCapabilitiesEntity = { type: 'ClientCapabilities'; }; -type Entity = - | ClientCapabilitiesEntity - | OrgSchemaThing - | AnyAnd<{ type: Exclude }>; +type Entity = ClientCapabilitiesEntity | OrgSchemaThing | { type: string }; // Channel account - https://github.com/Microsoft/botframework-sdk/blob/main/specs/botframework-activity/botframework-activity.md#channel-account + type ChannelAcount = { id: string; name?: string; From 10bf97bfefb6a6e22fc5261edb108cb7fc5e6cc1 Mon Sep 17 00:00:00 2001 From: William Wong Date: Mon, 16 Oct 2023 09:30:32 +0000 Subject: [PATCH 06/34] Fix type of Thing --- .../Attachment/Text/private/LinkDefinitions.tsx | 1 - .../Text/private/MarkdownTextContent.tsx | 14 ++++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/component/src/Attachment/Text/private/LinkDefinitions.tsx b/packages/component/src/Attachment/Text/private/LinkDefinitions.tsx index e571ae4d8b..7a1574a486 100644 --- a/packages/component/src/Attachment/Text/private/LinkDefinitions.tsx +++ b/packages/component/src/Attachment/Text/private/LinkDefinitions.tsx @@ -1,4 +1,3 @@ -// @ts-expect-error TS1479 should be fixed when bumping to typescript@5. import { fromMarkdown } from 'mdast-util-from-markdown'; import { hooks } from 'botframework-webchat-api'; import { onErrorResumeNext } from 'botframework-webchat-core'; diff --git a/packages/component/src/Attachment/Text/private/MarkdownTextContent.tsx b/packages/component/src/Attachment/Text/private/MarkdownTextContent.tsx index 8dede8dc6a..b259baafe3 100644 --- a/packages/component/src/Attachment/Text/private/MarkdownTextContent.tsx +++ b/packages/component/src/Attachment/Text/private/MarkdownTextContent.tsx @@ -4,7 +4,7 @@ import classNames from 'classnames'; import React, { memo, type MouseEventHandler, useCallback, useMemo } from 'react'; import { isClaim, type Claim } from '../../../types/external/OrgSchema/Claim'; -import { isThing } from '../../../types/external/OrgSchema/Thing'; +import { type AsEntity, isThing } from '../../../types/external/OrgSchema/Thing'; import { type PropsOf } from '../../../types/PropsOf'; import { type WebChatActivity } from 'botframework-webchat-core'; import CitationModalContext from './CitationModalContent'; @@ -16,6 +16,8 @@ import useStyleSet from '../../../hooks/useStyleSet'; const { useLocalizer } = hooks; +type ClaimAsEntity = AsEntity; + type Props = Readonly<{ entities?: WebChatActivity['entities']; markdown: string; @@ -58,13 +60,13 @@ const MarkdownTextContent = memo(({ entities, markdown }: Props) => { const handleCitationClick = useCallback['onCitationClick']>( url => { - const claim = entities.find( - (entity): entity is Claim => isThing(entity) && isClaim(entity) && entity['@id'] === url + const claim = entitiesRef.current?.find( + (entity): entity is ClaimAsEntity => isThing(entity) && isClaim(entity) && entity['@id'] === url ); claim && showClaimModal(claim); }, - [entities, showClaimModal] + [entitiesRef, showClaimModal] ); const handleClick = useCallback>( @@ -79,8 +81,8 @@ const MarkdownTextContent = memo(({ entities, markdown }: Props) => { return; } - const claim = entitiesRef.current?.find( - (entity): entity is Claim => isThing(entity) && isClaim(entity) && entity['@id'] === buttonElement.value + const claim = entitiesRef.current?.find( + (entity): entity is ClaimAsEntity => isThing(entity) && isClaim(entity) && entity['@id'] === buttonElement.value ); if (!claim) { From 8164cc88647c5519e57ca229c7e64766efbe563e Mon Sep 17 00:00:00 2001 From: William Wong Date: Mon, 16 Oct 2023 09:55:10 +0000 Subject: [PATCH 07/34] Add CSAT UI --- packages/api/src/localization/yue.json | 7 + packages/component/package-lock.json | 294 ++++++++++-------- packages/component/package.json | 5 +- .../CustomerSatisfactory.tsx | 152 +++++++++ .../CustomerSatisfactoryForScreenReader.tsx | 35 +++ .../customerSatisfactoryMiddleware.tsx | 72 +++++ .../private/Checkmark.tsx | 22 ++ .../private/RatingValue.ts | 9 + .../CustomerSatisfactory/private/Star.tsx | 22 ++ .../CustomerSatisfactory/private/StarBar.tsx | 36 +++ .../private/StarButton.tsx | 55 ++++ .../private/StarFilled.tsx | 22 ++ .../private/schema/exactString.ts | 5 + .../private/schema/reviewActionSchema.ts | 60 ++++ .../private/schema/thing.ts | 21 ++ .../private/useStrings.tsx | 35 +++ .../src/Attachment/createMiddleware.tsx | 2 + .../CustomerSatisfactoryAttachment.ts | 146 +++++++++ .../component/src/Styles/createStyleSet.ts | 2 + .../src/hooks/internal/useOpenURL.ts | 58 ++++ .../external/OrgSchema/ActionStatusType.ts | 6 + .../src/types/external/OrgSchema/Claim.ts | 19 +- .../types/external/OrgSchema/EntryPoint.ts | 21 ++ .../src/types/external/OrgSchema/Project.ts | 15 +- .../OrgSchema/PropertyValueSpecification.ts | 23 ++ .../src/types/external/OrgSchema/Rating.ts | 28 ++ .../types/external/OrgSchema/ReplyAction.ts | 15 +- .../src/types/external/OrgSchema/Review.ts | 23 ++ .../types/external/OrgSchema/ReviewAction.ts | 30 ++ .../src/types/external/OrgSchema/Thing.ts | 56 ++-- .../types/external/OrgSchema/VoteAction.ts | 11 +- 31 files changed, 1112 insertions(+), 195 deletions(-) create mode 100644 packages/component/src/Attachment/CustomerSatisfactory/CustomerSatisfactory.tsx create mode 100644 packages/component/src/Attachment/CustomerSatisfactory/CustomerSatisfactoryForScreenReader.tsx create mode 100644 packages/component/src/Attachment/CustomerSatisfactory/customerSatisfactoryMiddleware.tsx create mode 100644 packages/component/src/Attachment/CustomerSatisfactory/private/Checkmark.tsx create mode 100644 packages/component/src/Attachment/CustomerSatisfactory/private/RatingValue.ts create mode 100644 packages/component/src/Attachment/CustomerSatisfactory/private/Star.tsx create mode 100644 packages/component/src/Attachment/CustomerSatisfactory/private/StarBar.tsx create mode 100644 packages/component/src/Attachment/CustomerSatisfactory/private/StarButton.tsx create mode 100644 packages/component/src/Attachment/CustomerSatisfactory/private/StarFilled.tsx create mode 100644 packages/component/src/Attachment/CustomerSatisfactory/private/schema/exactString.ts create mode 100644 packages/component/src/Attachment/CustomerSatisfactory/private/schema/reviewActionSchema.ts create mode 100644 packages/component/src/Attachment/CustomerSatisfactory/private/schema/thing.ts create mode 100644 packages/component/src/Attachment/CustomerSatisfactory/private/useStrings.tsx create mode 100644 packages/component/src/Styles/StyleSet/CustomerSatisfactoryAttachment.ts create mode 100644 packages/component/src/hooks/internal/useOpenURL.ts create mode 100644 packages/component/src/types/external/OrgSchema/ActionStatusType.ts create mode 100644 packages/component/src/types/external/OrgSchema/EntryPoint.ts create mode 100644 packages/component/src/types/external/OrgSchema/PropertyValueSpecification.ts create mode 100644 packages/component/src/types/external/OrgSchema/Rating.ts create mode 100644 packages/component/src/types/external/OrgSchema/Review.ts create mode 100644 packages/component/src/types/external/OrgSchema/ReviewAction.ts diff --git a/packages/api/src/localization/yue.json b/packages/api/src/localization/yue.json index 5e163c2a75..432986a12a 100644 --- a/packages/api/src/localization/yue.json +++ b/packages/api/src/localization/yue.json @@ -42,6 +42,13 @@ "CONNECTIVITY_STATUS_ALT_RENDER_ERROR": "Render 出事,請睇下 console 或者同 bot 開發人員聯絡。", "CONNECTIVITY_STATUS_ALT_SLOW_CONNECTION": "接駁嘅時間比平時長。", "CONNECTIVITY_STATUS_ALT": "接駁情況:$1", + "CSAT_RATING_FEW_ALT": "$1 粒星", + "CSAT_RATING_MANY_ALT": "$1 粒星", + "CSAT_RATING_ONE_ALT": "一粒星", + "CSAT_RATING_OTHER_ALT": "$1 粒星", + "CSAT_RATING_TWO_ALT": "兩粒星", + "CSAT_SUBMIT_BUTTON_TEXT": "遞交", + "CSAT_SUBMITTED_TEXT": "遞交咗", "FILE_CONTENT_ALT": "'$1'", "FILE_CONTENT_DOWNLOADABLE_ALT": "下載檔案 '$1'", "FILE_CONTENT_DOWNLOADABLE_WITH_SIZE_ALT": "下載檔案 '$1' 檔案大小 $2", diff --git a/packages/component/package-lock.json b/packages/component/package-lock.json index cee3a158b0..88d4f976c3 100644 --- a/packages/component/package-lock.json +++ b/packages/component/package-lock.json @@ -28,7 +28,10 @@ "react-scroll-to-bottom": "4.2.0", "redux": "4.2.1", "simple-update-in": "2.2.0", - "use-ref-from": "0.0.2" + "url-template": "3.1.0", + "use-ref-from": "0.0.2", + "use-state-with-ref": "0.0.1", + "valibot": "0.19.0" }, "devDependencies": { "@babel/cli": "^7.18.10", @@ -54,7 +57,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.1.2.tgz", "integrity": "sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg==", - "devOptional": true, + "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.0" }, @@ -106,7 +109,7 @@ "version": "7.19.1", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.1.tgz", "integrity": "sha512-72a9ghR0gnESIa7jBN53U32FOVCEoztyIlKaNoU05zRhEecduGK9L9c3ww7Mp06JiR+0ls0GBPFJQwwtjn9ksg==", - "devOptional": true, + "dev": true, "engines": { "node": ">=6.9.0" } @@ -115,7 +118,7 @@ "version": "7.19.1", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.1.tgz", "integrity": "sha512-1H8VgqXme4UXCRv7/Wa1bq7RVymKOzC7znjyFM8KiEzwFqcKUKYNoQef4GhdklgNvoBXyW4gYhuBNCM5o1zImw==", - "devOptional": true, + "dev": true, "dependencies": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.18.6", @@ -145,7 +148,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "devOptional": true, + "dev": true, "bin": { "semver": "bin/semver.js" } @@ -154,7 +157,7 @@ "version": "7.19.0", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.0.tgz", "integrity": "sha512-S1ahxf1gZ2dpoiFgA+ohK9DIpz50bJ0CWs7Zlzb54Z4sG8qmdIrGrVqmy1sAtTVRb+9CU6U8VqT9L0Zj7hxHVg==", - "devOptional": true, + "dev": true, "dependencies": { "@babel/types": "^7.19.0", "@jridgewell/gen-mapping": "^0.3.2", @@ -193,7 +196,7 @@ "version": "7.19.1", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.1.tgz", "integrity": "sha512-LlLkkqhCMyz2lkQPvJNdIYU7O5YjWRgC2R4omjCTpZd8u8KMQzZvX4qce+/BluN1rcQiV7BoGUpmQ0LeHerbhg==", - "devOptional": true, + "dev": true, "dependencies": { "@babel/compat-data": "^7.19.1", "@babel/helper-validator-option": "^7.18.6", @@ -211,7 +214,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "devOptional": true, + "dev": true, "bin": { "semver": "bin/semver.js" } @@ -283,7 +286,7 @@ "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", - "devOptional": true, + "dev": true, "engines": { "node": ">=6.9.0" } @@ -304,7 +307,7 @@ "version": "7.19.0", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", - "devOptional": true, + "dev": true, "dependencies": { "@babel/template": "^7.18.10", "@babel/types": "^7.19.0" @@ -317,7 +320,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", - "devOptional": true, + "dev": true, "dependencies": { "@babel/types": "^7.18.6" }, @@ -352,7 +355,7 @@ "version": "7.19.0", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.0.tgz", "integrity": "sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ==", - "devOptional": true, + "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-module-imports": "^7.18.6", @@ -426,7 +429,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz", "integrity": "sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==", - "devOptional": true, + "dev": true, "dependencies": { "@babel/types": "^7.18.6" }, @@ -450,7 +453,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", - "devOptional": true, + "dev": true, "dependencies": { "@babel/types": "^7.18.6" }, @@ -478,7 +481,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", - "devOptional": true, + "dev": true, "engines": { "node": ">=6.9.0" } @@ -502,7 +505,7 @@ "version": "7.19.0", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.19.0.tgz", "integrity": "sha512-DRBCKGwIEdqY3+rPJgG/dKfQy9+08rHIAJx8q2p+HSWP87s2HCrQmaAMMyMll2kIXKCW0cO1RdQskx15Xakftg==", - "devOptional": true, + "dev": true, "dependencies": { "@babel/template": "^7.18.10", "@babel/traverse": "^7.19.0", @@ -529,7 +532,7 @@ "version": "7.19.1", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.1.tgz", "integrity": "sha512-h7RCSorm1DdTVGJf3P2Mhj3kdnkmF/EiysUkzS2TdgAYqyjFdMQJbVuXOBej2SBJaXan/lIVtT6KkGbyyq753A==", - "devOptional": true, + "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -1814,7 +1817,7 @@ "version": "7.18.10", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", - "devOptional": true, + "dev": true, "dependencies": { "@babel/code-frame": "^7.18.6", "@babel/parser": "^7.18.10", @@ -1828,7 +1831,7 @@ "version": "7.19.1", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.1.tgz", "integrity": "sha512-0j/ZfZMxKukDaag2PtOPDbwuELqIar6lLskVPPJDjXMXjfLb1Obo/1yjxIGqqAJrmfaTIY3z2wFLAQ7qSkLsuA==", - "devOptional": true, + "dev": true, "dependencies": { "@babel/code-frame": "^7.18.6", "@babel/generator": "^7.19.0", @@ -2028,7 +2031,7 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "devOptional": true, + "dev": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -2042,7 +2045,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "devOptional": true, + "dev": true, "engines": { "node": ">=6.0.0" } @@ -2051,7 +2054,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "devOptional": true, + "dev": true, "engines": { "node": ">=6.0.0" } @@ -2060,13 +2063,13 @@ "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "devOptional": true + "dev": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.17", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", - "devOptional": true, + "dev": true, "dependencies": { "@jridgewell/resolve-uri": "3.1.0", "@jridgewell/sourcemap-codec": "1.4.14" @@ -2354,7 +2357,7 @@ "version": "4.21.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", - "devOptional": true, + "dev": true, "funding": [ { "type": "opencollective", @@ -2412,7 +2415,7 @@ "version": "1.0.30001412", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001412.tgz", "integrity": "sha512-+TeEIee1gS5bYOiuf+PS/kp2mrXic37Hl66VY6EAfxasIk5fELTktK2oOezYed12H8w7jt3s512PpulQidPjwA==", - "devOptional": true, + "dev": true, "funding": [ { "type": "opencollective", @@ -2669,9 +2672,9 @@ } }, "node_modules/core-js-pure": { - "version": "3.27.2", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.27.2.tgz", - "integrity": "sha512-Cf2jqAbXgWH3VVzjyaaFkY1EBazxugUepGymDoeteyYr9ByX51kD2jdHZlsEF/xnJMyN3Prua7mQuzwMg6Zc9A==", + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.33.0.tgz", + "integrity": "sha512-FKSIDtJnds/YFIEaZ4HszRX7hkxGpNKM7FC9aJ9WLJbSd3lD4vOltFuVIBLR8asSx9frkTSqL0dw90SKQxgKrg==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -2806,7 +2809,7 @@ "version": "1.4.262", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.262.tgz", "integrity": "sha512-Ckn5haqmGh/xS8IbcgK3dnwAVnhDyo/WQnklWn6yaMucYTq7NNxwlGE8ElzEOnonzRLzUCo2Ot3vUb2GYUF2Hw==", - "devOptional": true + "dev": true }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -2837,7 +2840,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "devOptional": true, + "dev": true, "engines": { "node": ">=6" } @@ -2950,7 +2953,7 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "devOptional": true, + "dev": true, "engines": { "node": ">=6.9.0" } @@ -3024,7 +3027,7 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "devOptional": true, + "dev": true, "engines": { "node": ">=4" } @@ -3273,7 +3276,7 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "devOptional": true, + "dev": true, "bin": { "jsesc": "bin/jsesc" }, @@ -3290,7 +3293,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "devOptional": true, + "dev": true, "bin": { "json5": "lib/cli.js" }, @@ -3972,7 +3975,7 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", - "devOptional": true + "dev": true }, "node_modules/normalize-path": { "version": "3.0.0", @@ -4103,7 +4106,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "devOptional": true + "dev": true }, "node_modules/picomatch": { "version": "2.3.0", @@ -4137,18 +4140,6 @@ "react-is": "^16.13.1" } }, - "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react-dictate-button": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/react-dictate-button/-/react-dictate-button-2.0.1.tgz", @@ -4172,19 +4163,6 @@ "react-is": "^16.8.1" } }, - "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - }, - "peerDependencies": { - "react": "^18.2.0" - } - }, "node_modules/react-film": { "version": "3.1.1-main.df870ea", "resolved": "https://registry.npmjs.org/react-film/-/react-film-3.1.1-main.df870ea.tgz", @@ -4496,15 +4474,6 @@ "tslib": "^2.1.0" } }, - "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - } - }, "node_modules/semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", @@ -4740,7 +4709,7 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.9.tgz", "integrity": "sha512-/xsqn21EGVdXI3EXSum1Yckj3ZVZugqyOZQ/CxYPBD/R+ko9NSUScf8tFF4dOKY+2pvSSJA/S+5B8s4Zr4kyvg==", - "devOptional": true, + "dev": true, "funding": [ { "type": "opencollective", @@ -4762,6 +4731,14 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/url-template": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-3.1.0.tgz", + "integrity": "sha512-vB/eHWttzhN+NZzk9FcQB2h1cSEgb7zDYyvyxPhw02LYw7YqIzO+w1AqkcKvZ51gPH8o4+nyiWve/xuQqMdJZw==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/use-ref-from": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/use-ref-from/-/use-ref-from-0.0.2.tgz", @@ -4785,6 +4762,36 @@ "node": ">=6.9.0" } }, + "node_modules/use-state-with-ref": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/use-state-with-ref/-/use-state-with-ref-0.0.1.tgz", + "integrity": "sha512-UsG2p/gnl+wk6dObF0iUNgm3bEByW4ZRFd3ptklKARUzFkMGIQjyAkJrlw99+zyfXtwk1PfVGHKSDsGZ0xpj+A==", + "dependencies": { + "@babel/runtime-corejs3": "^7.22.15", + "use-ref-from": "^0.0.2", + "use-state-with-ref": "^0.0.1" + }, + "peerDependencies": { + "react": ">=16.9.0" + } + }, + "node_modules/use-state-with-ref/node_modules/@babel/runtime-corejs3": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.23.2.tgz", + "integrity": "sha512-54cIh74Z1rp4oIjsHjqN+WM4fMyCBYe+LpZ9jWm51CZ1fbH3SkAzQD/3XLoNkjbJ7YEmjobLXyvQrFypRHOrXw==", + "dependencies": { + "core-js-pure": "^3.30.2", + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/use-state-with-ref/node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -4794,6 +4801,11 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/valibot": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.19.0.tgz", + "integrity": "sha512-vzeuctzVirzoiDN9BTc4GludHaSgFYoy6AnbHOjfBUbX0DibqKuNCr2Ti4ox8eLdBdDSEZSZ3LwXZgjL6aE4RQ==" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4930,7 +4942,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.1.2.tgz", "integrity": "sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg==", - "devOptional": true, + "dev": true, "requires": { "@jridgewell/trace-mapping": "^0.3.0" } @@ -4964,13 +4976,13 @@ "version": "7.19.1", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.1.tgz", "integrity": "sha512-72a9ghR0gnESIa7jBN53U32FOVCEoztyIlKaNoU05zRhEecduGK9L9c3ww7Mp06JiR+0ls0GBPFJQwwtjn9ksg==", - "devOptional": true + "dev": true }, "@babel/core": { "version": "7.19.1", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.1.tgz", "integrity": "sha512-1H8VgqXme4UXCRv7/Wa1bq7RVymKOzC7znjyFM8KiEzwFqcKUKYNoQef4GhdklgNvoBXyW4gYhuBNCM5o1zImw==", - "devOptional": true, + "dev": true, "requires": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.18.6", @@ -4993,7 +5005,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "devOptional": true + "dev": true } } }, @@ -5001,7 +5013,7 @@ "version": "7.19.0", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.0.tgz", "integrity": "sha512-S1ahxf1gZ2dpoiFgA+ohK9DIpz50bJ0CWs7Zlzb54Z4sG8qmdIrGrVqmy1sAtTVRb+9CU6U8VqT9L0Zj7hxHVg==", - "devOptional": true, + "dev": true, "requires": { "@babel/types": "^7.19.0", "@jridgewell/gen-mapping": "^0.3.2", @@ -5031,7 +5043,7 @@ "version": "7.19.1", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.1.tgz", "integrity": "sha512-LlLkkqhCMyz2lkQPvJNdIYU7O5YjWRgC2R4omjCTpZd8u8KMQzZvX4qce+/BluN1rcQiV7BoGUpmQ0LeHerbhg==", - "devOptional": true, + "dev": true, "requires": { "@babel/compat-data": "^7.19.1", "@babel/helper-validator-option": "^7.18.6", @@ -5043,7 +5055,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "devOptional": true + "dev": true } } }, @@ -5098,7 +5110,7 @@ "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", - "devOptional": true + "dev": true }, "@babel/helper-explode-assignable-expression": { "version": "7.18.6", @@ -5113,7 +5125,7 @@ "version": "7.19.0", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", - "devOptional": true, + "dev": true, "requires": { "@babel/template": "^7.18.10", "@babel/types": "^7.19.0" @@ -5123,7 +5135,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", - "devOptional": true, + "dev": true, "requires": { "@babel/types": "^7.18.6" } @@ -5149,7 +5161,7 @@ "version": "7.19.0", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.0.tgz", "integrity": "sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ==", - "devOptional": true, + "dev": true, "requires": { "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-module-imports": "^7.18.6", @@ -5205,7 +5217,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz", "integrity": "sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==", - "devOptional": true, + "dev": true, "requires": { "@babel/types": "^7.18.6" } @@ -5223,7 +5235,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", - "devOptional": true, + "dev": true, "requires": { "@babel/types": "^7.18.6" } @@ -5242,7 +5254,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", - "devOptional": true + "dev": true }, "@babel/helper-wrap-function": { "version": "7.18.11", @@ -5260,7 +5272,7 @@ "version": "7.19.0", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.19.0.tgz", "integrity": "sha512-DRBCKGwIEdqY3+rPJgG/dKfQy9+08rHIAJx8q2p+HSWP87s2HCrQmaAMMyMll2kIXKCW0cO1RdQskx15Xakftg==", - "devOptional": true, + "dev": true, "requires": { "@babel/template": "^7.18.10", "@babel/traverse": "^7.19.0", @@ -5281,7 +5293,7 @@ "version": "7.19.1", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.1.tgz", "integrity": "sha512-h7RCSorm1DdTVGJf3P2Mhj3kdnkmF/EiysUkzS2TdgAYqyjFdMQJbVuXOBej2SBJaXan/lIVtT6KkGbyyq753A==", - "devOptional": true + "dev": true }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.18.6", @@ -6141,7 +6153,7 @@ "version": "7.18.10", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", - "devOptional": true, + "dev": true, "requires": { "@babel/code-frame": "^7.18.6", "@babel/parser": "^7.18.10", @@ -6152,7 +6164,7 @@ "version": "7.19.1", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.1.tgz", "integrity": "sha512-0j/ZfZMxKukDaag2PtOPDbwuELqIar6lLskVPPJDjXMXjfLb1Obo/1yjxIGqqAJrmfaTIY3z2wFLAQ7qSkLsuA==", - "devOptional": true, + "dev": true, "requires": { "@babel/code-frame": "^7.18.6", "@babel/generator": "^7.19.0", @@ -6326,7 +6338,7 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "devOptional": true, + "dev": true, "requires": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -6337,25 +6349,25 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "devOptional": true + "dev": true }, "@jridgewell/set-array": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "devOptional": true + "dev": true }, "@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "devOptional": true + "dev": true }, "@jridgewell/trace-mapping": { "version": "0.3.17", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", - "devOptional": true, + "dev": true, "requires": { "@jridgewell/resolve-uri": "3.1.0", "@jridgewell/sourcemap-codec": "1.4.14" @@ -6597,7 +6609,7 @@ "version": "4.21.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", - "devOptional": true, + "dev": true, "requires": { "caniuse-lite": "^1.0.30001400", "electron-to-chromium": "^1.4.251", @@ -6630,7 +6642,7 @@ "version": "1.0.30001412", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001412.tgz", "integrity": "sha512-+TeEIee1gS5bYOiuf+PS/kp2mrXic37Hl66VY6EAfxasIk5fELTktK2oOezYed12H8w7jt3s512PpulQidPjwA==", - "devOptional": true + "dev": true }, "chalk": { "version": "2.4.2", @@ -6819,9 +6831,9 @@ } }, "core-js-pure": { - "version": "3.27.2", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.27.2.tgz", - "integrity": "sha512-Cf2jqAbXgWH3VVzjyaaFkY1EBazxugUepGymDoeteyYr9ByX51kD2jdHZlsEF/xnJMyN3Prua7mQuzwMg6Zc9A==" + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.33.0.tgz", + "integrity": "sha512-FKSIDtJnds/YFIEaZ4HszRX7hkxGpNKM7FC9aJ9WLJbSd3lD4vOltFuVIBLR8asSx9frkTSqL0dw90SKQxgKrg==" }, "cosmiconfig": { "version": "7.0.1", @@ -6913,7 +6925,7 @@ "version": "1.4.262", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.262.tgz", "integrity": "sha512-Ckn5haqmGh/xS8IbcgK3dnwAVnhDyo/WQnklWn6yaMucYTq7NNxwlGE8ElzEOnonzRLzUCo2Ot3vUb2GYUF2Hw==", - "devOptional": true + "dev": true }, "emoji-regex": { "version": "8.0.0", @@ -6938,7 +6950,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "devOptional": true + "dev": true }, "escape-string-regexp": { "version": "1.0.5", @@ -7019,7 +7031,7 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "devOptional": true + "dev": true }, "get-caller-file": { "version": "2.0.5", @@ -7072,7 +7084,7 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "devOptional": true + "dev": true }, "growly": { "version": "1.3.0", @@ -7259,7 +7271,7 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "devOptional": true + "dev": true }, "json-parse-even-better-errors": { "version": "2.3.1", @@ -7270,7 +7282,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "devOptional": true + "dev": true }, "lines-and-columns": { "version": "1.2.4", @@ -7694,7 +7706,7 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", - "devOptional": true + "dev": true }, "normalize-path": { "version": "3.0.0", @@ -7789,7 +7801,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "devOptional": true + "dev": true }, "picomatch": { "version": "2.3.0", @@ -7814,15 +7826,6 @@ "react-is": "^16.13.1" } }, - "react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "peer": true, - "requires": { - "loose-envify": "^1.1.0" - } - }, "react-dictate-button": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/react-dictate-button/-/react-dictate-button-2.0.1.tgz", @@ -7845,16 +7848,6 @@ } } }, - "react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "peer": true, - "requires": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - } - }, "react-film": { "version": "3.1.1-main.df870ea", "resolved": "https://registry.npmjs.org/react-film/-/react-film-3.1.1-main.df870ea.tgz", @@ -8107,15 +8100,6 @@ "tslib": "^2.1.0" } }, - "scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "peer": true, - "requires": { - "loose-envify": "^1.1.0" - } - }, "semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", @@ -8289,12 +8273,17 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.9.tgz", "integrity": "sha512-/xsqn21EGVdXI3EXSum1Yckj3ZVZugqyOZQ/CxYPBD/R+ko9NSUScf8tFF4dOKY+2pvSSJA/S+5B8s4Zr4kyvg==", - "devOptional": true, + "dev": true, "requires": { "escalade": "^3.1.1", "picocolors": "^1.0.0" } }, + "url-template": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-3.1.0.tgz", + "integrity": "sha512-vB/eHWttzhN+NZzk9FcQB2h1cSEgb7zDYyvyxPhw02LYw7YqIzO+w1AqkcKvZ51gPH8o4+nyiWve/xuQqMdJZw==" + }, "use-ref-from": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/use-ref-from/-/use-ref-from-0.0.2.tgz", @@ -8314,12 +8303,43 @@ } } }, + "use-state-with-ref": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/use-state-with-ref/-/use-state-with-ref-0.0.1.tgz", + "integrity": "sha512-UsG2p/gnl+wk6dObF0iUNgm3bEByW4ZRFd3ptklKARUzFkMGIQjyAkJrlw99+zyfXtwk1PfVGHKSDsGZ0xpj+A==", + "requires": { + "@babel/runtime-corejs3": "^7.22.15", + "use-ref-from": "^0.0.2", + "use-state-with-ref": "^0.0.1" + }, + "dependencies": { + "@babel/runtime-corejs3": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.23.2.tgz", + "integrity": "sha512-54cIh74Z1rp4oIjsHjqN+WM4fMyCBYe+LpZ9jWm51CZ1fbH3SkAzQD/3XLoNkjbJ7YEmjobLXyvQrFypRHOrXw==", + "requires": { + "core-js-pure": "^3.30.2", + "regenerator-runtime": "^0.14.0" + } + }, + "regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + } + } + }, "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true }, + "valibot": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.19.0.tgz", + "integrity": "sha512-vzeuctzVirzoiDN9BTc4GludHaSgFYoy6AnbHOjfBUbX0DibqKuNCr2Ti4ox8eLdBdDSEZSZ3LwXZgjL6aE4RQ==" + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/packages/component/package.json b/packages/component/package.json index ba5ca903e7..4045c9b0d4 100644 --- a/packages/component/package.json +++ b/packages/component/package.json @@ -113,7 +113,10 @@ "react-scroll-to-bottom": "4.2.0", "redux": "4.2.1", "simple-update-in": "2.2.0", - "use-ref-from": "0.0.2" + "url-template": "3.1.0", + "use-ref-from": "0.0.2", + "use-state-with-ref": "0.0.1", + "valibot": "0.19.0" }, "peerDependencies": { "react": ">= 16.8.6", diff --git a/packages/component/src/Attachment/CustomerSatisfactory/CustomerSatisfactory.tsx b/packages/component/src/Attachment/CustomerSatisfactory/CustomerSatisfactory.tsx new file mode 100644 index 0000000000..9677d12b4b --- /dev/null +++ b/packages/component/src/Attachment/CustomerSatisfactory/CustomerSatisfactory.tsx @@ -0,0 +1,152 @@ +import { parseTemplate } from 'url-template'; +import { useRefFrom } from 'use-ref-from'; +import { useStateWithRef } from 'use-state-with-ref'; +import classNames from 'classnames'; +import React, { type FormEventHandler, Fragment, useCallback } from 'react'; + +import { ActionStatusType } from '../../types/external/OrgSchema/ActionStatusType'; +import { isValid, type RatingValue } from './private/RatingValue'; +import { type EntryPoint } from '../../types/external/OrgSchema/EntryPoint'; +import { type ReviewAction } from '../../types/external/OrgSchema/ReviewAction'; +import { useStyleSet } from '../../hooks'; +import Checkmark from './private/Checkmark'; +import RovingTabIndexComposer from '../../providers/RovingTabIndex/RovingTabIndexComposer'; +import StarBar from './private/StarBar'; +import useFocus from '../../hooks/useFocus'; +import useOpenURL from '../../hooks/internal/useOpenURL'; +import useStrings from './private/useStrings'; +import useUniqueId from '../../hooks/internal/useUniqueId'; + +declare global { + interface URLSearchParams { + entries(): Iterable<[string, string]>; + } +} + +// "target" must be set. +type SupportedReviewAction = ReviewAction & { target: EntryPoint | string }; + +type Props = Readonly<{ initialReviewAction: SupportedReviewAction }>; + +const CustomerSatisfactory = ({ initialReviewAction }: Props) => { + const [{ customerSatisfactoryAttachment: customerSatisfactoryAttachmentStyleSet }] = useStyleSet(); + const [reviewAction, setReviewAction, reviewActionRef] = useStateWithRef(initialReviewAction); + const { submitButtonText, submittedText } = useStrings(); + const focus = useFocus(); + const labelId = useUniqueId('webchat__customer-satisfactory'); + const openURL = useOpenURL(); + + const markAsSubmitted = useCallback( + () => setReviewAction(reviewAction => ({ ...reviewAction, actionStatus: ActionStatusType.CompletedActionStatus })), + [setReviewAction] + ); + const rawRatingValue = reviewAction.resultReview?.reviewRating?.ratingValue; + const setRating = useCallback( + (ratingValue: RatingValue) => { + setReviewAction(reviewAction => ({ + ...reviewAction, + resultReview: { + ...reviewAction.resultReview, + reviewRating: { + ...reviewAction.resultReview.reviewRating, + ratingValue + } + } + })); + }, + [setReviewAction] + ); + const submitted = reviewAction.actionStatus === ActionStatusType.CompletedActionStatus; + + const ratingValue: RatingValue | undefined = isValid(rawRatingValue) ? rawRatingValue : undefined; + + const submissionDisabled = typeof ratingValue !== 'number' || submitted; + + const submissionDisabledRef = useRefFrom(submissionDisabled); + + const handleSubmit = useCallback( + event => { + event.preventDefault(); + + if (submissionDisabledRef.current) { + return; + } + + const { current: reviewAction } = reviewActionRef; + + try { + // This is based from https://schema.org/docs/actions.html. + let url: URL; + + if (typeof reviewAction.target === 'string') { + url = new URL(reviewAction.target); + } else { + const ratingValueInput = reviewAction.resultReview?.reviewRating?.['ratingValue-input']; + const urlTemplateInputs: Map = new Map(); + + // TODO: We should expand this to support many `*-input`. + ratingValueInput?.valueName && + urlTemplateInputs.set( + ratingValueInput.valueName, + reviewAction?.resultReview?.reviewRating?.ratingValue || null + ); + + url = new URL( + parseTemplate(reviewAction.target.urlTemplate).expand(Object.fromEntries(urlTemplateInputs.entries())) + ); + } + + url && openURL(url); + } catch (error) { + console.error('botframework-webchat: Failed to send review action.', { error }); + } + + markAsSubmitted(); + focus('sendBox'); + }, + [focus, markAsSubmitted, openURL, reviewActionRef, submissionDisabledRef] + ); + + return ( +
+
+ {/* "id" is required for "aria-labelledby" */} + {/* eslint-disable-next-line react/forbid-dom-props */} +

+ {initialReviewAction.description} +

+ + + +
+ +
+ ); +}; + +export default CustomerSatisfactory; diff --git a/packages/component/src/Attachment/CustomerSatisfactory/CustomerSatisfactoryForScreenReader.tsx b/packages/component/src/Attachment/CustomerSatisfactory/CustomerSatisfactoryForScreenReader.tsx new file mode 100644 index 0000000000..a42c3d4ceb --- /dev/null +++ b/packages/component/src/Attachment/CustomerSatisfactory/CustomerSatisfactoryForScreenReader.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +import { type ReviewAction } from '../../types/external/OrgSchema/ReviewAction'; +import useStrings from './private/useStrings'; +import useUniqueId from '../../hooks/internal/useUniqueId'; + +type Props = Readonly<{ + initialReviewAction: ReviewAction; +}>; + +const CustomerSatisfactoryForScreenReader = ({ initialReviewAction }: Props) => { + const { getRatingAltText, submitButtonText } = useStrings(); + const labelId = useUniqueId('webchat__customer-satisfactory'); + + return ( +
+
+ {/* eslint-disable-next-line react/forbid-dom-props */} +

{initialReviewAction.description}

+
+ +
+ ); +}; + +export default CustomerSatisfactoryForScreenReader; diff --git a/packages/component/src/Attachment/CustomerSatisfactory/customerSatisfactoryMiddleware.tsx b/packages/component/src/Attachment/CustomerSatisfactory/customerSatisfactoryMiddleware.tsx new file mode 100644 index 0000000000..574b8f9375 --- /dev/null +++ b/packages/component/src/Attachment/CustomerSatisfactory/customerSatisfactoryMiddleware.tsx @@ -0,0 +1,72 @@ +import { parse } from 'valibot'; +import { type AttachmentMiddleware, type AttachmentForScreenReaderMiddleware } from 'botframework-webchat-api'; +import React, { Fragment } from 'react'; + +import { isReviewAction, type ReviewAction } from '../../types/external/OrgSchema/ReviewAction'; +import { type PropsOf } from '../../types/PropsOf'; +import CustomerSatisfactory from './CustomerSatisfactory'; +import CustomerSatisfactoryForScreenReader from './CustomerSatisfactoryForScreenReader'; +import reviewActionSchema from './private/schema/reviewActionSchema'; + +type SupportedReviewAction = PropsOf['initialReviewAction']; + +const customerSatisfactoryMiddleware: AttachmentMiddleware = + () => + next => + (...args) => { + const [arg0] = args; + + if (arg0) { + const { + attachment: { content, contentType } + } = arg0; + + if (contentType === 'application/ld+json' && isReviewAction(content)) { + try { + // TODO: Unsure why `@type` in valibot schema is marked as optional. + const reviewAction = parse(reviewActionSchema, content) as SupportedReviewAction; + + return ; + } catch (error) { + // TODO: We should use . + console.error(`botframework-webchat: Failed to render ReviewAction.`, { error }); + + return ; + } + } + } + + return next(...args); + }; + +export default customerSatisfactoryMiddleware; + +const forScreenReader: AttachmentForScreenReaderMiddleware = + () => + next => + (...args) => { + const [arg0] = args; + + if (arg0) { + const { + attachment: { content, contentType } + } = arg0; + + if (contentType === 'application/ld+json' && isReviewAction(content)) { + try { + const reviewAction = parse(reviewActionSchema, content) as ReviewAction; + + return () => ; + } catch (error) { + // TODO: We should use . + console.error(`botframework-webchat: Failed to render ReviewAction for screen reader.`, { error }); + + return () => ; + } + } + } + + return next(...args); + }; + +export { forScreenReader }; diff --git a/packages/component/src/Attachment/CustomerSatisfactory/private/Checkmark.tsx b/packages/component/src/Attachment/CustomerSatisfactory/private/Checkmark.tsx new file mode 100644 index 0000000000..878e5a7d4b --- /dev/null +++ b/packages/component/src/Attachment/CustomerSatisfactory/private/Checkmark.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +type Props = Readonly<{ className?: string }>; + +const Checkmark = ({ className }: Props) => ( + + + +); + +export default Checkmark; diff --git a/packages/component/src/Attachment/CustomerSatisfactory/private/RatingValue.ts b/packages/component/src/Attachment/CustomerSatisfactory/private/RatingValue.ts new file mode 100644 index 0000000000..2fa24a3510 --- /dev/null +++ b/packages/component/src/Attachment/CustomerSatisfactory/private/RatingValue.ts @@ -0,0 +1,9 @@ +// Rating can be any numbers between 1 and 5. +// eslint-disable-next-line no-magic-numbers +export type RatingValue = 1 | 2 | 3 | 4 | 5; + +export function isValid(ratingValue: any): ratingValue is RatingValue { + // Rating can be any numbers between 1 and 5. + // eslint-disable-next-line no-magic-numbers + return typeof ratingValue === 'number' && ratingValue >= 1 && ratingValue <= 5; +} diff --git a/packages/component/src/Attachment/CustomerSatisfactory/private/Star.tsx b/packages/component/src/Attachment/CustomerSatisfactory/private/Star.tsx new file mode 100644 index 0000000000..49253d6c31 --- /dev/null +++ b/packages/component/src/Attachment/CustomerSatisfactory/private/Star.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +type Props = Readonly<{ className?: string }>; + +const Star = ({ className }: Props) => ( + + + +); + +export default Star; diff --git a/packages/component/src/Attachment/CustomerSatisfactory/private/StarBar.tsx b/packages/component/src/Attachment/CustomerSatisfactory/private/StarBar.tsx new file mode 100644 index 0000000000..3ed0c67e9e --- /dev/null +++ b/packages/component/src/Attachment/CustomerSatisfactory/private/StarBar.tsx @@ -0,0 +1,36 @@ +import { useRefFrom } from 'use-ref-from'; +import classNames from 'classnames'; +import React, { useCallback } from 'react'; + +import { type RatingValue } from './RatingValue'; +import StarButton from './StarButton'; + +type Props = Readonly<{ + className?: string | undefined; + disabled?: boolean | undefined; + onChange?: (value: RatingValue) => void; + value?: RatingValue | undefined; +}>; + +const StarBar = ({ className, disabled, onChange, value }: Props) => { + const onChangeRef = useRefFrom(onChange); + + const handleStarButtonClick = useCallback((rating: RatingValue) => onChangeRef.current?.(rating), [onChangeRef]); + + return ( +
+ = 1} disabled={disabled} onClick={handleStarButtonClick} value={1} /> + {/* eslint-disable-next-line no-magic-numbers */} + = 2} disabled={disabled} onClick={handleStarButtonClick} value={2} /> + {/* eslint-disable-next-line no-magic-numbers */} + = 3} disabled={disabled} onClick={handleStarButtonClick} value={3} /> + {/* eslint-disable-next-line no-magic-numbers */} + = 4} disabled={disabled} onClick={handleStarButtonClick} value={4} /> + {/* eslint-disable-next-line no-magic-numbers */} + = 5} disabled={disabled} onClick={handleStarButtonClick} value={5} /> +
{value}
+
+ ); +}; + +export default StarBar; diff --git a/packages/component/src/Attachment/CustomerSatisfactory/private/StarButton.tsx b/packages/component/src/Attachment/CustomerSatisfactory/private/StarButton.tsx new file mode 100644 index 0000000000..7f28c633c4 --- /dev/null +++ b/packages/component/src/Attachment/CustomerSatisfactory/private/StarButton.tsx @@ -0,0 +1,55 @@ +import { useRefFrom } from 'use-ref-from'; +import classNames from 'classnames'; +import React, { ReactEventHandler, useCallback } from 'react'; + +import { type RatingValue } from './RatingValue'; +import Star from './Star'; +import StarFilled from './StarFilled'; +import useItemRef from '../../../providers/RovingTabIndex/useItemRef'; +import useStrings from './useStrings'; + +type Props = Readonly<{ + checked?: boolean | undefined; + className?: boolean | undefined; + disabled?: boolean | undefined; + onClick?: (index: RatingValue) => void; + value: RatingValue; +}>; + +const StarButton = ({ checked, className, disabled, onClick, value }: Props) => { + const { getRatingAltText } = useStrings(); + const disabledRef = useRefFrom(disabled); + const onClickRef = useRefFrom(onClick); + const ratingRef = useRefFrom(value); + const ref = useItemRef(value - 1); + + const handleClickAndFocus = useCallback( + event => { + event.preventDefault(); + + disabledRef.current || onClickRef.current?.(ratingRef.current); + }, + [disabledRef, onClickRef, ratingRef] + ); + + return ( + + ); +}; + +export default StarButton; diff --git a/packages/component/src/Attachment/CustomerSatisfactory/private/StarFilled.tsx b/packages/component/src/Attachment/CustomerSatisfactory/private/StarFilled.tsx new file mode 100644 index 0000000000..ab0949dc51 --- /dev/null +++ b/packages/component/src/Attachment/CustomerSatisfactory/private/StarFilled.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +type Props = Readonly<{ className?: string }>; + +const StarFilled = ({ className }: Props) => ( + + + +); + +export default StarFilled; diff --git a/packages/component/src/Attachment/CustomerSatisfactory/private/schema/exactString.ts b/packages/component/src/Attachment/CustomerSatisfactory/private/schema/exactString.ts new file mode 100644 index 0000000000..f06a713efe --- /dev/null +++ b/packages/component/src/Attachment/CustomerSatisfactory/private/schema/exactString.ts @@ -0,0 +1,5 @@ +import { type ErrorMessage, string, type StringSchema, value } from 'valibot'; + +export default function exactString(exactValue: T, errorMessage?: ErrorMessage) { + return string([value(exactValue, errorMessage)]) as StringSchema; +} diff --git a/packages/component/src/Attachment/CustomerSatisfactory/private/schema/reviewActionSchema.ts b/packages/component/src/Attachment/CustomerSatisfactory/private/schema/reviewActionSchema.ts new file mode 100644 index 0000000000..93ac7a6394 --- /dev/null +++ b/packages/component/src/Attachment/CustomerSatisfactory/private/schema/reviewActionSchema.ts @@ -0,0 +1,60 @@ +import { enumType, maxValue, minValue, number, optional, string, union, url } from 'valibot'; + +import { ActionStatusType } from '../../../../types/external/OrgSchema/ActionStatusType'; +import exactString from './exactString'; +import thing from './thing'; + +// This is stricter than Schema.org and our TypeScript types. +// Enforcing some rules to make sure the attachment received has all fields we need. +const reviewActionSchema = thing('ReviewAction', { + actionStatus: optional( + enumType( + [ + ActionStatusType.ActiveActionStatus, + ActionStatusType.CompletedActionStatus, + ActionStatusType.FailedActionStatus, + ActionStatusType.PotentialActionStatus + ], + '"actionStatus" must be one of the ActionStatusType' + ) + ), + description: optional(string('"description" must be of type string')), + resultReview: optional( + thing('Review', { + reviewRating: optional( + thing('Rating', { + ratingValue: optional( + number(`"resultReview.reviewRating.ratingValue" must be of type "number" and between 1 and 5`, [ + minValue(1), + // Rating is between 1 and 5. + // eslint-disable-next-line no-magic-numbers + maxValue(5) + ]) + ), + 'ratingValue-input': optional( + thing('PropertyValueSpecification', { + valueName: optional( + string(`"resultReview.reviewRating['ratingValue-input'].valueName" must be of type string`) + ) + }) + ) + }) + ) + }) + ), + target: union( + [ + thing('EntryPoint', { + actionPlatform: exactString( + 'https://directline.botframework.com', + `"target.actionPlatform" must be "https://directline.botframework.com"` + ), + urlTemplate: string([url(`"target.urlTemplate" must be a URL`)]) + }), + string([url()]) + ], + '"target" must be of type "EntryPoint" or URL' + ) +}); + +export default reviewActionSchema; diff --git a/packages/component/src/Attachment/CustomerSatisfactory/private/schema/thing.ts b/packages/component/src/Attachment/CustomerSatisfactory/private/schema/thing.ts new file mode 100644 index 0000000000..d27836107b --- /dev/null +++ b/packages/component/src/Attachment/CustomerSatisfactory/private/schema/thing.ts @@ -0,0 +1,21 @@ +import { merge, object, type ObjectShape, type ObjectOutput, optional, type Pipe } from 'valibot'; + +import exactString from './exactString'; + +// This is stricter than Schema.org. +// Enforcing some rules to make sure the attachment received has all fields we need. +function thing( + type: TThingType, + shape: TObjectShape, + pipe?: Pipe> +) { + return merge([ + object({ + '@context': optional(exactString('https://schema.org', 'object must be from context "https://schema.org"')), + '@type': exactString(type, `object must be of type "${type}"`) + }), + object(shape, pipe) + ]); +} + +export default thing; diff --git a/packages/component/src/Attachment/CustomerSatisfactory/private/useStrings.tsx b/packages/component/src/Attachment/CustomerSatisfactory/private/useStrings.tsx new file mode 100644 index 0000000000..f331569e11 --- /dev/null +++ b/packages/component/src/Attachment/CustomerSatisfactory/private/useStrings.tsx @@ -0,0 +1,35 @@ +import { hooks } from 'botframework-webchat-api'; +import { useCallback } from 'react'; + +const { useLocalizer } = hooks; + +const RATING_PLURAL_IDS = { + few: 'CSAT_RATING_FEW_ALT', + many: 'CSAT_RATING_MANY_ALT', + one: 'CSAT_RATING_ONE_ALT', + other: 'CSAT_RATING_OTHER_ALT', + two: 'CSAT_RATING_TWO_ALT' +}; + +export default function useStrings(): Readonly<{ + getRatingAltText: (rating: number) => string; + submitButtonText: string; + submittedText: string; +}> { + const localize = useLocalizer(); + const localizeWithPlural = useLocalizer({ plural: true }); + + const getRatingAltText = useCallback( + (rating: number) => localizeWithPlural(RATING_PLURAL_IDS, rating), + [localizeWithPlural] + ); + + const submitButtonText = localize('CSAT_SUBMIT_BUTTON_TEXT'); + const submittedText = localize('CSAT_SUBMITTED_TEXT'); + + return Object.freeze({ + getRatingAltText, + submitButtonText, + submittedText + } as const); +} diff --git a/packages/component/src/Attachment/createMiddleware.tsx b/packages/component/src/Attachment/createMiddleware.tsx index 0d1ff8067c..6e849b8bd8 100644 --- a/packages/component/src/Attachment/createMiddleware.tsx +++ b/packages/component/src/Attachment/createMiddleware.tsx @@ -1,6 +1,7 @@ import React from 'react'; import AudioAttachment from './AudioAttachment'; +import customerSatisfactoryMiddleware from './CustomerSatisfactory/customerSatisfactoryMiddleware'; import FileAttachment from './FileAttachment'; import ImageAttachment from './ImageAttachment'; import TextAttachment from './Text/TextAttachment'; @@ -18,6 +19,7 @@ function isTextAttachment( // TODO: [P4] Rename this file or the whole middleware, it looks either too simple or too comprehensive now export default function createCoreMiddleware(): AttachmentMiddleware[] { return [ + customerSatisfactoryMiddleware, () => next => (...args) => { diff --git a/packages/component/src/Styles/StyleSet/CustomerSatisfactoryAttachment.ts b/packages/component/src/Styles/StyleSet/CustomerSatisfactoryAttachment.ts new file mode 100644 index 0000000000..cd1906544c --- /dev/null +++ b/packages/component/src/Styles/StyleSet/CustomerSatisfactoryAttachment.ts @@ -0,0 +1,146 @@ +const DARK_THEME_SELECTOR = '@media (forced-colors: none) and (prefers-color-scheme: dark)'; +const FORCED_COLORS_SELECTOR = '@media (forced-colors: active)'; +const LIGHT_THEME_SELECTOR = '@media (forced-colors: none) and (prefers-color-scheme: light)'; +const NOT_FORCED_COLORS_SELECTOR = '@media (forced-colors: none)'; + +const DISABLED_SELECTOR = '&:disabled, &[aria-disabled="true"]'; +const NOT_DISABLED_SELECTOR = '&:not(:disabled):not([aria-disabled="true"])'; + +export default function CustomerSatisfactoryAttachment() { + return { + '&.webchat__customer-satisfactory': { + alignItems: 'flex-start', + display: 'flex', + flexDirection: 'column', + fontFamily: 'var(--webchat__font--primary)', + gap: 8, + padding: '10px 12px' + }, + + '& .webchat__customer-satisfactory__radio-group': { + display: 'flex', + flexDirection: 'column', + gap: 8 + }, + + '& .webchat__customer-satisfactory__label': { + margin: 0 + }, + + '& .webchat__customer-satisfactory__star-bar': { + display: 'flex', + + [FORCED_COLORS_SELECTOR]: { + color: 'ButtonBorder' + }, + + [NOT_FORCED_COLORS_SELECTOR]: { + [DARK_THEME_SELECTOR]: { + color: '#White' + }, + + [LIGHT_THEME_SELECTOR]: { + color: '#242424' + } + } + }, + + '& .webchat__customer-satisfactory__star-button': { + backgroundColor: 'transparent', + color: 'unset', + border: 0, + borderRadius: 5.33, + height: 32, + padding: 0, + width: 32, + + [NOT_DISABLED_SELECTOR]: { + '&:hover': { + [NOT_FORCED_COLORS_SELECTOR]: { + color: 'var(--webchat__color--accent)' + } + }, + + '&:active': { + [NOT_FORCED_COLORS_SELECTOR]: { + // Web Chat currently don't have an accent color for active. + color: 'var(--webchat__color--accent)' + } + } + }, + + '&:focus-visible': { + outlineOffset: -2.67, + outlineStyle: 'solid', + outlineWidth: 2.67, + + [NOT_FORCED_COLORS_SELECTOR]: { + [DARK_THEME_SELECTOR]: { + outlineColor: '#ADADAD' + }, + + [LIGHT_THEME_SELECTOR]: { + outlineColor: '#616161' + } + } + } + }, + + '&:not(.webchat__customer-satisfactory--submitted) .webchat__customer-satisfactory__star-button': { + cursor: 'pointer' + }, + + '& .webchat__customer-satisfactory__rating-value': { + alignSelf: 'center', + marginLeft: 4 + }, + + '& .webchat__customer-satisfactory__submit-button': { + appearance: 'none', + backgroundColor: 'Canvas', + borderRadius: 4, + borderStyle: 'solid', + borderWidth: 1, + fontFamily: 'unset', + fontSize: 14, + fontWeight: 600, + padding: '5px 12px', + + [FORCED_COLORS_SELECTOR]: { + borderColor: 'ButtonBorder' + }, + + [NOT_FORCED_COLORS_SELECTOR]: { + borderColor: '#D1D1D1' + } + }, + + '&.webchat__customer-satisfactory--submitted .webchat__customer-satisfactory__submit-button': { + backgroundColor: 'unset', + borderColor: 'transparent', + outline: 0, + paddingLeft: 0, + paddingRight: 0 + }, + + '&:not(.webchat__customer-satisfactory--submitted) .webchat__customer-satisfactory__submit-button': { + [DISABLED_SELECTOR]: { + [FORCED_COLORS_SELECTOR]: { + color: 'GrayText' + }, + + [NOT_FORCED_COLORS_SELECTOR]: { + backgroundColor: '#F0F0F0', + color: '#BDBDBD' + } + } + }, + + '& .webchat__customer-satisfactory__submit-button-text': { + alignItems: 'center', + display: 'flex', + gap: 8, + minHeight: 20 + } + }; +} diff --git a/packages/component/src/Styles/createStyleSet.ts b/packages/component/src/Styles/createStyleSet.ts index 9228a8b093..e3a9a43f29 100644 --- a/packages/component/src/Styles/createStyleSet.ts +++ b/packages/component/src/Styles/createStyleSet.ts @@ -12,6 +12,7 @@ import createCarouselFilmStripAttachment from './StyleSet/CarouselFilmStripAttac import createCarouselFlipper from './StyleSet/CarouselFlipper'; import createCitationModalDialogStyle from './StyleSet/CitationModalDialog'; import createConnectivityNotification from './StyleSet/ConnectivityNotification'; +import createCustomerSatisfactoryAttachment from './StyleSet/CustomerSatisfactoryAttachment'; import createCSSCustomPropertiesStyle from './StyleSet/CSSCustomProperties'; import createDictationInterimsStyle from './StyleSet/DictationInterims'; import createErrorBoxStyle from './StyleSet/ErrorBox'; @@ -102,6 +103,7 @@ export default function createStyleSet(styleOptions: StyleOptions) { // - Use CSS var instead of strictStyleOptions citationModalDialog: createCitationModalDialogStyle(), cssCustomProperties: createCSSCustomPropertiesStyle(strictStyleOptions), + customerSatisfactoryAttachment: createCustomerSatisfactoryAttachment(), linkDefinitions: createLinkDefinitionsStyle(), modalDialog: createModalDialogStyle(), renderMarkdown: createRenderMarkdownStyle(), diff --git a/packages/component/src/hooks/internal/useOpenURL.ts b/packages/component/src/hooks/internal/useOpenURL.ts new file mode 100644 index 0000000000..b376503c8e --- /dev/null +++ b/packages/component/src/hooks/internal/useOpenURL.ts @@ -0,0 +1,58 @@ +import { hooks } from 'botframework-webchat-api'; +import { useCallback } from 'react'; + +const { useSendMessage, useSendMessageBack, useSendPostBack } = hooks; + +export default function useOpenURL() { + const sendMessage = useSendMessage(); + const sendMessageBack = useSendMessageBack(); + const sendPostBack = useSendPostBack(); + + return useCallback( + (url: URL): void => { + const { protocol, searchParams } = url; + + if (protocol === 'ms-directline-imback:') { + const titleOrValue = searchParams.get('title') || searchParams.get('value'); + + if (!titleOrValue) { + throw new Error('When using "ms-directline-imback:" protocol, parameter "title" or "value" to be set.'); + } + + sendMessage(titleOrValue); + } else if (protocol === 'ms-directline-messageback:') { + let value: any; + + if (searchParams.has('value')) { + const rawValue = searchParams.get('value') as string; + + try { + value = JSON.parse(rawValue); + } catch (error) { + console.warn( + 'botframework-webchat: When using "ms-directline-messageback:" protocol, parameter "value" should be complex type or omitted.' + ); + + value = rawValue; + } + } + + sendMessageBack(value, searchParams.get('text') || undefined, searchParams.get('displayText') || undefined); + } else if (protocol === 'ms-directline-postback:') { + const value = searchParams.get('value'); + + sendPostBack( + value && + // This is not conform to Bot Framework Direct Line specification. + // However, this is what PVA is currently using. + searchParams.get('valuetype') === 'application/json' + ? JSON.parse(value) + : value + ); + } else { + throw new Error(`Cannot open URL with an unsupported protocol "${protocol}".`); + } + }, + [sendMessage, sendMessageBack, sendPostBack] + ); +} diff --git a/packages/component/src/types/external/OrgSchema/ActionStatusType.ts b/packages/component/src/types/external/OrgSchema/ActionStatusType.ts new file mode 100644 index 0000000000..aa9e420afe --- /dev/null +++ b/packages/component/src/types/external/OrgSchema/ActionStatusType.ts @@ -0,0 +1,6 @@ +export enum ActionStatusType { + ActiveActionStatus = 'ActiveActionStatus', + CompletedActionStatus = 'CompletedActionStatus', + FailedActionStatus = 'FailedActionStatus', + PotentialActionStatus = 'PotentialActionStatus' +} diff --git a/packages/component/src/types/external/OrgSchema/Claim.ts b/packages/component/src/types/external/OrgSchema/Claim.ts index 7a02536bea..2566b7a9e5 100644 --- a/packages/component/src/types/external/OrgSchema/Claim.ts +++ b/packages/component/src/types/external/OrgSchema/Claim.ts @@ -9,17 +9,18 @@ import { isThingOf, type Thing } from './Thing'; * * @see https://schema.org/Claim. */ -export type Claim = Thing<'Claim'> & { - /** The textual content of this CreativeWork. */ - text?: string; +export type Claim = Thing<'Claim'> & + Readonly<{ + /** The textual content of this CreativeWork. */ + text?: string; - /** The name of the item. */ - name?: string; + /** The name of the item. */ + name?: string; - /** URL of the item. */ - url?: string; -}; + /** URL of the item. */ + url?: string; + }>; -export function isClaim(thing: Thing): thing is Claim { +export function isClaim(thing: any): thing is Claim { return isThingOf(thing, 'Claim'); } diff --git a/packages/component/src/types/external/OrgSchema/EntryPoint.ts b/packages/component/src/types/external/OrgSchema/EntryPoint.ts new file mode 100644 index 0000000000..4e05e04ee8 --- /dev/null +++ b/packages/component/src/types/external/OrgSchema/EntryPoint.ts @@ -0,0 +1,21 @@ +import { isThingOf, type Thing } from './Thing'; + +/** + * An entry point, within some Web-based protocol. + * + * This is partial implementation of https://schema.org/EntryPoint. + * + * @see https://schema.org/EntryPoint + */ +export type EntryPoint = Thing<'EntryPoint'> & + Readonly<{ + /** Indicates the current disposition of the Action. */ + actionPlatform?: string | undefined | URL; + + /** An url template (RFC6570) that will be used to construct the target of the execution of the action. */ + urlTemplate?: string | undefined; + }>; + +export function isEntryPoint(thing: any, currentContext?: string): thing is EntryPoint { + return isThingOf(thing, 'EntryPoint', currentContext); +} diff --git a/packages/component/src/types/external/OrgSchema/Project.ts b/packages/component/src/types/external/OrgSchema/Project.ts index 40fc9928f8..13914a4780 100644 --- a/packages/component/src/types/external/OrgSchema/Project.ts +++ b/packages/component/src/types/external/OrgSchema/Project.ts @@ -7,14 +7,15 @@ import { isThingOf, type Thing } from './Thing'; * * @see https://schema.org/Project */ -export type Project = Thing<'Project'> & { - /** The name of the item. */ - name: string; +export type Project = Thing<'Project'> & + Readonly<{ + /** The name of the item. */ + name: string; - /** URL of the item. */ - url: string; -}; + /** URL of the item. */ + url: string; + }>; -export function isProject(thing: Thing): thing is Project { +export function isProject(thing: any): thing is Project { return isThingOf(thing, 'Project'); } diff --git a/packages/component/src/types/external/OrgSchema/PropertyValueSpecification.ts b/packages/component/src/types/external/OrgSchema/PropertyValueSpecification.ts new file mode 100644 index 0000000000..f5c2b45aee --- /dev/null +++ b/packages/component/src/types/external/OrgSchema/PropertyValueSpecification.ts @@ -0,0 +1,23 @@ +import { isThingOf, type Thing } from './Thing'; + +/** + * A Property value specification. + * + * This is partial implementation of https://schema.org/PropertyValueSpecification. + * + * @see https://schema.org/PropertyValueSpecification + */ +export type PropertyValueSpecification = Thing<'PropertyValueSpecification'> & + Readonly<{ + /** Indicates the name of the PropertyValueSpecification to be used in URL templates and form encoding in a manner analogous to HTML's input@name. */ + valueName?: string | undefined; + }>; + +export type WithInput> = T & + Readonly<{ + [K in keyof T as K extends string ? `${K}-input` : K]?: PropertyValueSpecification | undefined; + }>; + +export function isPropertyValueSpecification(thing: any, currentContext?: string): thing is PropertyValueSpecification { + return isThingOf(thing, 'PropertyValueSpecification', currentContext); +} diff --git a/packages/component/src/types/external/OrgSchema/Rating.ts b/packages/component/src/types/external/OrgSchema/Rating.ts new file mode 100644 index 0000000000..570c741fe5 --- /dev/null +++ b/packages/component/src/types/external/OrgSchema/Rating.ts @@ -0,0 +1,28 @@ +import { isThingOf, type Thing } from './Thing'; +import { type WithInput } from './PropertyValueSpecification'; + +/** + * A rating is an evaluation on a numeric scale, such as 1 to 5 stars. + * + * This is partial implementation of https://schema.org/Rating. + * + * @see https://schema.org/Rating + */ +export type Rating = Thing<'Rating'> & + WithInput< + Readonly<{ + /** + * The rating for the content. + * + * Usage guidelines: + * + * - Use values from 0123456789 (Unicode 'DIGIT ZERO' (U+0030) to 'DIGIT NINE' (U+0039)) rather than superficially similar Unicode symbols. + * - Use '.' (Unicode 'FULL STOP' (U+002E)) rather than ',' to indicate a decimal point. Avoid using these symbols as a readability separator. + */ + ratingValue?: number | string | undefined; + }> + >; + +export function isRating(thing: any, currentContext?: string): thing is Rating { + return isThingOf(thing, 'Rating', currentContext); +} diff --git a/packages/component/src/types/external/OrgSchema/ReplyAction.ts b/packages/component/src/types/external/OrgSchema/ReplyAction.ts index 6fea61de58..8657fb9336 100644 --- a/packages/component/src/types/external/OrgSchema/ReplyAction.ts +++ b/packages/component/src/types/external/OrgSchema/ReplyAction.ts @@ -8,14 +8,15 @@ import { type Project } from './Project'; * * @see https://schema.org/ReplyAction */ -export type ReplyAction = Thing<'ReplyAction'> & { - /** A description of the item. */ - description?: string; +export type ReplyAction = Thing<'ReplyAction'> & + Readonly<{ + /** A description of the item. */ + description?: string; - /** The service provider, service operator, or service performer; the goods producer. Another party (a seller) may offer those services or goods on behalf of the provider. A provider may also serve as the seller. Supersedes [carrier](https://schema.org/carrier). */ - provider?: Project; -}; + /** The service provider, service operator, or service performer; the goods producer. Another party (a seller) may offer those services or goods on behalf of the provider. A provider may also serve as the seller. Supersedes [carrier](https://schema.org/carrier). */ + provider?: Project; + }>; -export function isReplyAction(thing: Thing): thing is ReplyAction { +export function isReplyAction(thing: any): thing is ReplyAction { return isThingOf(thing, 'ReplyAction'); } diff --git a/packages/component/src/types/external/OrgSchema/Review.ts b/packages/component/src/types/external/OrgSchema/Review.ts new file mode 100644 index 0000000000..38203bfae0 --- /dev/null +++ b/packages/component/src/types/external/OrgSchema/Review.ts @@ -0,0 +1,23 @@ +import { isThingOf, type Thing } from './Thing'; +import { Rating } from './Rating'; + +/** + * A review of an item - for example, of a restaurant, movie, or store. + * + * This is partial implementation of https://schema.org/Review. + * + * @see https://schema.org/Review + */ +export type Review = Thing<'Review'> & + Readonly<{ + /** + * The rating given in this review. Note that reviews can themselves be rated. + * The `reviewRating` applies to rating given by the review. + * The [aggregateRating](https://schema.org/aggregateRating) property applies to the review itself, as a creative work. + */ + reviewRating?: Rating | undefined; + }>; + +export function isReview(thing: any, currentContext?: string): thing is Review { + return isThingOf(thing, 'Review', currentContext); +} diff --git a/packages/component/src/types/external/OrgSchema/ReviewAction.ts b/packages/component/src/types/external/OrgSchema/ReviewAction.ts new file mode 100644 index 0000000000..7062df9c06 --- /dev/null +++ b/packages/component/src/types/external/OrgSchema/ReviewAction.ts @@ -0,0 +1,30 @@ +import { ActionStatusType } from './ActionStatusType'; +import { EntryPoint } from './EntryPoint'; +import { isThingOf, type Thing } from './Thing'; +import { Review } from './Review'; + +/** + * The act of producing a balanced opinion about the object for an audience. An agent reviews an object with participants resulting in a review. + * + * This is partial implementation of https://schema.org/ReviewAction. + * + * @see https://schema.org/ReviewAction + */ +export type ReviewAction = Thing<'ReviewAction'> & + Readonly<{ + /** Indicates the current disposition of the Action. */ + actionStatus?: ActionStatusType | undefined; + + /** A description of the item. */ + description?: string | undefined; + + /** A sub property of result. The review that resulted in the performing of the action. */ + resultReview?: Review | undefined; + + /** Indicates a target EntryPoint, or url, for an Action. */ + target?: EntryPoint | string | undefined; + }>; + +export function isReviewAction(thing: any, currentContext?: string): thing is ReviewAction { + return isThingOf(thing, 'ReviewAction', currentContext); +} diff --git a/packages/component/src/types/external/OrgSchema/Thing.ts b/packages/component/src/types/external/OrgSchema/Thing.ts index 7aae6df6ee..45da8c969d 100644 --- a/packages/component/src/types/external/OrgSchema/Thing.ts +++ b/packages/component/src/types/external/OrgSchema/Thing.ts @@ -1,5 +1,3 @@ -import { type OrgSchemaThing } from 'botframework-webchat-core'; - /** * The most generic type of item. * @@ -7,44 +5,44 @@ import { type OrgSchemaThing } from 'botframework-webchat-core'; * * @see https://schema.org/Thing */ -export type Thing = OrgSchemaThing & { - '@id'?: string; +export type Thing = Readonly<{ + '@context'?: 'https://schema.org' | undefined; + '@id'?: string | undefined; + '@type': T; /** An alias for the item. */ - alternateName?: string; + alternateName?: string | undefined; /** The name of the item. */ - name?: string; -}; + name?: string | undefined; +}>; + +/** Adds auxiliary types for Thing when it appears in Direct Line activity as a member of `entities` field. */ +export type AsEntity = T & + Readonly<{ + '@context': 'https://schema.org'; + type: `https://schema.org/${T['@type']}`; + }>; -export function isThing(thing: { '@context'?: string; '@type'?: string; type?: string }): thing is Thing { +export function isThing(thing: any, currentContext?: string): thing is Thing { if (typeof thing === 'object' && thing) { - return ( - '@context' in thing && - '@type' in thing && - 'type' in thing && - thing['@context'] === 'https://schema.org' && - typeof thing['@type'] === 'string' && - thing.type === `https://schema.org/${thing['@type']}` - ); + const context = thing['@context'] || currentContext; + + if (context) { + return context === 'https://schema.org' && typeof thing['@type'] === 'string'; + } + + return typeof thing.type === 'string' && thing.type.startsWith(`https://schema.org/`); } return false; } -export function isThingOf( - thing: { '@context'?: string; '@type'?: string; type?: string }, - type: T -): thing is Thing { - if (typeof thing === 'object' && thing) { - return ( - '@context' in thing && - '@type' in thing && - 'type' in thing && - thing['@context'] === 'https://schema.org' && - thing['@type'] === type && - thing.type === `https://schema.org/${type}` - ); +export function isThingOf(thing: any, type: T, currentContext?: string): thing is Thing { + if (isThing(thing, currentContext)) { + if ((thing['@context'] || currentContext) === 'https://schema.org' && thing['@type']) { + return thing['@type'] === type; + } } return false; diff --git a/packages/component/src/types/external/OrgSchema/VoteAction.ts b/packages/component/src/types/external/OrgSchema/VoteAction.ts index 3bcbe70766..b9b2fd271c 100644 --- a/packages/component/src/types/external/OrgSchema/VoteAction.ts +++ b/packages/component/src/types/external/OrgSchema/VoteAction.ts @@ -7,11 +7,12 @@ import { isThingOf, type Thing } from './Thing'; * * @see https://schema.org/VoteAction */ -export type VoteAction = Thing<'VoteAction'> & { - /** A sub property of object. The options subject to this action. Supersedes [option](https://schema.org/option). */ - actionOption?: string; -}; +export type VoteAction = Thing<'VoteAction'> & + Readonly<{ + /** A sub property of object. The options subject to this action. Supersedes [option](https://schema.org/option). */ + actionOption?: string; + }>; -export function isVoteAction(thing: Thing): thing is VoteAction { +export function isVoteAction(thing: any): thing is VoteAction { return isThingOf(thing, 'VoteAction'); } From 6378e648922b6c22131faacb309296a4451e39ec Mon Sep 17 00:00:00 2001 From: William Wong Date: Mon, 16 Oct 2023 10:00:34 +0000 Subject: [PATCH 08/34] Combine Star and StarFilled --- .../CustomerSatisfactory/private/Star.tsx | 29 +++++++++---------- .../private/StarButton.tsx | 3 +- .../private/StarFilled.tsx | 22 -------------- 3 files changed, 15 insertions(+), 39 deletions(-) delete mode 100644 packages/component/src/Attachment/CustomerSatisfactory/private/StarFilled.tsx diff --git a/packages/component/src/Attachment/CustomerSatisfactory/private/Star.tsx b/packages/component/src/Attachment/CustomerSatisfactory/private/Star.tsx index 49253d6c31..61a6ddb540 100644 --- a/packages/component/src/Attachment/CustomerSatisfactory/private/Star.tsx +++ b/packages/component/src/Attachment/CustomerSatisfactory/private/Star.tsx @@ -1,21 +1,20 @@ import React from 'react'; -type Props = Readonly<{ className?: string }>; +type Props = Readonly<{ filled?: boolean }>; -const Star = ({ className }: Props) => ( - - +const Star = ({ filled }: Props) => ( + + {filled ? ( + + ) : ( + + )} ); diff --git a/packages/component/src/Attachment/CustomerSatisfactory/private/StarButton.tsx b/packages/component/src/Attachment/CustomerSatisfactory/private/StarButton.tsx index 7f28c633c4..9ac97fe2db 100644 --- a/packages/component/src/Attachment/CustomerSatisfactory/private/StarButton.tsx +++ b/packages/component/src/Attachment/CustomerSatisfactory/private/StarButton.tsx @@ -4,7 +4,6 @@ import React, { ReactEventHandler, useCallback } from 'react'; import { type RatingValue } from './RatingValue'; import Star from './Star'; -import StarFilled from './StarFilled'; import useItemRef from '../../../providers/RovingTabIndex/useItemRef'; import useStrings from './useStrings'; @@ -47,7 +46,7 @@ const StarButton = ({ checked, className, disabled, onClick, value }: Props) => tabIndex={disabled ? -1 : undefined} type="button" > - {checked ? : } + ); }; diff --git a/packages/component/src/Attachment/CustomerSatisfactory/private/StarFilled.tsx b/packages/component/src/Attachment/CustomerSatisfactory/private/StarFilled.tsx deleted file mode 100644 index ab0949dc51..0000000000 --- a/packages/component/src/Attachment/CustomerSatisfactory/private/StarFilled.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; - -type Props = Readonly<{ className?: string }>; - -const StarFilled = ({ className }: Props) => ( - - - -); - -export default StarFilled; From 70f2417cd02fe47b0296bb691df88e2bfd169a78 Mon Sep 17 00:00:00 2001 From: William Wong Date: Mon, 16 Oct 2023 10:06:47 +0000 Subject: [PATCH 09/34] Remove unused className prop --- .../CustomerSatisfactory/private/Checkmark.tsx | 14 ++------------ .../CustomerSatisfactory/private/StarBar.tsx | 6 ++---- .../CustomerSatisfactory/private/StarButton.tsx | 6 ++---- 3 files changed, 6 insertions(+), 20 deletions(-) diff --git a/packages/component/src/Attachment/CustomerSatisfactory/private/Checkmark.tsx b/packages/component/src/Attachment/CustomerSatisfactory/private/Checkmark.tsx index 878e5a7d4b..4a5027b9cb 100644 --- a/packages/component/src/Attachment/CustomerSatisfactory/private/Checkmark.tsx +++ b/packages/component/src/Attachment/CustomerSatisfactory/private/Checkmark.tsx @@ -1,17 +1,7 @@ import React from 'react'; -type Props = Readonly<{ className?: string }>; - -const Checkmark = ({ className }: Props) => ( - +const Checkmark = () => ( + void; value?: RatingValue | undefined; }>; -const StarBar = ({ className, disabled, onChange, value }: Props) => { +const StarBar = ({ disabled, onChange, value }: Props) => { const onChangeRef = useRefFrom(onChange); const handleStarButtonClick = useCallback((rating: RatingValue) => onChangeRef.current?.(rating), [onChangeRef]); return ( -
+
= 1} disabled={disabled} onClick={handleStarButtonClick} value={1} /> {/* eslint-disable-next-line no-magic-numbers */} = 2} disabled={disabled} onClick={handleStarButtonClick} value={2} /> diff --git a/packages/component/src/Attachment/CustomerSatisfactory/private/StarButton.tsx b/packages/component/src/Attachment/CustomerSatisfactory/private/StarButton.tsx index 9ac97fe2db..8d18692a4b 100644 --- a/packages/component/src/Attachment/CustomerSatisfactory/private/StarButton.tsx +++ b/packages/component/src/Attachment/CustomerSatisfactory/private/StarButton.tsx @@ -1,5 +1,4 @@ import { useRefFrom } from 'use-ref-from'; -import classNames from 'classnames'; import React, { ReactEventHandler, useCallback } from 'react'; import { type RatingValue } from './RatingValue'; @@ -9,13 +8,12 @@ import useStrings from './useStrings'; type Props = Readonly<{ checked?: boolean | undefined; - className?: boolean | undefined; disabled?: boolean | undefined; onClick?: (index: RatingValue) => void; value: RatingValue; }>; -const StarButton = ({ checked, className, disabled, onClick, value }: Props) => { +const StarButton = ({ checked, disabled, onClick, value }: Props) => { const { getRatingAltText } = useStrings(); const disabledRef = useRefFrom(disabled); const onClickRef = useRefFrom(onClick); @@ -36,7 +34,7 @@ const StarButton = ({ checked, className, disabled, onClick, value }: Props) => aria-checked={checked} aria-disabled={disabled} aria-label={getRatingAltText(value)} - className={classNames(className, 'webchat__customer-satisfactory__star-button')} + className="webchat__customer-satisfactory__star-button" onClick={handleClickAndFocus} onFocus={handleClickAndFocus} ref={ref} From edd29a719e9eeabf5d2d9ffae1e9f0a4211c4387 Mon Sep 17 00:00:00 2001 From: William Wong Date: Mon, 16 Oct 2023 10:07:53 +0000 Subject: [PATCH 10/34] Read reviewAction from prop --- .../CustomerSatisfactoryForScreenReader.tsx | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/component/src/Attachment/CustomerSatisfactory/CustomerSatisfactoryForScreenReader.tsx b/packages/component/src/Attachment/CustomerSatisfactory/CustomerSatisfactoryForScreenReader.tsx index a42c3d4ceb..4ba2244994 100644 --- a/packages/component/src/Attachment/CustomerSatisfactory/CustomerSatisfactoryForScreenReader.tsx +++ b/packages/component/src/Attachment/CustomerSatisfactory/CustomerSatisfactoryForScreenReader.tsx @@ -1,5 +1,6 @@ import React from 'react'; +import { ActionStatusType } from '../../types/external/OrgSchema/ActionStatusType'; import { type ReviewAction } from '../../types/external/OrgSchema/ReviewAction'; import useStrings from './private/useStrings'; import useUniqueId from '../../hooks/internal/useUniqueId'; @@ -9,25 +10,31 @@ type Props = Readonly<{ }>; const CustomerSatisfactoryForScreenReader = ({ initialReviewAction }: Props) => { - const { getRatingAltText, submitButtonText } = useStrings(); + const { getRatingAltText, submitButtonText, submittedText } = useStrings(); const labelId = useUniqueId('webchat__customer-satisfactory'); + const rawRatingValue = initialReviewAction.resultReview?.reviewRating?.ratingValue; + const submitted = initialReviewAction.actionStatus === ActionStatusType.CompletedActionStatus; + + const value = typeof rawRatingValue === 'number' ? rawRatingValue : 0; return (
{/* eslint-disable-next-line react/forbid-dom-props */}

{initialReviewAction.description}

-
- +
); }; From 13aa46719709930f1224d7f5ae766d2edb75bc66 Mon Sep 17 00:00:00 2001 From: William Wong Date: Mon, 16 Oct 2023 10:20:55 +0000 Subject: [PATCH 11/34] Fix END key --- .../src/providers/RovingTabIndex/RovingTabIndexComposer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/component/src/providers/RovingTabIndex/RovingTabIndexComposer.tsx b/packages/component/src/providers/RovingTabIndex/RovingTabIndexComposer.tsx index 56d41d625b..967dfc8b4a 100644 --- a/packages/component/src/providers/RovingTabIndex/RovingTabIndexComposer.tsx +++ b/packages/component/src/providers/RovingTabIndex/RovingTabIndexComposer.tsx @@ -37,7 +37,7 @@ const RovingTabIndexComposer = ({ children, onEscapeKey, orientation }: RovingTa let nextActiveItemIndex; if (typeof valueOrFunction === 'number') { - nextActiveItemIndex = valueOrFunction; + nextActiveItemIndex = valueOrFunction === Infinity ? itemRefsRef.current.length - 1 : valueOrFunction; } else { nextActiveItemIndex = valueOrFunction(activeItemIndexRef.current); } From 5672d36d601749ccf98bc719e19de60199559e48 Mon Sep 17 00:00:00 2001 From: William Wong Date: Mon, 16 Oct 2023 10:21:17 +0000 Subject: [PATCH 12/34] Fix aria-checked must always set --- .../src/Attachment/CustomerSatisfactory/private/StarButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/component/src/Attachment/CustomerSatisfactory/private/StarButton.tsx b/packages/component/src/Attachment/CustomerSatisfactory/private/StarButton.tsx index 8d18692a4b..907499d92f 100644 --- a/packages/component/src/Attachment/CustomerSatisfactory/private/StarButton.tsx +++ b/packages/component/src/Attachment/CustomerSatisfactory/private/StarButton.tsx @@ -31,7 +31,7 @@ const StarButton = ({ checked, disabled, onClick, value }: Props) => { return (