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

Use the same component to render every app icon #655

Merged
merged 3 commits into from
Mar 20, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions src/apps/Apps/Apps.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import {
unselectable,
} from '@aragon/ui'
import AppLayout from '../../components/AppLayout/AppLayout'
import AppIcon from '../../components/AppIcon/AppIcon'

import defaultIcon from '../../icons/app-default.svg'
import payrollIcon from './icons/payroll.svg'
import espressoIcon from './icons/espresso.svg'

Expand Down Expand Up @@ -65,7 +65,7 @@ class Apps extends React.Component {
{knownApps.map((app, i) => (
<Main key={i}>
<Icon>
<Img width="64" height="64" src={app.icon} alt="" />
<AppIcon size={64} src={app.icon} />
</Icon>
<Name>{app.name}</Name>
<TagWrapper>
Expand Down Expand Up @@ -133,10 +133,6 @@ const Icon = styled.div`
}
`

const Img = styled.img`
display: block;
`

const Name = styled.p`
display: flex;
width: 100%;
Expand Down Expand Up @@ -180,7 +176,7 @@ const statuses = {

const knownApps = [
{
icon: defaultIcon,
icon: null,
name: 'That Planning Suite',
status: 'alpha',
description: `Suite for open and fluid organizations.
Expand All @@ -205,7 +201,7 @@ const knownApps = [
link: 'https://github.com/espresso-org',
},
{
icon: defaultIcon,
icon: null,
name: 'Liquid democracy',
status: 'pre-alpha',
description: `Delegate your voting power to others,
Expand Down
10 changes: 4 additions & 6 deletions src/apps/Permissions/AppCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import styled from 'styled-components'
import { Text, Card, Badge, theme, unselectable } from '@aragon/ui'
import { AppType } from '../../prop-types'
import { shortenAddress } from '../../web3-utils'
import AppIcon from './AppIcon'
import AppIcon from '../../components/AppIcon/AppIcon'

class AppCard extends React.PureComponent {
static propTypes = {
Expand All @@ -22,7 +22,9 @@ class AppCard extends React.PureComponent {
const instanceTitle = `Address: ${proxyAddress}`
return (
<Main onClick={this.handleClick}>
<AppIconCard app={app} size={28} />
<div css="margin-bottom: 5px">
<AppIcon app={app} size={28} />
</div>
<Name>{name || 'Unknown'}</Name>
<IdentifierWrapper>
<Identifier title={instanceTitle}>{instanceLabel}</Identifier>
Expand All @@ -48,10 +50,6 @@ const Main = styled(Card).attrs({ width: '100%', height: '180px' })`
cursor: pointer;
`

const AppIconCard = styled(AppIcon)`
margin-bottom: 5px;
`

const Name = styled.p`
display: flex;
width: 100%;
Expand Down
42 changes: 0 additions & 42 deletions src/apps/Permissions/AppIcon.js

This file was deleted.

23 changes: 15 additions & 8 deletions src/apps/Permissions/AppInstanceLabel.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import styled from 'styled-components'
import { Badge, Viewport, breakpoint } from '@aragon/ui'
import { AppType, EthereumAddressType } from '../../prop-types'
import { shortenAddress } from '../../web3-utils'
import AppIcon from './AppIcon'
import AppIcon from '../../components/AppIcon/AppIcon'

class AppInstanceLabel extends React.PureComponent {
static propTypes = {
Expand All @@ -19,7 +19,20 @@ class AppInstanceLabel extends React.PureComponent {
<Main>
<Viewport>
{({ above }) =>
above('medium') && showIcon && <AppIconInRow app={app} />
above('medium') &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just started wondering if I should use above('medium') instead of !below('medium')

showIcon && (
<div
css={`
display: flex;
align-items: center;
height: 0;
margin-right: 10px;
margin-top: -1px;
`}
>
<AppIcon app={app} />
</div>
)
}
</Viewport>
<AppName>{app ? app.name : 'Unknown'}</AppName>
Expand All @@ -45,12 +58,6 @@ const Main = styled.div`
)}
`

const AppIconInRow = styled(AppIcon)`
height: 0;
margin-right: 10px;
margin-top: -1px;
`

const StyledBadge = styled(Badge.App)`
display: inline-block;

Expand Down
1 change: 0 additions & 1 deletion src/apps/Permissions/assets/kernel-icon.svg

This file was deleted.

88 changes: 88 additions & 0 deletions src/components/AppIcon/AppIcon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React from 'react'
import PropTypes from 'prop-types'
import { appIconUrl } from '../../utils'
import RemoteImage from '../RemoteImage'

import iconSvgHome from './assets/app-home.svg'
import iconSvgDefault from './assets/app-default.svg'

const DEFAULT_SIZE = 22
const DEFAULT_RADIUS = 5

const AppIcon = ({ app, src, size, radius, ...props }) => {
if (radius === -1) {
radius = size * (DEFAULT_RADIUS / DEFAULT_SIZE)
}
return (
<div
css={`
display: flex;
align-items: center;
overflow: hidden;
border-radius: ${radius}px;
`}
{...props}
>
{(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow!
Would it have been cleaner to use another component instead of a self executing function?

Copy link
Contributor Author

@bpierre bpierre Mar 20, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I don’t have any preference for one or the other (until we have pattern matching 💥), I tend to use this pattern because I find it more readable than using nested ternary operators, and declaring + calling a function at render time is something we do a lot with render props, so I don’t think it is an issue performance wise (as long as we don’t render at 60FPS!).

Having internal components is nice too, but the problem is that ESLint insists that every component requires prop types, which makes it super verbose. I couldn’t find any setting to only apply this rule to exported components.

Maybe we could directly call functions for this? That would also skip the extra step of creating a React element + having React calling it (insignificant gain in most cases, but it can be an argument to decide).

function MyComponent(props) {
  return (
    <div>
      {myComponentContent(props)}
    </div>
  )
}

function myComponentContent({ a, b }) {
  if (a) {
    return <span>{a}</span>
  }
  if (b) {
    return <span>{b}</span>
  }
  return <span>default</span>
}

Another solution would be to disable this rule after the main component, which would make it possible to have light internal components:

function MyComponent(props) {
  return (
    <div>
      <MyComponentContent {...props} />
    </div>
  )
}

MyComponent.propTypes = { /* … */ }

// Disabling the ESLint prop-types check for internal components.
/* eslint-disable react/prop-types */

function MyComponentContent({ a, b }) {
  if (a) {
    return <span>{a}</span>
  }
  if (b) {
    return <span>{b}</span>
  }
  return <span>default</span>
}

And for simple cases, I think the “IIFE in JSX” pattern, while not being the most elegant thing in the world, can do a good job too 😆

function MyComponent({ a, b }) {
  return (
    <div>
      {(() => {
        if (a) {
          return <span>{a}</span>
        }
        if (b) {
          return <span>{b}</span>
        }
        return <span>default</span>
      })()}
    </div>
  )
}

@AquiGorka @sohkai I don’t really have any preference, I think the three solutions are fine and I use them depending on the context. I pushed #2 for now as @AquiGorka was suggesting a preference for it rather than #3. Let me know if you have any opinion about it so we align!

EDIT:

Adding a few other possibilities for the sake of completeness:

Mutable variable (no early returns 😩):

function MyComponent({ a, b }) {
  let content = <span>default</span>
  if (a) {
    content = <span>{a}</span>
  }
  if (b) {
    content = <span>{b}</span>
  }
  return (
    <div>
      {content}
    </div>
  )
}

Wrapping function:

function MyComponent({ a, b }) {
  const wrap = content => <div>{content}</div>
  if (a) {
    return wrap(<span>{a}</span>)
  }
  if (b) {
    return wrap(<span>{b}</span>)
  }
  return wrap(<span>default</span>)
}

Nested ternary:

function MyComponent({ a, b }) {
  return (
    <div>
      {a ? (
        <span>{a}</span>
      ) : b ? (
        <span>{b}</span>
      ) : (
        <span>default</span>
      )}
    </div>
  )
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️ @bpierre thank you for taking the time to explain your line of thinking and yes I prefer #2 but it is only a personal preference and I can adhere to any team convention we agree on - and yes, because of prop types it becomes a bit too verbose.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool :-) Let’s not make it a convention for now then, and we can refer to this discussion if we want to avoid one of these patterns in the future!

if (src) {
return <IconBase size={size} src={src} />
}
if (!app) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could remove this condition if we flip it to be if (app)

return <IconDefault size={size} />
}
if (app.appId === 'home') {
return <IconHome size={size} />
}
if (app.baseUrl) {
const iconUrl = appIconUrl(app)
return (
// Tries to load the app icon while displaying the default one.
<RemoteImage size={size} src={iconUrl}>
{({ exists }) =>
exists ? (
<IconBase size={size} src={iconUrl} />
) : (
<IconDefault size={size} />
)
}
</RemoteImage>
)
}
return <IconDefault size={size} />
})()}
</div>
)
}

AppIcon.propTypes = {
app: PropTypes.object,
src: PropTypes.string,
radius: PropTypes.number,
size: PropTypes.number.isRequired,
}

AppIcon.defaultProps = {
app: null,
src: null,
radius: -1,
size: DEFAULT_SIZE,
}

// Base icon
const IconBase = ({ src, size, alt = '', ...props }) => (
<img {...props} src={src} width={size} height={size} alt={alt} />
)
IconBase.propTypes = {
alt: PropTypes.string,
src: PropTypes.string,
radius: PropTypes.number,
size: PropTypes.number.isRequired,
}

// Default icon
const IconDefault = props => <IconBase {...props} src={iconSvgDefault} />

// Home app icon
const IconHome = props => <IconBase {...props} src={iconSvgHome} />

export default AppIcon
20 changes: 20 additions & 0 deletions src/components/AppIcon/assets/app-default.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 2 additions & 3 deletions src/components/MenuPanel/MenuPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,13 @@ import {
unselectable,
} from '@aragon/ui'
import memoize from 'lodash.memoize'
import { appIconUrl } from '../../utils'
import { AppType, AppsStatusType, DaoAddressType } from '../../prop-types'
import { staticApps } from '../../static-apps'
import MenuPanelAppGroup from './MenuPanelAppGroup'
import MenuPanelAppsLoader from './MenuPanelAppsLoader'
import RemoteIcon from '../RemoteIcon'
import NotificationAlert from '../Notifications/NotificationAlert'
import OrganizationSwitcher from './OrganizationSwitcher/OrganizationSwitcher'
import AppIcon from '../AppIcon/AppIcon'

const APP_APPS_CENTER = staticApps.get('apps').app
const APP_HOME = staticApps.get('home').app
Expand All @@ -41,7 +40,7 @@ const prepareAppGroups = apps =>
{
appId: app.appId,
name: app.name,
icon: <RemoteIcon src={appIconUrl(app)} size={22} />,
icon: <AppIcon app={app} size={22} />,
instances: [instance],
},
])
Expand Down
4 changes: 2 additions & 2 deletions src/components/MenuPanel/MenuPanelAppGroup.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import { Spring, animated } from 'react-spring'
import { theme, IconBlank } from '@aragon/ui'
import { theme } from '@aragon/ui'
import color from 'onecolor'
import MenuPanelInstance from './MenuPanelInstance'
import springs from '../../springs'
Expand Down Expand Up @@ -67,7 +67,7 @@ class MenuPanelAppGroup extends React.PureComponent {
onClick={this.handleAppClick}
>
<span>
<span className="icon">{icon || <IconBlank />}</span>
<span className="icon">{icon}</span>
<span className="name">{name}</span>
</span>
</ButtonItem>
Expand Down
46 changes: 0 additions & 46 deletions src/components/RemoteIcon.js

This file was deleted.

Loading