Sortable list (#2618)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-02-10 22:45:14 +06:00 committed by GitHub
parent df7667289d
commit f7e495b609
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 420 additions and 415 deletions

View File

@ -234,10 +234,10 @@ export function createModel (builder: Builder): void {
attachTo: lead.class.Lead,
descriptor: view.viewlet.List,
config: [
{ key: '', props: { fixed: 'left' } },
{ key: 'title', props: { fixed: 'left' } },
{ key: 'state', props: { fixed: 'left' } },
{ key: 'doneState', props: { fixed: 'left' } },
{ key: '', props: { listProps: { fixed: 'left' } } },
{ key: 'title', props: { listProps: { fixed: 'left' } } },
{ key: 'state', props: { listProps: { fixed: 'left' } } },
{ key: 'doneState', props: { listProps: { fixed: 'left' } } },
{ key: '', presenter: view.component.GrowPresenter },
'attachments',
'comments',

View File

@ -540,7 +540,7 @@ export function createModel (builder: Builder): void {
presenter: tracker.component.PriorityEditor,
props: { type: 'priority', kind: 'list', size: 'small' }
},
{ key: '', presenter: tracker.component.IssuePresenter, props: { type: 'issue', fixed: 'left' } },
{ key: '', presenter: tracker.component.IssuePresenter, props: { type: 'issue', listProps: { fixed: 'left' } } },
{
key: '',
presenter: tracker.component.StatusEditor,
@ -558,8 +558,10 @@ export function createModel (builder: Builder): void {
size: 'small',
shape: 'round',
shouldShowPlaceholder: false,
excludeByKey: 'project',
optional: true
listProps: {
excludeByKey: 'project',
optional: true
}
}
},
{
@ -570,24 +572,26 @@ export function createModel (builder: Builder): void {
size: 'small',
shape: 'round',
shouldShowPlaceholder: false,
excludeByKey: 'sprint',
optional: true
listProps: {
excludeByKey: 'sprint',
optional: true
}
}
},
{
key: '',
presenter: tracker.component.EstimationEditor,
props: { kind: 'list', size: 'small', optional: true }
props: { kind: 'list', size: 'small', listProps: { optional: true } }
},
{
key: 'modifiedOn',
presenter: tracker.component.ModificationDatePresenter,
props: { fixed: 'right', optional: true }
props: { listProps: { fixed: 'right', optional: true } }
},
{
key: '$lookup.assignee',
presenter: tracker.component.AssigneePresenter,
props: { issueClass: tracker.class.Issue, defaultClass: contact.class.Employee, shouldShowLabel: false }
props: { defaultClass: contact.class.Employee, shouldShowLabel: false }
}
]
})
@ -595,11 +599,11 @@ export function createModel (builder: Builder): void {
const subIssuesOptions: ViewOptionsModel = {
groupBy: ['status', 'assignee', 'priority', 'sprint'],
orderBy: [
['rank', SortingOrder.Ascending],
['status', SortingOrder.Ascending],
['priority', SortingOrder.Ascending],
['modifiedOn', SortingOrder.Descending],
['dueDate', SortingOrder.Descending],
['rank', SortingOrder.Ascending]
['dueDate', SortingOrder.Descending]
],
groupDepth: 1,
other: []
@ -619,7 +623,11 @@ export function createModel (builder: Builder): void {
presenter: tracker.component.PriorityEditor,
props: { type: 'priority', kind: 'list', size: 'small' }
},
{ key: '', presenter: tracker.component.IssuePresenter, props: { type: 'issue', fixed: 'left' } },
{
key: '',
presenter: tracker.component.IssuePresenter,
props: { type: 'issue', listProps: { fixed: 'left' } }
},
{
key: '',
presenter: tracker.component.StatusEditor,
@ -637,24 +645,26 @@ export function createModel (builder: Builder): void {
size: 'small',
shape: 'round',
shouldShowPlaceholder: false,
excludeByKey: 'sprint',
optional: true
listProps: {
excludeByKey: 'sprint',
optional: true
}
}
},
{
key: '',
presenter: tracker.component.EstimationEditor,
props: { kind: 'list', size: 'small', optional: true }
props: { kind: 'list', size: 'small', listProps: { optional: true } }
},
{
key: 'modifiedOn',
presenter: tracker.component.ModificationDatePresenter,
props: { fixed: 'right', optional: true }
props: { listProps: { fixed: 'right', optional: true } }
},
{
key: '$lookup.assignee',
presenter: tracker.component.AssigneePresenter,
props: { issueClass: tracker.class.Issue, defaultClass: contact.class.Employee, shouldShowLabel: false }
props: { defaultClass: contact.class.Employee, shouldShowLabel: false }
}
]
},
@ -690,11 +700,15 @@ export function createModel (builder: Builder): void {
props: { kind: 'list', size: 'small', shape: 'round', shouldShowPlaceholder: false }
},
{ key: '', presenter: tracker.component.TemplateEstimationEditor, props: { kind: 'list', size: 'small' } },
{ key: 'modifiedOn', presenter: tracker.component.ModificationDatePresenter, props: { fixed: 'right' } },
{
key: 'modifiedOn',
presenter: tracker.component.ModificationDatePresenter,
props: { listProps: { fixed: 'right' } }
},
{
key: '$lookup.assignee',
presenter: tracker.component.AssigneePresenter,
props: { issueClass: tracker.class.IssueTemplate, defaultClass: contact.class.Employee, shouldShowLabel: false }
props: { defaultClass: contact.class.Employee, shouldShowLabel: false }
}
]
})

View File

@ -128,11 +128,8 @@
}
}
function cardDragOver (evt: CardDragEvent, object: ExtItem): void {
if (dragCard !== undefined) {
;(dragCard as any)[fieldName] = (dragCard as any)[fieldName]
if (!dontUpdateRank) {
dragCard.rank = doCalcRank(object, evt)
}
if (dragCard !== undefined && !dontUpdateRank) {
dragCard.rank = doCalcRank(object, evt)
}
}
function cardDrop (evt: CardDragEvent, object: ExtItem): void {

View File

@ -35,11 +35,13 @@
{/if}
{:then Ctor}
<ErrorBoundary>
<Ctor {...props} on:change on:close on:open on:click on:delete on:action>
{#if $$slots.default}
{#if $$slots.default !== undefined}
<Ctor {...props} on:change on:close on:open on:click on:delete on:action>
<slot />
{/if}
</Ctor>
</Ctor>
{:else}
<Ctor {...props} on:change on:close on:open on:click on:delete on:action />
{/if}
</ErrorBoundary>
{:catch err}
<pre style="max-height: 140px; overflow: auto;">

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { Issue } from '@hcengineering/tracker'
import { Component } from '@hcengineering/ui'
import { Viewlet, ViewOptions } from '@hcengineering/view'
import tracker from '../../plugin'
@ -10,9 +10,6 @@
export let query: DocumentQuery<Issue> = {}
export let space: Ref<Space> | undefined
// Extra properties
export let teams: Map<Ref<Team>, Team> | undefined
export let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]>
export let viewOptions: ViewOptions
const createItemDialog = CreateIssue
@ -32,8 +29,7 @@
viewOptions,
viewOptionsConfig: viewlet.viewOptions?.other,
space,
query,
props: { teams, issueStatuses }
query
}}
/>
{/if}

View File

@ -1,232 +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 { createEventDispatcher } from 'svelte'
import { getObjectValue, WithLookup } from '@hcengineering/core'
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import notification from '@hcengineering/notification'
import { tooltip, CheckBox, Component, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
import { AttributeModel } from '@hcengineering/view'
import tracker from '../../plugin'
import { IssuesGroupByKeys } from '../../utils'
import { FixedColumn } from '@hcengineering/view-resources'
import Circles from '../icons/Circles.svelte'
export let use: HTMLElement
export let docObject: Issue
export let model: AttributeModel[]
export let groupByKey: IssuesGroupByKeys | undefined
export let checked: boolean
export let selected: boolean
export let statuses: WithLookup<IssueStatus>[]
export let currentTeam: Team | undefined
const dispatch = createEventDispatcher()
$: compactMode = $deviceInfo.twoRows
</script>
<div
bind:this={use}
class="listGrid antiList__row row gap-2 flex-grow"
class:checking={checked}
class:mListGridFixed={selected}
class:mListGridSelected={selected}
on:contextmenu
on:focus
on:mouseover
>
<div class="flex-center relative" use:tooltip={{ label: tracker.string.SelectIssue, direction: 'bottom' }}>
<div class="antiList-cells__notifyCell">
<div class="antiList-cells__checkCell">
<CheckBox
{checked}
on:value={(event) => {
dispatch('check', { docs: [docObject], value: event.detail })
}}
/>
</div>
<Component
is={notification.component.NotificationPresenter}
showLoading={false}
props={{ value: docObject, kind: 'table' }}
/>
</div>
</div>
{#each model as attributeModel}
{#if attributeModel.props?.type === 'grow'}
<svelte:component this={attributeModel.presenter} />
{:else if (!groupByKey || attributeModel.props?.excludeByKey !== groupByKey) && !(attributeModel.props?.optional && compactMode)}
{#if attributeModel.props?.fixed}
<FixedColumn key={`issue_${attributeModel.key}`} justify={attributeModel.props.fixed}>
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
groupBy={groupByKey}
{...attributeModel.props}
{statuses}
{currentTeam}
/>
</FixedColumn>
{:else}
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
issueId={docObject._id}
groupBy={groupByKey}
{...attributeModel.props}
{statuses}
{currentTeam}
/>
{/if}
{/if}
{/each}
{#if compactMode}
<div class="panel-trigger" tabindex="-1">
<Circles />
<div class="space" />
<Circles />
</div>
<div class="hidden-panel" tabindex="-1">
<div class="header">
<Circles />
<div class="space" />
<Circles />
</div>
<div class="scroll-box gap-2">
{#each model as attributeModel}
{@const value = getObjectValue(attributeModel.key, docObject)}
{#if attributeModel.props?.optional && attributeModel.props?.excludeByKey !== groupByKey && value !== undefined}
<svelte:component
this={attributeModel.presenter}
value={value ?? ''}
issueId={docObject._id}
groupBy={groupByKey}
{...attributeModel.props}
{statuses}
{currentTeam}
/>
{/if}
{/each}
</div>
</div>
{/if}
</div>
<style lang="scss">
.row:not(:last-child) {
border-bottom: 1px solid var(--accent-bg-color);
}
.listGrid {
position: relative;
display: flex;
align-items: center;
padding: 0 0.75rem 0 0.875rem;
width: 100%;
height: 2.75rem;
min-height: 2.75rem;
color: var(--theme-caption-color);
&.checking {
background-color: var(--highlight-select);
border-bottom-color: var(--highlight-select);
&:hover {
background-color: var(--highlight-select-hover);
border-bottom-color: var(--highlight-select-hover);
}
}
&.mListGridSelected {
background-color: var(--highlight-hover);
}
.hidden-panel,
.panel-trigger {
position: absolute;
display: flex;
align-items: center;
top: 0;
bottom: 0;
height: 100%;
}
.hidden-panel {
overflow: hidden;
right: 0;
width: 80%;
background-color: var(--accent-bg-color);
opacity: 0;
pointer-events: none;
z-index: 2;
transition-property: opacity, width;
transition-duration: 0.15s;
transition-timing-function: var(--timing-main);
.header {
display: flex;
flex-direction: column;
justify-content: center;
margin: 0 0.25rem;
width: 0.375rem;
min-width: 0.375rem;
height: 100%;
opacity: 0.25;
}
.scroll-box {
overflow: auto visible;
display: flex;
align-items: center;
margin: 0.125rem 0.25rem 0;
padding: 0.25rem 0.25rem;
min-width: 0;
&::-webkit-scrollbar:horizontal {
height: 3px;
}
}
}
.panel-trigger {
flex-direction: column;
justify-content: center;
padding: 0 0.125rem;
right: 2.5rem;
width: 0.75rem;
border: 1px solid transparent;
border-radius: 0.25rem;
opacity: 0.1;
z-index: 1;
transition: opacity 0.15s var(--timing-main);
&:focus {
border-color: var(--primary-edit-border-color);
opacity: 0.25;
}
& > * {
pointer-events: none;
}
}
.hidden-panel:focus-within,
.hidden-panel:focus,
.panel-trigger:focus + .hidden-panel {
width: 100%;
opacity: 1;
pointer-events: all;
}
.space {
min-height: 0.1075rem;
}
}
</style>

View File

@ -1,8 +1,8 @@
<script lang="ts">
import { DocumentQuery, Ref, SortingOrder, Space, toIdMap, WithLookup } from '@hcengineering/core'
import { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import { IntlString, translate } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { getClient } from '@hcengineering/presentation'
import { Issue } from '@hcengineering/tracker'
import { Button, IconDetails, IconDetailsFilled } from '@hcengineering/ui'
import view, { Viewlet } from '@hcengineering/view'
import { FilterBar, getActiveViewletId, getViewOptions, setActiveViewletId } from '@hcengineering/view-resources'
@ -65,33 +65,6 @@
$: if (docWidth <= 900 && !docSize) docSize = true
$: if (docWidth > 900 && docSize) docSize = false
const teamQuery = createQuery()
let _teams: Map<Ref<Team>, Team> | undefined = undefined
$: teamQuery.query(tracker.class.Team, {}, (result) => {
_teams = toIdMap(result)
})
let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]>
const statusesQuery = createQuery()
statusesQuery.query(
tracker.class.IssueStatus,
{},
(statuses) => {
const st = new Map<Ref<Team>, WithLookup<IssueStatus>[]>()
for (const s of statuses) {
const id = s.attachedTo as Ref<Team>
st.set(id, [...(st.get(id) ?? []), s])
}
issueStatuses = st
},
{
lookup: { category: tracker.class.IssueStatusCategory },
sort: { rank: SortingOrder.Ascending }
}
)
$: viewOptions = getViewOptions(viewlet)
</script>
@ -120,8 +93,8 @@
<slot name="afterHeader" />
<FilterBar _class={tracker.class.Issue} query={searchQuery} {viewOptions} on:change={(e) => (resultQuery = e.detail)} />
<div class="flex w-full h-full clear-mins">
{#if viewlet && _teams && issueStatuses}
<IssuesContent {viewlet} query={resultQuery} {space} teams={_teams} {issueStatuses} {viewOptions} />
{#if viewlet}
<IssuesContent {viewlet} query={resultQuery} {space} {viewOptions} />
{/if}
{#if $$slots.aside !== undefined && asideShown}
<div class="popupPanel-body__aside flex" class:float={asideFloat} class:shown={asideShown}>

View File

@ -32,7 +32,7 @@
import { subIssueListProvider } from '../../../utils'
export let value: WithLookup<Issue>
export let currentTeam: Team | undefined
export let currentTeam: Team | undefined = undefined
export let kind: ButtonKind = 'link-bordered'
export let size: ButtonSize = 'inline'
@ -41,9 +41,23 @@
let btn: HTMLElement
$: team = currentTeam
let subIssues: Issue[] = []
let countComplate: number = 0
const teamQuery = createQuery()
$: if (currentTeam === undefined) {
teamQuery.query(
tracker.class.Team,
{
_id: value.space
},
(res) => ([team] = res)
)
} else {
teamQuery.unsubscribe()
}
const query = createQuery()
const statusesQuery = createQuery()
@ -103,7 +117,7 @@
SelectPopup,
{
value: subIssues.map((iss) => {
const text = currentTeam ? `${getIssueId(currentTeam, iss)} ${iss.title}` : iss.title
const text = team ? `${getIssueId(team, iss)} ${iss.title}` : iss.title
return { id: iss._id, text, isSelected: iss._id === value._id, ...getIssueStatusIcon(iss, statuses) }
}),

View File

@ -31,7 +31,7 @@
export let size: ButtonSize = 'large'
export let justify: 'left' | 'center' = 'left'
export let width: string | undefined = undefined
export let currentTeam: Team | undefined
export let currentTeam: Team | undefined = undefined
const client = getClient()
const dispatch = createEventDispatcher()

View File

@ -175,7 +175,9 @@
size: 'small',
shape: 'round',
shouldShowPlaceholder: false,
excludeByKey: 'sprint',
listProps: {
excludeByKey: 'sprint'
},
isEditable: false
}
},

View File

@ -13,6 +13,11 @@
const dispatch = createEventDispatcher()
$: grops = viewOptions.groupBy =
viewOptions.groupBy[viewOptions.groupBy.length - 1] === noCategory
? viewOptions.groupBy
: [...viewOptions.groupBy, noCategory]
const client = getClient()
const hierarchy = client.getHierarchy()
const lookup = buildConfigLookup(hierarchy, viewlet.attachTo, viewlet.config, viewlet.options?.lookup)
@ -57,7 +62,7 @@
<div class="antiCard">
<div class="antiCard-group grid">
{#each viewOptions.groupBy as group, i}
{#each grops as group, i}
<span class="label"><Label label={i === 0 ? view.string.Grouping : view.string.Then} /></span>
<div class="value grouping">
<DropdownLabelsIntl

View File

@ -129,9 +129,13 @@
return props.length
}
let dragItem: Doc | undefined = undefined
let listDiv: HTMLDivElement
</script>
<div class="list-container">
<div class="list-container" bind:this={listDiv}>
<ListCategories
newObjectProps={space ? { space } : {}}
{elementByIndex}
@ -157,6 +161,8 @@
{flatHeaders}
{disableHeader}
{props}
{listDiv}
bind:dragItem
/>
</div>

View File

@ -18,7 +18,7 @@
import { getClient } from '@hcengineering/presentation'
import { AnyComponent } from '@hcengineering/ui'
import { AttributeModel, BuildModelKey, CategoryOption, ViewOptionModel, ViewOptions } from '@hcengineering/view'
import { onDestroy } from 'svelte'
import { createEventDispatcher, onDestroy } from 'svelte'
import { buildModel, getAdditionalHeader, getCategories, getPresenter, groupBy } from '../../utils'
import { CategoryQuery, noCategory } from '../../viewOptions'
import ListCategory from './ListCategory.svelte'
@ -44,6 +44,8 @@
export let newObjectProps: Record<string, any>
export let docByIndex: Map<number, Doc>
export let viewOptionsConfig: ViewOptionModel[] | undefined
export let dragItem: Doc | undefined
export let listDiv: HTMLDivElement
$: groupByKey = viewOptions.groupBy[level] ?? noCategory
$: groupedDocs = groupBy(docs, groupByKey)
@ -110,12 +112,14 @@
let res = initIndex
for (let index = 0; index < i; index++) {
const cat = categories[index]
res += groupedDocs[cat]?.length
res += groupedDocs[cat]?.length ?? 0
}
return res
}
$: extraHeaders = getAdditionalHeader(client, _class)
const dispatch = createEventDispatcher()
</script>
{#each categories as category, i}
@ -133,6 +137,7 @@
{level}
{viewOptions}
{groupByKey}
{lookup}
{config}
{docByIndex}
{itemModels}
@ -147,9 +152,17 @@
on:check
on:uncheckAll
on:row-focus
on:dragstart={(e) => {
dispatch('dragstart', {
target: e.detail.target,
index: e.detail.index + getInitIndex(categories, i)
})
}}
{flatHeaders}
{disableHeader}
{props}
{listDiv}
bind:dragItem
/>
{/key}
{/each}

View File

@ -13,8 +13,10 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Doc, Lookup, Ref, Space } from '@hcengineering/core'
import { Class, Doc, DocumentUpdate, Lookup, Ref, Space } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { calcRank, DocWithRank } from '@hcengineering/task'
import {
AnyComponent,
CheckBox,
@ -58,28 +60,31 @@
export let newObjectProps: Record<string, any>
export let docByIndex: Map<number, Doc>
export let viewOptionsConfig: ViewOptionModel[] | undefined
export let dragItem: Doc | undefined
export let listDiv: HTMLDivElement
$: lastLevel = level + 1 >= viewOptions.groupBy.length
const autoFoldLimit = 20
const defaultLimit = 20
const singleCategoryLimit = 200
$: initialLimit = !lastLevel ? items.length : singleCat ? singleCategoryLimit : defaultLimit
$: initialLimit = !lastLevel ? undefined : singleCat ? singleCategoryLimit : defaultLimit
$: limit = initialLimit
let collapsed = true
const dispatch = createEventDispatcher()
function limitGroup (items: Doc[], limit: number): Doc[] {
return items.slice(0, limit)
function limitGroup (items: Doc[], limit: number | undefined): Doc[] {
const res = limit !== undefined ? items.slice(0, limit) : items
return res
}
function initCollapsed (singleCat: boolean, lastLevel: boolean, category: any): void {
function initCollapsed (singleCat: boolean, lastLevel: boolean): void {
collapsed = !disableHeader && !singleCat && items.length > (lastLevel ? autoFoldLimit : singleCategoryLimit)
}
$: initCollapsed(singleCat, lastLevel, category)
$: initCollapsed(singleCat, lastLevel)
const handleRowFocused = (object: Doc) => {
dispatch('row-focus', object)
@ -108,96 +113,272 @@
function isSelected (doc: Doc, focusStore: FocusSelection): boolean {
return focusStore.focus?._id === doc._id
}
$: byRank = viewOptions.orderBy[0] === 'rank'
const client = getClient()
let dragItemIndex: number | undefined
function dragswap (ev: MouseEvent, i: number): boolean {
if (dragItemIndex === undefined || !byRank) return false
const s = dragItemIndex
if (i < s) {
return ev.offsetY < (ev.target as HTMLElement).offsetHeight / 2
} else if (i > s) {
return ev.offsetY > (ev.target as HTMLElement).offsetHeight / 2
}
return false
}
function dragOverCat (ev: MouseEvent) {
ev.preventDefault()
ev.stopPropagation()
}
let div: HTMLDivElement
function isBorder (ev: MouseEvent, direction: 'top' | 'bottom'): boolean {
const target = ev.target as HTMLDivElement
return Math.abs(ev.clientY - target.getBoundingClientRect()[direction]) < 5
}
function dragEnterCat (ev: MouseEvent) {
ev.preventDefault()
if (dragItemIndex === undefined && dragItem !== undefined) {
const index = items.findIndex((p) => p._id === dragItem?._id)
if (index !== -1) {
dragItemIndex = index
return
}
if (isBorder(ev, 'top')) {
items.unshift(dragItem)
dragItemIndex = 0
items = items
dispatch('row-focus', dragItem)
} else if (isBorder(ev, 'bottom')) {
items.push(dragItem)
dragItemIndex = items.length - 1
items = items
dispatch('row-focus', dragItem)
}
}
}
function dragLeaveCat (ev: MouseEvent) {
ev.stopPropagation()
if (dragItemIndex !== undefined) {
items.splice(dragItemIndex, 1)
items = items
dragItemIndex = undefined
}
}
function dragItemLeave (ev: MouseEvent, i: number) {
if (dragItemIndex !== undefined) {
const isLastItem = i === limited.length - 1
const isFirstItemWithoutHeader = i === 0 && disableHeader
if (isFirstItemWithoutHeader && isBorder(ev, 'top')) {
return
}
if (isLastItem && isBorder(ev, 'bottom')) {
return
}
ev.stopPropagation()
ev.preventDefault()
}
}
function dragover (ev: MouseEvent, i: number) {
if (dragItemIndex === undefined || !lastLevel) return
ev.preventDefault()
ev.stopPropagation()
const s = dragItemIndex
if (dragswap(ev, i) && items[i] !== undefined && items[s] !== undefined) {
;[items[i], items[s]] = [items[s], items[i]]
items = items
dragItemIndex = i
dispatch('row-focus', dragItem)
}
}
function dropItemHandle (ev: MouseEvent) {
ev.stopPropagation()
ev.preventDefault()
const update: DocumentUpdate<Doc> = {}
if (dragItemIndex !== undefined) {
const prev = limited[dragItemIndex - 1] as DocWithRank
const next = limited[dragItemIndex + 1] as DocWithRank
try {
const newRank = calcRank(prev, next)
if ((dragItem as DocWithRank)?.rank !== newRank) {
;(update as any).rank = newRank
}
} catch {}
}
drop(update)
}
async function drop (update: DocumentUpdate<Doc> = {}) {
if (dragItem !== undefined) {
for (const key in newObjectProps) {
const value = newObjectProps[key]
if ((dragItem as any)[key] !== value) {
;(update as any)[key] = value
}
}
if (Object.keys(update).length > 0) {
await client.update(dragItem, update)
}
}
dragItem = undefined
dragItemIndex = undefined
}
const dragEndListener: any = (ev: DragEvent, initIndex: number) => {
ev.preventDefault()
const rect = listDiv.getBoundingClientRect()
const inRect = ev.clientY > rect.top && ev.clientY < rect.top + rect.height
if (!inRect) {
if (items.findIndex((p) => p._id === dragItem?._id) === -1 && dragItem !== undefined) {
items = [...items.slice(0, initIndex), dragItem, ...items.slice(initIndex)]
}
if (level === 0) {
dragItem = undefined
}
}
}
function dragStartHandler (e: CustomEvent<any>) {
const { target, index } = e.detail
dragItemIndex = index
;(target as EventTarget).addEventListener('dragend', (e) => dragEndListener(e, index))
}
function dragStart (ev: DragEvent, docObject: Doc, i: number) {
if (ev.dataTransfer) {
ev.dataTransfer.effectAllowed = 'move'
ev.dataTransfer.dropEffect = 'move'
}
ev.target?.addEventListener('dragend', (e) => dragEndListener(e, i))
dragItem = docObject
dragItemIndex = i
dispatch('dragstart', {
target: ev.target,
index: i
})
}
</script>
{#if !disableHeader}
<ListHeader
{groupByKey}
{category}
{space}
{level}
limited={limited.length}
{items}
{headerComponent}
{createItemDialog}
{createItemLabel}
{extraHeaders}
{newObjectProps}
flat={flatHeaders}
{props}
on:more={() => {
limit += 20
}}
on:collapse={() => {
collapsed = !collapsed
}}
/>
{/if}
<ExpandCollapse isExpanded={!collapsed} duration={400}>
{#if !lastLevel}
<div class="p-2">
<ListCategories
{elementByIndex}
{indexById}
docs={items}
{_class}
{space}
{lookup}
{loadingPropsLength}
{baseMenuClass}
{config}
{selectedObjectIds}
{createItemDialog}
{createItemLabel}
{viewOptions}
{newObjectProps}
{flatHeaders}
{props}
level={level + 1}
{initIndex}
{docByIndex}
{viewOptionsConfig}
on:check
on:uncheckAll
on:row-focus
/>
</div>
{:else if itemModels}
{#if limited}
{#each limited as docObject, i (docObject._id)}
<ListItem
{docObject}
<div
bind:this={div}
on:drop|preventDefault={drop}
on:dragover={dragOverCat}
on:dragenter={dragEnterCat}
on:dragleave={dragLeaveCat}
>
{#if !disableHeader}
<ListHeader
{groupByKey}
{category}
{space}
{level}
limited={limited.length}
{items}
{headerComponent}
{createItemDialog}
{createItemLabel}
{extraHeaders}
{newObjectProps}
flat={flatHeaders}
{props}
on:more={() => {
if (limit !== undefined) limit += 20
}}
on:collapse={() => {
collapsed = !collapsed
}}
/>
{/if}
<ExpandCollapse isExpanded={!collapsed || dragItemIndex !== undefined} duration={400}>
{#if !lastLevel}
<div class="p-2">
<ListCategories
{elementByIndex}
{docByIndex}
{indexById}
model={itemModels}
index={initIndex + i}
{groupByKey}
selected={isSelected(docObject, $focusStore)}
checked={selectedObjectIdsSet.has(docObject._id)}
on:check={(ev) => dispatch('check', { docs: ev.detail.docs, value: ev.detail.value })}
on:contextmenu={(event) => handleMenuOpened(event, docObject, initIndex + i)}
on:focus={() => {}}
on:mouseover={() => handleRowFocused(docObject)}
docs={items}
{_class}
{space}
{lookup}
{loadingPropsLength}
{baseMenuClass}
{config}
{selectedObjectIds}
{createItemDialog}
{createItemLabel}
{viewOptions}
{newObjectProps}
{flatHeaders}
{props}
level={level + 1}
{initIndex}
{docByIndex}
{viewOptionsConfig}
{listDiv}
bind:dragItem
on:check
on:uncheckAll
on:row-focus
on:dragstart={dragStartHandler}
/>
{/each}
{/if}
{:else if loadingPropsLength !== undefined}
{#each Array(Math.max(loadingPropsLength, limit)) as _, rowIndex}
<div class="listGrid row">
<div class="flex-center clear-mins h-full">
<div class="gridElement">
<CheckBox checked={false} />
<div class="ml-4">
<Spinner size="small" />
</div>
{:else if itemModels}
{#if limited}
{#each limited as docObject, i (docObject._id)}
<ListItem
{docObject}
{elementByIndex}
{docByIndex}
{indexById}
model={itemModels}
index={initIndex + i}
{groupByKey}
selected={isSelected(docObject, $focusStore)}
checked={selectedObjectIdsSet.has(docObject._id)}
on:dragstart={(e) => dragStart(e, docObject, i)}
on:dragenter={(e) => {
if (dragItemIndex !== undefined) {
e.stopPropagation()
e.preventDefault()
}
}}
on:dragleave={(e) => dragItemLeave(e, i)}
on:dragover={(e) => dragover(e, i)}
on:drop={dropItemHandle}
on:check={(ev) => dispatch('check', { docs: ev.detail.docs, value: ev.detail.value })}
on:contextmenu={(event) => handleMenuOpened(event, docObject, initIndex + i)}
on:focus={() => {}}
on:mouseover={() => handleRowFocused(docObject)}
{props}
/>
{/each}
{/if}
{:else if loadingPropsLength !== undefined}
{#each Array(Math.max(loadingPropsLength, limit ?? 0)) as _, rowIndex}
<div class="listGrid row">
<div class="flex-center clear-mins h-full">
<div class="gridElement">
<CheckBox checked={false} />
<div class="ml-4">
<Spinner size="small" />
</div>
</div>
</div>
</div>
</div>
{/each}
{/if}
</ExpandCollapse>
{/each}
{/if}
</ExpandCollapse>
</div>
<style lang="scss">
.row:not(:last-child) {

View File

@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { AnyAttribute, Doc, getObjectValue, Ref } from '@hcengineering/core'
import core, { AnyAttribute, Doc, getObjectValue, Ref } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { getClient, updateAttribute } from '@hcengineering/presentation'
import { CheckBox, Component, deviceOptionsStore as deviceInfo, tooltip } from '@hcengineering/ui'
@ -65,6 +65,14 @@
if (attribute.isLookup) return
return (value: any) => onChange(value, docObject, attribute.key, attr)
}
function joinProps (attribute: AttributeModel, object: Doc, props: Record<string, any>) {
const { listProps, ...clearAttributeProps } = attribute.props as any
if (attribute.attribute?.type._class === core.class.EnumOf) {
return { ...clearAttributeProps, type: attribute.attribute.type, ...props }
}
return { object, ...clearAttributeProps, ...props }
}
</script>
<div
@ -72,10 +80,19 @@
class="listGrid antiList__row row gap-2 flex-grow"
class:checking={checked}
class:mListGridSelected={selected}
draggable={true}
on:contextmenu
on:focus
on:mouseover
on:dragover
on:dragenter
on:dragleave
on:drop
on:dragstart
>
<div class="draggable-container">
<div class="draggable-mark"><Circles /></div>
</div>
<div class="flex-center relative" use:tooltip={{ label: view.string.Select, direction: 'bottom' }}>
<div class="antiList-cells__notifyCell">
<div class="antiList-cells__checkCell">
@ -94,30 +111,27 @@
</div>
</div>
{#each model as attributeModel}
{@const listProps = attributeModel.props?.listProps}
{#if attributeModel.props?.type === 'grow'}
<svelte:component this={attributeModel.presenter} />
{:else if (!groupByKey || attributeModel.props?.excludeByKey !== groupByKey) && !(attributeModel.props?.optional && compactMode)}
{#if attributeModel.props?.fixed}
<FixedColumn key={`list_item_${attributeModel.key}`} justify={attributeModel.props.fixed}>
{:else if (!groupByKey || listProps?.excludeByKey !== groupByKey) && !(listProps?.optional && compactMode)}
{#if listProps?.fixed}
<FixedColumn key={`list_item_${attributeModel.key}`} justify={listProps.fixed}>
<svelte:component
this={attributeModel.presenter}
{...props}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
object={docObject}
onChange={getOnChange(docObject, attributeModel)}
kind={'list'}
{...attributeModel.props}
onChange={getOnChange(docObject, attributeModel)}
{...joinProps(attributeModel, docObject, props)}
/>
</FixedColumn>
{:else}
<svelte:component
this={attributeModel.presenter}
{...props}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
object={docObject}
onChange={getOnChange(docObject, attributeModel)}
kind={'list'}
{...attributeModel.props}
{...joinProps(attributeModel, docObject, props)}
/>
{/if}
{/if}
@ -136,16 +150,15 @@
</div>
<div class="scroll-box gap-2">
{#each model as attributeModel}
{@const listProps = attributeModel.props?.listProps}
{@const value = getObjectValue(attributeModel.key, docObject)}
{#if attributeModel.props?.optional && attributeModel.props?.excludeByKey !== groupByKey && value !== undefined}
{#if listProps?.optional && listProps?.excludeByKey !== groupByKey && value !== undefined}
<svelte:component
this={attributeModel.presenter}
{...props}
value={value ?? ''}
objectId={docObject._id}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
onChange={getOnChange(docObject, attributeModel)}
groupBy={groupByKey}
{...attributeModel.props}
kind={'list'}
{...joinProps(attributeModel, docObject, props)}
/>
{/if}
{/each}
@ -183,6 +196,29 @@
background-color: var(--highlight-hover);
}
.draggable-container {
position: absolute;
left: 0;
display: flex;
align-items: center;
height: 100%;
width: 1.5rem;
cursor: grabbing;
.draggable-mark {
opacity: 0;
width: 0.375rem;
height: 1rem;
margin-left: 0.75rem;
transition: opacity 0.1s;
}
}
&:hover {
.draggable-mark {
opacity: 0.4;
}
}
.hidden-panel,
.panel-trigger {
position: absolute;

View File

@ -421,8 +421,6 @@ export interface AttributeModel {
// Extra properties for component
props?: Record<string, any>
sortingKey: string | string[]
optional?: boolean
excludeByKey?: string
// Extra icon if applicable
icon?: Asset