commit
d58adf0582
12 changed files with 281 additions and 1 deletions
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -35,6 +35,7 @@ const ImageElement = React.memo((props: Props): JSX.Element|null => {
|
|||
|
||||
return (
|
||||
<img
|
||||
className='ImageElement'
|
||||
src={imageDataUrl}
|
||||
alt={block.title}
|
||||
/>
|
||||
|
|
26
webapp/src/components/gallery/gallery.scss
Normal file
26
webapp/src/components/gallery/gallery.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
54
webapp/src/components/gallery/gallery.tsx
Normal file
54
webapp/src/components/gallery/gallery.tsx
Normal 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
|
44
webapp/src/components/gallery/galleryCard.scss
Normal file
44
webapp/src/components/gallery/galleryCard.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
80
webapp/src/components/gallery/galleryCard.tsx
Normal file
80
webapp/src/components/gallery/galleryCard.tsx
Normal 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
|
|
@ -36,6 +36,10 @@
|
|||
width: 14px;
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
&.GalleryIcon {
|
||||
fill: rgba(var(--sidebar-fg), 0.3);
|
||||
stroke: unset;
|
||||
}
|
||||
}
|
||||
|
||||
>.IconButton {
|
||||
|
|
|
@ -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/>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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/>
|
||||
}
|
||||
}
|
||||
|
|
6
webapp/src/widgets/icons/gallery.scss
Normal file
6
webapp/src/widgets/icons/gallery.scss
Normal file
|
@ -0,0 +1,6 @@
|
|||
.GalleryIcon {
|
||||
fill: rgba(var(--body-color), 0.7);
|
||||
stroke: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
20
webapp/src/widgets/icons/gallery.tsx
Normal file
20
webapp/src/widgets/icons/gallery.tsx
Normal 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>
|
||||
)
|
||||
}
|
Loading…
Reference in a new issue