Merge pull request #216 from jespino/gallery-view

Gallery view
This commit is contained in:
Jesús Espino 2021-03-30 20:22:19 +02:00 committed by GitHub
commit d58adf0582
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 281 additions and 1 deletions

View file

@ -5,7 +5,7 @@ import {Utils} from '../utils'
import {IBlock, MutableBlock} from './block'
import {FilterGroup} from './filterGroup'
type IViewType = 'board' | 'table' // | 'calendar' | 'list' | 'gallery'
type IViewType = 'board' | 'table' | 'gallery' // | 'calendar' | 'list'
type ISortOption = { propertyId: '__title' | string, reversed: boolean }
interface BoardView extends IBlock {

View file

@ -19,6 +19,7 @@ import ViewHeader from './viewHeader/viewHeader'
import ViewTitle from './viewTitle'
import Kanban from './kanban/kanban'
import Table from './table/table'
import Gallery from './gallery/gallery'
type Props = {
boardTree: BoardTree
@ -155,6 +156,13 @@ class CenterPanel extends React.Component<Props, State> {
addCard={(show) => this.addCard('', show)}
onCardClicked={this.cardClicked}
/>}
{activeView.viewType === 'gallery' &&
<Gallery
boardTree={boardTree}
readonly={this.props.readonly}
showCard={this.showCard}
addCard={(show) => this.addCard('', show)}
/>}
</div>
</div>
</div>

View file

@ -35,6 +35,7 @@ const ImageElement = React.memo((props: Props): JSX.Element|null => {
return (
<img
className='ImageElement'
src={imageDataUrl}
alt={block.title}
/>

View file

@ -0,0 +1,26 @@
.Gallery {
display: flex;
flex-wrap: wrap;
.octo-gallery-new {
border: 1px solid rgba(var(--body-color), 0.09);
border-radius: var(--default-rad);
color: rgba(var(--body-color), 0.3);
cursor: pointer;
width: 280px;
height: 200px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: 10px;
&.selected {
background-color: rgba(90, 200, 255, 0.2);
}
&:hover {
background-color: rgba(var(--body-color), 0.05);
}
}
}

View file

@ -0,0 +1,54 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import {FormattedMessage} from 'react-intl'
import {BoardTree} from '../../viewModel/boardTree'
import './gallery.scss'
import GalleryCard from './galleryCard'
type Props = {
boardTree: BoardTree
readonly: boolean
showCard: (cardId?: string) => void
addCard: (show: boolean) => Promise<void>
}
const Gallery = (props: Props): JSX.Element => {
const {boardTree} = props
const {cards} = boardTree
return (
<div className='octo-table-body Gallery'>
{cards.map((card) => {
const tableRow = (
<GalleryCard
key={card.id + card.updateAt}
card={card}
showCard={props.showCard}
/>)
return tableRow
})}
{/* Add New row */}
{!props.readonly &&
<div
className='octo-gallery-new'
onClick={() => {
props.addCard(false)
}}
>
<FormattedMessage
id='TableComponent.plus-new'
defaultMessage='+ New'
/>
</div>
}
</div>
)
}
export default Gallery

View file

@ -0,0 +1,44 @@
.GalleryCard {
border: 1px solid rgba(var(--body-color), 0.09);
border-radius: var(--default-rad);
width: 280px;
height: 200px;
display: flex;
flex-direction: column;
margin-right: 10px;
margin-bottom: 10px;
&:hover {
background-color: rgba(var(--body-color), 0.05);
}
.gallery-item {
background-color: rgba(var(--body-color), 0.03);
flex-grow: 1;
overflow: hidden;
padding: 0 10px;
font-size: 0.7em;
opacity: 0.7;
}
.gallery-image {
flex-grow: 1;
overflow: hidden;
.ImageElement {
width: 100%;
}
}
.gallery-title {
flex-grow: 0;
border-top: 1px solid rgba(var(--body-color), 0.09);
margin: 0;
padding: 5px 10px;
display: flex;
.octo-icon {
margin-right: 5px;
}
}
}

View file

@ -0,0 +1,80 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState, useEffect} from 'react'
import {FormattedMessage} from 'react-intl'
import {Card} from '../../blocks/card'
import {CardTree, MutableCardTree} from '../../viewModel/cardTree'
import {IContentBlock} from '../../blocks/contentBlock'
import useCardListener from '../../hooks/cardListener'
import ImageElement from '../content/imageElement'
import ContentElement from '../content/contentElement'
import './galleryCard.scss'
type Props = {
card: Card
showCard: (cardId: string) => void
}
const GalleryCard = React.memo((props: Props) => {
const {card} = props
const [cardTree, setCardTree] = useState<CardTree>()
useCardListener(
card.id,
async (blocks) => {
const newCardTree = cardTree ? MutableCardTree.incrementalUpdate(cardTree, blocks) : await MutableCardTree.sync(card.id)
setCardTree(newCardTree)
},
async () => {
const newCardTree = await MutableCardTree.sync(card.id)
setCardTree(newCardTree)
},
)
useEffect(() => {
const f = async () => setCardTree(await MutableCardTree.sync(card.id))
f()
}, [])
let images: IContentBlock[] = []
if (cardTree) {
images = cardTree.contents.filter((content) => content.type === 'image')
}
return (
<div
className='GalleryCard'
onClick={() => props.showCard(props.card.id)}
>
{images?.length > 0 &&
<div className='gallery-image'>
<ImageElement block={images[0]}/>
</div>}
{images?.length === 0 &&
<div className='gallery-item'>
{cardTree && images?.length === 0 && cardTree.contents.map((block) => (
<ContentElement
key={block.id}
block={block}
readonly={true}
/>
))}
</div>}
<div className='gallery-title'>
{ card.icon ? <div className='octo-icon'>{card.icon}</div> : undefined }
<div key='__title'>
{card.title ||
<FormattedMessage
id='KanbanCard.untitled'
defaultMessage='Untitled'
/>}
</div>
</div>
</div>
)
})
export default GalleryCard

View file

@ -36,6 +36,10 @@
width: 14px;
margin-right: 8px;
flex-shrink: 0;
&.GalleryIcon {
fill: rgba(var(--sidebar-fg), 0.3);
stroke: unset;
}
}
>.IconButton {

View file

@ -13,6 +13,7 @@ import DisclosureTriangle from '../../widgets/icons/disclosureTriangle'
import DuplicateIcon from '../../widgets/icons/duplicate'
import OptionsIcon from '../../widgets/icons/options'
import TableIcon from '../../widgets/icons/table'
import GalleryIcon from '../../widgets/icons/gallery'
import Menu from '../../widgets/menu'
import MenuWrapper from '../../widgets/menuWrapper'
@ -35,6 +36,7 @@ const SidebarBoardItem = React.memo((props: Props) => {
switch (viewType) {
case 'board': return <BoardIcon/>
case 'table': return <TableIcon/>
case 'gallery': return <GalleryIcon/>
default: return <div/>
}
}

View file

@ -14,6 +14,7 @@ import BoardIcon from '../widgets/icons/board'
import DeleteIcon from '../widgets/icons/delete'
import DuplicateIcon from '../widgets/icons/duplicate'
import TableIcon from '../widgets/icons/table'
import GalleryIcon from '../widgets/icons/gallery'
import Menu from '../widgets/menu'
type Props = {
@ -122,6 +123,33 @@ export class ViewMenu extends React.PureComponent<Props> {
})
}
private handleAddViewGallery = async () => {
const {board, boardTree, showView, intl} = this.props
Utils.log('addview-gallery')
const view = new MutableBoardView()
view.title = intl.formatMessage({id: 'View.NewGalleryTitle', defaultMessage: 'Gallery view'})
view.viewType = 'gallery'
view.parentId = board.id
view.rootId = board.rootId
const oldViewId = boardTree.activeView.id
await mutator.insertBlock(
view,
'add view',
async () => {
// This delay is needed because OctoListener has a default 100 ms notification delay before updates
setTimeout(() => {
Utils.log(`showView: ${view.id}`)
showView(view.id)
}, 120)
},
async () => {
showView(oldViewId)
})
}
render(): JSX.Element {
const {boardTree} = this.props
return (
@ -169,6 +197,12 @@ export class ViewMenu extends React.PureComponent<Props> {
icon={<TableIcon/>}
onClick={this.handleAddViewTable}
/>
<Menu.Text
id='gallery'
name='Gallery'
icon={<GalleryIcon/>}
onClick={this.handleAddViewGallery}
/>
</Menu.SubMenu>
}
</Menu>
@ -179,6 +213,7 @@ export class ViewMenu extends React.PureComponent<Props> {
switch (viewType) {
case 'board': return <BoardIcon/>
case 'table': return <TableIcon/>
case 'gallery': return <GalleryIcon/>
default: return <div/>
}
}

View file

@ -0,0 +1,6 @@
.GalleryIcon {
fill: rgba(var(--body-color), 0.7);
stroke: none;
width: 24px;
height: 24px;
}

View file

@ -0,0 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import './gallery.scss'
export default function GalleryIcon(): JSX.Element {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
className='GalleryIcon Icon'
viewBox='0 0 512 512'
>
<path
d='M464 64H48C21.49 64 0 85.49 0 112v288c0 26.51 21.49 48 48 48h416c26.51 0 48-21.49 48-48V112c0-26.51-21.49-48-48-48zm-6 336H54a6 6 0 0 1-6-6V118a6 6 0 0 1 6-6h404a6 6 0 0 1 6 6v276a6 6 0 0 1-6 6zM128 152c-22.091 0-40 17.909-40 40s17.909 40 40 40 40-17.909 40-40-17.909-40-40-40zM96 352h320v-80l-87.515-87.515c-4.686-4.686-12.284-4.686-16.971 0L192 304l-39.515-39.515c-4.686-4.686-12.284-4.686-16.971 0L96 304v48z'
/>
</svg>
)
}