Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

App Store Drawer #636

Merged
merged 27 commits into from
May 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0a629dc
creating side drawer
aozien Feb 23, 2023
fe15ce8
basic store grid listing
aozien Feb 23, 2023
837b670
added store listing and opening iframe
aozien Feb 24, 2023
0b3cfb0
added basic message channel to iframe
aozien Feb 25, 2023
6d74dd4
add sample
aozien Feb 25, 2023
b8f6511
create app store folder and move files
aozien Feb 26, 2023
f78575d
Added support for shareing local files with app
aozien Feb 26, 2023
1228bef
improve message structures
aozien Feb 26, 2023
c2470b4
Complete the missing App drawer functions
aozien Feb 26, 2023
df6e615
Files Re-structure
aozien Feb 26, 2023
0e5409d
update file info for github and share sources
aozien Feb 26, 2023
c59dd05
Use local path for the sample action
aozien Feb 27, 2023
9f241b6
Refactor AppStoreIFrame
aozien Feb 27, 2023
c0bc890
Change icon position
aozien Mar 8, 2023
dee82ff
Change App Store Icon
aozien Mar 8, 2023
9eec4ee
Added icons in Apps Drawer
aozien Mar 8, 2023
ca30cf1
Merge branch 'main' into appstore-side-panel
aozien Mar 20, 2023
5c07f13
Add feature=apps flag in query parameters
aozien Mar 20, 2023
3f490a6
removed duplicate example.html file
aozien Mar 20, 2023
b20f041
Added dep on setIsAppStoreEnabled
aozien Mar 20, 2023
26f751b
update PanelTitle Doc
aozien Mar 23, 2023
7aa151c
Rename AppStorePanel file, add Query Param Logic
aozien Mar 23, 2023
06b5ae3
bug fix
aozien Mar 25, 2023
cf66655
Merge branch 'main' into appstore-side-panel
aozien May 15, 2023
fd608a9
use existInFeature to enable disable drawer
aozien May 15, 2023
29678db
installing cypress react router
aozien May 15, 2023
6be26a5
adding tests for enablin/disabling appstore
aozien May 15, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 17 additions & 0 deletions cypress/e2e/appStore/appStore.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
describe('appStore side drawer', () => {
context('enable/disable feature using url parameter', () => {
beforeEach(() => {
cy.setCookie('isFirstTime', 'false')
cy.visit('/')
})

it('should not show app-store icon when url parameter is not present', () => {
cy.findByRole('button', {name: /Open App Store/}).should('not.exist')
})

it('should show app-store icon when url parameter is present', () => {
cy.routerNavigate('/share/v/p?feature=apps', {replace: true})
cy.findByRole('button', {name: /Open App Store/}).should('exist')
})
})
})
1 change: 1 addition & 0 deletions cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
import '@testing-library/cypress/add-commands'
import 'cypress-react-router/add-commands'

/**
* Allow access to elements inside iframe and chain commands from there.
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bldrs",
"version": "1.0.0-r669",
"version": "1.0.0-r670",
"main": "src/index.jsx",
"license": "MIT",
"homepage": "https://github.com/bldrs-ai/Share",
Expand Down Expand Up @@ -43,6 +43,7 @@
"@sentry/react": "^7.31.1",
"@sentry/tracing": "^7.31.1",
"clsx": "^1.2.1",
"cypress-react-router": "^2.0.1",
"material-ui-popup-state": "^5.0.4",
"matrix-widget-api": "^1.1.1",
"normalize.css": "^8.0.1",
Expand Down
112 changes: 112 additions & 0 deletions public/widgets/example.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<!-- was based on example: https://javascriptbit.com/transfer-data-between-parent-window-iframe-channel-messaging-api/ -->

<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hosted App Sample</title>
</head>

<body style="background-color: whitesmoke">
<form id="iframe-form">
<label for="iframe-message">Send message to the parent
<input id="iframe-message" type="text" />
</label>
<button type="submit">Send</button>
</form>
<button id="loadfile" type="">loadfile</button>
<h2>Message received is shown here:</h2>
<div id="message-from-parent"></div>


<script>
// child.js
(async function () {
var port2;
// Listen for the intial port transfer message
var msgHandlers = {}
var $messageContainer = document.querySelector("#message-from-parent");
window.addEventListener("message", initPort);
// Setup the transfered port
function initPort(e) {
if (e.data === "init") {
port2 = e.ports[0];
port2.onmessage = onMessage;
} else {
var msgObj = e.data;
onMessage({
data: msgObj
});
}
}

// Handle messages received on port2
function onMessage(e) {
console.log(e)
$messageContainer.textContent = `Processing response of type ${e.data.action}`;

if (e?.data?.action && msgHandlers[e.data.action]
&& typeof msgHandlers[e.data.action] === 'function') {
msgHandlers[e.data.action](e.data.response)
}
}

// Sending message to the parent
var $form = document.querySelector("#iframe-form");
$form.addEventListener("submit", function (e) {
e.preventDefault();
var message = document.querySelector("#iframe-message").value;
port2.postMessage(message);
});

// Sending message to the parent
var loadfileBtn = document.querySelector("#loadfile");
loadfileBtn.addEventListener("click", function (e) {
e.preventDefault();
port2.postMessage('getLoadedFile');
});

msgHandlers['getLoadedFile'] = async (response) => {
if (!response) $messageContainer.textContent = "Received empty data"
if (response.source === 'local') {
readLocalFile(response.info[0], (r) => {
$messageContainer.textContent = r
})
} else if (response.source === 'github') {
$messageContainer.textContent = await (await fetch(response.info.url)).text()
}
else if (response.source === 'share') {
$messageContainer.textContent = await (await fetch(response.info.url)).text()
}
}
msgHandlers['getSelectedElements'] = (response) => {
if (!response) $messageContainer.textContent = "Received empty data"
$messageContainer.textContent = response
}
})();

function readLocalFile(file, callback) {
if (!file) {
return;
}
let reader = new FileReader();
reader.onload = function (e) {
let contents = e.target.result;
callback(contents)
};
reader.readAsText(file);
}


/*
var xmlhttp = new XMLHttpRequest();
xmlhttp.open('GET', 'blob:link', true);
xmlhttp.setRequestHeader('Content-type','application/x-www-form-urlencoded');
xmlhttp.responseType = 'arraybuffer/blob';
xmlhttp.send();*/
</script>
</body>

</html>
9 changes: 9 additions & 0 deletions src/Components/AppStore/AppStoreData.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[
{
"appName": "vyzn",
"description": "Perform environmental analysis on your model - DRAFT",
"image": "https://www.vyzn.tech/wp-content/themes/stuiq-base/assets/images/logo.svg",
"action": "/widgets/example.html",
"icon": "https://www.vyzn.tech/wp-content/uploads/2023/02/cropped-loop_favicon-180x180.png"
}
]
102 changes: 102 additions & 0 deletions src/Components/AppStore/AppStoreListing.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React, {useCallback} from 'react'
import Box from '@mui/material/Box'
import Paper from '@mui/material/Paper'
import Grid from '@mui/material/Unstable_Grid2'
import Typography from '@mui/material/Typography'
import useStore from '../../store/useStore'
import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent'
import CardMedia from '@mui/material/CardMedia'
import {CardActionArea} from '@mui/material'
import {IFrameCommunicationChannel} from './AppStoreMessagesHandler'
import AppStoreData from './AppStoreData.json'


/** @return {React.Component} */
export function AppStoreListing() {
const setSelectedStoreApp = useStore((state) => state.setSelectedStoreApp)
return (
<>
<Grid container spacing={2}>
{AppStoreData.map((item, index) => (
<Grid item={true} xs={6} sm={6} md={6} key={index}>
<AppStoreEntry
clickHandler={setSelectedStoreApp}
item={item}
/>
</Grid>
))}
</Grid>
</>
)
}


/** @return {React.Component} */
export function AppStoreEntry({
item,
clickHandler,
}) {
return (
<Paper>
<Card>
<CardActionArea onClick={() => {
clickHandler(item)
}}
>
<CardMedia
component="img"
height="140"
image={item.image}
alt={item.name}
sx={{
objectFit: 'unset',
background: '#f0f0f0',
padding: '1em',
}}
/>
<CardContent>
<Typography variant="h5" component="div">
{item.name}
</Typography>
<Typography variant="body2" color="text.secondary">
{item.description}
</Typography>
</CardContent>
</CardActionArea>
</Card>
</Paper>
)
}

/** @return {React.Component} */
export function AppStoreIFrame({
item,
}) {
const appFrameRef = useCallback((elt) => {
if (elt) {
elt.addEventListener('load', () => {
new IFrameCommunicationChannel(elt)
})
}
}, [])

return (
<Box sx={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
width: '100%',
height: '100%',
}}
>
<iframe
ref={appFrameRef}
title={item.name}
src={item.action}
width='100%'
height='100%'
/>
</Box>
)
}
56 changes: 56 additions & 0 deletions src/Components/AppStore/AppStoreMessagesHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import useStore from '../../store/useStore'


/** */
export class IFrameCommunicationChannel {
channel = null
port1 = null
iframe = null

/**
* constructor must be called after the iframe is loaded
*
* @param {object} iframe the iframe html element
*/
constructor(iframe) {
/* Step 1 : Message channel is created */
this.channel = new MessageChannel()
this.port1 = this.channel.port1
/* Step 2: Using the copy of port1 */
// Hooking up onMessage handler to receive messages from iframe,
// listening to mesages on port1.
this.port1.onmessage = this.messageHandler
/* Step 3: Sending out the port2 on load */
// Transfer port2 to the iframe
iframe.contentWindow.postMessage('init', iframe.src, [this.channel.port2])
this.iframe = iframe
}

/**
* Handle incoming messages from the iframe through the MessageChannel
*
* @param {*} event the data received from the iframe
*/
messageHandler = (event) => {
switch (event.data) {
case 'getLoadedFile':
this.sendMessage(event.data, useStore.getState().loadedFileInfo)
break
case 'getSelectedElements':
this.sendMessage(event.data, useStore.getState().selectedElements)
break
default:
break
}
}

/**
* Send any kind of data to the iframe through the MessageChannel
*
* @param {*} data the data to be sent to the iframe
*/
sendMessage = (action, response) => {
this.port1.postMessage({action, response})
}
}

59 changes: 59 additions & 0 deletions src/Components/AppStore/AppStorePanel.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react'
import Box from '@mui/material/Box'
import {BackButton, CloseButton, FullScreenButton} from '../Buttons'
import useStore from '../../store/useStore'
import {AppStoreListing, AppStoreIFrame} from './AppStoreListing'
import {PanelWithTitle} from '../SideDrawer/SideDrawerPanels'


/** @return {React.Component} */
export function AppStorePanel() {
const toggleAppStoreDrawer = useStore((state) => state.toggleAppStoreDrawer)

return (
<PanelWithTitle title={'App Store'}
controlsGroup={
<>
<Box>
<CloseButton
onClick={toggleAppStoreDrawer}
/>
</Box>
</>
}
>
<AppStoreListing/>
</PanelWithTitle>
)
}

/** @return {React.Component} */
export function AppPreviewPanel({item}) {
const toggleAppStoreDrawer = useStore((state) => state.toggleAppStoreDrawer)
const setSelectedStoreApp = useStore((state) => state.setSelectedStoreApp)
return (
<PanelWithTitle title={item.appName}
iconSrc={item.icon}
controlsGroup={
<>
<Box>
<BackButton
onClick={() => {
setSelectedStoreApp(null)
}}
/>
<FullScreenButton onClick={() => {
window.open(item.action, '_blank')
}}
/>
<CloseButton
onClick={toggleAppStoreDrawer}
/>
</Box>
</>
}
>
<AppStoreIFrame item={item}/>
</PanelWithTitle>
)
}