mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-26 04:23:58 +03:00
Add Timeline component (#2535)
Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
parent
ba8ab45dc2
commit
8a5b84802c
@ -87,8 +87,7 @@ export default mergeIds(viewId, view, {
|
||||
General: '' as IntlString,
|
||||
Navigation: '' as IntlString,
|
||||
Editor: '' as IntlString,
|
||||
MarkdownFormatting: '' as IntlString,
|
||||
List: '' as IntlString
|
||||
MarkdownFormatting: '' as IntlString
|
||||
},
|
||||
function: {
|
||||
FilterObjectInResult: '' as Resource<(filter: Filter, onUpdate: () => void) => Promise<ObjQueryType<any>>>,
|
||||
|
@ -122,6 +122,9 @@
|
||||
--incoming-msg: rgba(67, 67, 72, .3);
|
||||
--outcoming-msg: rgba(67, 67, 72, .6);
|
||||
|
||||
--trans-content-05: rgba(138, 143, 152, .05);
|
||||
--trans-content-10: rgba(138, 143, 152, .1);
|
||||
|
||||
--theme-bg-color: #18181e;
|
||||
--theme-bg-selection: #282830;
|
||||
--theme-bg-checked: #262b39;
|
||||
@ -226,7 +229,7 @@
|
||||
--dark-color: #90959d;
|
||||
--content-color: #3c4149;
|
||||
--accent-color: #282a30;
|
||||
--caption-color: #282a30;
|
||||
--caption-color: #131416;
|
||||
--white-color: #fff;
|
||||
--caret-color: #6e5ed2;
|
||||
--warning-color: #f2994a; // Dark
|
||||
@ -275,6 +278,9 @@
|
||||
--incoming-msg: rgba(67, 67, 72, .3);
|
||||
--outcoming-msg: rgba(67, 67, 72, .6);
|
||||
|
||||
--trans-content-05: rgba(60, 65, 73, .05);
|
||||
--trans-content-10: rgba(60, 65, 73, .1);
|
||||
|
||||
--theme-bg-color: #FFFFFF;
|
||||
--theme-bg-selection: #F1F1F4;
|
||||
--theme-menu-color: #E7E7E7;
|
||||
|
@ -124,7 +124,10 @@ p:last-child { margin-block-end: 0; }
|
||||
|
||||
.text-center { text-align: center; }
|
||||
|
||||
.firstLetter span::first-letter { text-transform: uppercase; }
|
||||
.firstLetter span{
|
||||
display: inline-block;
|
||||
&::first-letter { text-transform: uppercase; }
|
||||
}
|
||||
|
||||
.inline-height2 {
|
||||
line-height: 200%;
|
||||
|
@ -30,6 +30,19 @@
|
||||
export let noStretch: boolean = false
|
||||
export let divScroll: HTMLElement | undefined = undefined
|
||||
|
||||
export function scroll (top: number, left?: number, behavior: 'auto' | 'smooth' = 'auto') {
|
||||
if (divScroll && divHScroll) {
|
||||
if (top !== 0) divScroll.scroll({ top, left: 0, behavior })
|
||||
if (left !== 0 || left !== undefined) divHScroll.scroll({ top: 0, left, behavior })
|
||||
}
|
||||
}
|
||||
export function scrollBy (top: number, left?: number, behavior: 'auto' | 'smooth' = 'auto') {
|
||||
if (divScroll && divHScroll) {
|
||||
if (top !== 0) divScroll.scrollBy({ top, left: 0, behavior })
|
||||
if (left !== 0 || left !== undefined) divHScroll.scrollBy({ top: 0, left, behavior })
|
||||
}
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let mask: 'top' | 'bottom' | 'both' | 'none' = 'none'
|
||||
|
@ -43,6 +43,7 @@
|
||||
{#if items.length > 0}
|
||||
<div class="tablist-container {kind} {size}">
|
||||
{#each items as item, i}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
bind:this={tabs[i]}
|
||||
class="button"
|
||||
|
776
packages/ui/src/components/Timeline.svelte
Normal file
776
packages/ui/src/components/Timeline.svelte
Normal file
@ -0,0 +1,776 @@
|
||||
<!--
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { fly } from 'svelte/transition'
|
||||
import { Timestamp } from '@hcengineering/core'
|
||||
import { TimelinePoint, TimelineRow, TimelineState } from '../types'
|
||||
import ui, {
|
||||
CheckBox,
|
||||
Icon,
|
||||
Scroller,
|
||||
Button,
|
||||
resizeObserver,
|
||||
MILLISECONDS_IN_DAY,
|
||||
MILLISECONDS_IN_WEEK,
|
||||
IconArrowLeft,
|
||||
IconArrowRight,
|
||||
IconAdd
|
||||
} from '..'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
|
||||
export let selectedRows: number[] = []
|
||||
export let selectedRow: number | undefined = undefined
|
||||
export let lines: TimelineRow[] | undefined = undefined
|
||||
export let currentTime: Timestamp = new Date().setHours(0, 0, 0, 0)
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const NOT_ENDED = MILLISECONDS_IN_WEEK * 4
|
||||
let currentDate: Date = new Date(currentTime)
|
||||
$: currentDate = new Date(currentTime)
|
||||
|
||||
export const onObjectChecked = (row: number, value: boolean) => {
|
||||
dispatch('check', { row, value })
|
||||
}
|
||||
export const selectRow = (row: number) => {
|
||||
selectedRow = row
|
||||
}
|
||||
const handleRowFocused = (row: number) => {
|
||||
dispatch('row-focus', row)
|
||||
}
|
||||
|
||||
let panelWidth: number = 320
|
||||
const dayWidth: number = 5
|
||||
let container: HTMLElement
|
||||
let viewbox: HTMLElement
|
||||
let scroller: Scroller
|
||||
let scrollDir: 'horizontal' | 'vertical' | 'none' = 'none'
|
||||
|
||||
const locale = new Intl.NumberFormat().resolvedOptions().locale
|
||||
const nillPoint: TimelinePoint = { label: '', date: currentDate, x: 0 }
|
||||
const nillRect: DOMRect = { x: 0, y: 0, width: 0, height: 0, left: 0, right: 0, top: 0, bottom: 0, toJSON: () => {} }
|
||||
const time: TimelineState = {
|
||||
todayMarker: nillPoint,
|
||||
offsetView: 0,
|
||||
renderedRange: { left: nillPoint, right: nillPoint, firstDays: [] },
|
||||
rows: undefined,
|
||||
months: [],
|
||||
days: [],
|
||||
timelineBox: nillRect,
|
||||
viewBox: nillRect
|
||||
}
|
||||
|
||||
const checkRange = (reverse: boolean) => {
|
||||
if (reverse) {
|
||||
if (time.offsetView * -1 - time.viewBox.width <= time.renderedRange.left.x) renderPrevMonth()
|
||||
} else {
|
||||
if (time.offsetView * -1 + time.viewBox.width * 2 >= time.renderedRange.right.x) renderNextMonth()
|
||||
}
|
||||
}
|
||||
|
||||
const getDateByOffset = (x: number): { date: Date; delta: number } => {
|
||||
const deltaDays = Math.floor(x / dayWidth)
|
||||
const calcDay = new Date(currentTime + deltaDays * MILLISECONDS_IN_DAY)
|
||||
return { date: calcDay, delta: deltaDays }
|
||||
}
|
||||
const getOffsetByDate = (date: Timestamp | Date): number => {
|
||||
const tempDay = new Date(date).setHours(0, 0, 0, 0)
|
||||
const deltaDays = Math.floor((tempDay - currentTime) / MILLISECONDS_IN_DAY)
|
||||
return deltaDays * dayWidth
|
||||
}
|
||||
const getNextMonth = (date: Date): TimelinePoint => {
|
||||
const fDate = new Date(date.getFullYear(), date.getMonth() + 1, 1, 0, 0)
|
||||
const offDate = getOffsetByDate(fDate)
|
||||
const lDate = Intl.DateTimeFormat(locale, { month: 'long' }).format(fDate)
|
||||
return { date: fDate, x: offDate, label: lDate }
|
||||
}
|
||||
const getNextWeek = (date: Date, reverse?: boolean): TimelinePoint => {
|
||||
const fDate = new Date(date.getTime() + MILLISECONDS_IN_WEEK * (reverse ? -1 : 1))
|
||||
const offDate = getOffsetByDate(fDate)
|
||||
const lDate = fDate.getDate().toString()
|
||||
return { date: fDate, x: offDate, label: lDate }
|
||||
}
|
||||
|
||||
const renderPrevMonth = () => {
|
||||
const oldRange: TimelinePoint = time.renderedRange.left
|
||||
const newDate: Date = new Date(oldRange.date.getFullYear(), oldRange.date.getMonth() - 1, 1, 0, 0)
|
||||
const newRange: number = getOffsetByDate(newDate)
|
||||
const newLabel: string = Intl.DateTimeFormat(locale, { month: 'long' }).format(newDate)
|
||||
const newPoint: TimelinePoint = {
|
||||
x: newRange,
|
||||
date: newDate,
|
||||
label: newLabel
|
||||
}
|
||||
time.renderedRange.left = newPoint
|
||||
time.months = [newPoint, ...time.months]
|
||||
while (getNextWeek(time.days[0].date, true).x > newPoint.x) {
|
||||
const prevDay: TimelinePoint = getNextWeek(time.days[0].date, true)
|
||||
time.days = [prevDay, ...time.days]
|
||||
}
|
||||
}
|
||||
const renderNextMonth = () => {
|
||||
const oldRange: TimelinePoint = time.renderedRange.right
|
||||
const newDate: Date = new Date(oldRange.date.getFullYear(), oldRange.date.getMonth() + 1, 1, 0, 0)
|
||||
const newRange: number = getOffsetByDate(newDate)
|
||||
const newLabel: string = Intl.DateTimeFormat(locale, { month: 'long' }).format(newDate)
|
||||
const newPoint: TimelinePoint = {
|
||||
x: newRange,
|
||||
date: newDate,
|
||||
label: newLabel
|
||||
}
|
||||
time.renderedRange.right = newPoint
|
||||
time.months = [...time.months, newPoint]
|
||||
while (getNextWeek(time.days[time.days.length - 1].date).x < newPoint.x) {
|
||||
const nextDay: TimelinePoint = getNextWeek(time.days[time.days.length - 1].date)
|
||||
time.days = [...time.days, nextDay]
|
||||
}
|
||||
}
|
||||
|
||||
const wheelEvent = (e: WheelEvent) => {
|
||||
e = e || window.event
|
||||
const deltaX = -e.deltaX
|
||||
const deltaY = e.deltaY
|
||||
if (scrollDir === 'none' && (Math.abs(deltaX) > 2 || Math.abs(deltaY) > 2)) {
|
||||
if (Math.abs(deltaX) > Math.abs(deltaY)) scrollDir = 'horizontal'
|
||||
else scrollDir = 'vertical'
|
||||
} else if (Math.abs(deltaX) <= 4 && Math.abs(deltaY) <= 4) scrollDir = 'none'
|
||||
time.offsetView += deltaX
|
||||
if (scrollDir === 'horizontal') {
|
||||
mouseMoveEvent(e)
|
||||
checkRange(deltaX > 0)
|
||||
}
|
||||
if (scrollDir === 'vertical') scroller.scrollBy(deltaY)
|
||||
e.preventDefault ? e.preventDefault() : (e.returnValue = false)
|
||||
}
|
||||
const mouseMoveEvent = (e: MouseEvent) => {
|
||||
const cur = e.x - time.viewBox.left
|
||||
if (cur >= 0 && cur <= time.viewBox.width) {
|
||||
const offset = cur - time.offsetView
|
||||
const t = getDateByOffset(offset)
|
||||
time.cursorMarker = {
|
||||
label: t.date.getDate().toString(),
|
||||
x: offset,
|
||||
date: t.date
|
||||
}
|
||||
}
|
||||
}
|
||||
const mouseOutEvent = (e: MouseEvent) => {
|
||||
time.cursorMarker = undefined
|
||||
}
|
||||
const clickEvent = (e: MouseEvent) => {
|
||||
console.log('[Timeline] Cursor: ', time.cursorMarker)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
container.addEventListener('wheel', wheelEvent)
|
||||
container.addEventListener('mousemove', mouseMoveEvent)
|
||||
container.addEventListener('mouseout', mouseOutEvent)
|
||||
container.addEventListener('click', clickEvent)
|
||||
|
||||
time.timelineBox = container.getBoundingClientRect()
|
||||
time.viewBox = viewbox.getBoundingClientRect()
|
||||
time.offsetView = Math.floor(time.viewBox.width / 2)
|
||||
time.todayMarker.x = 0
|
||||
time.todayMarker.date = currentDate
|
||||
time.todayMarker.label = Intl.DateTimeFormat(locale, { day: 'numeric', month: 'short' }).format(currentDate)
|
||||
|
||||
let mass: number[] = [currentTime]
|
||||
lines?.forEach((line) => {
|
||||
if (line.items !== undefined) {
|
||||
let tr: number[] = []
|
||||
line.items.forEach((it) => {
|
||||
if (it.startDate) tr = [...tr, it.startDate]
|
||||
if (it.targetDate) tr = [...tr, it.targetDate]
|
||||
else if (it.startDate) tr = [...tr, it.startDate + NOT_ENDED]
|
||||
})
|
||||
if (tr.length > 0) {
|
||||
mass = [...mass, ...tr]
|
||||
tr.sort()
|
||||
const minD: Date = new Date(tr[0])
|
||||
const maxD: Date = new Date(tr[tr.length - 1])
|
||||
const r = {
|
||||
min: {
|
||||
date: minD,
|
||||
x: getOffsetByDate(minD)
|
||||
},
|
||||
max: {
|
||||
date: maxD,
|
||||
x: getOffsetByDate(maxD)
|
||||
}
|
||||
}
|
||||
time.rows ? time.rows.push(r) : (time.rows = [r])
|
||||
} else time.rows ? time.rows.push(null) : (time.rows = [null])
|
||||
} else time.rows ? time.rows.push(null) : (time.rows = [null])
|
||||
})
|
||||
mass.sort()
|
||||
|
||||
let leftRange: number = getOffsetByDate(mass[0]) - time.viewBox.width * 1.5
|
||||
const leftDate: Date = new Date(getDateByOffset(leftRange).date.setDate(1))
|
||||
leftRange = getOffsetByDate(leftDate)
|
||||
time.renderedRange.left = {
|
||||
x: leftRange,
|
||||
date: leftDate,
|
||||
label: Intl.DateTimeFormat(locale, { month: 'long' }).format(leftDate)
|
||||
}
|
||||
let rightRange: number = getOffsetByDate(mass[mass.length - 1]) + time.viewBox.width * 1.5
|
||||
const tr: Date = new Date(getDateByOffset(rightRange).date)
|
||||
const rightDate: Date = new Date(new Date(tr.getFullYear(), tr.getMonth() + 1, 1, 0, 0).getTime() - 1)
|
||||
rightRange = getOffsetByDate(rightDate)
|
||||
time.renderedRange.right = {
|
||||
x: rightRange,
|
||||
date: rightDate,
|
||||
label: Intl.DateTimeFormat(locale, { month: 'long' }).format(rightDate)
|
||||
}
|
||||
|
||||
time.months = [time.renderedRange.left]
|
||||
let i = 0
|
||||
do {
|
||||
const nextMonth: TimelinePoint = getNextMonth(time.months[i].date)
|
||||
time.months = [...time.months, nextMonth]
|
||||
i++
|
||||
} while (getNextMonth(time.months[i].date).x <= time.renderedRange.right.x)
|
||||
time.days = [
|
||||
{
|
||||
x: time.renderedRange.left.x,
|
||||
date: time.renderedRange.left.date,
|
||||
label: '1'
|
||||
}
|
||||
]
|
||||
i = 0
|
||||
do {
|
||||
const nextWeek: TimelinePoint = getNextWeek(time.days[i].date)
|
||||
time.days = [...time.days, nextWeek]
|
||||
i++
|
||||
} while (getNextWeek(time.days[i].date).x <= time.renderedRange.right.x)
|
||||
})
|
||||
|
||||
let moving: boolean = false
|
||||
let sX: number
|
||||
const splitterStart = (e: MouseEvent) => {
|
||||
if (time.timelineBox.width <= 450) return
|
||||
sX = (e.x - time.viewBox.left) * -1
|
||||
document.addEventListener('mouseup', splitterEnd)
|
||||
document.addEventListener('mousemove', splitterMove)
|
||||
moving = true
|
||||
}
|
||||
const splitterMove = (e: MouseEvent) => {
|
||||
if (e.x - time.timelineBox.left + sX < 300) panelWidth = 300
|
||||
else if (time.timelineBox.right - e.x + sX < 150) panelWidth = time.timelineBox.width - 150
|
||||
else panelWidth = e.x - time.timelineBox.left + sX
|
||||
}
|
||||
const splitterEnd = (e: MouseEvent) => {
|
||||
document.removeEventListener('mousemove', splitterMove)
|
||||
document.removeEventListener('mouseup', splitterEnd)
|
||||
time.viewBox = viewbox.getBoundingClientRect()
|
||||
moving = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="timeline-container"
|
||||
bind:this={container}
|
||||
use:resizeObserver={() => {
|
||||
time.timelineBox = container.getBoundingClientRect()
|
||||
time.viewBox = viewbox.getBoundingClientRect()
|
||||
}}
|
||||
>
|
||||
<div class="timeline-header">
|
||||
<div class="timeline-header__title" style:width={`${panelWidth}px`}>
|
||||
<Button
|
||||
label={ui.string.Today}
|
||||
on:click={() => {
|
||||
time.offsetView = Math.floor(time.viewBox.width / 2)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="timeline-header__time" bind:this={viewbox}>
|
||||
<div class="timeline-header__time-content" style:transform={`translateX(${time.offsetView}px)`}>
|
||||
{#if time.months}
|
||||
{#each time.months as month}
|
||||
<div class="month firstLetter" style:left={`${month.x}px`}>
|
||||
{#if month.date.getMonth() === 0}
|
||||
<b class="caption-color">{month.date.getFullYear()}</b>
|
||||
{/if}
|
||||
<span style="firstLetter">{month.label}</span>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if time.days}
|
||||
{#each time.days as day}
|
||||
<div class="day" style:left={`${day.x}px`}>{day.label}</div>
|
||||
{/each}
|
||||
{/if}
|
||||
<div class="cursor" style:left={`${time.todayMarker.x}px`}>{time.todayMarker.date.getDate()}</div>
|
||||
<!-- {#if time.cursorMarker}
|
||||
<div class="cursor" style:left={`${time.cursorMarker.x}px`}>{time.cursorMarker.label}</div>
|
||||
{/if} -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="timeline-background__headers" style:width={`${panelWidth}px`} />
|
||||
<div class="timeline-background__viewbox" style:left={`${panelWidth}px`}>
|
||||
<div class="timeline-wrapped_content" style:transform={`translateX(${time.offsetView}px)`}>
|
||||
{#if time.months}
|
||||
{#each time.months as month}
|
||||
<div class="monthMarker" style:left={`${month.x}px`} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if lines}
|
||||
<Scroller bind:this={scroller}>
|
||||
{#each lines as line, row}
|
||||
{@const rangeRow = time.rows ? time.rows[row] : null}
|
||||
<div
|
||||
class="listGrid"
|
||||
class:mListGridChecked={selectedRows.find((x) => x === row) !== undefined}
|
||||
class:mListGridSelected={selectedRow === row}
|
||||
on:focus={() => {}}
|
||||
on:mousemove={(ev) => {
|
||||
if (row !== selectedRow) {
|
||||
handleRowFocused(row)
|
||||
}
|
||||
ev.preventDefault()
|
||||
}}
|
||||
>
|
||||
<div class="headerWrapper" style:width={`${panelWidth}px`}>
|
||||
<div class="gridElement">
|
||||
<div class="eListGridCheckBox">
|
||||
<CheckBox
|
||||
checked={selectedRows.filter((i) => i === row).length > 0}
|
||||
on:value={(event) => onObjectChecked(row, event.detail)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<slot {row} />
|
||||
</div>
|
||||
<div class="contentWrapper" class:nullRow={rangeRow === null && !moving}>
|
||||
<div class="timeline-wrapped_content" style:transform={`translateX(${time.offsetView}px)`}>
|
||||
{#if line.items}
|
||||
{#each line.items as item}
|
||||
{#if item.startDate}
|
||||
{@const target = item.targetDate ?? item.startDate + NOT_ENDED}
|
||||
<div
|
||||
class="project-item"
|
||||
class:noTarget={item.targetDate === null}
|
||||
style:left={`${getOffsetByDate(item.startDate)}px`}
|
||||
style:right={`${getOffsetByDate(target) + dayWidth - 1}px`}
|
||||
style:width={`${getOffsetByDate(target) - getOffsetByDate(item.startDate) + dayWidth - 1}px`}
|
||||
>
|
||||
<div class="project-presenter gap-2">
|
||||
{#if item.icon}<Icon
|
||||
icon={item.icon}
|
||||
size={item.iconSize ?? 'small'}
|
||||
iconProps={item.iconProps}
|
||||
/>{/if}
|
||||
{#if item.presenter}<svelte:component this={item.presenter} {...item.props} />{/if}
|
||||
{#if item.label}<span>{item.label}</span>{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{#if line.items}
|
||||
{#if rangeRow !== null && -time.offsetView + time.viewBox.width < rangeRow.min.x}
|
||||
<button
|
||||
transition:fly={{ duration: 150, x: 50, opacity: 0 }}
|
||||
class="timeline-action__button right"
|
||||
on:click={() => {
|
||||
if (rangeRow !== null) time.offsetView = -getOffsetByDate(rangeRow.min.date) + dayWidth * 5
|
||||
}}
|
||||
>
|
||||
<IconArrowRight size={'small'} />
|
||||
</button>
|
||||
{/if}
|
||||
{#if rangeRow !== null && -time.offsetView > rangeRow.max.x}
|
||||
<button
|
||||
transition:fly={{ duration: 150, x: -50, opacity: 0 }}
|
||||
class="timeline-action__button left"
|
||||
on:click={() => {
|
||||
if (rangeRow !== null) time.offsetView = -getOffsetByDate(rangeRow.min.date) + dayWidth * 5
|
||||
}}
|
||||
>
|
||||
<IconArrowLeft size={'small'} />
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if rangeRow === null && selectedRow === row && time.cursorMarker && !moving}
|
||||
<button class="timeline-action__button add" style:left={`${time.offsetView + time.cursorMarker.x}px`}>
|
||||
<IconAdd size={'small'} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</Scroller>
|
||||
<div class="timeline-foreground__viewbox" style:left={`${panelWidth}px`}>
|
||||
<div class="timeline-wrapped_content" style:transform={`translateX(${time.offsetView}px)`}>
|
||||
<div class="todayMarker" style:left={`${time.todayMarker.x}px`} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="timeline-splitter" class:moving style:left={`${panelWidth}px`} on:mousedown={splitterStart} />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.timeline-container {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
|
||||
& > * {
|
||||
overscroll-behavior-x: contain;
|
||||
}
|
||||
}
|
||||
.timeline-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 4rem;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
.timeline-header__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
padding: 0 2.25rem;
|
||||
height: 100%;
|
||||
background-color: var(--body-accent);
|
||||
box-shadow: var(--accent-shadow);
|
||||
// z-index: 2;
|
||||
}
|
||||
.timeline-header__time {
|
||||
// overflow: hidden;
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
background-color: var(--body-color);
|
||||
mask-image: linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 0, 0, 0) 0,
|
||||
rgba(0, 0, 0, 1) 2rem,
|
||||
rgba(0, 0, 0, 1) calc(100% - 2rem),
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
|
||||
&-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
will-change: transform;
|
||||
|
||||
.day,
|
||||
.month {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
.month {
|
||||
width: max-content;
|
||||
top: 0.25rem;
|
||||
font-size: 1rem;
|
||||
color: var(--accent-color);
|
||||
|
||||
&:first-letter {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
.day {
|
||||
bottom: 0.5rem;
|
||||
font-size: 1rem;
|
||||
color: var(--content-color);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.cursor {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-bottom: 1px;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
bottom: 0.375rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
background-color: var(--primary-bg-color);
|
||||
border-radius: 50%;
|
||||
transform: translateX(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.todayMarker,
|
||||
.monthMarker {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
.monthMarker {
|
||||
border-left: 1px dashed var(--highlight-select);
|
||||
}
|
||||
.todayMarker {
|
||||
border-left: 1px solid var(--primary-bg-color);
|
||||
}
|
||||
|
||||
.timeline-background__headers,
|
||||
.timeline-background__viewbox,
|
||||
.timeline-foreground__viewbox {
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 4rem;
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
}
|
||||
.timeline-background__headers {
|
||||
left: 0;
|
||||
background-color: var(--body-accent);
|
||||
}
|
||||
.timeline-background__viewbox,
|
||||
.timeline-foreground__viewbox {
|
||||
right: 0;
|
||||
mask-image: linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 0, 0, 0) 0,
|
||||
rgba(0, 0, 0, 1) 2rem,
|
||||
rgba(0, 0, 0, 1) calc(100% - 2rem),
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
}
|
||||
.timeline-foreground__viewbox {
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.timeline-splitter,
|
||||
.timeline-splitter::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.timeline-splitter {
|
||||
width: 1px;
|
||||
background-color: var(--divider-color);
|
||||
cursor: col-resize;
|
||||
z-index: 3;
|
||||
transition-property: width, background-color;
|
||||
transition-timing-function: var(--timing-main);
|
||||
transition-duration: 0.1s;
|
||||
transition-delay: 0s;
|
||||
|
||||
&:hover {
|
||||
width: 3px;
|
||||
background-color: var(--button-border-hover);
|
||||
transition-duration: 0.15s;
|
||||
transition-delay: 0.3s;
|
||||
}
|
||||
&::before {
|
||||
content: '';
|
||||
width: 10px;
|
||||
left: 50%;
|
||||
}
|
||||
&.moving {
|
||||
width: 2px;
|
||||
background-color: var(--primary-edit-border-color);
|
||||
transition-duration: 0.1s;
|
||||
transition-delay: 0s;
|
||||
}
|
||||
}
|
||||
|
||||
.headerWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 1.15rem;
|
||||
// border-bottom: 1px solid var(--accent-bg-color);
|
||||
}
|
||||
.contentWrapper {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
mask-image: linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 0, 0, 0) 0,
|
||||
rgba(0, 0, 0, 1) 2rem,
|
||||
rgba(0, 0, 0, 1) calc(100% - 2rem),
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
|
||||
&.nullRow {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.timeline-wrapped_content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.timeline-action__button,
|
||||
.project-item {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
box-shadow: var(--button-shadow);
|
||||
}
|
||||
.project-item {
|
||||
top: 0.25rem;
|
||||
bottom: 0.25rem;
|
||||
background-color: var(--button-bg-color);
|
||||
border: 1px solid var(--button-border-color);
|
||||
border-radius: 0.75rem;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--button-bg-hover);
|
||||
border-color: var(--button-border-hover);
|
||||
}
|
||||
&.noTarget {
|
||||
mask-image: linear-gradient(to left, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 1) 2rem);
|
||||
border-right-color: transparent;
|
||||
}
|
||||
|
||||
.project-presenter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.space {
|
||||
flex-shrink: 0;
|
||||
width: 0.25rem;
|
||||
min-width: 0.25rem;
|
||||
max-width: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.timeline-action__button {
|
||||
top: 0.625rem;
|
||||
bottom: 0.625rem;
|
||||
width: 2rem;
|
||||
color: var(--content-color);
|
||||
background-color: var(--button-bg-color);
|
||||
border: 1px solid var(--button-border-color);
|
||||
border-radius: 0.5rem;
|
||||
|
||||
// color: var(--caption-color);
|
||||
// font-size: 0.65rem;
|
||||
// font-weight: 600;
|
||||
|
||||
&:hover {
|
||||
color: var(--accent-color);
|
||||
background-color: var(--button-bg-hover);
|
||||
border-color: var(--button-border-hover);
|
||||
}
|
||||
|
||||
&.left {
|
||||
left: 1rem;
|
||||
}
|
||||
&.right {
|
||||
right: 1rem;
|
||||
}
|
||||
&.add {
|
||||
transform: translateX(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.listGrid {
|
||||
display: flex;
|
||||
justify-content: stretch;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
height: 3.25rem;
|
||||
min-height: 0;
|
||||
color: var(--caption-color);
|
||||
z-index: 2;
|
||||
|
||||
&.mListGridChecked {
|
||||
.headerWrapper {
|
||||
background-color: var(--highlight-select);
|
||||
}
|
||||
.contentWrapper {
|
||||
background-color: var(--trans-content-05);
|
||||
}
|
||||
.eListGridCheckBox {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.mListGridSelected {
|
||||
.headerWrapper {
|
||||
background-color: var(--highlight-select-hover);
|
||||
}
|
||||
.contentWrapper {
|
||||
background-color: var(--trans-content-10);
|
||||
}
|
||||
}
|
||||
|
||||
.eListGridCheckBox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover .eListGridCheckBox {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.filler {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.gridElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
margin-left: 0.5rem;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.iconPresenter {
|
||||
padding-left: 0.45rem;
|
||||
}
|
||||
|
||||
.projectPresenter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
width: 5.5rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
</style>
|
@ -100,6 +100,7 @@ export { default as Scroller } from './components/Scroller.svelte'
|
||||
export { default as ScrollerBar } from './components/ScrollerBar.svelte'
|
||||
export { default as TabList } from './components/TabList.svelte'
|
||||
export { default as Chevron } from './components/Chevron.svelte'
|
||||
export { default as Timeline } from './components/Timeline.svelte'
|
||||
|
||||
export { default as IconAdd } from './components/icons/Add.svelte'
|
||||
export { default as IconBack } from './components/icons/Back.svelte'
|
||||
|
@ -13,6 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
import type { Asset, IntlString } from '@hcengineering/platform'
|
||||
import { Timestamp } from '@hcengineering/core'
|
||||
import { /* Metadata, Plugin, plugin, */ Resource /*, Service */ } from '@hcengineering/platform'
|
||||
import { /* getContext, */ SvelteComponent } from 'svelte'
|
||||
|
||||
@ -102,6 +103,7 @@ export interface TabItem {
|
||||
icon?: Asset | AnySvelteComponent
|
||||
color?: string
|
||||
tooltip?: IntlString
|
||||
action?: () => void
|
||||
}
|
||||
|
||||
export type ButtonKind =
|
||||
@ -238,3 +240,43 @@ export interface DeviceOptions {
|
||||
twoRows: boolean
|
||||
theme?: any
|
||||
}
|
||||
|
||||
export interface TimelineItem {
|
||||
icon?: Asset | AnySvelteComponent
|
||||
iconSize?: IconSize
|
||||
iconProps?: Record<string, any>
|
||||
presenter?: AnySvelteComponent
|
||||
props?: Record<string, any>
|
||||
label?: string
|
||||
|
||||
startDate: Timestamp
|
||||
targetDate: Timestamp | undefined
|
||||
}
|
||||
export interface TimelineRow {
|
||||
items: TimelineItem[] | undefined
|
||||
}
|
||||
export interface TimelinePoint {
|
||||
date: Date
|
||||
x: number
|
||||
label?: string
|
||||
}
|
||||
export interface TimelineMinMax {
|
||||
min: TimelinePoint
|
||||
max: TimelinePoint
|
||||
}
|
||||
export type TTimelineRow = TimelineMinMax | null
|
||||
export interface TimelineState {
|
||||
todayMarker: TimelinePoint
|
||||
cursorMarker?: TimelinePoint
|
||||
offsetView: number
|
||||
renderedRange: {
|
||||
left: TimelinePoint
|
||||
right: TimelinePoint
|
||||
firstDays: number[]
|
||||
}
|
||||
rows: TTimelineRow[] | undefined
|
||||
months: TimelinePoint[]
|
||||
days: TimelinePoint[]
|
||||
timelineBox: DOMRect
|
||||
viewBox: DOMRect
|
||||
}
|
||||
|
@ -70,7 +70,9 @@ loadMetadata(tracker.icon, {
|
||||
CopyBranch: `${icons}#copyBranch`,
|
||||
Duplicate: `${icons}#duplicate`,
|
||||
TimeReport: `${icons}#timeReport`,
|
||||
Estimation: `${icons}#timeReport`
|
||||
Estimation: `${icons}#timeReport`,
|
||||
|
||||
Timeline: `${icons}#timeline`
|
||||
})
|
||||
|
||||
addStringsLoader(trackerId, async (lang: string) => await import(`../lang/${lang}.json`))
|
||||
|
@ -18,8 +18,10 @@
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Project } from '@hcengineering/tracker'
|
||||
import { Button, IconAdd, Label, showPopup } from '@hcengineering/ui'
|
||||
import { Button, IconAdd, Label, showPopup, TabList } from '@hcengineering/ui'
|
||||
import type { TabItem } from '@hcengineering/ui'
|
||||
import tracker from '../../plugin'
|
||||
import view from '@hcengineering/view'
|
||||
import { getIncludedProjectStatuses, projectsTitleMap, ProjectsViewMode } from '../../utils'
|
||||
import NewProject from './NewProject.svelte'
|
||||
import ProjectsListBrowser from './ProjectsListBrowser.svelte'
|
||||
@ -28,6 +30,7 @@
|
||||
export let query: DocumentQuery<Project> = {}
|
||||
export let search: string = ''
|
||||
export let mode: ProjectsViewMode = 'all'
|
||||
export let viewMode: 'list' | 'timeline' = 'list'
|
||||
|
||||
const ENTRIES_LIMIT = 200
|
||||
const resultProjectsQuery = createQuery()
|
||||
@ -72,6 +75,17 @@
|
||||
|
||||
mode = newMode
|
||||
}
|
||||
|
||||
const modeList: TabItem[] = [
|
||||
{ id: 'all', labelIntl: tracker.string.AllProjects, action: () => handleViewModeChanged('all') },
|
||||
{ id: 'backlog', labelIntl: tracker.string.BacklogProjects, action: () => handleViewModeChanged('backlog') },
|
||||
{ id: 'active', labelIntl: tracker.string.ActiveProjects, action: () => handleViewModeChanged('active') },
|
||||
{ id: 'closed', labelIntl: tracker.string.ClosedProjects, action: () => handleViewModeChanged('closed') }
|
||||
]
|
||||
const viewList: TabItem[] = [
|
||||
{ id: 'list', icon: view.icon.List, tooltip: view.string.List },
|
||||
{ id: 'timeline', icon: view.icon.Timeline, tooltip: view.string.Timeline }
|
||||
]
|
||||
</script>
|
||||
|
||||
<div class="fs-title flex-between header">
|
||||
@ -84,45 +98,15 @@
|
||||
<Button size="small" icon={IconAdd} label={tracker.string.Project} kind={'primary'} on:click={showCreateDialog} />
|
||||
</div>
|
||||
<div class="itemsContainer">
|
||||
<div class="flex-center">
|
||||
<div class="flex-center">
|
||||
<div class="buttonWrapper">
|
||||
<Button
|
||||
size="small"
|
||||
shape="rectangle-right"
|
||||
selected={mode === 'all'}
|
||||
label={tracker.string.AllProjects}
|
||||
on:click={() => handleViewModeChanged('all')}
|
||||
<div class="flex-row-center">
|
||||
<TabList
|
||||
items={modeList}
|
||||
selected={mode}
|
||||
kind={'normal'}
|
||||
on:select={(result) => {
|
||||
if (result.detail !== undefined && result.detail.action) result.detail.action()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="buttonWrapper">
|
||||
<Button
|
||||
size="small"
|
||||
shape="rectangle"
|
||||
selected={mode === 'backlog'}
|
||||
label={tracker.string.BacklogProjects}
|
||||
on:click={() => handleViewModeChanged('backlog')}
|
||||
/>
|
||||
</div>
|
||||
<div class="buttonWrapper">
|
||||
<Button
|
||||
size="small"
|
||||
shape="rectangle"
|
||||
selected={mode === 'active'}
|
||||
label={tracker.string.ActiveProjects}
|
||||
on:click={() => handleViewModeChanged('active')}
|
||||
/>
|
||||
</div>
|
||||
<div class="buttonWrapper">
|
||||
<Button
|
||||
size="small"
|
||||
shape="rectangle-left"
|
||||
selected={mode === 'closed'}
|
||||
label={tracker.string.ClosedProjects}
|
||||
on:click={() => handleViewModeChanged('closed')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="ml-3 filterButton">
|
||||
<Button
|
||||
size="small"
|
||||
@ -134,19 +118,15 @@
|
||||
/>
|
||||
</div> -->
|
||||
</div>
|
||||
<!-- <div class="flex-center">
|
||||
<div class="flex-center">
|
||||
<div class="buttonWrapper">
|
||||
<Button selected size="small" shape="rectangle-right" icon={tracker.icon.ProjectsList} />
|
||||
</div>
|
||||
<div class="buttonWrapper">
|
||||
<Button size="small" shape="rectangle-left" icon={tracker.icon.ProjectsTimeline} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<Button size="small" icon={IconOptions} />
|
||||
</div>
|
||||
</div> -->
|
||||
<TabList
|
||||
items={viewList}
|
||||
selected={viewMode}
|
||||
kind={'secondary'}
|
||||
size={'small'}
|
||||
on:select={(result) => {
|
||||
if (result.detail !== undefined && result.detail.id !== viewMode) viewMode = result.detail.id
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ProjectsListBrowser
|
||||
_class={tracker.class.Project}
|
||||
@ -163,6 +143,7 @@
|
||||
{ key: '', presenter: tracker.component.ProjectStatusPresenter }
|
||||
]}
|
||||
projects={resultProjects}
|
||||
{viewMode}
|
||||
/>
|
||||
|
||||
<style lang="scss">
|
||||
@ -182,16 +163,10 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.65rem 1.35rem 0.65rem 2.25rem;
|
||||
border-bottom: 1px solid var(--theme-button-border-hovered);
|
||||
}
|
||||
|
||||
.buttonWrapper {
|
||||
margin-right: 1px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
padding: 0.65rem 0.75rem 0.65rem 2.25rem;
|
||||
background-color: var(--board-bg-color);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
// .filterButton {
|
||||
|
@ -28,20 +28,7 @@
|
||||
|
||||
{#if value}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="flex-presenter flex-grow" on:click={navigateToProject}>
|
||||
<span title={value.label} class="projectLabel flex-grow">{value.label}</span>
|
||||
</div>
|
||||
<span title={value.label} class="fs-bold caption-color overflow-label clear-mins" on:click={navigateToProject}>
|
||||
{value.label}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.projectLabel {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
color: var(--theme-caption-color);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
|
@ -40,7 +40,6 @@
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
{#if isEditable}
|
||||
<ProjectStatusSelector
|
||||
{kind}
|
||||
{size}
|
||||
@ -48,20 +47,8 @@
|
||||
{justify}
|
||||
{isEditable}
|
||||
{shouldShowLabel}
|
||||
showTooltip={{ label: tracker.string.SetStatus }}
|
||||
showTooltip={isEditable ? { label: tracker.string.SetStatus } : undefined}
|
||||
selectedProjectStatus={value.status}
|
||||
onProjectStatusChange={handleProjectStatusChanged}
|
||||
/>
|
||||
{:else}
|
||||
<ProjectStatusSelector
|
||||
{kind}
|
||||
{size}
|
||||
{width}
|
||||
{justify}
|
||||
{isEditable}
|
||||
{shouldShowLabel}
|
||||
selectedProjectStatus={value.status}
|
||||
onProjectStatusChange={handleProjectStatusChanged}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
@ -26,19 +26,23 @@
|
||||
import { Project } from '@hcengineering/tracker'
|
||||
import { onMount } from 'svelte'
|
||||
import ProjectsList from './ProjectsList.svelte'
|
||||
import ProjectsTimeline from './ProjectsTimeline.svelte'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let itemsConfig: (BuildModelKey | string)[]
|
||||
export let loadingProps: LoadingProps | undefined = undefined
|
||||
export let projects: Project[] = []
|
||||
export let viewMode: 'list' | 'timeline' = 'list'
|
||||
|
||||
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
|
||||
if (dir === 'vertical') {
|
||||
projectsList.onElementSelected(offset, of)
|
||||
if (viewMode === 'list') projectsList.onElementSelected(offset, of)
|
||||
else projectsTimeline.onElementSelected(offset, of)
|
||||
}
|
||||
})
|
||||
|
||||
let projectsList: ProjectsList
|
||||
let projectsTimeline: ProjectsTimeline
|
||||
|
||||
$: if (projectsList !== undefined) {
|
||||
listProvider.update(projects)
|
||||
@ -55,7 +59,8 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
<ProjectsList
|
||||
{#if viewMode === 'list'}
|
||||
<ProjectsList
|
||||
bind:this={projectsList}
|
||||
{_class}
|
||||
{itemsConfig}
|
||||
@ -69,4 +74,21 @@
|
||||
on:check={(event) => {
|
||||
listProvider.updateSelection(event.detail.docs, event.detail.value)
|
||||
}}
|
||||
/>
|
||||
/>
|
||||
{:else}
|
||||
<ProjectsTimeline
|
||||
bind:this={projectsTimeline}
|
||||
{_class}
|
||||
{itemsConfig}
|
||||
{loadingProps}
|
||||
{projects}
|
||||
selectedObjectIds={$selectionStore ?? []}
|
||||
selectedRowIndex={listProvider.current($focusStore)}
|
||||
on:row-focus={(event) => {
|
||||
listProvider.updateFocus(event.detail ?? undefined)
|
||||
}}
|
||||
on:check={(event) => {
|
||||
listProvider.updateSelection(event.detail.docs, event.detail.value)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
@ -0,0 +1,516 @@
|
||||
<!--
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact from '@hcengineering/contact'
|
||||
import { Class, Doc, FindOptions, getObjectValue, Ref, Timestamp } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Issue, Project } from '@hcengineering/tracker'
|
||||
import { CheckBox, Spinner, Timeline, TimelineRow } from '@hcengineering/ui'
|
||||
import { AttributeModel, BuildModelKey } from '@hcengineering/view'
|
||||
import { buildModel, getObjectPresenter, LoadingProps } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
import ProjectPresenter from './ProjectPresenter.svelte'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let itemsConfig: (BuildModelKey | string)[]
|
||||
export let selectedObjectIds: Doc[] = []
|
||||
export let selectedRowIndex: number | undefined = undefined
|
||||
export let projects: Project[] | undefined = undefined
|
||||
export let loadingProps: LoadingProps | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const baseOptions: FindOptions<Issue> = {
|
||||
lookup: {
|
||||
assignee: contact.class.Employee,
|
||||
status: tracker.class.IssueStatus
|
||||
}
|
||||
}
|
||||
|
||||
let personPresenter: AttributeModel
|
||||
|
||||
$: options = { ...baseOptions } as FindOptions<Project>
|
||||
$: selectedObjectIdsSet = new Set<Ref<Doc>>(selectedObjectIds.map((it) => it._id))
|
||||
let selectedRows: number[] = []
|
||||
$: if (selectedObjectIdsSet.size > 0 && projects !== undefined) {
|
||||
const tRows: number[] = []
|
||||
selectedObjectIdsSet.forEach((it) => {
|
||||
const index = projects?.findIndex((f) => f._id === it)
|
||||
if (index !== undefined) tRows.push(index)
|
||||
})
|
||||
selectedRows = tRows
|
||||
} else selectedRows = []
|
||||
|
||||
$: getObjectPresenter(client, contact.class.Person, { key: '' }).then((p) => {
|
||||
personPresenter = p
|
||||
})
|
||||
|
||||
export const onObjectChecked = (docs: Doc[], value: boolean) => {
|
||||
dispatch('check', { docs, value })
|
||||
}
|
||||
|
||||
const handleRowFocused = (object: Doc) => {
|
||||
dispatch('row-focus', object)
|
||||
}
|
||||
|
||||
export const onElementSelected = (offset: 1 | -1 | 0, docObject?: Doc) => {
|
||||
if (!projects) return
|
||||
|
||||
let position =
|
||||
(docObject !== undefined ? projects?.findIndex((x) => x._id === docObject?._id) : selectedRowIndex) ?? -1
|
||||
|
||||
position += offset
|
||||
if (position < 0) position = 0
|
||||
if (position >= projects.length) position = projects.length - 1
|
||||
selectedRowIndex = position
|
||||
handleRowFocused(projects[position])
|
||||
|
||||
// if (objectRef) {
|
||||
// objectRef.scrollIntoView({ behavior: 'auto', block: 'nearest' })
|
||||
// }
|
||||
}
|
||||
|
||||
const getLoadingElementsLength = (props: LoadingProps, options?: FindOptions<Doc>) => {
|
||||
if (options?.limit && options?.limit > 0) {
|
||||
return Math.min(options.limit, props.length)
|
||||
}
|
||||
|
||||
return props.length
|
||||
}
|
||||
|
||||
let itemModels: AttributeModel[] | undefined = undefined
|
||||
$: buildModel({ client, _class, keys: itemsConfig, lookup: options.lookup }).then((res) => (itemModels = res))
|
||||
|
||||
let lines: TimelineRow[] | undefined
|
||||
$: lines = projects?.map((proj) => {
|
||||
const tR: TimelineRow = { items: [] }
|
||||
tR.items = [
|
||||
{
|
||||
icon: proj.icon,
|
||||
presenter: ProjectPresenter,
|
||||
props: { value: proj },
|
||||
startDate: proj.startDate as Timestamp,
|
||||
targetDate: proj.targetDate as Timestamp
|
||||
}
|
||||
]
|
||||
return tR
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if projects && itemModels && lines}
|
||||
<Timeline
|
||||
{lines}
|
||||
{selectedRows}
|
||||
selectedRow={selectedRowIndex}
|
||||
on:row-focus={(ev) => {
|
||||
if (ev.detail !== undefined && projects !== undefined) handleRowFocused(projects[ev.detail])
|
||||
}}
|
||||
on:check={(ev) => {
|
||||
if (ev.detail !== undefined && projects !== undefined) onObjectChecked([projects[ev.detail.row]], ev.detail.value)
|
||||
}}
|
||||
>
|
||||
<svelte:fragment let:row>
|
||||
{#each itemModels as attributeModel, attributeModelIndex}
|
||||
{#if attributeModelIndex === 0}
|
||||
<div class="gridElement">
|
||||
<div class="iconPresenter">
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
value={getObjectValue(attributeModel.key, projects[row]) ?? ''}
|
||||
{...attributeModel.props}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if attributeModelIndex === 1}
|
||||
<div class="projectPresenter flex-grow">
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
value={getObjectValue(attributeModel.key, projects[row]) ?? ''}
|
||||
{...attributeModel.props}
|
||||
/>
|
||||
</div>
|
||||
<div class="filler" />
|
||||
{:else}
|
||||
<div class="gridElement">
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
value={getObjectValue(attributeModel.key, projects[row]) ?? ''}
|
||||
parentId={projects[row]._id}
|
||||
{...attributeModel.props}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</svelte:fragment>
|
||||
</Timeline>
|
||||
{:else if loadingProps !== undefined}
|
||||
{#each Array(getLoadingElementsLength(loadingProps, options)) as _, rowIndex}
|
||||
<div class="listGrid" class:fixed={rowIndex === selectedRowIndex}>
|
||||
<div class="contentWrapper">
|
||||
<div class="gridElement">
|
||||
<CheckBox checked={false} />
|
||||
<div class="ml-4">
|
||||
<Spinner size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.timeline-container {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
|
||||
& > * {
|
||||
overscroll-behavior-x: contain;
|
||||
}
|
||||
}
|
||||
.timeline-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 4rem;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
.timeline-header__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
padding: 0 2.25rem;
|
||||
height: 100%;
|
||||
background-color: var(--body-accent);
|
||||
box-shadow: var(--accent-shadow);
|
||||
// z-index: 2;
|
||||
}
|
||||
.timeline-header__time {
|
||||
// overflow: hidden;
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
background-color: var(--body-color);
|
||||
mask-image: linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 0, 0, 0) 0,
|
||||
rgba(0, 0, 0, 1) 2rem,
|
||||
rgba(0, 0, 0, 1) calc(100% - 2rem),
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
|
||||
&-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
will-change: transform;
|
||||
|
||||
.day,
|
||||
.month {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
.month {
|
||||
width: max-content;
|
||||
top: 0.25rem;
|
||||
font-size: 1rem;
|
||||
color: var(--accent-color);
|
||||
|
||||
&:first-letter {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
.day {
|
||||
bottom: 0.5rem;
|
||||
font-size: 1rem;
|
||||
color: var(--content-color);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.cursor {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-bottom: 1px;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
bottom: 0.375rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
background-color: var(--primary-bg-color);
|
||||
border-radius: 50%;
|
||||
transform: translateX(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.todayMarker,
|
||||
.monthMarker {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
.monthMarker {
|
||||
border-left: 1px dashed var(--highlight-select);
|
||||
}
|
||||
.todayMarker {
|
||||
border-left: 1px solid var(--primary-bg-color);
|
||||
}
|
||||
|
||||
.timeline-background__headers,
|
||||
.timeline-background__viewbox,
|
||||
.timeline-foreground__viewbox {
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 4rem;
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
}
|
||||
.timeline-background__headers {
|
||||
left: 0;
|
||||
background-color: var(--body-accent);
|
||||
}
|
||||
.timeline-background__viewbox,
|
||||
.timeline-foreground__viewbox {
|
||||
right: 0;
|
||||
mask-image: linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 0, 0, 0) 0,
|
||||
rgba(0, 0, 0, 1) 2rem,
|
||||
rgba(0, 0, 0, 1) calc(100% - 2rem),
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
}
|
||||
.timeline-foreground__viewbox {
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.timeline-splitter,
|
||||
.timeline-splitter::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.timeline-splitter {
|
||||
width: 1px;
|
||||
background-color: var(--divider-color);
|
||||
cursor: col-resize;
|
||||
z-index: 3;
|
||||
transition-property: width, background-color;
|
||||
transition-timing-function: var(--timing-main);
|
||||
transition-duration: 0.1s;
|
||||
transition-delay: 0s;
|
||||
|
||||
&:hover {
|
||||
width: 3px;
|
||||
background-color: var(--button-border-hover);
|
||||
transition-duration: 0.15s;
|
||||
transition-delay: 0.3s;
|
||||
}
|
||||
&::before {
|
||||
content: '';
|
||||
width: 10px;
|
||||
left: 50%;
|
||||
}
|
||||
&.moving {
|
||||
width: 2px;
|
||||
background-color: var(--primary-edit-border-color);
|
||||
transition-duration: 0.1s;
|
||||
transition-delay: 0s;
|
||||
}
|
||||
}
|
||||
|
||||
.headerWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 1.15rem;
|
||||
// border-bottom: 1px solid var(--accent-bg-color);
|
||||
}
|
||||
.contentWrapper {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
mask-image: linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 0, 0, 0) 0,
|
||||
rgba(0, 0, 0, 1) 2rem,
|
||||
rgba(0, 0, 0, 1) calc(100% - 2rem),
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
|
||||
&.nullRow {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.timeline-wrapped_content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.timeline-action__button,
|
||||
.project-item {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
box-shadow: var(--button-shadow);
|
||||
}
|
||||
.project-item {
|
||||
top: 0.25rem;
|
||||
bottom: 0.25rem;
|
||||
background-color: var(--button-bg-color);
|
||||
border: 1px solid var(--button-border-color);
|
||||
border-radius: 0.75rem;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--button-bg-hover);
|
||||
border-color: var(--button-border-hover);
|
||||
}
|
||||
&.noTarget {
|
||||
mask-image: linear-gradient(to left, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 1) 2rem);
|
||||
border-right-color: transparent;
|
||||
}
|
||||
|
||||
.project-presenter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.space {
|
||||
flex-shrink: 0;
|
||||
width: 0.25rem;
|
||||
min-width: 0.25rem;
|
||||
max-width: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.timeline-action__button {
|
||||
top: 0.625rem;
|
||||
bottom: 0.625rem;
|
||||
width: 2rem;
|
||||
color: var(--content-color);
|
||||
background-color: var(--button-bg-color);
|
||||
border: 1px solid var(--button-border-color);
|
||||
border-radius: 0.5rem;
|
||||
|
||||
&:hover {
|
||||
color: var(--accent-color);
|
||||
background-color: var(--button-bg-hover);
|
||||
border-color: var(--button-border-hover);
|
||||
}
|
||||
|
||||
&.left {
|
||||
left: 1rem;
|
||||
}
|
||||
&.right {
|
||||
right: 1rem;
|
||||
}
|
||||
&.add {
|
||||
transform: translateX(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.listGrid {
|
||||
display: flex;
|
||||
justify-content: stretch;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
height: 3.25rem;
|
||||
min-height: 0;
|
||||
color: var(--caption-color);
|
||||
z-index: 2;
|
||||
|
||||
&.mListGridChecked {
|
||||
.headerWrapper {
|
||||
background-color: var(--highlight-select);
|
||||
}
|
||||
.contentWrapper {
|
||||
background-color: var(--trans-content-05);
|
||||
}
|
||||
.eListGridCheckBox {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.mListGridSelected {
|
||||
.headerWrapper {
|
||||
background-color: var(--highlight-select-hover);
|
||||
}
|
||||
.contentWrapper {
|
||||
background-color: var(--trans-content-10);
|
||||
}
|
||||
}
|
||||
|
||||
.eListGridCheckBox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover .eListGridCheckBox {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.filler {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.gridElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
margin-left: 0.5rem;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
.projectPresenter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
width: 5.5rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
</style>
|
@ -467,7 +467,9 @@ export default plugin(trackerId, {
|
||||
Duplicate: '' as Asset,
|
||||
|
||||
TimeReport: '' as Asset,
|
||||
Estimation: '' as Asset
|
||||
Estimation: '' as Asset,
|
||||
|
||||
Timeline: '' as Asset
|
||||
},
|
||||
category: {
|
||||
Other: '' as Ref<TagCategory>,
|
||||
|
@ -19,6 +19,11 @@
|
||||
<path d="M1 12.6C1 12.0399 1 11.7599 1.10899 11.546C1.20487 11.3578 1.35785 11.2049 1.54601 11.109C1.75992 11 2.03995 11 2.6 11H5.4C5.96005 11 6.24008 11 6.45399 11.109C6.64215 11.2049 6.79513 11.3578 6.89101 11.546C7 11.7599 7 12.0399 7 12.6V13.4C7 13.9601 7 14.2401 6.89101 14.454C6.79513 14.6422 6.64215 14.7951 6.45399 14.891C6.24008 15 5.96005 15 5.4 15H2.6C2.03995 15 1.75992 15 1.54601 14.891C1.35785 14.7951 1.20487 14.6422 1.10899 14.454C1 14.2401 1 13.9601 1 13.4V12.6Z" />
|
||||
<path d="M9 12.6C9 12.0399 9 11.7599 9.10899 11.546C9.20487 11.3578 9.35785 11.2049 9.54601 11.109C9.75992 11 10.0399 11 10.6 11H13.4C13.9601 11 14.2401 11 14.454 11.109C14.6422 11.2049 14.7951 11.3578 14.891 11.546C15 11.7599 15 12.0399 15 12.6V13.4C15 13.9601 15 14.2401 14.891 14.454C14.7951 14.6422 14.6422 14.7951 14.454 14.891C14.2401 15 13.9601 15 13.4 15H10.6C10.0399 15 9.75992 15 9.54601 14.891C9.35785 14.7951 9.20487 14.6422 9.10899 14.454C9 14.2401 9 13.9601 9 13.4V12.6Z" />
|
||||
</symbol>
|
||||
<symbol id="timeline" viewBox="0 0 14 14">
|
||||
<rect x="6" y="1" width="7" height="2.5" rx="0.5" />
|
||||
<rect x="1" y="5.75" width="9" height="2.5" rx="0.5" />
|
||||
<rect x="4" y="10.5" width="9" height="2.5" rx="0.5" />
|
||||
</symbol>
|
||||
<symbol id="delete" viewBox="0 0 16 16">
|
||||
<path d="M13.1,3.8h-2c-0.2,0-0.4-0.2-0.5-0.4l-0.2-0.8c-0.1-0.6-0.6-1-1.2-1H6.8c-0.6,0-1.1,0.4-1.2,1L5.4,3.3 c0,0.3-0.2,0.4-0.5,0.4h-2C2.7,3.8,2.4,4,2.4,4.3s0.2,0.5,0.5,0.5h0.3c0.1,1.1,0.3,6.2,0.5,7.9c0.1,1.1,0.8,1.8,1.9,1.9 c0.8,0,1.6,0,2.4,0c0.8,0,1.5,0,2.3,0c1.1,0,1.8-0.7,1.9-1.9c0.2-1.7,0.4-6.7,0.5-7.9h0.3c0.3,0,0.5-0.2,0.5-0.5S13.4,3.8,13.1,3.8z M6.4,3.5l0.1-0.8c0-0.1,0.1-0.2,0.3-0.2h2.5c0.1,0,0.2,0.1,0.2,0.2l0.1,0.8c0,0.1,0.1,0.2,0.1,0.3H6.3C6.3,3.7,6.3,3.6,6.4,3.5z M11.3,12.5c-0.1,0.9-0.7,1-0.9,1c-1.6,0-3.2,0-4.7,0c-0.5,0-0.8-0.3-0.9-1C4.6,10.9,4.4,6,4.3,4.8h7.5C11.7,6,11.4,10.9,11.3,12.5z"/>
|
||||
</symbol>
|
||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 21 KiB |
@ -51,6 +51,7 @@
|
||||
"View": "View",
|
||||
"MarkupEditor": "Edit of rich content field",
|
||||
"List": "List",
|
||||
"Timeline": "Timeline",
|
||||
"Select": "Select",
|
||||
"NoGrouping": "No grouping",
|
||||
"Grouping": "Grouping",
|
||||
|
@ -49,6 +49,7 @@
|
||||
"View": "Вид",
|
||||
"MarkupEditor": "Изменение форматированного поля",
|
||||
"List": "Список",
|
||||
"Timeline": "Временная шкала",
|
||||
"Select": "Выбрать",
|
||||
"NoGrouping": "Нет группировки",
|
||||
"Grouping": "Группировка",
|
||||
|
@ -21,6 +21,7 @@ loadMetadata(view.icon, {
|
||||
Table: `${icons}#table`,
|
||||
List: `${icons}#list`,
|
||||
Card: `${icons}#card`,
|
||||
Timeline: `${icons}#timeline`,
|
||||
Delete: `${icons}#delete`,
|
||||
Move: `${icons}#move`,
|
||||
MoreH: `${icons}#more-h`,
|
||||
|
@ -581,12 +581,15 @@ const view = plugin(viewId, {
|
||||
View: '' as IntlString,
|
||||
FilteredViews: '' as IntlString,
|
||||
NewFilteredView: '' as IntlString,
|
||||
FilteredViewName: '' as IntlString
|
||||
FilteredViewName: '' as IntlString,
|
||||
List: '' as IntlString,
|
||||
Timeline: '' as IntlString
|
||||
},
|
||||
icon: {
|
||||
Table: '' as Asset,
|
||||
List: '' as Asset,
|
||||
Card: '' as Asset,
|
||||
Timeline: '' as Asset,
|
||||
Delete: '' as Asset,
|
||||
MoreH: '' as Asset,
|
||||
Move: '' as Asset,
|
||||
|
Loading…
Reference in New Issue
Block a user