[UBER-82] Update components (#3227)

Signed-off-by: Ruslan Bayandinov <wazsone@ya.ru>
This commit is contained in:
Ruslan Bayandinov
2023-05-22 18:03:22 +07:00
committed by GitHub
parent fb5c4b1f41
commit 2f4cb2c2a7
25 changed files with 72 additions and 1216 deletions
@@ -28,18 +28,9 @@
ViewletSettingButton,
FilterBar
} from '@hcengineering/view-resources'
import {
Button,
IconAdd,
Label,
SearchEdit,
TabItem,
TabList,
resolvedLocationStore,
showPopup
} from '@hcengineering/ui'
import { Button, IconAdd, Label, SearchEdit, TabList, resolvedLocationStore, showPopup } from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import { ComponentsFilterMode, componentsTitleMap, getIncludedComponentStatuses } from '../../utils'
import { ComponentsFilterMode, componentsTitleMap } from '../../utils'
import tracker from '../../plugin'
import ComponentsContent from './ComponentsContent.svelte'
import NewComponent from './NewComponent.svelte'
@@ -53,20 +44,12 @@
const viewletQuery = createQuery()
const space = typeof query.space === 'string' ? query.space : tracker.project.DefaultProject
const filterModeList: TabItem[] = [
{ id: 'all', labelIntl: tracker.string.AllComponents, action: () => handleFilterModeChanged('all') },
{ id: 'backlog', labelIntl: tracker.string.BacklogComponents, action: () => handleFilterModeChanged('backlog') },
{ id: 'active', labelIntl: tracker.string.ActiveComponents, action: () => handleFilterModeChanged('active') },
{ id: 'closed', labelIntl: tracker.string.ClosedComponents, action: () => handleFilterModeChanged('closed') }
]
let viewlet: WithLookup<Viewlet> | undefined
let viewlets: WithLookup<Viewlet>[] | undefined
let viewletKey = makeViewletKey()
let searchQuery: DocumentQuery<Component> = { ...query }
let resultQuery: DocumentQuery<Component> = { ...searchQuery }
let includedComponentsQuery: DocumentQuery<Component>
let asideFloat = false
let asideShown = true
@@ -74,19 +57,11 @@
let docWidth: number
let docSize = false
function handleFilterModeChanged (newMode: ComponentsFilterMode) {
if (newMode !== filterMode) {
filterMode = newMode
}
}
function showCreateDialog () {
showPopup(NewComponent, { space, targetElement: null }, 'top')
}
$: title = componentsTitleMap[filterMode]
$: includedComponentStatuses = getIncludedComponentStatuses(filterMode)
$: includedComponentsQuery = { status: { $in: includedComponentStatuses } }
$: searchQuery = search === '' ? { ...query } : { ...query, $search: search }
$: resultQuery = { ...searchQuery }
@@ -146,9 +121,6 @@
{/if}
</div>
</div>
<div class="ac-header tabs-start full divide">
<TabList items={filterModeList} selected={filterMode} kind={'plain'} on:select={({ detail }) => detail?.action?.()} />
</div>
<FilterBar
_class={tracker.class.Component}
@@ -159,7 +131,7 @@
<div class="flex w-full h-full clear-mins">
{#if viewlet}
<ComponentsContent {viewlet} query={{ ...resultQuery, ...includedComponentsQuery }} {space} {viewOptions} />
<ComponentsContent {viewlet} query={{ ...resultQuery }} {space} {viewOptions} />
{/if}
{#if $$slots.aside !== undefined && asideShown}
<div class="popupPanel-body__aside flex" class:float={asideFloat} class:shown={asideShown}>
@@ -15,42 +15,31 @@
<script lang="ts">
import { WithLookup } from '@hcengineering/core'
import { Component } from '@hcengineering/tracker'
import { Icon, getCurrentResolvedLocation, navigate, tooltip } from '@hcengineering/ui'
import { Icon, tooltip } from '@hcengineering/ui'
import tracker from '../../plugin'
import view from '@hcengineering/view'
import { DocNavLink } from '@hcengineering/view-resources'
export let value: WithLookup<Component>
export let shouldShowAvatar = true
export let onClick: (() => void) | undefined = undefined
export let disabled = false
export let inline: boolean = false
function navigateToComponent () {
if (disabled) {
return
}
if (onClick) {
onClick()
}
const loc = getCurrentResolvedLocation()
loc.path[4] = 'components'
loc.path[5] = value._id
loc.path.length = 6
loc.fragment = undefined
navigate(loc)
}
export let noUnderline = false
export let kind: 'list' | undefined = undefined
</script>
{#if value}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="flex-presenter" class:inline-presenter={inline} on:click={navigateToComponent}>
{#if !inline && shouldShowAvatar}
<div class="icon" use:tooltip={{ label: tracker.string.Component }}>
<Icon icon={tracker.icon.Component} size={'small'} />
</div>
{/if}
<span title={value.label} class="overflow-label label" class:no-underline={disabled}>
{value.label}
<DocNavLink object={value} {onClick} {disabled} {noUnderline} {inline} component={view.component.EditDoc}>
<span class="flex-presenter" class:inline class:list={kind === 'list'}>
{#if !inline && shouldShowAvatar}
<div class="icon" use:tooltip={{ label: tracker.string.Component }}>
<Icon icon={tracker.icon.Component} size={'small'} />
</div>
{/if}
<span title={value.label} class="label nowrap">
{value.label}
</span>
</span>
</div>
</DocNavLink>
{/if}
@@ -1,8 +0,0 @@
<script lang="ts">
import { Component } from '@hcengineering/tracker'
import ComponentStatusPresenter from './ComponentStatusPresenter.svelte'
export let object: Component
</script>
<ComponentStatusPresenter value={object} shouldShowLabel />
@@ -1,54 +0,0 @@
<!--
// 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 { Component, ComponentStatus } from '@hcengineering/tracker'
import { getClient } from '@hcengineering/presentation'
import type { ButtonKind, ButtonSize } from '@hcengineering/ui'
import tracker from '../../plugin'
import ComponentStatusSelector from './ComponentStatusSelector.svelte'
export let value: Component
export let isEditable: boolean = true
export let shouldShowLabel: boolean = false
export let kind: ButtonKind = 'link'
export let size: ButtonSize = 'large'
export let justify: 'left' | 'center' = 'left'
export let width: string | undefined = '100%'
const client = getClient()
const handleComponentStatusChanged = async (newStatus: ComponentStatus | undefined) => {
if (!isEditable || newStatus === undefined || value.status === newStatus) {
return
}
await client.update(value, { status: newStatus })
}
</script>
{#if value}
<ComponentStatusSelector
{kind}
{size}
{width}
{justify}
{isEditable}
{shouldShowLabel}
showTooltip={isEditable ? { label: tracker.string.SetStatus } : undefined}
selectedComponentStatus={value.status}
onComponentStatusChange={handleComponentStatusChanged}
/>
{/if}
@@ -1,83 +0,0 @@
<!--
// 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 { ComponentStatus } from '@hcengineering/tracker'
import { Button, showPopup, SelectPopup, eventToHTMLElement, Icon } from '@hcengineering/ui'
import type { ButtonKind, ButtonSize, LabelAndProps } from '@hcengineering/ui'
import tracker from '../../plugin'
import { defaultComponentStatuses, componentStatusAssets } from '../../utils'
export let selectedComponentStatus: ComponentStatus | undefined
export let shouldShowLabel: boolean = true
export let onComponentStatusChange: ((newComponentStatus: ComponentStatus | undefined) => void) | undefined =
undefined
export let isEditable: boolean = true
export let kind: ButtonKind = 'no-border'
export let size: ButtonSize = 'small'
export let justify: 'left' | 'center' = 'center'
export let width: string | undefined = 'min-content'
export let showTooltip: LabelAndProps | undefined = undefined
$: selectedStatusIcon = selectedComponentStatus
? componentStatusAssets[selectedComponentStatus].icon
: tracker.icon.ComponentStatusBacklog
$: selectedStatusLabel = shouldShowLabel
? selectedComponentStatus
? componentStatusAssets[selectedComponentStatus].label
: tracker.string.Backlog
: undefined
$: statusesInfo = defaultComponentStatuses.map((s) => ({
id: s,
isSelected: componentStatusAssets[s].label === selectedStatusLabel,
...componentStatusAssets[s]
}))
const handleComponentStatusEditorOpened = (event: MouseEvent) => {
if (!isEditable) {
return
}
showPopup(
SelectPopup,
{ value: statusesInfo, placeholder: tracker.string.SetStatus, searchable: true },
eventToHTMLElement(event),
onComponentStatusChange
)
}
</script>
{#if kind === 'list'}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="flex-no-shrink clear-mins cursor-pointer content-pointer-events-none"
on:click={handleComponentStatusEditorOpened}
>
<Icon icon={selectedStatusIcon} {size} />
</div>
{:else}
<Button
{kind}
{size}
{width}
{justify}
disabled={!isEditable}
icon={selectedStatusIcon}
label={selectedStatusLabel}
{showTooltip}
on:click={handleComponentStatusEditorOpened}
/>
{/if}
@@ -13,60 +13,17 @@
// limitations under the License.
-->
<script lang="ts">
import { DocumentQuery, Ref } from '@hcengineering/core'
import { DocumentQuery } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import { Component } from '@hcengineering/tracker'
import {
closePopup,
closeTooltip,
getCurrentResolvedLocation,
navigate,
resolvedLocationStore
} from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import tracker from '../../plugin'
import { ComponentsFilterMode } from '../../utils'
import ComponentBrowser from './ComponentBrowser.svelte'
import EditComponent from './EditComponent.svelte'
export let label: IntlString = tracker.string.Components
export let query: DocumentQuery<Component> = {}
export let search: string = ''
export let filterMode: ComponentsFilterMode = 'all'
let componentId: Ref<Component> | undefined
let component: Component | undefined
onDestroy(
resolvedLocationStore.subscribe(async (loc) => {
closeTooltip()
closePopup()
componentId = loc.path[5] as Ref<Component>
})
)
const componentQuery = createQuery()
$: if (componentId !== undefined) {
componentQuery.query(tracker.class.Component, { _id: componentId }, (result) => {
component = result.shift()
})
} else {
componentQuery.unsubscribe()
component = undefined
}
</script>
{#if component}
<EditComponent
{component}
on:component={(evt) => {
const loc = getCurrentResolvedLocation()
loc.path[5] = evt.detail
navigate(loc)
}}
/>
{:else}
<ComponentBrowser {label} {query} {search} {filterMode} />
{/if}
<ComponentBrowser {label} {query} {search} {filterMode} />
@@ -16,8 +16,7 @@
import { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import { Component } from '@hcengineering/tracker'
import { Component as ViewComponent } from '@hcengineering/ui'
import { BuildModelKey, Viewlet, ViewOptions } from '@hcengineering/view'
import contact from '@hcengineering/contact'
import { Viewlet, ViewOptions } from '@hcengineering/view'
import tracker from '../../plugin'
import CreateComponent from './NewComponent.svelte'
@@ -28,18 +27,6 @@
const createItemDialog = CreateComponent
const createItemLabel = tracker.string.Component
const retrieveMembers = (s: Component) => s.members
function updateConfig (config: (string | BuildModelKey)[]): (string | BuildModelKey)[] {
return config.map((it) => {
if (typeof it === 'string') {
return it
}
return it.presenter === contact.component.MembersPresenter
? { ...it, props: { ...it.props, retrieveMembers } }
: it
})
}
</script>
{#if viewlet?.$lookup?.descriptor?.component}
@@ -47,7 +34,7 @@
is={viewlet.$lookup.descriptor.component}
props={{
_class: tracker.class.Component,
config: updateConfig(viewlet.config),
config: viewlet.config,
options: viewlet.options,
createItemDialog,
createItemLabel,
@@ -1,89 +0,0 @@
<!--
// Copyright © 2023 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 { Class, Doc, DocumentQuery, FindOptions, Ref } from '@hcengineering/core'
import { BuildModelKey, ViewOptionModel, ViewOptions, ViewQueryOption } from '@hcengineering/view'
import {
ActionContext,
ListSelectionProvider,
SelectDirection,
selectionStore,
focusStore,
buildConfigLookup
} from '@hcengineering/view-resources'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Component } from '@hcengineering/tracker'
import { getResource } from '@hcengineering/platform'
import Timeline from './Timeline.svelte'
export let _class: Ref<Class<Component>>
export let query: DocumentQuery<Component> = {}
export let options: FindOptions<Component> | undefined = undefined
export let config: (string | BuildModelKey)[]
export let viewOptions: ViewOptions
export let viewOptionsConfig: ViewOptionModel[] | undefined = undefined
const selectionProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
if (timeline && dir === 'vertical') {
timeline.onElementSelected(offset, of)
}
})
const client = getClient()
const hierarchy = client.getHierarchy()
const componentsQuery = createQuery()
let timeline: Timeline | undefined
let components: Component[] | undefined
let resultOptions: FindOptions<Component> | undefined
let resultQuery: DocumentQuery<Component> = query
// TODO: move to "view-resources" utils
async function getResultQuery<T extends Doc> (
query: DocumentQuery<T>,
viewOptions: ViewOptionModel[] | undefined,
viewOptionsStore: ViewOptions
): Promise<DocumentQuery<T>> {
if (viewOptions === undefined) return query
let result = hierarchy.clone(query)
for (const viewOption of viewOptions) {
if (viewOption.actionTarget !== 'query') continue
const queryOption = viewOption as ViewQueryOption
const f = await getResource(queryOption.action)
result = f(viewOptionsStore[queryOption.key] ?? queryOption.defaultValue, query)
}
return result
}
$: orderBy = viewOptions.orderBy
$: lookup = buildConfigLookup(client.getHierarchy(), _class, config, options?.lookup)
$: resultOptions = { ...options, lookup, sort: { [orderBy[0]]: orderBy[1] } }
$: getResultQuery(query, viewOptionsConfig, viewOptions).then((res) => (resultQuery = { ...res, ...query }))
$: componentsQuery.query(_class, resultQuery, (result) => (components = result), resultOptions)
</script>
<ActionContext context={{ mode: 'browser' }} />
<Timeline
bind:this={timeline}
{_class}
{components}
itemsConfig={config}
options={resultOptions}
selectedObjectIds={$selectionStore ?? []}
selectedRowIndex={selectionProvider.current($focusStore)}
on:row-focus={(event) => selectionProvider.updateFocus(event.detail ?? undefined)}
on:check={(event) => selectionProvider.updateSelection(event.detail.docs, event.detail.value)}
/>
@@ -1,65 +1,43 @@
<script lang="ts">
import { getClient } from '@hcengineering/presentation'
import { StyledTextBox } from '@hcengineering/text-editor'
import { Component } from '@hcengineering/tracker'
import { Button, EditBox, Icon, showPopup } from '@hcengineering/ui'
import { DocAttributeBar } from '@hcengineering/view-resources'
import { createEventDispatcher, onDestroy } from 'svelte'
import { activeComponent } from '../../issues'
import { EditBox } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import tracker from '../../plugin'
import IssuesView from '../issues/IssuesView.svelte'
import ComponentPopup from './ComponentPopup.svelte'
import { onMount } from 'svelte'
export let component: Component
export let object: Component
const client = getClient()
const dispatch = createEventDispatcher()
async function change (field: string, value: any) {
await client.update(component, { [field]: value })
}
function selectComponent (evt: MouseEvent): void {
showPopup(ComponentPopup, { _class: tracker.class.Component }, evt.target as HTMLElement, (value) => {
if (value != null) {
component = value
dispatch('component', component._id)
}
})
let oldLabel = ''
let rawLabel = ''
function change<K extends keyof Component> (field: K, value: Component[K]) {
client.update(object, { [field]: value })
}
$: $activeComponent = component?._id
$: if (oldLabel !== object.label) {
oldLabel = object.label
rawLabel = object.label
}
onDestroy(() => {
$activeComponent = undefined
})
onMount(() => dispatch('open', { ignoreKeys: ['label'] }))
</script>
<IssuesView
query={{ component: component._id, space: component.space }}
space={component.space}
label={component.label}
>
<svelte:fragment slot="label_selector">
<Button size={'small'} kind={'link'} on:click={selectComponent}>
<svelte:fragment slot="content">
<div class="ac-header__icon"><Icon icon={tracker.icon.Component} size={'small'} /></div>
<span class="ac-header__title">{component.label}</span>
</svelte:fragment>
</Button>
</svelte:fragment>
<svelte:fragment slot="aside">
<div class="popupPanel-body__aside-content">
<EditBox kind={'large-style'} bind:value={component.label} on:change={() => change('label', component.label)} />
<div class="mt-2">
<StyledTextBox
alwaysEdit={true}
showButtons={false}
placeholder={tracker.string.Description}
content={component.description ?? ''}
on:value={(evt) => change('description', evt.detail)}
/>
</div>
</div>
<DocAttributeBar object={component} mixins={[]} ignoreKeys={['icon', 'label', 'description']} />
</svelte:fragment>
</IssuesView>
<EditBox
bind:value={rawLabel}
placeholder={tracker.string.Component}
kind="large-style"
focusable
on:blur={() => {
const trimmedLabel = rawLabel.trim()
if (trimmedLabel.length === 0) {
rawLabel = oldLabel
} else if (trimmedLabel !== object.label) {
change('label', trimmedLabel)
}
}}
/>
@@ -16,12 +16,11 @@
import { Data, Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { Card, getClient, SpaceSelector } from '@hcengineering/presentation'
import { EmployeeBox, UserBoxList } from '@hcengineering/contact-resources'
import { Component, ComponentStatus, Project } from '@hcengineering/tracker'
import { DatePresenter, EditBox } from '@hcengineering/ui'
import { EmployeeBox } from '@hcengineering/contact-resources'
import { Component, Project } from '@hcengineering/tracker'
import { EditBox } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import tracker from '../../plugin'
import ComponentStatusSelector from './ComponentStatusSelector.svelte'
import { StyledTextArea } from '@hcengineering/text-editor'
export let space: Ref<Project>
@@ -31,26 +30,14 @@
const object: Data<Component> = {
label: '' as IntlString,
description: '',
status: ComponentStatus.Backlog,
lead: null,
members: [],
comments: 0,
attachments: 0,
startDate: null,
targetDate: null
attachments: 0
}
async function onSave () {
await client.createDoc(tracker.class.Component, space, object)
}
const handleComponentStatusChanged = (newComponentStatus: ComponentStatus | undefined) => {
if (newComponentStatus === undefined) {
return
}
object.status = newComponentStatus
}
</script>
<Card
@@ -77,12 +64,6 @@
emphasized
/>
<svelte:fragment slot="pool">
<ComponentStatusSelector
selectedComponentStatus={object.status}
onComponentStatusChange={handleComponentStatusChanged}
kind={'secondary'}
size={'large'}
/>
<EmployeeBox
label={tracker.string.ComponentLead}
placeholder={tracker.string.AssignTo}
@@ -93,26 +74,5 @@
kind={'secondary'}
size={'large'}
/>
<UserBoxList
bind:items={object.members}
label={tracker.string.ComponentMembersSearchPlaceholder}
kind={'secondary'}
size={'large'}
/>
<!-- TODO: add labels after customize IssueNeedsToBeCompletedByThisDate -->
<DatePresenter
bind:value={object.startDate}
labelNull={tracker.string.StartDate}
kind={'secondary'}
size={'large'}
editable
/>
<DatePresenter
bind:value={object.targetDate}
labelNull={tracker.string.TargetDate}
kind={'secondary'}
size={'large'}
editable
/>
</svelte:fragment>
</Card>
@@ -1,32 +0,0 @@
<!--
// 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 { Component } from '@hcengineering/tracker'
import { getClient } from '@hcengineering/presentation'
import { ButtonKind, DueDatePresenter } from '@hcengineering/ui'
export let value: Component
export let kind: ButtonKind = 'list'
const client = getClient()
$: dueDateMs = value.targetDate
const handleDueDateChanged = async (newDate: number | null) => {
await client.update(value, { targetDate: newDate })
}
</script>
<DueDatePresenter value={dueDateMs} {kind} shouldRender onChange={handleDueDateChanged} />
@@ -1,490 +0,0 @@
<!--
// 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 { Class, Doc, FindOptions, getObjectValue, Ref, Timestamp } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Component } from '@hcengineering/tracker'
import { CheckBox, Spinner, Timeline, TimelineRow } from '@hcengineering/ui'
import { AttributeModel, BuildModelKey } from '@hcengineering/view'
import { buildModel, LoadingProps } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import ComponentPresenter from './ComponentPresenter.svelte'
export let _class: Ref<Class<Doc>>
export let itemsConfig: (BuildModelKey | string)[]
export let selectedObjectIds: Doc[] = []
export let selectedRowIndex: number | undefined = undefined
export let components: Component[] | undefined = undefined
export let loadingProps: LoadingProps | undefined = undefined
export let options: FindOptions<Component> | undefined = undefined
const dispatch = createEventDispatcher()
const client = getClient()
$: selectedObjectIdsSet = new Set<Ref<Doc>>(selectedObjectIds.map((it) => it._id))
let selectedRows: number[] = []
$: if (selectedObjectIdsSet.size > 0 && components !== undefined) {
const tRows: number[] = []
selectedObjectIdsSet.forEach((it) => {
const index = components?.findIndex((f) => f._id === it)
if (index !== undefined) tRows.push(index)
})
selectedRows = tRows
} else selectedRows = []
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 (!components) return
let position =
(docObject !== undefined ? components?.findIndex((x) => x._id === docObject?._id) : selectedRowIndex) ?? -1
position += offset
if (position < 0) position = 0
if (position >= components.length) position = components.length - 1
selectedRowIndex = position
handleRowFocused(components[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 = components?.map((proj) => {
const tR: TimelineRow = { items: [] }
tR.items = [
{
presenter: ComponentPresenter,
props: { value: proj },
startDate: proj.startDate as Timestamp,
targetDate: proj.targetDate as Timestamp
}
]
return tR
})
</script>
{#if components && itemModels && lines}
<Timeline
{lines}
{selectedRows}
selectedRow={selectedRowIndex}
on:row-focus={(ev) => {
if (ev.detail !== undefined && components !== undefined) handleRowFocused(components[ev.detail])
}}
on:check={(ev) => {
if (ev.detail !== undefined && components !== undefined) {
onObjectChecked([components[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, components[row]) ?? ''}
object={components[row]}
{...attributeModel.props}
/>
</div>
</div>
{:else if attributeModelIndex === 1}
<div class="componentPresenter flex-grow">
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, components[row]) ?? ''}
object={components[row]}
{...attributeModel.props}
/>
</div>
<div class="filler" />
{:else}
<div class="gridElement">
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, components[row]) ?? ''}
object={components[row]}
{...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-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;
}
}
.componentPresenter {
display: flex;
align-items: center;
flex-shrink: 0;
width: 5.5rem;
margin-left: 0.5rem;
}
</style>
@@ -52,11 +52,10 @@
if (issue.component) {
const component = await client.findOne(tracker.class.Component, { _id: issue.component })
projectLead = component?.lead || undefined
projectMembers = component?.members || []
} else {
projectLead = undefined
projectMembers = []
}
projectMembers = []
if (hasSpace(issue)) {
const project = await client.findOne(tracker.class.Project, { _id: issue.space })
if (project !== undefined) {
@@ -13,8 +13,6 @@
// limitations under the License.
-->
<script lang="ts">
import { EmployeeAccount } from '@hcengineering/contact'
import { getCurrentAccount } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Component, Issue, Project } from '@hcengineering/tracker'
import { Button, Label } from '@hcengineering/ui'
@@ -41,12 +39,8 @@
async function createMissingComponent (cur: Component): Promise<void> {
await client.createDoc(cur._class, targetProject._id, {
label: cur.label,
members: [(getCurrentAccount() as EmployeeAccount).employee],
status: cur.status,
startDate: cur.startDate,
attachments: 0,
description: cur.description,
targetDate: cur.targetDate,
comments: 0,
lead: cur.lead
})