diff --git a/source/community/reactnative/experimental/adaptive-card-builder/AdaptiveCardBuilder.js b/source/community/reactnative/experimental/adaptive-card-builder/AdaptiveCardBuilder.js new file mode 100644 index 0000000000..2d902d2c9c --- /dev/null +++ b/source/community/reactnative/experimental/adaptive-card-builder/AdaptiveCardBuilder.js @@ -0,0 +1,196 @@ + +import * as Constants from './constants'; + +/** + * @description - This builder creates adaptive card from `HeroCard` / `ThumbnailCard` . + * @param cardContent - Content of the card to be converted + * @param type - Type of the card. + */ +export const buildAdaptiveCard = (cardContent, type) => { + + const AdaptiveCard = { + "type": Constants.TypeAdaptiveCard, + "version": Constants.AdaptiveCardVersion, + "body": [], + "actions": [] + } + + const ColumnSet = { + "type": Constants.TypeColumnSet, + "columns": [ + { + "type": Constants.TypeColumn, + "width": Constants.ThumbNailWidth, + "items": [] + }, + { + "type": Constants.TypeColumn, + "width": Constants.WidthStretch, + "items": [] + } + ] + } + + if (cardContent && type) { + switch (type) { + case Constants.TypeHeroCard: + pushTextBlocks(cardContent, AdaptiveCard.body); + pushImages(cardContent, AdaptiveCard.body); + pushActions(cardContent, AdaptiveCard.actions); + break; + case Constants.TypeThumbnailCard: + pushImages(cardContent, ColumnSet.columns[0].items); + pushTextBlocks(cardContent, ColumnSet.columns[1].items); + pushActions(cardContent, ColumnSet.columns[1].items, true); + AdaptiveCard.body.push(ColumnSet); + delete AdaptiveCard.actions; + break; + default: + elementsContainer = []; + break; + } + + /** + * `tap` to container `selectAction` + */ + if (cardContent.tap && cardContent.tap.type && cardContent.tap.value) { + const body = AdaptiveCard.body; + AdaptiveCard.body = []; + const containerBody = {}; + containerBody.type = Constants.TypeContainer; + containerBody.items = body; + containerBody.selectAction = cardAction(cardContent.tap); + AdaptiveCard.body[0] = containerBody; + } + + } + return AdaptiveCard; +} + +/** + * @description - This method pushes text blocks to the adaptive card container. + * @param cardContent - Content of the card to be converted. + * @param textBlockContainer - Container where the tex blocks to be inserted + */ +pushTextBlocks = (cardContent, textBlockContainer) => { + if (isNotEmpty(cardContent.title)) + textBlockContainer.push(textBlock(cardContent.title, Constants.TypeTitle)); + if (isNotEmpty(cardContent.subtitle)) + textBlockContainer.push(textBlock(cardContent.subtitle, Constants.TypeSubTitle)); + if (isNotEmpty(cardContent.text)) + textBlockContainer.push(textBlock(cardContent.text)); + return textBlockContainer; +} + +/** + * @description - This method pushes images to the adaptive card container. + * @param cardContent - Content of the card to be converted. + * @param imageContainer - Container where the images to be inserted + */ +pushImages = (cardContent, imageContainer) => { + if (cardContent.images && cardContent.images.length > 0) { + cardContent.images.forEach(image => { + imageContainer.push(cardImage(image)); + }) + } +} + +/** + * @description - This method pushes actions to the adaptive card container. + * @param cardContent - Content of the card to be converted. + * @param actionContainer - Container where the actions to be inserted + * @param isNested - A boolean decides where the actions need to be added in the container + */ + pushActions = (cardContent, actionContainer, isNested) => { + if (cardContent.buttons && cardContent.buttons.length > 0) { + if (!isNested) { + cardContent.buttons.forEach(button => { + actionContainer.push(cardAction(button)); + }) + } else { + const nestedContainer = { + "type": Constants.TypeColumnSet + } + const columns = []; + cardContent.buttons.forEach(button => { + const column = { + "type": Constants.TypeColumn, + "items": [] + }; + column.items.push(cardAction(button)); + columns.push(column); + }) + nestedContainer.columns = columns; + actionContainer.push(nestedContainer);; + } + } +} + +/** + * @description - Convert card types `title, subtitle and text` to Adaptive card type format. + * @param content - The content to be displayed + * @param type - Element type to be converted. + */ +textBlock = (content, type) => { + const textBlock = {}; + textBlock.type = Constants.TypeTextBlock; + textBlock.text = content; + switch (type) { + case Constants.TypeTitle: + textBlock.size = Constants.SizeMedium; + textBlock.weight = Constants.WeightBold; + break; + case Constants.TypeSubTitle: + textBlock.isSubtle = true; + textBlock.wrap = true; + break; + default: + textBlock.wrap = true; + break; + } + return textBlock; + +} + +/** + * @description - Convert `button` to `actions` + * @param button - button content. + */ +cardAction = (button) => { + const action = Object.assign({}, button); + if (button.type === Constants.TypeOpenUrl) { + action.type = Constants.ActionOpenUrl; + action.url = button.value; + } else { + action.data = { + "type": button.type, + "data": button.value + }; + action.type = Constants.ActionSubmit; + } + delete action.value; + return action; +} + +/** + * @description - Format `image` properties to adaptive card format + * @param image - image content. + */ +cardImage = (image) => { + const cardImage = Object.assign({}, image); + cardImage.type = Constants.ImageType; + cardImage.size = Constants.ImageSize; + if (image.tap) { + cardImage.selectAction = cardAction(image.tap); + delete cardImage.tap; + } + return cardImage; +} + +/** + * @description - Checks whether a given value is empty or not. + * @param param - param need to be checked. + */ +isNotEmpty = (param) => { + return param && param !== Constants.EmptyString && param.length > 0; +} \ No newline at end of file diff --git a/source/community/reactnative/experimental/adaptive-card-builder/README.md b/source/community/reactnative/experimental/adaptive-card-builder/README.md new file mode 100644 index 0000000000..b57205dd8c --- /dev/null +++ b/source/community/reactnative/experimental/adaptive-card-builder/README.md @@ -0,0 +1,25 @@ + +## Description + +This module uses our [AdaptiveCards renderer package](https://www.npmjs.com/package/adaptivecards-reactnative) and a mapper to support card types other than AdaptiveCards. Here we map the payloads of MS cards such as HeroCard, ThumbnailCard, etc to Adaptive card payload format. + +## Supported Cards + +As of now, this mapper supports below types of cards. + +* [ Hero Card ](https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/cards/cards-reference#example-hero-card) +* [ Thumbnail Card](https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/cards/cards-reference#thumbnail-card) + +## Payload Mapper + +The payload mapper file `AdaptiveCardBuilder.js` builds the payload for AdaptiveCard from any HeroCard/ThumbnailCard payload content. + +## Test + +Follow the below steps to run the sample project in local machine. + +* Clone the Repo `https://github.com/microsoft/AdaptiveCards.git` +* Navigate to source/reactnative/ **>** Run **`npm install`** +* iOS **> `react-native run-ios`** +* Android **> `react-native run-android`** +* Click on **`Other Cards`** section \ No newline at end of file diff --git a/source/community/reactnative/experimental/adaptive-card-builder/constants.js b/source/community/reactnative/experimental/adaptive-card-builder/constants.js new file mode 100644 index 0000000000..61525addec --- /dev/null +++ b/source/community/reactnative/experimental/adaptive-card-builder/constants.js @@ -0,0 +1,32 @@ +export const TypeAdaptiveCard = "AdaptiveCard"; +export const AdaptiveCardVersion = "1.2"; + +export const TypeHeroCard = "application/vnd.microsoft.card.hero"; +export const TypeThumbnailCard = "application/vnd.microsoft.card.thumbnail"; + +export const EmptyString = ""; + +export const FullWidth = '100%'; + +export const CenterString = "center"; + +export const ActionOpenUrl = "Action.OpenUrl"; +export const ActionSubmit = "Action.Submit"; +export const TypeOpenUrl = "openUrl"; + +export const ImageType = "Image"; +export const ImageSize = "auto"; + +export const TypeTextBlock = "TextBlock"; +export const TypeTitle = "Title"; +export const TypeSubTitle = "SubTitle"; +export const TypeContainer = "Container"; +export const TypeColumnSet = "ColumnSet"; +export const TypeColumn = "Column"; + + +export const SizeMedium = "medium"; +export const WeightBold = "bold"; + +export const ThumbNailWidth = "100px"; +export const WidthStretch = "stretch"; \ No newline at end of file diff --git a/source/community/reactnative/experimental/adaptive-card-builder/payloads/Hero.Card.2.json b/source/community/reactnative/experimental/adaptive-card-builder/payloads/Hero.Card.2.json new file mode 100644 index 0000000000..13e2f660a5 --- /dev/null +++ b/source/community/reactnative/experimental/adaptive-card-builder/payloads/Hero.Card.2.json @@ -0,0 +1,23 @@ +{ + "contentType": "application/vnd.microsoft.card.hero", + "content": { + "title": "Seattle Center Monorail", + "images": [ + { + "url":"https://upload.wikimedia.org/wikipedia/commons/thumb/4/49/Seattle_monorail01_2008-02-25.jpg/1024px-Seattle_monorail01_2008-02-25.jpg" + } + ], + "buttons": [ + { + "type": "openUrl", + "title": "Official website", + "value": "https://www.seattlemonorail.com" + }, + { + "type": "Imback", + "title": "Okay", + "value": "Test Okay" + } + ] + } + } \ No newline at end of file diff --git a/source/community/reactnative/experimental/adaptive-card-builder/payloads/Hero.Card.json b/source/community/reactnative/experimental/adaptive-card-builder/payloads/Hero.Card.json new file mode 100644 index 0000000000..04475f0408 --- /dev/null +++ b/source/community/reactnative/experimental/adaptive-card-builder/payloads/Hero.Card.json @@ -0,0 +1,30 @@ +{ + "contentType": "application/vnd.microsoft.card.hero", + "content": { + "title": "Seattle Center Monorail", + "subtitle": "Seattle Center Monorail", + "text": "The Seattle Center Monorail is an elevated train line between Seattle Center (near the Space Needle) and downtown Seattle. It was built for the 1962 World's Fair. Its original two trains, completed in 1961, are still in service.", + "images": [ + { + "url":"https://upload.wikimedia.org/wikipedia/commons/thumb/4/49/Seattle_monorail01_2008-02-25.jpg/1024px-Seattle_monorail01_2008-02-25.jpg" + } + ], + "buttons": [ + { + "type": "openUrl", + "title": "Official website", + "value": "https://www.seattlemonorail.com" + }, + { + "type": "openUrl", + "title": "Wikipeda page", + "value": "https://en.wikipedia.org/wiki/Seattle_Center_Monorail" + } + ], + "tap" :{ + "type": "Submit", + "title": "Official website", + "value": "It's a Container Select Action" + } + } + } \ No newline at end of file diff --git a/source/community/reactnative/experimental/adaptive-card-builder/payloads/Thumbnail.card.2.json b/source/community/reactnative/experimental/adaptive-card-builder/payloads/Thumbnail.card.2.json new file mode 100755 index 0000000000..2edcf62aae --- /dev/null +++ b/source/community/reactnative/experimental/adaptive-card-builder/payloads/Thumbnail.card.2.json @@ -0,0 +1,27 @@ +{ + "contentType": "application/vnd.microsoft.card.thumbnail", + "content": { + "title": "Bender", + "text": "Bender Bending Rodríguez is a main character in the animated television series Futurama. He was created by series creators Matt Groening and David X. Cohen, and is voiced by John DiMaggio", + "images": [ + { + "url": "https://upload.wikimedia.org/wikipedia/en/a/a6/Bender_Rodriguez.png", + "alt": "Bender Rodríguez" + } + ], + "buttons": [ + { + "type": "imBack", + "title": "Thumbs Up", + "image": "http://moopz.com/assets_c/2012/06/emoji-thumbs-up-150-thumb-autox125-140616.jpg", + "value": "I like it" + }, + { + "type": "imBack", + "title": "Thumbs Down", + "image": "http://yourfaceisstupid.com/wp-content/uploads/2014/08/thumbs-down.png", + "value": "I don't like it" + } + ] + } + } \ No newline at end of file diff --git a/source/community/reactnative/experimental/adaptive-card-builder/payloads/Thumbnail.card.json b/source/community/reactnative/experimental/adaptive-card-builder/payloads/Thumbnail.card.json new file mode 100755 index 0000000000..b7b7a5031e --- /dev/null +++ b/source/community/reactnative/experimental/adaptive-card-builder/payloads/Thumbnail.card.json @@ -0,0 +1,32 @@ +{ + "contentType": "application/vnd.microsoft.card.thumbnail", + "content": { + "title": "Bender", + "subtitle": "Tale of a robot who dared to love.", + "text": "Bender Bending Rodríguez is a main character in the animated television series Futurama. He was created by series creators Matt Groening and David X. Cohen, and is voiced by John DiMaggio", + "images": [ + { + "url": "https://upload.wikimedia.org/wikipedia/en/a/a6/Bender_Rodriguez.png", + "alt": "Bender Rodríguez" + } + ], + "buttons": [ + { + "type": "imBack", + "title": "Thumbs Up", + "image": "http://moopz.com/assets_c/2012/06/emoji-thumbs-up-150-thumb-autox125-140616.jpg", + "value": "I like it" + }, + { + "type": "imBack", + "title": "Thumbs Down", + "image": "http://yourfaceisstupid.com/wp-content/uploads/2014/08/thumbs-down.png", + "value": "I don't like it" + } + ], + "tap": { + "type": "imBack", + "value": "Tapped it!" + } + } + } \ No newline at end of file diff --git a/source/community/reactnative/experimental/adaptive-card-builder/payloads/index.js b/source/community/reactnative/experimental/adaptive-card-builder/payloads/index.js new file mode 100644 index 0000000000..c96ed190ab --- /dev/null +++ b/source/community/reactnative/experimental/adaptive-card-builder/payloads/index.js @@ -0,0 +1,18 @@ +export default payloads = [ + { + "title": "Hero card with all props", + "json": require('./Hero.Card.json') + }, + { + "title": "Hero card", + "json": require('./Hero.Card.2.json') + }, + { + "title": "Thumbnail card with all props", + "json": require('./Thumbnail.card.json') + }, + { + "title": "Thumbnail card", + "json": require('./Thumbnail.card.2.json') + } +] \ No newline at end of file diff --git a/source/community/reactnative/src/adaptive-card.js b/source/community/reactnative/src/adaptive-card.js index f9fc54eb6e..60be683749 100644 --- a/source/community/reactnative/src/adaptive-card.js +++ b/source/community/reactnative/src/adaptive-card.js @@ -20,6 +20,7 @@ import { SelectAction } from './components/actions'; import ResourceInformation from './utils/resource-information'; import { ContainerWrapper } from './components/containers'; import { ThemeConfigManager } from './utils/theme-config'; +import { ModelFactory } from './models'; export default class AdaptiveCard extends React.Component { @@ -32,9 +33,15 @@ export default class AdaptiveCard extends React.Component { this.payload = props.payload; + if (this.props.isActionShowCard) { + this.cardModel = props.payload; + }else{ + this.cardModel = ModelFactory.createElement(props.payload); + } this.state = { showErrors: false, payload: this.payload, + cardModel: this.cardModel, } // hostConfig @@ -53,31 +60,12 @@ export default class AdaptiveCard extends React.Component { } toggleVisibilityForElementWithID = (idArray) => { - this.toggleObjectWithIDArray(this.payload, [...idArray]); + this.toggleCardModelObject(this.cardModel, [...idArray]); this.setState({ - payload: this.payload, + cardModel: this.cardModel, }) } - /** - * @description Toggles the visibility of the components by their ids recursively - * @param {Object} object - the object to be searched for ids - * @param {Array} idArrayValue - the array of IDs to be toggled - */ - toggleObjectWithIDArray = (object, idArrayValue) => { - if (idArrayValue.length === 0) return - if (object.hasOwnProperty('id')) { - this.checkTargetElementsForID(object, idArrayValue); - if (idArrayValue.length === 0) return - } - Object.keys(object).forEach(element => { - if (idArrayValue.length === 0) return - if (typeof object[element] == "object") { - this.toggleObjectWithIDArray(object[element], idArrayValue); - } - }); - return; - } /** * @description Checks the elements recursively to change the isVisible property @@ -88,7 +76,7 @@ export default class AdaptiveCard extends React.Component { targetElements.forEach(target => { if (target instanceof String || typeof target === 'string'){ if(target == object["id"]){ - this.toggleObjectVisibility(object); + object.isVisible = !object.isVisible; var index = targetElements.indexOf(object["id"]); if (index !== -1) targetElements.splice(index, 1); return @@ -98,7 +86,7 @@ export default class AdaptiveCard extends React.Component { if (!Utils.isNullOrEmpty(target["isVisible"])) { object.isVisible = target["isVisible"] } else { - this.toggleObjectVisibility(object); + object.isVisible = !object.isVisible; } var index = targetElements.indexOf(target); if (index !== -1) targetElements.splice(index, 1); @@ -109,36 +97,32 @@ export default class AdaptiveCard extends React.Component { } /** - * @description Toggles the isVisible property of an Object - * @param {Object} object - the object to be toggles - */ - toggleObjectVisibility = (object) => { - if (!Utils.isNullOrEmpty(object.isVisible)) { - object.isVisible = !object.isVisible - } else { - object.isVisible = false; - } - } - - /** - * @description Conveniece method to toggle the visibility of the component by a single id recursively + * @description Method to toggle the visibility of the component by looking in its children * @param {Object} object - the object to be searched for ids * @param {string} idValue - the id of the component to be toggled */ - toggleObjectWithID = (object, idValue) => { - if (object.hasOwnProperty('id') && object["id"] == idValue) { - if (!Utils.isNullOrEmpty(object.isVisible)) { - object.isVisible = !object.isVisible - } else { - object.isVisible = false; - } - return; + + toggleCardModelObject = (object,idArrayValue) => { + if (idArrayValue.length === 0) return + if (object.hasOwnProperty('id')) { + this.checkTargetElementsForID(object, idArrayValue); + if (idArrayValue.length === 0) return + } + if((object.children !== undefined) && object.children.length !== 0 ){ + object.children.forEach(element => { + if (idArrayValue.length === 0) return + this.toggleCardModelObject(element, idArrayValue); + }); } - Object.keys(object).forEach(element => { - if (typeof object[element] == "object") { - this.toggleObjectWithID(object[element], idValue); + //Adaptive cards has actions array in addition to the body which is added as children + if(object.type === 'AdaptiveCard'){ + if((object.actions !== undefined) && object.actions.length !== 0 ){ + object.actions.forEach(element => { + if (idArrayValue.length === 0) return + this.toggleCardModelObject(element, idArrayValue); + }); } - }); + } return; } @@ -174,23 +158,21 @@ export default class AdaptiveCard extends React.Component { */ parsePayload = () => { let children = []; - const { body } = this.state.payload; - if (!body) + if (this.state.cardModel.children.length === 0) return children; - - children = Registry.getManager().parseRegistryComponents(body, this.onParseError); - return children.map((ChildElement, index) => React.cloneElement(ChildElement, { containerStyle: this.state.payload.style, isFirst: index === 0 })); + children = Registry.getManager().parseRegistryComponents(this.state.cardModel.children, this.onParseError); + return children.map((ChildElement, index) => React.cloneElement(ChildElement, { containerStyle: this.state.cardModel.style, isFirst: index === 0 })); } getAdaptiveCardContent() { var adaptiveCardContent = ( - + {this.parsePayload()} - {!Utils.isNullOrEmpty(this.state.payload.actions) && - } + {!Utils.isNullOrEmpty(this.state.cardModel.actions) && + } ); diff --git a/source/community/reactnative/src/components/actions/action-button.js b/source/community/reactnative/src/components/actions/action-button.js index 12c029d8cf..d525646a47 100644 --- a/source/community/reactnative/src/components/actions/action-button.js +++ b/source/community/reactnative/src/components/actions/action-button.js @@ -132,7 +132,7 @@ export class ActionButton extends React.Component { } changeShowCardState = () => { - this.showCardHandler(this.payload.card); + this.showCardHandler(this.payload.children[0]); } parseHostConfig() { diff --git a/source/community/reactnative/src/components/containers/column.js b/source/community/reactnative/src/components/containers/column.js index 0217ceeac2..0cd27ee086 100644 --- a/source/community/reactnative/src/components/containers/column.js +++ b/source/community/reactnative/src/components/containers/column.js @@ -44,6 +44,13 @@ export class Column extends React.Component { if (!this.column) return children; + if (this.column.isFallbackActivated) { + if (this.column.fallbackType == "drop") { + return null; + } else if (!Utils.isNullOrEmpty(element.fallback)) { + return Registry.getManager().parseComponent(this.column.fallback, this.context.onParseError); + } + } // parse elements if (!Utils.isNullOrEmpty(this.column.items) && (this.column.isVisible !== false)) { children = Registry.getManager().parseRegistryComponents(this.column.items, this.context.onParseError); @@ -138,11 +145,11 @@ export class Column extends React.Component { widthPercentage = (pixelWidth / deviceWidth) * 100 } else if (width == Constants.AlignStretch) { - containerStyle.push({ flex: 1 }) + containerStyle.push({ flex: 2}); } else if (width == Constants.Auto) { if (!containsNumber) { - containerStyle.push({ alignSelf: 'auto' }) + containerStyle.push({ flex: 1 }) } else { widthPercentage = defaultWidthPercentage } @@ -213,12 +220,12 @@ export class Column extends React.Component { spacingStyle.push({ marginLeft: this.spacing }) } spacingStyle.push({ flexGrow: 1 }); - + let widthPercentage = this.calculateWidthPercentage(containerViewStyle); if (!Utils.isNullOrEmpty(widthPercentage)) { let spacePercentage = widthPercentage; - if (!this.isForemostElement()) - spacePercentage = (this.spacing / deviceWidth) * 100 +spacePercentage; + if (!this.isForemostElement()) + spacePercentage = (this.spacing / deviceWidth) * 100 + spacePercentage; containerViewStyle.push({ width: spacePercentage.toString() + '%' }); } diff --git a/source/community/reactnative/src/components/containers/container.js b/source/community/reactnative/src/components/containers/container.js index 415228c7ab..0c72e649a0 100644 --- a/source/community/reactnative/src/components/containers/container.js +++ b/source/community/reactnative/src/components/containers/container.js @@ -37,6 +37,14 @@ export class Container extends React.Component { return children; } + if (this.payload.isFallbackActivated){ + if(this.payload.fallbackType == "drop"){ + return null; + }else if(!Utils.isNullOrEmpty(element.fallback)){ + return Registry.getManager().parseComponent(this.payload.fallback,this.context.onParseError); + } + } + children = Registry.getManager().parseRegistryComponents(this.payload.items, this.context.onParseError); return children.map((ChildElement, index) => React.cloneElement(ChildElement, { containerStyle: this.payload.style, isFirst: index === 0 })); } diff --git a/source/community/reactnative/src/components/elements/image.js b/source/community/reactnative/src/components/elements/image.js index 462b5c2d8f..50f3bd0375 100644 --- a/source/community/reactnative/src/components/elements/image.js +++ b/source/community/reactnative/src/components/elements/image.js @@ -111,8 +111,10 @@ export class Img extends React.Component { let sizeStyle = []; let sizeValue = Utils.parseHostConfigEnum(Enums.Size, this.payload.size, Enums.Size.Auto) /* This W2H ratio is calculated to determine the height required w.r.to pre-determined sizes */ - const w2hratio = this.state.imageHeight / this.state.imageWidth; - + var w2hratio = this.state.imageHeight / this.state.imageWidth; + if (!Utils.isaNumber(w2hratio)) { + w2hratio = 1; + } /** * Scenario 1 : Either height or width has string value (Ex: '80px'), * use the integer portion. diff --git a/source/community/reactnative/src/components/inputs/number-input.js b/source/community/reactnative/src/components/inputs/number-input.js index cbbf20d2ff..0ce12b56d1 100644 --- a/source/community/reactnative/src/components/inputs/number-input.js +++ b/source/community/reactnative/src/components/inputs/number-input.js @@ -24,7 +24,7 @@ export class NumberInput extends React.Component { this.parse(); this.state = { isError: this.isInvalid(this.payload.value), - numberValue: this.payload.value.toString(), + numberValue: this.payload.value ? this.payload.value.toString() : Constants.EmptyString } } diff --git a/source/community/reactnative/src/components/registration/registry.js b/source/community/reactnative/src/components/registration/registry.js index f13c1b18fa..b4f4b7e326 100644 --- a/source/community/reactnative/src/components/registration/registry.js +++ b/source/community/reactnative/src/components/registration/registry.js @@ -102,11 +102,11 @@ export class Registry { } } RequiredPropertySchema = { - 'Container': { 'type': 'Container', 'items': 'Array' }, + 'Container': { 'type': 'Container', 'children': 'Array' }, 'ColumnSet': { 'type': 'ColumnSet' }, 'Column': { 'items': 'Array' }, - 'FactSet': { 'type': 'FactSet', 'facts': 'Array' }, - 'ImageSet': { 'type': 'ImageSet', 'images': 'Array' }, + 'FactSet': { 'type': 'FactSet', 'children': 'Array' }, + 'ImageSet': { 'type': 'ImageSet', 'children': 'Array' }, 'TextBlock': { 'type': 'TextBlock' }, 'Image': { 'type': 'Image', 'url': 'String' }, @@ -123,7 +123,7 @@ export class Registry { 'Action.ShowCard': { 'type': 'Action.ShowCard', 'card': 'Object' }, 'Action.Submit': { 'type': 'Action.Submit' }, 'Action.OpenUrl': { 'type': 'Action.OpenUrl', 'url': 'String' }, - 'ActionSet': { 'type': 'ActionSet', 'actions': 'Array' }, + 'ActionSet': { 'type': 'ActionSet' }, }; /** @@ -143,12 +143,33 @@ export class Registry { if (!componentArray) return parsedElement; componentArray.map((element, index) => { - const Element = this.getComponentOfType(element.type); + const currentElement = this.parseComponent(element,onParseError,index); + if (currentElement){ + parsedElement.push(currentElement); + } + }); + return parsedElement; + } + + /** + * @description Parse an individual component + * @param {Array} element - Json + */ + parseComponent = (element, onParseError, index = 0) => { + const Element = this.getComponentOfType(element.type); + if (Element) { /** * Validate the schema and invoke onParseError handler incase of any error. */ let isValid = true; + if (element.isFallbackActivated){ + if(element.fallbackType == "drop"){ + return null; + }else if(!Utils.isNullOrEmpty(element.fallback)){ + return this.parseComponent(element.fallback,onParseError); + } + } for (var key in this.validateSchemaForType(element.type)) { if (!element.hasOwnProperty(key)) { let error = { "error": Enums.ValidationError.PropertyCantBeNull, "message": `Required property ${key} for ${element.type} is missing` }; @@ -159,7 +180,7 @@ export class Registry { if (isValid) { if (element.isVisible !== false) { const elementKey = Utils.isNullOrEmpty(element.id) ? `${element.type}-${index}` : `${element.type}-${index}-${element.id}`; - parsedElement.push(); + return (); } } } else { @@ -167,8 +188,6 @@ export class Registry { onParseError(error); return null; } - }); - return parsedElement; } } diff --git a/source/community/reactnative/src/models/action-model.js b/source/community/reactnative/src/models/action-model.js new file mode 100644 index 0000000000..a0a859d2e5 --- /dev/null +++ b/source/community/reactnative/src/models/action-model.js @@ -0,0 +1,50 @@ +import {BaseModel} from './base-model' +import { ElementType } from '../utils/enums' +import { ModelFactory } from './model-factory' + +export class BaseActionModel extends BaseModel{ + constructor(payload, parent) { + super(payload, parent); + this.title = payload.title; + this.iconUrl = payload.iconUrl; + this.sentiment = payload.sentiment; + this.ignoreInputValidation = payload.ignoreInputValidation; + } +} + +export class SubmitActionModel extends BaseActionModel{ + data; + type = ElementType.ActionSubmit; + constructor(payload, parent) { + super(payload, parent); + this.data = payload.data; + } +} + +export class OpenUrlActionModel extends BaseActionModel{ + url; + type = ElementType.ActionOpenUrl; + constructor(payload, parent) { + super(payload, parent); + this.url = payload.url; + } +} + +export class ShowCardActionModel extends BaseActionModel{ + card; + type = ElementType.ActionShowCard; + constructor(payload, parent) { + super(payload, parent); + this.card = ModelFactory.createElement(payload.card, this); + this.children = [this.card]; + } +} + +export class ToggleVisibilityActionModel extends BaseActionModel{ + targetElements; + type = ElementType.ActionToggleVisibility; + constructor(payload, parent) { + super(payload, parent); + this.targetElements = payload.targetElements; + } +} diff --git a/source/community/reactnative/src/models/base-model.js b/source/community/reactnative/src/models/base-model.js new file mode 100644 index 0000000000..1b6dabedf3 --- /dev/null +++ b/source/community/reactnative/src/models/base-model.js @@ -0,0 +1,39 @@ +import * as Utils from '../utils/util'; +import { ModelFactory } from './model-factory' + +export class BaseModel { + id; + type; + parent; + children = []; + payload; + selectAction; + isVisible = true; + isFallbackActivated = false; + fallback; + fallbackType; + + constructor(payload, parent) { + this.parent = parent; + this.id = payload.id; + this.spacing = payload.spacing; + this.separator = payload.separator; + if (this.id === undefined) { + this.id = Utils.generateID(); + } + if (payload.selectAction) { + this.selectAction = payload.selectAction; + } + if (payload.isVisible){ + this.isVisible = payload.isVisible; + } + if (payload.fallback){ + if (payload.fallback == "drop"){ + this.fallbackType = "drop" + }else{ + this.fallback = ModelFactory.createElement(payload.fallback, parent); + } + } + + } +} diff --git a/source/community/reactnative/src/models/container-model.js b/source/community/reactnative/src/models/container-model.js new file mode 100644 index 0000000000..36acf5cc42 --- /dev/null +++ b/source/community/reactnative/src/models/container-model.js @@ -0,0 +1,155 @@ +import { BaseModel } from './base-model' +import { ModelFactory } from './model-factory'; +import { ElementType } from '../utils/enums' +import {ImageModel} from './element-model' + +class BaseContainerModel extends BaseModel { + constructor(payload, parent) { + super(payload, parent); + if (payload.backgroundImage) { + this.backgroundImage = payload.backgroundImage;; + } + this.verticalContentAlignment = payload.verticalContentAlignment; + this.style = payload.style; + this.bleed = payload.bleed; + } +} + +export class AdaptiveCardModel extends BaseContainerModel { + constructor(payload, parent) { + super(payload, parent); + this.type = ElementType.AdaptiveCard; + this.fallbackText = payload.fallbackText; + this.version = payload.version; + this.speak = payload.speak; + this.children = []; + this.actions = []; + this.children.push(...ModelFactory.createGroup(payload.body, this)); + this.actions.push(...ModelFactory.createGroup(payload.actions, this)); + this.show = true; + } +} + +export class ContainerModel extends BaseContainerModel { + constructor(payload, parent) { + super(payload, parent); + this.type = ElementType.Container; + this.children = []; + this.children.push(...ModelFactory.createGroup(payload.items, this)); + this.height = payload.height; + } + + get items(){ + return this.children; + } +} + +export class ColumnSetModel extends BaseContainerModel { + constructor(payload, parent) { + super(payload, parent); + this.type = ElementType.ColumnSet; + this.children = []; + if (payload.columns) { + payload.columns.forEach((item) => { + let column = new ColumnModel(item, this); + if (column) { + this.children.push(column); + } + }); + } + this.height = payload.height; + } + get columns() { + return this.children; + } +} + +export class ColumnModel extends BaseContainerModel { + constructor(payload, parent) { + super(payload, parent); + this.type = ElementType.Column; + this.children = []; + this.children.push(...ModelFactory.createGroup(payload.items, this)); + this.height = payload.height; + if (payload.width) { + if (payload.width === 'auto' || payload.width === 'stretch') { + this.width = payload.width; + } + else { + let columnWidth = parseInt(payload.width, 10); + if (columnWidth < 0) { + columnWidth = 0; + } + this.width = columnWidth; + } + } + } + get items() { + return this.children; + } +} + +export class FactModel { + constructor(payload) { + this.type = 'Fact'; + this.title = payload.title; + this.value = payload.value; + } +} + +export class FactSetModel extends BaseContainerModel { + constructor(payload, parent) { + super(payload, parent); + this.type = ElementType.FactSet; + this.children = []; + if (payload.facts) { + payload.facts.forEach((item) => { + let fact = new FactModel(item); + if (fact) { + this.children.push(fact); + } + }); + } + } + get facts() { + return this.children; + } +} + +export class ImageSetModel extends BaseContainerModel { + constructor(payload, parent) { + super(payload, parent); + this.type = ElementType.ImageSet; + this.children = []; + this.imageSize = payload.imageSize; + if (payload.images) { + payload.images.forEach((item) => { + let image = new ImageModel(item, this); + if (image) { + this.children.push(image); + } + }); + } + } + get images() { + return this.children; + } +} + +export class ActionSetModel extends BaseContainerModel{ + constructor(payload, parent) { + super(payload, parent); + this.type = ElementType.ActionSet; + this.children = []; + this.children.push(...ModelFactory.createGroup(payload.actions, this)); + this.height = payload.height; + } + + get actions() { + return this.children; + } + +} + + + diff --git a/source/community/reactnative/src/models/element-model.js b/source/community/reactnative/src/models/element-model.js new file mode 100644 index 0000000000..c7bc1adac5 --- /dev/null +++ b/source/community/reactnative/src/models/element-model.js @@ -0,0 +1,75 @@ +import {BaseModel} from './base-model' +import { ElementType } from '../utils/enums' + + +export class TextBlockModel extends BaseModel { + type = ElementType.TextBlock; + + constructor(payload, parent) { + super(payload, parent); + this.text = payload.text; + this.color = payload.color; + this.horizontalAlignment = payload.horizontalAlignment; + this.isSubtle = payload.isSubtle || false; + this.maxLines = payload.maxLines; + this.size = payload.size; + this.weight = payload.weight; + this.wrap = payload.wrap || false; + this.fontStyle = payload.fontStyle; + } +} + +export class ImageModel extends BaseModel { + type = ElementType.Image; + + constructor(payload, parent) { + super(payload, parent); + this.url = payload.url; + this.altText = payload.altText; + this.horizontalAlignment = payload.horizontalAlignment; + this.size = payload.size; + this.style = payload.style; + this.backgroundColor = payload.backgroundColor; + this.size = payload.size; + this.width = payload.width; + this.height = payload.height; + } +} + +export class MediaModel extends BaseModel { + type = ElementType.Media; + sources = []; + + constructor(payload, parent) { + super(payload, parent); + + if (payload.sources) { + payload.sources.forEach((item) => { + if (item) { + this.sources.push(item); + } + }); + } + this.poster = payload.poster; + this.altText = payload.altText; + } +} + +export class RichTextBlockModel extends BaseModel { + type = ElementType.RichTextBlock; + + constructor(payload, parent) { + super(payload, parent); + this.text = payload.text; + this.color = payload.color; + this.horizontalAlignment = payload.horizontalAlignment; + this.isSubtle = payload.isSubtle || false; + this.maxLines = payload.maxLines; + this.size = payload.size; + this.weight = payload.weight; + this.wrap = payload.wrap || false; + this.paragraphs = payload.paragraphs; + this.fontStyle = payload.fontStyle; + } +} + diff --git a/source/community/reactnative/src/models/index.js b/source/community/reactnative/src/models/index.js new file mode 100644 index 0000000000..1080ae405d --- /dev/null +++ b/source/community/reactnative/src/models/index.js @@ -0,0 +1,6 @@ +export * from './base-model' +export * from './action-model' +export * from './container-model' +export * from './element-model' +export * from './input-model' +export * from './model-factory' \ No newline at end of file diff --git a/source/community/reactnative/src/models/input-model.js b/source/community/reactnative/src/models/input-model.js new file mode 100644 index 0000000000..63683ab20d --- /dev/null +++ b/source/community/reactnative/src/models/input-model.js @@ -0,0 +1,130 @@ +import {BaseModel} from './base-model' +import { ElementType } from '../utils/enums' + +export class BaseInputModel extends BaseModel{ + constructor(payload, parent) { + super(payload, parent); + this.placeholder = payload.placeholder; + this.value = payload.value; + this.inlineAction = payload.inlineAction; + this.validation = payload.validation; + } +} + +export class TextInputModel extends BaseInputModel { + type = ElementType.TextInput; + + constructor(payload, parent) { + super(payload, parent); + this.isMultiline = payload.isMultiline || false; + this.maxLength = payload.maxLength; + this.style = payload.style; + } + +} + +export class NumberInputModel extends BaseInputModel { + type = ElementType.NumberInput; + + constructor(payload, parent) { + super(payload, parent); + this.max = payload.max; + this.min = payload.min; + } +} + +export class DateInputModel extends BaseInputModel { + type = ElementType.DateInput; + + constructor(payload, parent) { + super(payload, parent); + this.max = payload.max; + this.min = payload.min; + } +} + +export class TimeInputModel extends BaseInputModel { + type = ElementType.TimeInput; + + constructor(payload, parent) { + super(payload, parent); + this.max = payload.max; + this.min = payload.min; + } + +} + +export class ToggleInputModel extends BaseInputModel { + type = ElementType.ToggleInput; + + constructor(payload, parent) { + super(payload, parent); + this.title = payload.title; + this.valueOff = payload.valueOff; + this.valueOn = payload.valueOn; + this.value = payload.value === payload.valueOn; + this.wrap = payload.wrap; + } +} + +export class ChoiceSetModel extends BaseInputModel { + type = ElementType.ChoiceSetInput; + + constructor(payload, parent) { + super(payload, parent); + this.isMultiSelect = payload.isMultiSelect; + this.style = payload.style; + this.wrap = payload.wrap; + if (payload.choices) { + payload.choices.forEach((item, index) => { + let choice = new ChoiceModel(item, this); + if (choice) { + this.children.push(choice); + } + }); + } + + if (payload.value) { + let selected = (payload.value).split(','); + if (selected) { + selected.forEach(current => { + let choice = this.choices.find(c => c.value === current); + if (choice) { + choice.selected = true; + } + }); + } + } + } + + get choices() { + return this.children; + } + + +} + +export class ChoiceModel extends BaseInputModel { + parent; + type = 'Input.Choice'; + title; + value; + selected; + + constructor(payload, parent) { + super(payload, parent); + + this.title = payload.title; + this.value = payload.value; + this.selected = false; + } +} + + + + + + + + + diff --git a/source/community/reactnative/src/models/model-factory.js b/source/community/reactnative/src/models/model-factory.js new file mode 100644 index 0000000000..325759c69b --- /dev/null +++ b/source/community/reactnative/src/models/model-factory.js @@ -0,0 +1,84 @@ +import * as Models from './index' +import { ElementType } from '../utils/enums' +import * as Utils from '../utils/util' + +export class ModelFactory { + static createElement(payload, parent) { + if (!payload) { + return undefined; + } + switch (payload.type) { + case ElementType.Image: + return new Models.ImageModel(payload, parent); + case ElementType.Media: + return new Models.MediaModel(payload, parent); + case ElementType.TextBlock: + return new Models.TextBlockModel(payload, parent); + case ElementType.RichTextBlock: + return new Models.RichTextBlockModel(payload, parent); + case ElementType.Column: + return new Models.ColumnModel(payload, parent); + case ElementType.ColumnSet: + return new Models.ColumnSetModel(payload, parent); + case ElementType.Container: + return new Models.ContainerModel(payload, parent); + case ElementType.FactSet: + return new Models.FactSetModel(payload, parent); + case ElementType.ImageSet: + return new Models.ImageSetModel(payload, parent); + case ElementType.TextInput: + return new Models.TextInputModel(payload, parent); + case ElementType.DateInput: + return new Models.DateInputModel(payload, parent); + case ElementType.TimeInput: + return new Models.TimeInputModel(payload, parent); + case ElementType.NumberInput: + return new Models.NumberInputModel(payload, parent); + case ElementType.ChoiceSetInput: + return new Models.ChoiceSetModel(payload, parent); + case ElementType.ToggleInput: + return new Models.ToggleInputModel(payload, parent); + case ElementType.AdaptiveCard: + return new Models.AdaptiveCardModel(payload, parent); + case ElementType.ActionOpenUrl: + return new Models.OpenUrlActionModel(payload, parent); + case ElementType.ActionSubmit: + return new Models.SubmitActionModel(payload, parent); + case ElementType.ActionShowCard: + return new Models.ShowCardActionModel(payload, parent); + case ElementType.ActionToggleVisibility: + return new Models.ToggleVisibilityActionModel(payload, parent); + case ElementType.ActionSet: + return new Models.ActionSetModel(payload, parent); + default: + return ModelFactory.checkForFallBack(payload, parent); + } + } + static createGroup(payload, parent) { + let modelGroup = []; + if (payload && payload.length > 0) { + payload.forEach((item) => { + let model = ModelFactory.createElement(item, parent); + if (model) { + modelGroup.push(model); + } + }); + } + return modelGroup; + } + + static checkForFallBack (payload, parent) { + if (!Utils.isNullOrEmpty(payload.fallback)){ + if (payload.fallback !== "drop"){ + return ModelFactory.createElement(payload.fallback, parent); + } + else{ + return undefined; + } + } + else{ + parent.isFallbackActivated = true; + return undefined; + } + } +} \ No newline at end of file diff --git a/source/community/reactnative/src/utils/enums.js b/source/community/reactnative/src/utils/enums.js index 1ef58198c3..441f946d79 100644 --- a/source/community/reactnative/src/utils/enums.js +++ b/source/community/reactnative/src/utils/enums.js @@ -150,4 +150,31 @@ export const Sentiment = Object.freeze({ export const FontStyle = Object.freeze({ Default: 0, Monospace: 1, +}); + +export const ElementType = Object.freeze({ + AdaptiveCard: 'AdaptiveCard', + Container: 'Container', + ColumnSet: 'ColumnSet', + ImageSet: 'ImageSet', + Column: 'Column', + FactSet: 'FactSet', + + TextInput: 'Input.Text', + NumberInput: 'Input.Number', + ToggleInput: 'Input.Toggle', + DateInput: 'Input.Date', + TimeInput: 'Input.Time', + ChoiceSetInput: 'Input.ChoiceSet', + + TextBlock: 'TextBlock', + Media: 'Media', + Image: 'Image', + RichTextBlock: 'RichTextBlock', + + ActionShowCard: 'Action.ShowCard', + ActionSubmit: 'Action.Submit', + ActionOpenUrl: 'Action.OpenUrl', + ActionToggleVisibility: 'Action.ToggleVisibility', + ActionSet: 'ActionSet' }); \ No newline at end of file diff --git a/source/community/reactnative/src/utils/util.js b/source/community/reactnative/src/utils/util.js index 996731b182..5e983ea35e 100644 --- a/source/community/reactnative/src/utils/util.js +++ b/source/community/reactnative/src/utils/util.js @@ -211,4 +211,15 @@ export function hexToRGB(color) { else { return color; } +} + +/** + * @description Generates an unique ID for the element if its not part of the payload + * @return {string} ID as string + */ +/** + * argb in hex to css rgba + */ +export function generateID() { + return Math.random().toString(36).substr(2, 9); } \ No newline at end of file diff --git a/source/community/reactnative/src/visualizer/assets/more-android.png b/source/community/reactnative/src/visualizer/assets/more-android.png new file mode 100644 index 0000000000..20314351c0 Binary files /dev/null and b/source/community/reactnative/src/visualizer/assets/more-android.png differ diff --git a/source/community/reactnative/src/visualizer/assets/more-ios.png b/source/community/reactnative/src/visualizer/assets/more-ios.png new file mode 100644 index 0000000000..a503029389 Binary files /dev/null and b/source/community/reactnative/src/visualizer/assets/more-ios.png differ diff --git a/source/community/reactnative/src/visualizer/assets/right_arrow.png b/source/community/reactnative/src/visualizer/assets/right_arrow.png new file mode 100644 index 0000000000..659d430b77 Binary files /dev/null and b/source/community/reactnative/src/visualizer/assets/right_arrow.png differ diff --git a/source/community/reactnative/src/visualizer/constants.js b/source/community/reactnative/src/visualizer/constants.js new file mode 100644 index 0000000000..7677b4cd40 --- /dev/null +++ b/source/community/reactnative/src/visualizer/constants.js @@ -0,0 +1,7 @@ +export const AdaptiveCards = "Adaptive Cards"; +export const OtherCards = "Other Cards"; +export const DialogFlow = "Dialogflow"; +export const PayloadHeader = "Payloads"; + +export const InProgress = "InProgress"; +export const InProgressText = "This task is not completed yet."; \ No newline at end of file diff --git a/source/community/reactnative/src/visualizer/more-options.js b/source/community/reactnative/src/visualizer/more-options.js new file mode 100644 index 0000000000..fbb647e057 --- /dev/null +++ b/source/community/reactnative/src/visualizer/more-options.js @@ -0,0 +1,77 @@ +import React from 'react'; +import { + TouchableOpacity, + View, + StyleSheet, + Text, + Platform, + Alert +} from 'react-native'; + +import * as Constants from './constants'; + +const options = [Constants.AdaptiveCards, Constants.OtherCards, Constants.DialogFlow]; + +export default class MoreOptions extends React.Component { + + constructor(props) { + super(props); + } + + render() { + return ( + + {options.map((option, index) => (this.renderOption(option, index)))} + + ); + } + + /** + * @description This renders each options + */ + renderOption = (option, key) => ( + this.onPress(option)}> + + {option} + + + ); + + /** + * @description Click action for options + */ + onPress = (option) => { + if (option == Constants.DialogFlow) + Alert.alert( + Constants.InProgress, + Constants.InProgressText, + [ + { text: "Okay", onPress: () => this.props.closeMoreOptions() }, + ], + { cancelable: false } + ) + else this.props.moreOptionClick(option); + } +} + +const styles = StyleSheet.create({ + container: { + marginRight: 10, + marginTop: Platform.OS === "ios" ? 50 : 10, + alignSelf: "flex-end", + paddingHorizontal: 20, + backgroundColor: "white" + }, + moreOption: { + paddingTop: 10, + paddingBottom: 10, + }, + option: { + color: "black", + fontWeight: "300", + fontSize: 17 + } +}); \ No newline at end of file diff --git a/source/community/reactnative/src/visualizer/payloads.js b/source/community/reactnative/src/visualizer/payloads.js new file mode 100644 index 0000000000..d71d38c269 --- /dev/null +++ b/source/community/reactnative/src/visualizer/payloads.js @@ -0,0 +1,111 @@ + +import AdaptiveCardPayloads from './payloads/payloads'; +import OtherCardPayloads from '../../experimental/adaptive-card-builder/payloads'; + +// sample scenarios +const calendarReminderPayload = require('./payloads/scenarios/calendar-reminder.json'); +const flightUpdatePayload = require('./payloads/scenarios/flight-update.json'); +const inputFormPayload = require('./payloads/scenarios/input-form.json'); +const restaurantPayload = require('./payloads/scenarios/restaurant.json'); +const containerPayload = require('./payloads/scenarios/container-item.json'); +const weatherPayload = require('./payloads/scenarios/weather-large.json'); +const activityUpdatePayload = require('./payloads/scenarios/activity-update.json'); +const foodOrderPayload = require('./payloads/scenarios/food-order.json'); +const imageGalleryPayload = require('./payloads/scenarios/image-gallery.json'); +const sportingEventPayload = require('./payloads/scenarios/sporting-event.json'); +const mediaPayload = require('./payloads/scenarios/media.json'); +const markdownPayload = require('./payloads/scenarios/markdown.json'); + +/** + * @description Return the unique element types present in the given payload json + * @param {object} json - payload json + * @return {Array} - Array of element types +*/ +const getTags = (json) => { + let tags = new Set(); + // elements + json.body.map(element => { + tags.add(element.type); + }); + // actions + if (json.actions && json.actions.length > 0) { + tags.add("Actions"); + } + return Array.from(tags); +} + +const AdaptiveCardScenarios = [{ + title: 'Calendar reminder', + json: calendarReminderPayload, + tags: getTags(calendarReminderPayload), + icon: require('./assets/calendar.png') +}, { + title: 'Flight update', + json: flightUpdatePayload, + tags: getTags(flightUpdatePayload), + icon: require('./assets/flight.png') +}, { + title: 'Weather Large', + json: weatherPayload, + tags: getTags(weatherPayload), + icon: require('./assets/cloud.png') +}, { + title: 'Activity Update', + json: activityUpdatePayload, + tags: getTags(activityUpdatePayload), + icon: require('./assets/done.png') +}, +{ + title: 'Food order', + json: foodOrderPayload, + tags: getTags(foodOrderPayload), + icon: require('./assets/fastfood.png') +}, +{ + title: 'Image gallery', + json: imageGalleryPayload, + tags: getTags(imageGalleryPayload), + icon: require('./assets/photo_library.png') +}, +{ + title: 'Sporting event', + json: sportingEventPayload, + tags: getTags(sportingEventPayload), + icon: require('./assets/run.png') +}, { + title: 'Restaurant', + json: restaurantPayload, + tags: getTags(restaurantPayload), + icon: require('./assets/restaurant.png') +}, +{ + title: 'Input form', + json: inputFormPayload, + tags: getTags(inputFormPayload), + icon: require('./assets/form.png') +}, +{ + title: 'Media', + json: mediaPayload, + tags: getTags(mediaPayload), + icon: require('./assets/video_library.png') +}, +{ + title: 'Stock Update', + json: containerPayload, + tags: getTags(containerPayload), + icon: require('./assets/square.png') +}, +{ + title: 'Markdown', + json: markdownPayload, + tags: getTags(markdownPayload), + icon: require('./assets/code.png') +}]; + + +export { AdaptiveCardPayloads, AdaptiveCardScenarios, OtherCardPayloads }; + + + + diff --git a/source/community/reactnative/src/visualizer/payloads/payloads/Fallback.json b/source/community/reactnative/src/visualizer/payloads/payloads/Fallback.json new file mode 100644 index 0000000000..4fd7dc74d7 --- /dev/null +++ b/source/community/reactnative/src/visualizer/payloads/payloads/Fallback.json @@ -0,0 +1,79 @@ +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.1", + "body": [ + { + "type": "Graph", + "poster": "https://...", + "sources": [], + "fallback": { + "type": "TextBlock", + "text": "This is a graph text", + "wrap": true + } + }, + { + "type": "Container", + "items": [ + { + "type": "TextBlock", + "text": "This text block makes no sense without the graph below it." + }, + { + "type": "Graph", + "xAxis": "Profit" + } + ], + "fallback": "drop" + }, + { + "type": "Container", + "items": [ + { + "type": "TextBlock", + "text": "This text block makes no sense without the graph below it." + }, + { + "type": "Graph", + "xAxis": "Profit" + } + ], + "fallback": { + "type": "Container", + "items": [ + { + "type": "TextBlock", + "text": "To view a graph, click this card" + } + ] + } + } + ], + "actions": [ + { + "type": "Action.OpenUrl", + "title": "View", + "url": "https://msn.com" + }, + { + "type": "Action.ShowCard", + "title": "Set due date", + "card": { + "type": "AdaptiveCard", + "body": [ + { + "type": "Input.Date", + "id": "dueDate" + } + ], + "actions": [ + { + "type": "Action.Submit", + "title": "OK" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/source/community/reactnative/src/visualizer/payloads/payloads/index.js b/source/community/reactnative/src/visualizer/payloads/payloads/index.js index f0d945941d..43eb410b03 100644 --- a/source/community/reactnative/src/visualizer/payloads/payloads/index.js +++ b/source/community/reactnative/src/visualizer/payloads/payloads/index.js @@ -155,6 +155,10 @@ export default payloads = [ "title": "FactSetWrapping.json", "json": require('./FactSetWrapping.json') }, + { + "title": "Fallback.json", + "json": require('./Fallback.json') + }, { "title": "Feedback.json", "json": require('./Feedback.json') diff --git a/source/community/reactnative/src/visualizer/visualizer.js b/source/community/reactnative/src/visualizer/visualizer.js index d12ac4f627..73fffbedaf 100644 --- a/source/community/reactnative/src/visualizer/visualizer.js +++ b/source/community/reactnative/src/visualizer/visualizer.js @@ -8,123 +8,55 @@ import { FlatList, View, StyleSheet, + Image, + Text, + Platform, + TouchableOpacity, Modal } from 'react-native'; import Renderer from './renderer'; import { PayloadItem } from './payload-item.js'; import SegmentedControl from './segmented-control'; +import MoreOptions from './more-options'; +import * as Constants from './constants'; +import * as Payloads from './payloads'; +import * as AdaptiveCardBuilder from "../../experimental/adaptive-card-builder/AdaptiveCardBuilder"; + + +const moreIcon = Platform.select({ + ios: require("./assets/more-ios.png"), + android: require("./assets/more-android.png"), + windows: require("./assets/more-android.png") +}) -// sample scenarios -const calendarReminderPayload = require('./payloads/scenarios/calendar-reminder.json'); -const flightUpdatePayload = require('./payloads/scenarios/flight-update.json'); -const inputFormPayload = require('./payloads/scenarios/input-form.json'); -const restaturantPayload = require('./payloads/scenarios/restaurant.json'); -const containerPayload = require('./payloads/scenarios/container-item.json'); -const weatherPayload = require('./payloads/scenarios/weather-large.json'); -const activityUpdatePayload = require('./payloads/scenarios/activity-update.json'); -const foodOrderPayload = require('./payloads/scenarios/food-order.json'); -const imageGalleryPayload = require('./payloads/scenarios/image-gallery.json'); -const sportingEventPayload = require('./payloads/scenarios/sporting-event.json'); -const mediaPayload = require('./payloads/scenarios/media.json'); -const markdownPayload = require('./payloads/scenarios/markdown.json'); - -import payloads from '../visualizer/payloads/payloads/'; export default class Visualizer extends React.Component { + state = { isModalVisible: false, + isMoreVisible: false, selectedPayload: null, + activeOption: Constants.AdaptiveCards, + payloads: Payloads.AdaptiveCardPayloads, + scenarios: Payloads.AdaptiveCardScenarios, activeIndex: 0 // payload - scenarios selector }; constructor(props) { super(props); - - this.scenarios = [{ - title: 'Calendar reminder', - json: calendarReminderPayload, - tags: this.getTags(calendarReminderPayload), - icon: require('./assets/calendar.png') - }, { - title: 'Flight update', - json: flightUpdatePayload, - tags: this.getTags(flightUpdatePayload), - icon: require('./assets/flight.png') - }, { - title: 'Weather Large', - json: weatherPayload, - tags: this.getTags(weatherPayload), - icon: require('./assets/cloud.png') - }, { - title: 'Activity Update', - json: activityUpdatePayload, - tags: this.getTags(activityUpdatePayload), - icon: require('./assets/done.png') - }, - { - title: 'Food order', - json: foodOrderPayload, - tags: this.getTags(foodOrderPayload), - icon: require('./assets/fastfood.png') - }, - { - title: 'Image gallery', - json: imageGalleryPayload, - tags: this.getTags(imageGalleryPayload), - icon: require('./assets/photo_library.png') - }, - { - title: 'Sporting event', - json: sportingEventPayload, - tags: this.getTags(sportingEventPayload), - icon: require('./assets/run.png') - }, { - title: 'Restaurant', - json: restaturantPayload, - tags: this.getTags(restaturantPayload), - icon: require('./assets/restaurant.png') - }, - { - title: 'Input form', - json: inputFormPayload, - tags: this.getTags(inputFormPayload), - icon: require('./assets/form.png') - }, - { - title: 'Media', - json: mediaPayload, - tags: this.getTags(mediaPayload), - icon: require('./assets/video_library.png') - }, - { - title: 'Stock Update', - json: containerPayload, - tags: this.getTags(containerPayload), - icon: require('./assets/square.png') - }, - { - title: 'Markdown', - json: markdownPayload, - tags: this.getTags(markdownPayload), - icon: require('./assets/code.png') - }]; } render() { - const segmentedItems = [ - { title: 'Payloads', value: 'payloads' }, - { title: 'Scenarios', value: 'scenarios' } - ]; - - const items = this.state.activeIndex === 0 ? payloads : this.scenarios; + const { activeIndex, payloads, scenarios } = this.state; + const items = activeIndex === 0 ? payloads : scenarios; return ( - this.segmentedControlStatusDidChange(index)} - /> + this.onMoreOptionsClick()} style={styles.moreContainer}> + + + {scenarios && scenarios.length > 0 ? this.segmentedControl() : this.header()} index.toString()} @@ -147,18 +79,58 @@ export default class Visualizer extends React.Component { onModalClose={this.closeModal} /> + this.closeMoreOptions()}> + {this.modalLayout()} + - ) + ); } + /** + * @description Segment control for payloads and scenarios. + */ + segmentedControl = () => { + const segmentedItems = [ + { title: 'Payloads', value: 'payloads' }, + { title: 'Scenarios', value: 'scenarios' } + ]; + return ( + this.segmentedControlStatusDidChange(index)} + /> + ); + } + + /** + * @description Add header for payloads as there is no scenarios + */ + header = () => ( + + + {Constants.PayloadHeader} + + + ); + /** * @description Present the modal * @param {object} payload - Selected payload */ payloadDidSelect = (payload) => { + /* Check if the payload is HeroCard / ThumbnailCard */ + const notAdaptiveCard = payload.json.contentType && + (payload.json.contentType === "application/vnd.microsoft.card.hero" || + payload.json.contentType === "application/vnd.microsoft.card.thumbnail"); + this.setState({ isModalVisible: true, - selectedPayload: payload.json + selectedPayload: notAdaptiveCard ? AdaptiveCardBuilder.buildAdaptiveCard(payload.json.content, payload.json.contentType) : payload.json }) } @@ -172,31 +144,90 @@ export default class Visualizer extends React.Component { } /** - * @description Return the unique element types present in the given payload json - * @param {object} json - payload json - * @return {Array} - Array of element types + * @description Invoked on payload type segmented control status change + * @param {number} index - index of the selected item */ - getTags = (json) => { - let tags = new Set(); - // elements - json.body.map(element => { - tags.add(element.type); + segmentedControlStatusDidChange = (index) => { + this.setState({ + activeIndex: index }); - // actions - if (json.actions && json.actions.length > 0) { - tags.add("Actions"); + } + + /** + * @description Layout of the more option modal + */ + modalLayout = () => ( + this.closeMoreOptions()} + style={styles.moreOptionsModal} + > + + + ); + + /** + * @description Click action for more option + * @param option - selected option + */ + moreOptionClick = (option) => { + const { activeOption } = this.state; + switch (option) { + case Constants.AdaptiveCards: + if (activeOption !== Constants.AdaptiveCards) + this.setState({ + isMoreVisible: false, + activeOption: Constants.AdaptiveCards, + payloads: Payloads.AdaptiveCardPayloads, + scenarios: Payloads.AdaptiveCardScenarios, + activeIndex : 0 + }); + else this.closeMoreOptions(); + break; + case Constants.OtherCards: + if (activeOption !== Constants.OtherCards) + this.setState({ + isMoreVisible: false, + activeOption: Constants.OtherCards, + payloads: Payloads.OtherCardPayloads, + scenarios: [], + activeIndex : 0 + }); + else this.closeMoreOptions(); + break; + default: + if (activeOption !== Constants.AdaptiveCards) + this.setState({ + isMoreVisible: false, + activeOption: Constants.AdaptiveCards, + payloads: Payloads.AdaptiveCardPayloads, + scenarios: Payloads.AdaptiveCardScenarios, + activeIndex : 0 + }); + else this.closeMoreOptions(); + break; } - return Array.from(tags); } /** - * @description Invoked on payload type segmented control status change - * @param {number} index - index of the selected item + * @description Click action for more icon, shows the options modal */ - segmentedControlStatusDidChange = (index) => { + onMoreOptionsClick = () => { this.setState({ - activeIndex: index - }); + isMoreVisible: true + }) + } + + /** + * @description Dismiss the more options modal + */ + closeMoreOptions = () => { + this.setState({ + isMoreVisible: false + }) } } @@ -209,9 +240,36 @@ const styles = StyleSheet.create({ height: 150, backgroundColor: '#f7f7f7', }, - title: { - fontSize: 20, - fontWeight: 'bold', + header: { marginVertical: 12, + justifyContent: "center", + height: 34, + backgroundColor: '#0078D7' + }, + title: { + fontSize: 17, + fontWeight: '400', + color: "white", + alignSelf: "center" + }, + moreContainer: { + alignSelf: "flex-end" + }, + moreIcon: { + padding: 10, + width: 25, + height: 25 + }, + moreOptionsModal: { + flex: 1, + backgroundColor: "rgba(0, 0, 0, 0.1)" + }, + more: { + marginTop: 50, + marginRight: 10, + alignSelf: "flex-end", + width: 100, + height: 100, + backgroundColor: "white" } }); \ No newline at end of file