Squashed commit of the following: (#3059)

Signed-off-by: Sergei Ogorelkov <sergei.ogorelkov@icloud.com>
This commit is contained in:
Sergei Ogorelkov 2023-04-28 13:35:20 +04:00 committed by GitHub
parent 8acdcf7c73
commit 45bdad87b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 411 additions and 531 deletions

View File

@ -342,7 +342,7 @@ export class TTimeSpendReport extends TAttachedDoc implements TimeSpendReport {
@UX(tracker.string.Component, tracker.icon.Component, 'COMPONENT')
export class TComponent extends TDoc implements Component {
@Prop(TypeString(), tracker.string.Title)
// @Index(IndexKind.FullText)
@Index(IndexKind.FullText)
label!: string
@Prop(TypeMarkup(), tracker.string.Description)
@ -1357,6 +1357,10 @@ export function createModel (builder: Builder): void {
filters: []
})
builder.mixin(tracker.class.Component, core.class.Class, view.mixin.ClassFilters, {
filters: []
})
builder.createDoc(
presentation.class.ObjectSearchCategory,
core.space.Model,
@ -1892,4 +1896,107 @@ export function createModel (builder: Builder): void {
},
tracker.action.SetSprintLead
)
const componentListViewOptions: ViewOptionsModel = {
groupBy: ['lead'],
orderBy: [
['startDate', SortingOrder.Descending],
['modifiedOn', SortingOrder.Descending]
],
other: []
}
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: tracker.class.Component,
descriptor: view.viewlet.List,
viewOptions: componentListViewOptions,
config: [
{ key: '', presenter: tracker.component.IconPresenter },
{
key: '',
presenter: tracker.component.ComponentPresenter,
props: { kind: 'list', shouldShowAvatar: false }
},
{ key: '', presenter: view.component.GrowPresenter, props: { type: 'grow' } },
{
key: '$lookup.lead',
presenter: tracker.component.LeadPresenter,
props: { _class: tracker.class.Component, defaultClass: contact.class.Employee, shouldShowLabel: false }
},
{
key: '',
presenter: contact.component.MembersPresenter,
props: {
kind: 'link',
intlTitle: tracker.string.ComponentMembersTitle,
intlSearchPh: tracker.string.ComponentMembersSearchPlaceholder
}
},
{ key: '', presenter: tracker.component.TargetDatePresenter },
{ key: '', presenter: tracker.component.ComponentStatusPresenter, props: { width: 'min-content' } },
{ key: '', presenter: tracker.component.DeleteComponentPresenter }
]
},
tracker.viewlet.ComponentList
)
builder.createDoc(
view.class.ViewletDescriptor,
core.space.Model,
{
label: view.string.Timeline,
icon: view.icon.Timeline,
component: tracker.component.ComponentsTimeline
},
tracker.viewlet.Timeline
)
const componentTimelineViewOptions: ViewOptionsModel = {
groupBy: [],
orderBy: [
['startDate', SortingOrder.Descending],
['modifiedOn', SortingOrder.Descending]
],
other: [],
groupDepth: 1
}
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: tracker.class.Component,
descriptor: tracker.viewlet.Timeline,
viewOptions: componentTimelineViewOptions,
config: [
{ key: '', presenter: tracker.component.IconPresenter },
{
key: '',
presenter: tracker.component.ComponentPresenter,
props: { kind: 'list', shouldShowAvatar: false }
},
{
key: '$lookup.lead',
presenter: tracker.component.LeadPresenter,
props: { _class: tracker.class.Component, defaultClass: contact.class.Employee, shouldShowLabel: false }
},
{
key: '',
presenter: contact.component.MembersPresenter,
props: {
kind: 'link',
intlTitle: tracker.string.ComponentMembersTitle,
intlSearchPh: tracker.string.ComponentMembersSearchPlaceholder
}
},
{ key: '', presenter: tracker.component.TargetDatePresenter },
{ key: '', presenter: tracker.component.ComponentStatusPresenter, props: { width: 'min-content' } },
{ key: '', presenter: tracker.component.DeleteComponentPresenter }
]
},
tracker.viewlet.ComponentsTimeline
)
}

View File

@ -60,7 +60,9 @@ export default mergeIds(trackerId, tracker, {
IssueList: '' as Ref<Viewlet>,
IssueTemplateList: '' as Ref<Viewlet>,
IssueKanban: '' as Ref<Viewlet>,
SprintList: '' as Ref<Viewlet>
SprintList: '' as Ref<Viewlet>,
ComponentList: '' as Ref<Viewlet>,
ComponentsTimeline: '' as Ref<Viewlet>
},
ids: {
TxIssueCreated: '' as Ref<TxViewlet>,

View File

@ -1,5 +1,5 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
// 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
@ -13,81 +13,104 @@
// limitations under the License.
-->
<script lang="ts">
import contact from '@hcengineering/contact'
import { DocumentQuery, FindOptions, SortingOrder } from '@hcengineering/core'
import { DocumentQuery, WithLookup } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import { Component } from '@hcengineering/tracker'
import { Button, IconAdd, Label, showPopup, TabList } from '@hcengineering/ui'
import type { TabItem } from '@hcengineering/ui'
import { createQuery } from '@hcengineering/presentation'
import view, { Viewlet } from '@hcengineering/view'
import {
makeViewletKey,
updateActiveViewlet,
activeViewlet,
getViewOptions,
viewOptionStore,
FilterButton,
ViewletSettingButton,
FilterBar
} from '@hcengineering/view-resources'
import {
ActionIcon,
Button,
IconAdd,
IconMoreH,
Label,
SearchEdit,
TabItem,
TabList,
resolvedLocationStore,
showPopup
} from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import { ComponentsFilterMode, componentsTitleMap, getIncludedComponentStatuses } from '../../utils'
import tracker from '../../plugin'
import view from '@hcengineering/view'
import { getIncludedComponentStatuses, componentsTitleMap, ComponentsViewMode } from '../../utils'
import ComponentsContent from './ComponentsContent.svelte'
import NewComponent from './NewComponent.svelte'
import ComponentsListBrowser from './ComponentsListBrowser.svelte'
export let label: IntlString
export let query: DocumentQuery<Component> = {}
export let search: string = ''
export let mode: ComponentsViewMode = 'all'
export let viewMode: 'list' | 'timeline' = 'list'
const ENTRIES_LIMIT = 200
const resultComponentsQuery = createQuery()
const componentOptions: FindOptions<Component> = {
sort: { modifiedOn: SortingOrder.Descending },
limit: ENTRIES_LIMIT,
lookup: { lead: contact.class.Employee, members: contact.class.Employee }
}
let resultComponents: Component[] = []
$: includedComponentStatuses = getIncludedComponentStatuses(mode)
$: title = componentsTitleMap[mode]
$: includedComponentsQuery = { status: { $in: includedComponentStatuses } }
$: baseQuery = {
...includedComponentsQuery,
...query
}
$: resultQuery = search === '' ? baseQuery : { $search: search, ...baseQuery }
$: resultComponentsQuery.query<Component>(
tracker.class.Component,
{ ...resultQuery },
(result) => {
resultComponents = result
},
componentOptions
)
export let search = ''
export let filterMode: ComponentsFilterMode = 'all'
export let panelWidth: number = 0
const viewletQuery = createQuery()
const space = typeof query.space === 'string' ? query.space : tracker.project.DefaultProject
const showCreateDialog = async () => {
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
let docWidth: number
let docSize = false
function handleFilterModeChanged (newMode: ComponentsFilterMode) {
if (newMode !== filterMode) {
filterMode = newMode
}
}
function showCreateDialog () {
showPopup(NewComponent, { space, targetElement: null }, 'top')
}
const handleViewModeChanged = (newMode: ComponentsViewMode) => {
if (newMode === undefined || newMode === mode) {
return
}
$: title = componentsTitleMap[filterMode]
$: includedComponentStatuses = getIncludedComponentStatuses(filterMode)
$: includedComponentsQuery = { status: { $in: includedComponentStatuses } }
$: searchQuery = search === '' ? { ...query } : { ...query, $search: search }
$: resultQuery = { ...searchQuery }
mode = newMode
$: viewletQuery.query(view.class.Viewlet, { attachTo: tracker.class.Component }, (res) => (viewlets = res), {
lookup: { descriptor: view.class.ViewletDescriptor }
})
$: viewlet = viewlets && updateActiveViewlet(viewlets, $activeViewlet[viewletKey])
$: viewOptions = getViewOptions(viewlet, $viewOptionStore)
$: views =
viewlets?.map((v) => ({ id: v._id, icon: v.$lookup?.descriptor?.icon, tooltip: v.$lookup?.descriptor?.label })) ??
[]
$: if (panelWidth < 900 && !asideFloat) asideFloat = true
$: if (panelWidth >= 900 && asideFloat) {
asideFloat = false
asideShown = false
}
const modeList: TabItem[] = [
{ id: 'all', labelIntl: tracker.string.AllComponents, action: () => handleViewModeChanged('all') },
{ id: 'backlog', labelIntl: tracker.string.BacklogComponents, action: () => handleViewModeChanged('backlog') },
{ id: 'active', labelIntl: tracker.string.ActiveComponents, action: () => handleViewModeChanged('active') },
{ id: 'closed', labelIntl: tracker.string.ClosedComponents, 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 }
]
$: if (docWidth <= 900 && !docSize) docSize = true
$: if (docWidth > 900 && docSize) docSize = false
const retrieveMembers = (p: Component) => p.members
onDestroy(resolvedLocationStore.subscribe((loc) => (viewletKey = makeViewletKey(loc))))
</script>
<div class="ac-header full divide caption-height">
@ -99,77 +122,57 @@
</div>
<div class="ac-header-full medium-gap mb-1">
<TabList
items={viewList}
selected={viewMode}
on:select={(result) => {
if (result.detail !== undefined && result.detail.id !== viewMode) viewMode = result.detail.id
}}
/>
<Button icon={IconAdd} label={tracker.string.Component} kind={'primary'} on:click={showCreateDialog} />
{#if viewlets && viewlets.length > 1}
<TabList
items={views}
selected={viewlet?._id}
kind="normal"
on:select={({ detail }) =>
(viewlet = viewlets && detail?.id ? updateActiveViewlet(viewlets, detail.id) : viewlet)}
/>
{/if}
<Button icon={IconAdd} label={tracker.string.Component} kind="primary" on:click={showCreateDialog} />
</div>
</div>
<div class="ac-header full divide search-start">
<div class="ac-header-full small-gap">
<SearchEdit bind:value={search} />
<ActionIcon icon={IconMoreH} size="small" />
<div class="buttons-divider" />
<FilterButton _class={tracker.class.Component} {space} />
</div>
<div class="ac-header-full medium-gap">
{#if viewlet}
<ViewletSettingButton bind:viewOptions {viewlet} />
<ActionIcon icon={IconMoreH} size="small" />
{/if}
</div>
</div>
<div class="ac-header full divide search-start">
<div class="ac-header-full small-gap">
<TabList
items={modeList}
selected={mode}
kind={'normal'}
on:select={(result) => {
if (result.detail !== undefined && result.detail.action) result.detail.action()
}}
items={filterModeList}
selected={filterMode}
kind="normal"
on:select={({ detail }) => detail?.action?.()}
/>
</div>
</div>
<ComponentsListBrowser
<FilterBar
_class={tracker.class.Component}
itemsConfig={[
{ key: '', presenter: tracker.component.IconPresenter },
{ key: '', presenter: tracker.component.ComponentPresenter, props: { kind: 'list', shouldShowAvatar: false } },
{
key: '$lookup.lead',
presenter: tracker.component.LeadPresenter,
props: { _class: tracker.class.Component, defaultClass: contact.class.Employee, shouldShowLabel: false }
},
{
key: '',
presenter: contact.component.MembersPresenter,
props: {
kind: 'link',
intlTitle: tracker.string.ComponentMembersTitle,
intlSearchPh: tracker.string.ComponentMembersSearchPlaceholder,
retrieveMembers
}
},
{ key: '', presenter: tracker.component.TargetDatePresenter },
{ key: '', presenter: tracker.component.ComponentStatusPresenter },
{ key: '', presenter: tracker.component.DeleteComponentPresenter, props: { space } }
]}
components={resultComponents}
{viewMode}
query={searchQuery}
{viewOptions}
on:change={({ detail }) => (resultQuery = detail)}
/>
<style lang="scss">
.header {
padding: 0.5rem 0.75rem 0.5rem 2.25rem;
}
.componentTitle {
display: flex;
margin-left: 0.25rem;
color: var(--content-color);
font-size: 0.8125rem;
font-weight: 500;
}
.itemsContainer {
display: flex;
align-items: center;
justify-content: space-between;
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);
}
</style>
<div class="flex w-full h-full clear-mins">
{#if viewlet}
<ComponentsContent {viewlet} query={{ ...resultQuery, ...includedComponentsQuery }} {space} {viewOptions} />
{/if}
{#if $$slots.aside !== undefined && asideShown}
<div class="popupPanel-body__aside flex" class:float={asideFloat} class:shown={asideShown}>
<slot name="aside" />
</div>
{/if}
</div>

View File

@ -26,14 +26,14 @@
} from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import tracker from '../../plugin'
import { ComponentsViewMode } from '../../utils'
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 mode: ComponentsViewMode = 'all'
export let filterMode: ComponentsFilterMode = 'all'
let componentId: Ref<Component> | undefined
let component: Component | undefined
@ -68,5 +68,5 @@
}}
/>
{:else}
<ComponentBrowser {label} {query} {search} {mode} />
<ComponentBrowser {label} {query} {search} {filterMode} />
{/if}

View File

@ -0,0 +1,61 @@
<!--
// 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 { 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 tracker from '../../plugin'
import CreateComponent from './NewComponent.svelte'
export let viewlet: WithLookup<Viewlet>
export let viewOptions: ViewOptions
export let query: DocumentQuery<Component> = {}
export let space: Ref<Space> | undefined
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}
<ViewComponent
is={viewlet.$lookup.descriptor.component}
props={{
_class: tracker.class.Component,
config: updateConfig(viewlet.config),
options: viewlet.options,
createItemDialog,
createItemLabel,
viewOptions,
viewOptionsConfig: viewlet.viewOptions?.other,
viewlet,
space,
query
}}
/>
{/if}

View File

@ -1,243 +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 } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Component, Issue } from '@hcengineering/tracker'
import { CheckBox, Spinner, tooltip } from '@hcengineering/ui'
import { BuildModelKey } from '@hcengineering/view'
import { buildModel, LoadingProps } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import tracker from '../../plugin'
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
const dispatch = createEventDispatcher()
const client = getClient()
const objectRefs: HTMLElement[] = []
const baseOptions: FindOptions<Issue> = {
lookup: {
status: tracker.class.IssueStatus
}
}
$: options = { ...baseOptions } as FindOptions<Component>
$: selectedObjectIdsSet = new Set<Ref<Doc>>(selectedObjectIds.map((it) => it._id))
$: objectRefs.length = components?.length ?? 0
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
}
const objectRef = objectRefs[position]
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
}
</script>
{#await buildModel({ client, _class, keys: itemsConfig, lookup: options.lookup }) then itemModels}
<div class="listRoot">
{#if components}
{#each components as docObject (docObject._id)}
<div
bind:this={objectRefs[components.findIndex((x) => x === docObject)]}
class="listGrid"
class:mListGridChecked={selectedObjectIdsSet.has(docObject._id)}
class:mListGridFixed={selectedRowIndex === components.findIndex((x) => x === docObject)}
class:mListGridSelected={selectedRowIndex === components.findIndex((x) => x === docObject)}
on:focus={() => {}}
on:mouseover={() => handleRowFocused(docObject)}
>
<div class="contentWrapper">
{#each itemModels as attributeModel, attributeModelIndex}
{#if attributeModelIndex === 0}
<div class="gridElement">
<div
class="eListGridCheckBox"
use:tooltip={{ direction: 'bottom', label: tracker.string.SelectIssue }}
>
<CheckBox
checked={selectedObjectIdsSet.has(docObject._id)}
on:value={(event) => {
onObjectChecked([docObject], event.detail)
}}
/>
</div>
<div class="iconPresenter">
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
{...attributeModel.props}
/>
</div>
</div>
{:else if attributeModelIndex === 1}
<div class="componentPresenter flex-grow">
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
{...attributeModel.props}
/>
</div>
<div class="filler" />
{:else}
<div class="gridElement">
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
parentId={docObject._id}
{...attributeModel.props}
/>
</div>
{/if}
{/each}
</div>
</div>
{/each}
{: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}
</div>
{/await}
<style lang="scss">
.listRoot {
width: 100%;
}
.contentWrapper {
display: flex;
align-items: center;
height: 100%;
padding-left: 0.75rem;
padding-right: 1.15rem;
}
.listGrid {
width: 100%;
height: 3.25rem;
color: var(--caption-color);
border-bottom: 1px solid var(--divider-color);
&.mListGridChecked {
background-color: var(--highlight-select);
border-bottom-color: var(--highlight-select-border);
.eListGridCheckBox {
opacity: 1;
}
}
&.mListGridSelected {
background-color: var(--highlight-hover);
}
&.mListGridChecked.mListGridSelected {
background-color: var(--highlight-select-hover);
}
.eListGridCheckBox {
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
&:hover {
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;
}
.componentPresenter {
display: flex;
align-items: center;
flex-shrink: 0;
width: 5.5rem;
margin-left: 0.5rem;
}
</style>

View File

@ -1,97 +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 type { Class, Doc, Ref } from '@hcengineering/core'
import { BuildModelKey } from '@hcengineering/view'
import {
ActionContext,
focusStore,
ListSelectionProvider,
SelectDirection,
selectionStore,
LoadingProps
} from '@hcengineering/view-resources'
import { Component } from '@hcengineering/tracker'
import { onMount } from 'svelte'
import ComponentsList from './ComponentsList.svelte'
import ComponentTimeline from './ComponentTimeline.svelte'
import { Scroller } from '@hcengineering/ui'
export let _class: Ref<Class<Doc>>
export let itemsConfig: (BuildModelKey | string)[]
export let loadingProps: LoadingProps | undefined = undefined
export let components: Component[] = []
export let viewMode: 'list' | 'timeline' = 'list'
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
if (dir === 'vertical') {
if (viewMode === 'list') componentsList.onElementSelected(offset, of)
else componentTimeline.onElementSelected(offset, of)
}
})
let componentsList: ComponentsList
let componentTimeline: ComponentTimeline
$: if (componentsList !== undefined) {
listProvider.update(components)
}
onMount(() => {
;(document.activeElement as HTMLElement)?.blur()
})
</script>
<ActionContext
context={{
mode: 'browser'
}}
/>
{#if viewMode === 'list'}
<Scroller>
<ComponentsList
bind:this={componentsList}
{_class}
{itemsConfig}
{loadingProps}
{components}
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)
}}
/>
</Scroller>
{:else}
<ComponentTimeline
bind:this={componentTimeline}
{_class}
{itemsConfig}
{loadingProps}
{components}
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}

View File

@ -0,0 +1,89 @@
<!--
// 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)}
/>

View File

@ -17,11 +17,9 @@
import { Button, ButtonSize, LabelAndProps, showPopup } from '@hcengineering/ui'
import { getClient, MessageBox } from '@hcengineering/presentation'
import type { Component } from '@hcengineering/tracker'
import tracker from '../../plugin'
import { Ref, Space } from '@hcengineering/core'
import { createEventDispatcher } from 'svelte'
import tracker from '../../plugin'
export let space: Ref<Space>
export let value: Component
export let size: ButtonSize = 'medium'
export let justify: 'left' | 'center' = 'center'
@ -49,7 +47,7 @@
}
async function removeComponent () {
await client.removeDoc(tracker.class.Component, space, value._id)
await client.remove(value)
}
</script>

View File

@ -15,7 +15,7 @@
<script lang="ts">
import contact, { Employee } from '@hcengineering/contact'
import { Class, Doc, Ref } from '@hcengineering/core'
import { Component, Sprint } from '@hcengineering/tracker'
import { Component } from '@hcengineering/tracker'
import { getClient } from '@hcengineering/presentation'
import { UsersPopup } from '@hcengineering/contact-resources'
import { AttributeModel } from '@hcengineering/view'
@ -26,9 +26,8 @@
import LeadPopup from './LeadPopup.svelte'
export let value: Employee | null
export let _class: Ref<Class<Component | Sprint>>
export let object: Component
export let size: IconSize = 'x-small'
export let parentId: Ref<Doc>
export let defaultClass: Ref<Class<Doc>> | undefined = undefined
export let isEditable: boolean = true
export let shouldShowLabel: boolean = false
@ -55,15 +54,8 @@
return
}
const currentParent = await client.findOne(_class, { _id: parentId as Ref<Component> })
if (currentParent === undefined) {
return
}
const newLead = result === null ? null : result._id
await client.update(currentParent, { lead: newLead })
await client.update(object, { lead: newLead })
}
const handleLeadEditorOpened = async (event: MouseEvent) => {

View File

@ -15,9 +15,10 @@
<script lang="ts">
import { Component } from '@hcengineering/tracker'
import { getClient } from '@hcengineering/presentation'
import { DueDatePresenter } from '@hcengineering/ui'
import { ButtonKind, DueDatePresenter } from '@hcengineering/ui'
export let value: Component
export let kind: ButtonKind = 'list'
const client = getClient()
@ -28,4 +29,4 @@
}
</script>
<DueDatePresenter value={dueDateMs} shouldRender={true} onChange={handleDueDateChanged} />
<DueDatePresenter value={dueDateMs} {kind} shouldRender onChange={handleDueDateChanged} />

View File

@ -15,12 +15,11 @@
<script lang="ts">
import { Class, Doc, FindOptions, getObjectValue, Ref, Timestamp } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Component, Issue } from '@hcengineering/tracker'
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 tracker from '../../plugin'
import ComponentPresenter from './ComponentPresenter.svelte'
export let _class: Ref<Class<Doc>>
@ -29,18 +28,12 @@
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()
const baseOptions: FindOptions<Issue> = {
lookup: {
status: tracker.class.IssueStatus
}
}
$: options = { ...baseOptions } as FindOptions<Component>
$: selectedObjectIdsSet = new Set<Ref<Doc>>(selectedObjectIds.map((it) => it._id))
let selectedRows: number[] = []
$: if (selectedObjectIdsSet.size > 0 && components !== undefined) {
@ -86,7 +79,7 @@
}
let itemModels: AttributeModel[] | undefined = undefined
$: buildModel({ client, _class, keys: itemsConfig, lookup: options.lookup }).then((res) => (itemModels = res))
$: buildModel({ client, _class, keys: itemsConfig, lookup: options?.lookup }).then((res) => (itemModels = res))
let lines: TimelineRow[] | undefined
$: lines = components?.map((proj) => {
@ -125,6 +118,7 @@
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, components[row]) ?? ''}
object={components[row]}
{...attributeModel.props}
/>
</div>
@ -134,6 +128,7 @@
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, components[row]) ?? ''}
object={components[row]}
{...attributeModel.props}
/>
</div>
@ -143,7 +138,7 @@
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, components[row]) ?? ''}
parentId={components[row]._id}
object={components[row]}
{...attributeModel.props}
/>
</div>

View File

@ -179,35 +179,3 @@
</div>
{/if}
</div>
<style lang="scss">
.header {
padding: 0.5rem 0.75rem 0.5rem 2.25rem;
}
.title {
display: flex;
margin-left: 0.25rem;
color: var(--content-color);
font-size: 0.8125rem;
font-weight: 500;
}
.itemsContainer {
display: flex;
align-items: center;
justify-content: space-between;
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);
}
.buttonWrapper {
margin-right: 1px;
&:last-child {
margin-right: 0;
}
}
</style>

View File

@ -30,6 +30,7 @@ import { showPopup } from '@hcengineering/ui'
import ComponentEditor from './components/components/ComponentEditor.svelte'
import ComponentPresenter from './components/components/ComponentPresenter.svelte'
import Components from './components/components/Components.svelte'
import ComponentsTimeline from './components/components/ComponentsTimeline.svelte'
import ComponentStatusEditor from './components/components/ComponentStatusEditor.svelte'
import ComponentStatusPresenter from './components/components/ComponentStatusPresenter.svelte'
import ComponentTitlePresenter from './components/components/ComponentTitlePresenter.svelte'
@ -381,6 +382,7 @@ export default async (): Promise<Resources> => ({
Inbox,
MyIssues,
Components,
ComponentsTimeline,
Views,
IssuePresenter,
ComponentPresenter,

View File

@ -24,7 +24,8 @@ export default mergeIds(trackerId, tracker, {
viewlet: {
SubIssues: '' as Ref<Viewlet>,
List: '' as Ref<ViewletDescriptor>,
Kanban: '' as Ref<ViewletDescriptor>
Kanban: '' as Ref<ViewletDescriptor>,
Timeline: '' as Ref<ViewletDescriptor>
},
string: {
More: '' as IntlString,
@ -319,6 +320,7 @@ export default mergeIds(trackerId, tracker, {
Active: '' as AnyComponent,
Backlog: '' as AnyComponent,
Components: '' as AnyComponent,
ComponentsTimeline: '' as AnyComponent,
IssuePresenter: '' as AnyComponent,
ComponentTitlePresenter: '' as AnyComponent,
ComponentPresenter: '' as AnyComponent,

View File

@ -227,13 +227,13 @@ export const getArraysUnion = (a: any[], b: any[]): any[] => {
return Array.from(union)
}
export type ComponentsViewMode = 'all' | 'backlog' | 'active' | 'closed'
export type ComponentsFilterMode = 'all' | 'backlog' | 'active' | 'closed'
export type SprintViewMode = 'all' | 'planned' | 'active' | 'closed'
export type ScrumRecordViewMode = 'timeReports' | 'objects'
export const getIncludedComponentStatuses = (mode: ComponentsViewMode): ComponentStatus[] => {
export const getIncludedComponentStatuses = (mode: ComponentsFilterMode): ComponentStatus[] => {
switch (mode) {
case 'all': {
return defaultComponentStatuses
@ -273,7 +273,7 @@ export const getIncludedSprintStatuses = (mode: SprintViewMode): SprintStatus[]
}
}
export const componentsTitleMap: Record<ComponentsViewMode, IntlString> = Object.freeze({
export const componentsTitleMap: Record<ComponentsFilterMode, IntlString> = Object.freeze({
all: tracker.string.AllComponents,
backlog: tracker.string.BacklogComponents,
active: tracker.string.ActiveComponents,