Add Timeline component (#2535)

Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
Alexander Platov
2023-01-24 20:43:06 +07:00
committed by GitHub
parent ba8ab45dc2
commit 8a5b84802c
20 changed files with 1469 additions and 126 deletions
+3 -1
View File
@@ -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>
+3 -1
View File
@@ -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>,
+5
View File
@@ -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

+1
View File
@@ -51,6 +51,7 @@
"View": "View",
"MarkupEditor": "Edit of rich content field",
"List": "List",
"Timeline": "Timeline",
"Select": "Select",
"NoGrouping": "No grouping",
"Grouping": "Grouping",
+1
View File
@@ -49,6 +49,7 @@
"View": "Вид",
"MarkupEditor": "Изменение форматированного поля",
"List": "Список",
"Timeline": "Временная шкала",
"Select": "Выбрать",
"NoGrouping": "Нет группировки",
"Grouping": "Группировка",
+1
View File
@@ -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`,
+4 -1
View File
@@ -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,