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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 996b513c21..53a2746639 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Resolves [#4841](https://github.com/microsoft/BotFramework-WebChat/issues/4841). Added link definitions UI in Markdown, by [@compulim](https://github.com/compulim), in PR [#4846](https://github.com/microsoft/BotFramework-WebChat/pull/4846) - Resolves [#4842](https://github.com/microsoft/BotFramework-WebChat/issues/4842). Added provenance in activity status, by [@compulim](https://github.com/compulim), in PR [#4846](https://github.com/microsoft/BotFramework-WebChat/pull/4846) - Resolves [#4856](https://github.com/microsoft/BotFramework-WebChat/issues/4856). Added types for `useStyleSet`, by [@compulim](https://github.com/compulim), in PR [#4857](https://github.com/microsoft/BotFramework-WebChat/pull/4857) +- Resolves [#2770](https://github.com/microsoft/BotFramework-WebChat/issues/2770). Added customer satisfactory (CSAT) card, by [@compulim](https://github.com/compulim), in PR [#4899](https://github.com/microsoft/BotFramework-WebChat/pull/4899) ## [4.15.9] - 2023-08-25 diff --git a/__tests__/__image_snapshots__/html/basic-js-customer-satisfactory-attachment-should-work-1-snap.png b/__tests__/__image_snapshots__/html/basic-js-customer-satisfactory-attachment-should-work-1-snap.png new file mode 100644 index 0000000000..eae6dec2ed Binary files /dev/null and b/__tests__/__image_snapshots__/html/basic-js-customer-satisfactory-attachment-should-work-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/basic-js-customer-satisfactory-attachment-should-work-2-snap.png b/__tests__/__image_snapshots__/html/basic-js-customer-satisfactory-attachment-should-work-2-snap.png new file mode 100644 index 0000000000..5d6836befe Binary files /dev/null and b/__tests__/__image_snapshots__/html/basic-js-customer-satisfactory-attachment-should-work-2-snap.png differ diff --git a/__tests__/__image_snapshots__/html/basic-js-customer-satisfactory-attachment-should-work-3-snap.png b/__tests__/__image_snapshots__/html/basic-js-customer-satisfactory-attachment-should-work-3-snap.png new file mode 100644 index 0000000000..87e39133a2 Binary files /dev/null and b/__tests__/__image_snapshots__/html/basic-js-customer-satisfactory-attachment-should-work-3-snap.png differ diff --git a/__tests__/__image_snapshots__/html/basic-js-customer-satisfactory-attachment-should-work-4-snap.png b/__tests__/__image_snapshots__/html/basic-js-customer-satisfactory-attachment-should-work-4-snap.png new file mode 100644 index 0000000000..5d6836befe Binary files /dev/null and b/__tests__/__image_snapshots__/html/basic-js-customer-satisfactory-attachment-should-work-4-snap.png differ diff --git a/__tests__/__image_snapshots__/html/basic-js-customer-satisfactory-attachment-should-work-5-snap.png b/__tests__/__image_snapshots__/html/basic-js-customer-satisfactory-attachment-should-work-5-snap.png new file mode 100644 index 0000000000..53f2dbb5d2 Binary files /dev/null and b/__tests__/__image_snapshots__/html/basic-js-customer-satisfactory-attachment-should-work-5-snap.png differ diff --git a/__tests__/__image_snapshots__/html/basic-js-customer-satisfactory-attachment-should-work-6-snap.png b/__tests__/__image_snapshots__/html/basic-js-customer-satisfactory-attachment-should-work-6-snap.png new file mode 100644 index 0000000000..96a8d19891 Binary files /dev/null and b/__tests__/__image_snapshots__/html/basic-js-customer-satisfactory-attachment-should-work-6-snap.png differ diff --git a/__tests__/__image_snapshots__/html/basic-js-customer-satisfactory-attachment-should-work-7-snap.png b/__tests__/__image_snapshots__/html/basic-js-customer-satisfactory-attachment-should-work-7-snap.png new file mode 100644 index 0000000000..6186710905 Binary files /dev/null and b/__tests__/__image_snapshots__/html/basic-js-customer-satisfactory-attachment-should-work-7-snap.png differ diff --git a/__tests__/__image_snapshots__/html/basic-js-customer-satisfactory-attachment-should-work-8-snap.png b/__tests__/__image_snapshots__/html/basic-js-customer-satisfactory-attachment-should-work-8-snap.png new file mode 100644 index 0000000000..e42577ce60 Binary files /dev/null and b/__tests__/__image_snapshots__/html/basic-js-customer-satisfactory-attachment-should-work-8-snap.png differ diff --git a/__tests__/__image_snapshots__/html/completed-js-customer-satisfactory-attachment-with-completed-action-should-work-1-snap.png b/__tests__/__image_snapshots__/html/completed-js-customer-satisfactory-attachment-with-completed-action-should-work-1-snap.png new file mode 100644 index 0000000000..db61345b13 Binary files /dev/null and b/__tests__/__image_snapshots__/html/completed-js-customer-satisfactory-attachment-with-completed-action-should-work-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/no-tooltip-js-customer-satisfactory-attachment-without-tooltips-should-work-1-snap.png b/__tests__/__image_snapshots__/html/no-tooltip-js-customer-satisfactory-attachment-without-tooltips-should-work-1-snap.png new file mode 100644 index 0000000000..06b0a84b15 Binary files /dev/null and b/__tests__/__image_snapshots__/html/no-tooltip-js-customer-satisfactory-attachment-without-tooltips-should-work-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/target-url-js-customer-satisfactory-attachment-with-target-of-non-templated-url-should-work-1-snap.png b/__tests__/__image_snapshots__/html/target-url-js-customer-satisfactory-attachment-with-target-of-non-templated-url-should-work-1-snap.png new file mode 100644 index 0000000000..eae6dec2ed Binary files /dev/null and b/__tests__/__image_snapshots__/html/target-url-js-customer-satisfactory-attachment-with-target-of-non-templated-url-should-work-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/target-url-js-customer-satisfactory-attachment-with-target-of-non-templated-url-should-work-2-snap.png b/__tests__/__image_snapshots__/html/target-url-js-customer-satisfactory-attachment-with-target-of-non-templated-url-should-work-2-snap.png new file mode 100644 index 0000000000..5d6836befe Binary files /dev/null and b/__tests__/__image_snapshots__/html/target-url-js-customer-satisfactory-attachment-with-target-of-non-templated-url-should-work-2-snap.png differ diff --git a/__tests__/__image_snapshots__/html/target-url-js-customer-satisfactory-attachment-with-target-of-non-templated-url-should-work-3-snap.png b/__tests__/__image_snapshots__/html/target-url-js-customer-satisfactory-attachment-with-target-of-non-templated-url-should-work-3-snap.png new file mode 100644 index 0000000000..758be403a4 Binary files /dev/null and b/__tests__/__image_snapshots__/html/target-url-js-customer-satisfactory-attachment-with-target-of-non-templated-url-should-work-3-snap.png differ diff --git a/__tests__/__image_snapshots__/html/target-url-js-customer-satisfactory-attachment-with-target-of-non-templated-url-should-work-4-snap.png b/__tests__/__image_snapshots__/html/target-url-js-customer-satisfactory-attachment-with-target-of-non-templated-url-should-work-4-snap.png new file mode 100644 index 0000000000..3a8d92f5dc Binary files /dev/null and b/__tests__/__image_snapshots__/html/target-url-js-customer-satisfactory-attachment-with-target-of-non-templated-url-should-work-4-snap.png differ diff --git a/__tests__/html/attachment/customerSatisfactory/basic.html b/__tests__/html/attachment/customerSatisfactory/basic.html new file mode 100644 index 0000000000..70ddb53be6 --- /dev/null +++ b/__tests__/html/attachment/customerSatisfactory/basic.html @@ -0,0 +1,147 @@ + + + + + + + + + +
+ + + diff --git a/__tests__/html/attachment/customerSatisfactory/basic.js b/__tests__/html/attachment/customerSatisfactory/basic.js new file mode 100644 index 0000000000..0eafbe1895 --- /dev/null +++ b/__tests__/html/attachment/customerSatisfactory/basic.js @@ -0,0 +1,5 @@ +/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */ + +describe('customer satisfactory attachment', () => { + test('should work', () => runHTML('attachment/customerSatisfactory/basic')); +}); diff --git a/__tests__/html/attachment/customerSatisfactory/completed.html b/__tests__/html/attachment/customerSatisfactory/completed.html new file mode 100644 index 0000000000..de23320d05 --- /dev/null +++ b/__tests__/html/attachment/customerSatisfactory/completed.html @@ -0,0 +1,76 @@ + + + + + + + + + +
+ + + diff --git a/__tests__/html/attachment/customerSatisfactory/completed.js b/__tests__/html/attachment/customerSatisfactory/completed.js new file mode 100644 index 0000000000..b9fa7c3122 --- /dev/null +++ b/__tests__/html/attachment/customerSatisfactory/completed.js @@ -0,0 +1,5 @@ +/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */ + +describe('customer satisfactory attachment with completed action', () => { + test('should work', () => runHTML('attachment/customerSatisfactory/completed')); +}); diff --git a/__tests__/html/attachment/customerSatisfactory/imBack.title.html b/__tests__/html/attachment/customerSatisfactory/imBack.title.html new file mode 100644 index 0000000000..8c37b6b546 --- /dev/null +++ b/__tests__/html/attachment/customerSatisfactory/imBack.title.html @@ -0,0 +1,76 @@ + + + + + + + + + +
+ + + diff --git a/__tests__/html/attachment/customerSatisfactory/imBack.title.js b/__tests__/html/attachment/customerSatisfactory/imBack.title.js new file mode 100644 index 0000000000..8cade20ee8 --- /dev/null +++ b/__tests__/html/attachment/customerSatisfactory/imBack.title.js @@ -0,0 +1,5 @@ +/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */ + +describe('customer satisfactory attachment with imBack of title', () => { + test('should work', () => runHTML('attachment/customerSatisfactory/imBack.title')); +}); diff --git a/__tests__/html/attachment/customerSatisfactory/imBack.value.html b/__tests__/html/attachment/customerSatisfactory/imBack.value.html new file mode 100644 index 0000000000..ebfa641967 --- /dev/null +++ b/__tests__/html/attachment/customerSatisfactory/imBack.value.html @@ -0,0 +1,76 @@ + + + + + + + + + +
+ + + diff --git a/__tests__/html/attachment/customerSatisfactory/imBack.value.js b/__tests__/html/attachment/customerSatisfactory/imBack.value.js new file mode 100644 index 0000000000..e38cae2674 --- /dev/null +++ b/__tests__/html/attachment/customerSatisfactory/imBack.value.js @@ -0,0 +1,5 @@ +/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */ + +describe('customer satisfactory attachment with imBack of value', () => { + test('should work', () => runHTML('attachment/customerSatisfactory/imBack.value')); +}); diff --git a/__tests__/html/attachment/customerSatisfactory/liveRegion.completed.html b/__tests__/html/attachment/customerSatisfactory/liveRegion.completed.html new file mode 100644 index 0000000000..3595956a80 --- /dev/null +++ b/__tests__/html/attachment/customerSatisfactory/liveRegion.completed.html @@ -0,0 +1,56 @@ + + + + + + + + + +
+ + + diff --git a/__tests__/html/attachment/customerSatisfactory/liveRegion.completed.js b/__tests__/html/attachment/customerSatisfactory/liveRegion.completed.js new file mode 100644 index 0000000000..97bdb26f5c --- /dev/null +++ b/__tests__/html/attachment/customerSatisfactory/liveRegion.completed.js @@ -0,0 +1,5 @@ +/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */ + +describe('customer satisfactory attachment with completed action', () => { + test('live region should work', () => runHTML('attachment/customerSatisfactory/liveRegion.completed')); +}); diff --git a/__tests__/html/attachment/customerSatisfactory/liveRegion.html b/__tests__/html/attachment/customerSatisfactory/liveRegion.html new file mode 100644 index 0000000000..03dd5797bb --- /dev/null +++ b/__tests__/html/attachment/customerSatisfactory/liveRegion.html @@ -0,0 +1,68 @@ + + + + + + + + + +
+ + + diff --git a/__tests__/html/attachment/customerSatisfactory/liveRegion.js b/__tests__/html/attachment/customerSatisfactory/liveRegion.js new file mode 100644 index 0000000000..04cc26d2f4 --- /dev/null +++ b/__tests__/html/attachment/customerSatisfactory/liveRegion.js @@ -0,0 +1,5 @@ +/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */ + +describe('customer satisfactory attachment', () => { + test('live region should work', () => runHTML('attachment/customerSatisfactory/liveRegion')); +}); diff --git a/__tests__/html/attachment/customerSatisfactory/messageBack.html b/__tests__/html/attachment/customerSatisfactory/messageBack.html new file mode 100644 index 0000000000..93bccd5c38 --- /dev/null +++ b/__tests__/html/attachment/customerSatisfactory/messageBack.html @@ -0,0 +1,79 @@ + + + + + + + + + +
+ + + diff --git a/__tests__/html/attachment/customerSatisfactory/messageBack.js b/__tests__/html/attachment/customerSatisfactory/messageBack.js new file mode 100644 index 0000000000..ceb5b5edb8 --- /dev/null +++ b/__tests__/html/attachment/customerSatisfactory/messageBack.js @@ -0,0 +1,5 @@ +/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */ + +describe('customer satisfactory attachment with messageBack', () => { + test('should work', () => runHTML('attachment/customerSatisfactory/messageBack')); +}); diff --git a/__tests__/html/attachment/customerSatisfactory/noTooltip.html b/__tests__/html/attachment/customerSatisfactory/noTooltip.html new file mode 100644 index 0000000000..b115f11fad --- /dev/null +++ b/__tests__/html/attachment/customerSatisfactory/noTooltip.html @@ -0,0 +1,74 @@ + + + + + + + + + +
+ + + diff --git a/__tests__/html/attachment/customerSatisfactory/noTooltip.js b/__tests__/html/attachment/customerSatisfactory/noTooltip.js new file mode 100644 index 0000000000..c93034fb2b --- /dev/null +++ b/__tests__/html/attachment/customerSatisfactory/noTooltip.js @@ -0,0 +1,5 @@ +/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */ + +describe('customer satisfactory attachment without tooltips', () => { + test('should work', () => runHTML('attachment/customerSatisfactory/noTooltip')); +}); diff --git a/__tests__/html/attachment/customerSatisfactory/postBack.string.html b/__tests__/html/attachment/customerSatisfactory/postBack.string.html new file mode 100644 index 0000000000..7e68895523 --- /dev/null +++ b/__tests__/html/attachment/customerSatisfactory/postBack.string.html @@ -0,0 +1,77 @@ + + + + + + + + + +
+ + + diff --git a/__tests__/html/attachment/customerSatisfactory/postBack.string.js b/__tests__/html/attachment/customerSatisfactory/postBack.string.js new file mode 100644 index 0000000000..4e4eb0b592 --- /dev/null +++ b/__tests__/html/attachment/customerSatisfactory/postBack.string.js @@ -0,0 +1,5 @@ +/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */ + +describe('customer satisfactory attachment with postBack of string', () => { + test('should work', () => runHTML('attachment/customerSatisfactory/postBack.string')); +}); diff --git a/__tests__/html/attachment/customerSatisfactory/target.url.html b/__tests__/html/attachment/customerSatisfactory/target.url.html new file mode 100644 index 0000000000..0e2a9f4a93 --- /dev/null +++ b/__tests__/html/attachment/customerSatisfactory/target.url.html @@ -0,0 +1,85 @@ + + + + + + + + + +
+ + + diff --git a/__tests__/html/attachment/customerSatisfactory/target.url.js b/__tests__/html/attachment/customerSatisfactory/target.url.js new file mode 100644 index 0000000000..53b00b8915 --- /dev/null +++ b/__tests__/html/attachment/customerSatisfactory/target.url.js @@ -0,0 +1,5 @@ +/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */ + +describe('customer satisfactory attachment', () => { + test('with target of non-templated URL should work', () => runHTML('attachment/customerSatisfactory/target.url')); +}); diff --git a/__tests__/html/suggestedActions.styleOptions.html b/__tests__/html/suggestedActions.styleOptions.html index dd238907ad..9b902ccb7a 100644 --- a/__tests__/html/suggestedActions.styleOptions.html +++ b/__tests__/html/suggestedActions.styleOptions.html @@ -1,4 +1,4 @@ - + @@ -9,224 +9,236 @@
diff --git a/docs/CUSTOMER_SATISFACTORY_CARD.md b/docs/CUSTOMER_SATISFACTORY_CARD.md new file mode 100644 index 0000000000..88778d9219 --- /dev/null +++ b/docs/CUSTOMER_SATISFACTORY_CARD.md @@ -0,0 +1,121 @@ +# Customer Satisfactory Card (CSAT card) + +> This is related to PR [#4899](https://github.com/microsoft/BotFramework-WebChat/pull/4899). + +## Description + +Power Virtual Agents and many customers implemented customer satisfactory (CSAT) cards using Adaptive Cards. However, as Adaptive Cards is designed for general purpose. Thus, the accessibility experience is not great for CSAT cards. + +We are adding new attachment renderer for CSAT cards by leveraging https://schema.org/ReviewAction. + +![image](https://github.com/microsoft/BotFramework-WebChat/assets/1622400/d60092de-3f71-4914-8a08-5f16128ea9e2) + +![image](https://github.com/microsoft/BotFramework-WebChat/assets/1622400/902dffff-a2fe-4163-8182-0a58969f0f33) + +## Design + +We are using https://schema.org/ReviewAction for the representation of the CSAT card. The following is excerpt of the activity containing the CSAT card. + +```json +{ + "type": "message", + "attachments": [ + { + "content": { + "@context": "https://schema.org", + "@type": "ReviewAction", + "actionStatus": "PotentialActionStatus", + "description": "Great! Please rate your experience.", + "resultReview": { + "@type": "Review", + "reviewRating": { + "@type": "Rating", + "ratingValue-input": { + "@type": "PropertyValueSpecification", + "valueName": "rate" + } + } + }, + "target": { + "@type": "EntryPoint", + "actionPlatform": "https://directline.botframework.com", + "urlTemplate": "ms-directline-postback:?value={rate}" + } + }, + "contentType": "application/ld+json" + } + ] +} +``` + +### Explanation of fields + +| Property | Description | +| ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | +| `content['@context']` | Must be `"https://schema.org"`. | +| `content['@type']` | Must be [`"ReviewAction"`](https://schema.org/ReviewAction). | +| `content.actionStatus` | If value is [`"CompletedActionStatus"`](https://schema.org/ActionStatusType), the card is submitted. Otherwise, the card is ready to submit. | +| `content.description` | Text of prompting the user for rating. | +| `content.resultReview['@type']` | Must be [`"Review"`](https://schema.org/Review). | +| `content.resultReview.reviewRating['@type']` | Must be [`"Rating"`](https://schema.org/Rating). | +| `content.resultReview.reviewRating.ratingValue` | Initial value of rating, must be `1` to `5` or `undefined`, default to `undefined`. | +| `content.resultReview.reviewRating['ratingValue-input']` | How `ratingValue` should be used as "input" parameters (a.k.a. request body). See https://schema.org/docs/actions.html for details. | +| `content.resultReview.reviewRating['ratingValue-input']['@type']` | Must be [`"PropertyValueSpecification"`](https://schema.org/PropertyValueSpecification). | +| `content.resultReview.reviewRating['ratingValue-input'].valueName` | The name used in URL template. | +| `content.target` | Must be URL or thing of `"EntryPoint"`. | +| `content.target['@type']` | Must be [`"EntryPoint"`](https://schema.org/EntryPoint). | +| `content.target.actionPlatform` | Must be `"https://directline.botframework.com"`. | +| `content.target.urlTemplate` | [RFC 6570 URL template](https://datatracker.ietf.org/doc/html/rfc6570) to send the action, see below. | +| `contentType` | Must be `"application/ld+json"`. | + +Current designs and limitations: + +- Subclasses are not supported. `resultReview` must be thing of [`"Review"`](https://schema.org/Review). [`"UserReview"`](https://schema.org/UserReview) and other subclass of `"Review"` is not supported +- Rating must be 1 to 5 stars. [`resultReview.reviewRating.bestRating`](https://schema.org/bestRating) and [`resultReview.reviewRating.worstRating`](https://schema.org/worstRating) are ignored +- If `actionStatus` is [`CompletedActionStatus`](https://schema.org/ActionStatusType), it indicates the review is submitted, thus, `target` is optional. Otherwise, review should be submittable and `target` must be specified +- Despite `target` could be a non-templated URL, it is highly recommended `target` is a thing of [`EntryPoint`](https://schema.org/EntryPoint) + +### Target URL + +> Currently, only `ratingValue-input` is supported. In the future, all fields in the Thing will be supported. + +When the user change the rating, it will change the `resultReview.reviewRating.ratingValue`. In Schema.org actions, the `ratingValue-input` will be used to describe how the `ratingValue` is used in the request body of the action. + +In the example payload, `ratingValue-input.valueName` is `rate`. When filling the `target.urlTemplate`, it will replace the variable `rate` with the user rating. + +If the user rated 4, the URL template will be expanded as by replacing variable `rate` with `4`: + +```diff +- ms-directline-postback:?value={rate} ++ ms-directline-postback:?value=4 +``` + +Thus, the postback activity payload will become: + +```json +{ + "channelData": { + "postBack": true + }, + "text": "4", + "type": "message" +} +``` + +> Note: when using postback for a string value, it will send via `text` field. + +Followings are supported protocol and their query parameters: + +| Protocol | Parameters | Description | +| ---------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `ms-directline-postback:` | `value` | If `valuetype` is `application/json`, will send as JSON via `activity.value`, otherwise, will send as string via `activity.text`. | +| `ms-directline-postback:` | `valuetype` | If `value` is a complex object, specify "application/json". This is not recommended because non-compliance with Direct Line protocol. | +| `ms-directline-imback:` | `title` | Send as string via `activity.text`. | +| `ms-directline-imback:` | `value` | If `title` parameter is not specified, will send this value as `activity.text`. | +| `ms-directline-messageback:` | `value` | Send as complex object via `activity.value`. | +| `ms-directline-messageback:` | `text` | Send as string via `activity.text`. | +| `ms-directline-messageback:` | `displaytext` | String to display as in the sent message. | + +> `http:`/`https:` are not supported at the moment. + +> For details of parameters, please refer to [Direct Line Activity spec](https://github.com/Microsoft/botframework-sdk/blob/main/specs/botframework-activity/botframework-activity.md). diff --git a/jest.config.js b/jest.config.js index f0bed07004..b79bd440ce 100644 --- a/jest.config.js +++ b/jest.config.js @@ -106,7 +106,7 @@ module.exports = { // jest-environment-jsdom import packages as browser. // Packages, such as "uuid", export itself for browser as ES5 + ESM. // Since jest@28 cannot consume ESM yet, we need to transpile these packages. - '/node_modules/(?!(character-entities|decode-named-character-reference|micromark|micromark-core-commonmark|micromark-factory-destination|micromark-factory-label|micromark-factory-space|micromark-factory-title|micromark-factory-whitespace|micromark-util-resolve-all|micromark-util-character|micromark-util-chunked|micromark-util-classify-character|micromark-util-combine-extensions|micromark-util-decode-numeric-character-reference|micromark-util-decode-string|micromark-util-encode|micromark-util-html-tag-name|micromark-util-normalize-identifier|micromark-util-sanitize-uri|micromark-util-subtokenize|mdast-util-from-markdown|mdast-util-to-string|unist-util-stringify-position|uuid)/)', + '/node_modules/(?!(character-entities|decode-named-character-reference|micromark|micromark-core-commonmark|micromark-factory-destination|micromark-factory-label|micromark-factory-space|micromark-factory-title|micromark-factory-whitespace|micromark-util-resolve-all|micromark-util-character|micromark-util-chunked|micromark-util-classify-character|micromark-util-combine-extensions|micromark-util-decode-numeric-character-reference|micromark-util-decode-string|micromark-util-encode|micromark-util-html-tag-name|micromark-util-normalize-identifier|micromark-util-sanitize-uri|micromark-util-subtokenize|mdast-util-from-markdown|mdast-util-to-string|url-template|unist-util-stringify-position|uuid)/)', ...defaults.transformIgnorePatterns.filter(pattern => pattern !== '/node_modules/') ] }; 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", 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/ActivityStatus/OthersActivityStatus.tsx b/packages/component/src/ActivityStatus/OthersActivityStatus.tsx index ac76fb25ff..5b95f5079f 100644 --- a/packages/component/src/ActivityStatus/OthersActivityStatus.tsx +++ b/packages/component/src/ActivityStatus/OthersActivityStatus.tsx @@ -3,7 +3,7 @@ import classNames from 'classnames'; import React, { memo, type ReactNode, useMemo } from 'react'; import { isReplyAction, type ReplyAction } from '../types/external/OrgSchema/ReplyAction'; -import { isThing, type Thing } from '../types/external/OrgSchema/Thing'; +import { isThingAsEntity, type Thing } from '../types/external/OrgSchema/Thing'; import { isVoteAction, type VoteAction } from '../types/external/OrgSchema/VoteAction'; import { type TypeOfArray } from '../types/internal/TypeOfArray'; import Feedback from './private/Feedback/Feedback'; @@ -32,7 +32,7 @@ const OthersActivityStatus = memo(({ activity }: Props) => { const entities = activity.entities as Array | undefined; const replyAction = entities?.find( - (entity): entity is ReplyAction => isThing(entity) && isReplyAction(entity) + (entity): entity is ReplyAction => isThingAsEntity(entity) && isReplyAction(entity) ); const { timestamp } = activity; @@ -43,7 +43,7 @@ const OthersActivityStatus = memo(({ activity }: Props) => { new Set( (entities || []).filter( (entity): entity is DownvoteAction | UpvoteAction => - isThing(entity) && isVoteAction(entity) && (isDownvoteAction(entity) || isUpvoteAction(entity)) + isThingAsEntity(entity) && isVoteAction(entity) && (isDownvoteAction(entity) || isUpvoteAction(entity)) ) ) ), diff --git a/packages/component/src/Attachment/CustomerSatisfactory/CustomerSatisfactory.tsx b/packages/component/src/Attachment/CustomerSatisfactory/CustomerSatisfactory.tsx new file mode 100644 index 0000000000..155732c400 --- /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 || { '@type': 'Review' }), + reviewRating: { + ...(reviewAction.resultReview?.reviewRating || { '@type': 'Rating' }), + 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..4ba2244994 --- /dev/null +++ b/packages/component/src/Attachment/CustomerSatisfactory/CustomerSatisfactoryForScreenReader.tsx @@ -0,0 +1,42 @@ +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'; + +type Props = Readonly<{ + initialReviewAction: ReviewAction; +}>; + +const CustomerSatisfactoryForScreenReader = ({ initialReviewAction }: Props) => { + 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}

+
+ +
+ ); +}; + +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..3b7771a128 --- /dev/null +++ b/packages/component/src/Attachment/CustomerSatisfactory/private/Checkmark.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const Checkmark = () => ( + + + +); + +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..61a6ddb540 --- /dev/null +++ b/packages/component/src/Attachment/CustomerSatisfactory/private/Star.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +type Props = Readonly<{ filled?: boolean }>; + +const Star = ({ filled }: Props) => ( + + {filled ? ( + + ) : ( + + )} + +); + +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..0d5261ff4c --- /dev/null +++ b/packages/component/src/Attachment/CustomerSatisfactory/private/StarBar.tsx @@ -0,0 +1,73 @@ +import { useRefFrom } from 'use-ref-from'; +import React, { useCallback } from 'react'; + +import { type RatingValue } from './RatingValue'; +import { type ReviewAction } from '../../../types/external/OrgSchema/ReviewAction'; +import StarButton from './StarButton'; + +type Props = Readonly<{ + disabled?: boolean | undefined; + onChange?: (value: RatingValue) => void; + reviewAction: ReviewAction; + value?: RatingValue | undefined; +}>; + +const StarBar = ({ disabled, onChange, reviewAction }: Props) => { + const onChangeRef = useRefFrom(onChange); + const rawValue = reviewAction.resultReview?.reviewRating?.ratingValue; + const titles: [string, string, string, string, string] | undefined = + reviewAction.resultReview?.reviewRating?.description; + const value = typeof rawValue === 'number' ? rawValue : undefined; + + const handleStarButtonClick = useCallback((rating: RatingValue) => onChangeRef.current?.(rating), [onChangeRef]); + + return ( +
+ = 1} + disabled={disabled} + onClick={handleStarButtonClick} + title={titles?.[0]} + value={1} + /> + = 2} + disabled={disabled} + onClick={handleStarButtonClick} + title={titles?.[1]} + value={2} + /> + = 3} + disabled={disabled} + onClick={handleStarButtonClick} + // eslint-disable-next-line no-magic-numbers + title={titles?.[2]} + value={3} + /> + = 4} + disabled={disabled} + onClick={handleStarButtonClick} + // eslint-disable-next-line no-magic-numbers + title={titles?.[3]} + value={4} + /> + = 5} + disabled={disabled} + onClick={handleStarButtonClick} + // eslint-disable-next-line no-magic-numbers + title={titles?.[4]} + 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..20480d1551 --- /dev/null +++ b/packages/component/src/Attachment/CustomerSatisfactory/private/StarButton.tsx @@ -0,0 +1,54 @@ +import { useRefFrom } from 'use-ref-from'; +import React, { ReactEventHandler, useCallback } from 'react'; + +import { type RatingValue } from './RatingValue'; +import Star from './Star'; +import useItemRef from '../../../providers/RovingTabIndex/useItemRef'; +import useStrings from './useStrings'; + +type Props = Readonly<{ + checked?: boolean | undefined; + disabled?: boolean | undefined; + onClick?: (index: RatingValue) => void; + title?: string | undefined; + value: RatingValue; +}>; + +const StarButton = ({ checked, disabled, onClick, title, 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/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..032d120500 --- /dev/null +++ b/packages/component/src/Attachment/CustomerSatisfactory/private/schema/reviewActionSchema.ts @@ -0,0 +1,104 @@ +import { + array, + custom, + enumType, + length, + maxValue, + minValue, + number, + optional, + string, + type ArraySchema, + type Output, + type StringSchema, + 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', { + description: optional( + array( + string(), + `"resultReview.reviewRating.description" must be an array with 5 elements of type "string"`, + // Currently, we expect 5 tooltips for rating 1-5. + // eslint-disable-next-line no-magic-numbers + [length(5)] + ) as ArraySchema< + StringSchema, + [ + Output, + Output, + Output, + Output, + Output + ] + > + ), + 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: optional( + 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' + ) + ) + }, + [ + custom( + input => input.actionStatus === ActionStatusType.CompletedActionStatus || !!input.target, + '"target" must be present if "actionStatus" is not "CompletedActionStatus"' + ) + ] +); + +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..275632fe57 --- /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 + }); +} 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..19bad3fcaf 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, isThingAsEntity } 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 => isThingAsEntity(entity) && isClaim(entity) && entity['@id'] === url ); claim && showClaimModal(claim); }, - [entities, showClaimModal] + [entitiesRef, showClaimModal] ); const handleClick = useCallback>( @@ -79,8 +81,9 @@ 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 => + isThingAsEntity(entity) && isClaim(entity) && entity['@id'] === buttonElement.value ); if (!claim) { 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/Middleware/AttachmentForScreenReader/createCoreMiddleware.tsx b/packages/component/src/Middleware/AttachmentForScreenReader/createCoreMiddleware.tsx index c254663da4..baf332538a 100644 --- a/packages/component/src/Middleware/AttachmentForScreenReader/createCoreMiddleware.tsx +++ b/packages/component/src/Middleware/AttachmentForScreenReader/createCoreMiddleware.tsx @@ -1,6 +1,7 @@ import { AttachmentForScreenReaderMiddleware } from 'botframework-webchat-api'; import React from 'react'; +import { forScreenReader as customerSatisfactoryForScreenReader } from '../../Attachment/CustomerSatisfactory/customerSatisfactoryMiddleware'; import AudioAttachment from './AudioAttachment'; import FileAttachment from './FileAttachment'; import ImageAttachment from './ImageAttachment'; @@ -9,6 +10,7 @@ import VideoAttachment from './VideoAttachment'; export default function createCoreMiddleware(): AttachmentForScreenReaderMiddleware[] { return [ + customerSatisfactoryForScreenReader, () => 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..3293914c04 --- /dev/null +++ b/packages/component/src/Styles/StyleSet/CustomerSatisfactoryAttachment.ts @@ -0,0 +1,149 @@ +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', + 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 + }, + + '&.webchat__customer-satisfactory--submitted .webchat__customer-satisfactory__submit-check-mark': { + color: '#107C10' + }, + + '&: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..ea1532c547 --- /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/providers/RovingTabIndex/RovingTabIndexComposer.tsx b/packages/component/src/providers/RovingTabIndex/RovingTabIndexComposer.tsx index 28c06db473..126083e99e 100644 --- a/packages/component/src/providers/RovingTabIndex/RovingTabIndexComposer.tsx +++ b/packages/component/src/providers/RovingTabIndex/RovingTabIndexComposer.tsx @@ -3,19 +3,24 @@ import PropTypes from 'prop-types'; import React, { useCallback, useEffect, useMemo, useRef } from 'react'; +import isNumber from './private/isNumber'; 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 +28,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]); @@ -33,7 +38,7 @@ const RovingTabIndexComposer: FC = ({ children, onEs let nextActiveItemIndex; if (typeof valueOrFunction === 'number') { - nextActiveItemIndex = valueOrFunction; + nextActiveItemIndex = valueOrFunction === Infinity ? itemRefsRef.current.length - 1 : valueOrFunction; } else { nextActiveItemIndex = valueOrFunction(activeItemIndexRef.current); } @@ -55,7 +60,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 +91,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; @@ -107,12 +112,9 @@ const RovingTabIndexComposer: FC = ({ children, onEs // Thus, the next item may not be immediately next to the current one. const itemIndices = itemRefsRef.current.map((_, index) => index); const nextIndex = itemIndices.indexOf(value) - 1; + const nextActiveItemIndex = nextIndex < 0 ? itemIndices[itemIndices.length - 1] : itemIndices[+nextIndex]; - if (nextIndex < 0) { - return itemIndices[itemIndices.length - 1]; - } - - return itemIndices[+nextIndex]; + return isNumber(nextActiveItemIndex) ? nextActiveItemIndex : undefined; }); break; @@ -144,20 +146,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 +183,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/private/isNumber.ts b/packages/component/src/providers/RovingTabIndex/private/isNumber.ts new file mode 100644 index 0000000000..e857f64e9e --- /dev/null +++ b/packages/component/src/providers/RovingTabIndex/private/isNumber.ts @@ -0,0 +1,3 @@ +export default function isNumber(value: unknown): value is number { + return typeof value === 'number'; +} 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(); 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, 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..06eac032bf 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: unknown): 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..2a7e04e236 --- /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: unknown, 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..b51422d40f 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: unknown): 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..81259a3a1e --- /dev/null +++ b/packages/component/src/types/external/OrgSchema/PropertyValueSpecification.ts @@ -0,0 +1,26 @@ +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: unknown, + 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..93214fb830 --- /dev/null +++ b/packages/component/src/types/external/OrgSchema/Rating.ts @@ -0,0 +1,31 @@ +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<{ + /** A description of the item. */ + description?: [string, string, string, string, string] | undefined; + + /** + * 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: unknown, 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..124e0fe566 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: unknown): 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..e90709cf6f --- /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: unknown, 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..f1dbeffac4 --- /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: unknown, currentContext?: string): thing is ReviewAction { + return isThingOf(thing, 'ReviewAction', currentContext); +} diff --git a/packages/component/src/types/external/OrgSchema/Thing.spec.ts b/packages/component/src/types/external/OrgSchema/Thing.spec.ts index 8a71b46bbd..5f3366d455 100644 --- a/packages/component/src/types/external/OrgSchema/Thing.spec.ts +++ b/packages/component/src/types/external/OrgSchema/Thing.spec.ts @@ -8,11 +8,6 @@ describe('isThing', () => { )); }); - test('should return false with conflicting type', () => - expect(isThing({ '@context': 'https://schema.org', '@type': 'Action', type: `https://schema.org/Person` })).toBe( - false - )); - test('should return false when conflict with unknown @context', () => expect(isThing({ '@context': 'https://abc.com', type: `https://schema.org/Person` } as any)).toBe(false)); @@ -39,11 +34,6 @@ describe('isThingOf', () => { isThingOf({ '@context': 'https://schema.org', '@type': 'Action', type: 'https://schema.org/Person' }, 'Person') ).toBe(false)); - test('should return false for Person with @context and @type of Person but type of Action', () => - expect( - isThingOf({ '@context': 'https://schema.org', '@type': 'Person', type: 'https://schema.org/Action' }, 'Person') - ).toBe(false)); - test('should return false for Person with @context, @type, and type of Action', () => expect( isThingOf({ '@context': 'https://schema.org', '@type': 'Action', type: 'https://schema.org/Action' }, 'Person') diff --git a/packages/component/src/types/external/OrgSchema/Thing.ts b/packages/component/src/types/external/OrgSchema/Thing.ts index 7aae6df6ee..971dfe2ca9 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,52 @@ 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: unknown, 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 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: unknown, 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; +} + +export function isThingAsEntity(thing: unknown, currentContext?: string): thing is AsEntity> { + // Needs bracket notation for TypeScript checking against `unknown`. + // eslint-disable-next-line dot-notation + if (typeof thing['type'] === 'string' && thing['type'].startsWith(`https://schema.org/`)) { + return isThing(thing, currentContext); } return false; diff --git a/packages/component/src/types/external/OrgSchema/VoteAction.ts b/packages/component/src/types/external/OrgSchema/VoteAction.ts index 3bcbe70766..708864875f 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: unknown): thing is VoteAction { return isThingOf(thing, 'VoteAction'); } 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; diff --git a/packages/test/harness/src/browser/globals/checkAccessibility.js b/packages/test/harness/src/browser/globals/checkAccessibility.js index 4bc7787a15..23e2160880 100644 --- a/packages/test/harness/src/browser/globals/checkAccessibility.js +++ b/packages/test/harness/src/browser/globals/checkAccessibility.js @@ -43,7 +43,7 @@ export default function initializeCheckAccessibility() { try { console.log('[TESTHARNESS] Accessibility checks started.'); - const results = await axe.run(); + const results = await axe.run(window.checkAccessibilityRunOptions); const { violations } = results; if (!violations?.length) { diff --git a/packages/test/harness/src/browser/globals/run.js b/packages/test/harness/src/browser/globals/run.js index 98da680c46..f2d04bff53 100644 --- a/packages/test/harness/src/browser/globals/run.js +++ b/packages/test/harness/src/browser/globals/run.js @@ -26,6 +26,8 @@ export default function () { document.body.prepend(header); + window.checkAccessibilityRunOptions = doneOptions?.axeCoreRunOptions || {}; + window.addEventListener('error', event => host.error(event.error)); // Run the test, signal start by host.ready().