mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-08 18:57:42 +02:00
Add Timeline component (#2535)
Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
@@ -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>
|
||||
<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="flex-row-center">
|
||||
<TabList
|
||||
items={modeList}
|
||||
selected={mode}
|
||||
kind={'normal'}
|
||||
on:select={(result) => {
|
||||
if (result.detail !== undefined && result.detail.action) result.detail.action()
|
||||
}}
|
||||
/>
|
||||
<!-- <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,28 +40,15 @@
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
{#if isEditable}
|
||||
<ProjectStatusSelector
|
||||
{kind}
|
||||
{size}
|
||||
{width}
|
||||
{justify}
|
||||
{isEditable}
|
||||
{shouldShowLabel}
|
||||
showTooltip={{ label: tracker.string.SetStatus }}
|
||||
selectedProjectStatus={value.status}
|
||||
onProjectStatusChange={handleProjectStatusChanged}
|
||||
/>
|
||||
{:else}
|
||||
<ProjectStatusSelector
|
||||
{kind}
|
||||
{size}
|
||||
{width}
|
||||
{justify}
|
||||
{isEditable}
|
||||
{shouldShowLabel}
|
||||
selectedProjectStatus={value.status}
|
||||
onProjectStatusChange={handleProjectStatusChanged}
|
||||
/>
|
||||
{/if}
|
||||
<ProjectStatusSelector
|
||||
{kind}
|
||||
{size}
|
||||
{width}
|
||||
{justify}
|
||||
{isEditable}
|
||||
{shouldShowLabel}
|
||||
showTooltip={isEditable ? { label: tracker.string.SetStatus } : undefined}
|
||||
selectedProjectStatus={value.status}
|
||||
onProjectStatusChange={handleProjectStatusChanged}
|
||||
/>
|
||||
{/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,18 +59,36 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
<ProjectsList
|
||||
bind:this={projectsList}
|
||||
{_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 viewMode === 'list'}
|
||||
<ProjectsList
|
||||
bind:this={projectsList}
|
||||
{_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)
|
||||
}}
|
||||
/>
|
||||
{: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,
|
||||
|
||||
Reference in New Issue
Block a user