Use list for sub issues (#2514)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2023-01-18 12:14:59 +07:00 committed by GitHub
parent 1e14668439
commit 28f3e6169c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 706 additions and 518 deletions

3
.gitignore vendored
View File

@ -77,4 +77,5 @@ dist_cache
tsconfig.tsbuildinfo
ingest-attachment-*.zip
tsdoc-metadata.json
pods/front/dist
pods/front/dist
*.cpuprofile

View File

@ -550,6 +550,74 @@ export function createModel (builder: Builder): void {
]
})
const subIssuesOptions: ViewOptionsModel = {
groupBy: ['status', 'assignee', 'priority', 'sprint'],
orderBy: [
['status', SortingOrder.Ascending],
['priority', SortingOrder.Ascending],
['modifiedOn', SortingOrder.Descending],
['dueDate', SortingOrder.Descending],
['rank', SortingOrder.Ascending]
],
other: []
}
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: tracker.class.Issue,
descriptor: view.viewlet.List,
viewOptions: subIssuesOptions,
variant: 'subissue',
config: [
{
key: '',
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.StatusEditor,
props: { kind: 'list', size: 'small', justify: 'center' }
},
{ key: '', presenter: tracker.component.TitlePresenter, props: { shouldUseMargin: true, showParent: false } },
{ key: '', presenter: tracker.component.SubIssuesSelector, props: {} },
{ key: '', presenter: view.component.GrowPresenter, props: { type: 'grow' } },
{ key: '', presenter: tracker.component.DueDatePresenter, props: { kind: 'list' } },
{
key: '',
presenter: tracker.component.SprintEditor,
props: {
kind: 'list',
size: 'small',
shape: 'round',
shouldShowPlaceholder: false,
excludeByKey: 'sprint',
optional: true
}
},
{
key: '',
presenter: tracker.component.EstimationEditor,
props: { kind: 'list', size: 'small', optional: true }
},
{
key: 'modifiedOn',
presenter: tracker.component.ModificationDatePresenter,
props: { fixed: 'right', optional: true }
},
{
key: '$lookup.assignee',
presenter: tracker.component.AssigneePresenter,
props: { issueClass: tracker.class.Issue, defaultClass: contact.class.Employee, shouldShowLabel: false }
}
]
},
tracker.viewlet.SubIssues
)
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: tracker.class.IssueTemplate,
descriptor: view.viewlet.List,

View File

@ -63,8 +63,7 @@ export default mergeIds(viewId, view, {
HTMLEditor: '' as AnyComponent,
MarkupEditor: '' as AnyComponent,
MarkupEditorPopup: '' as AnyComponent,
ListView: '' as AnyComponent,
GrowPresenter: '' as AnyComponent
ListView: '' as AnyComponent
},
string: {
Table: '' as IntlString,

View File

@ -20,6 +20,7 @@ import core from './component'
import { _createMixinProxy, _mixinClass, _toDoc } from './proxy'
import type { Tx, TxCreateDoc, TxMixin, TxRemoveDoc, TxUpdateDoc } from './tx'
import { TxProcessor } from './tx'
import getTypeOf from './typeof'
/**
* @public
@ -463,11 +464,12 @@ export class Hierarchy {
if (typeof obj === 'function') {
return obj
}
const result: any = Array.isArray(obj) ? [] : {}
const isArray = Array.isArray(obj)
const result: any = isArray ? [] : Object.assign({}, obj)
for (const key in obj) {
// include prototype properties
const value = obj[key]
const type = {}.toString.call(value).slice(8, -1)
const type = getTypeOf(value)
if (type === 'Array') {
result[key] = this.clone(value)
} else if (type === 'Object') {
@ -477,7 +479,9 @@ export class Hierarchy {
} else if (type === 'Date') {
result[key] = new Date(value.getTime())
} else {
result[key] = value
if (isArray) {
result[key] = value
}
}
}
return result

View File

@ -0,0 +1,35 @@
const se = typeof Symbol !== 'undefined'
const ste = se && typeof Symbol.toStringTag !== 'undefined'
export default function getTypeOf (obj: any): string {
const typeofObj = typeof obj
if (typeofObj !== 'object') {
return typeofObj
}
if (obj === null) {
return 'null'
}
if (Array.isArray(obj) && (!ste || !(Symbol.toStringTag in obj))) {
return 'Array'
}
const stringTag = ste && obj[Symbol.toStringTag]
if (typeof stringTag === 'string') {
return stringTag
}
const objPrototype = Object.getPrototypeOf(obj)
if (objPrototype === RegExp.prototype) {
return 'RegExp'
}
if (objPrototype === Date.prototype) {
return 'Date'
}
if (objPrototype === null) {
return 'Object'
}
return {}.toString.call(obj).slice(8, -1)
}

View File

@ -14,7 +14,7 @@
// limitations under the License.
//
import { Status, OK, unknownError } from './status'
import { Status, OK, unknownError, Severity } from './status'
/**
* @public
@ -68,7 +68,9 @@ async function broadcastEvent (event: string, data: any): Promise<void> {
* @returns
*/
export async function setPlatformStatus (status: Status): Promise<void> {
// console.log(await translate(status.code, status.params))
if (status.severity === Severity.ERROR) {
console.trace('Platform Error Status', status)
}
return await broadcastEvent(PlatformEvent, status)
}

View File

@ -12,11 +12,26 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts" context="module">
const providers = new Map<string, AvatarProvider | null>()
async function getProvider (client: Client, providerId: Ref<AvatarProvider>): Promise<AvatarProvider | undefined> {
const p = providers.get(providerId)
if (p !== undefined) {
return p ?? undefined
}
const res = await getClient().findOne(contact.class.AvatarProvider, { _id: providerId })
providers.set(providerId, res ?? null)
return res
}
</script>
<script lang="ts">
import contact, { AvatarType, AvatarProvider } from '@hcengineering/contact'
import contact, { AvatarProvider, AvatarType } from '@hcengineering/contact'
import { Client, Ref } from '@hcengineering/core'
import { Asset, getResource } from '@hcengineering/platform'
import { AnySvelteComponent, Icon, IconSize } from '@hcengineering/ui'
import { getBlobURL, getAvatarProviderId, getClient } from '../utils'
import { getAvatarProviderId, getBlobURL, getClient } from '../utils'
import AvatarIcon from './icons/Avatar.svelte'
export let avatar: string | null | undefined = undefined
@ -35,8 +50,7 @@
})
} else if (avatar) {
const avatarProviderId = getAvatarProviderId(avatar)
avatarProvider =
avatarProviderId && (await getClient().findOne(contact.class.AvatarProvider, { _id: avatarProviderId }))
avatarProvider = avatarProviderId && (await getProvider(getClient(), avatarProviderId))
if (!avatarProvider || avatarProvider.type === AvatarType.COLOR) {
url = undefined

View File

@ -19,11 +19,19 @@
export let label: IntlString
export let params: Record<string, any> = {}
$: translation = translate(label, params)
let _value: string | undefined = undefined
$: if (label !== undefined) {
translate(label, params ?? {}).then((r) => {
_value = r
})
} else {
_value = label
}
</script>
{#await translation}
{#if _value}
{_value}
{:else}
{label}
{:then text}
{text}
{/await}
{/if}

View File

@ -23,7 +23,6 @@ export default mergeIds(attachmentId, attachment, {
string: {
NoAttachments: '' as IntlString,
UploadDropFilesHere: '' as IntlString,
Attachments: '' as IntlString,
Photos: '' as IntlString,
FileBrowser: '' as IntlString,
FileBrowserFileCounter: '' as IntlString,

View File

@ -86,6 +86,7 @@ export default plugin(attachmentId, {
FileBrowserTypeFilterAudio: '' as IntlString,
FileBrowserTypeFilterVideos: '' as IntlString,
FileBrowserTypeFilterPDFs: '' as IntlString,
DeleteFile: '' as IntlString
DeleteFile: '' as IntlString,
Attachments: '' as IntlString
}
})

View File

@ -30,7 +30,7 @@
TabList
} from '@hcengineering/ui'
import view, { Viewlet, ViewletPreference } from '@hcengineering/view'
import { FilterButton, ViewletSettingButton } from '@hcengineering/view-resources'
import { FilterButton, getViewOptions, ViewletSettingButton } from '@hcengineering/view-resources'
import calendar from '../plugin'
import { deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
@ -101,6 +101,8 @@
})
$: twoRows = $deviceInfo.twoRows
$: viewOptions = getViewOptions(selectedViewlet)
</script>
<div class="ac-header withSettings" class:full={!twoRows} class:mini={twoRows}>
@ -133,7 +135,7 @@
}}
/>
{/if}
<ViewletSettingButton viewlet={selectedViewlet} />
<ViewletSettingButton bind:viewOptions viewlet={selectedViewlet} />
</div>
</div>
@ -148,6 +150,7 @@
space,
options: selectedViewlet.options,
config: preference?.config ?? selectedViewlet.config,
viewOptions,
viewlet: selectedViewlet,
query: resultQuery,
search,

View File

@ -18,7 +18,13 @@
import { createQuery, getClient } from '@hcengineering/presentation'
import { Button, Icon, IconAdd, Label, Loading, SearchEdit, showPopup } from '@hcengineering/ui'
import view, { Viewlet, ViewletPreference } from '@hcengineering/view'
import { ActionContext, FilterButton, TableBrowser, ViewletSettingButton } from '@hcengineering/view-resources'
import {
ActionContext,
FilterButton,
getViewOptions,
TableBrowser,
ViewletSettingButton
} from '@hcengineering/view-resources'
import contact from '../plugin'
import CreateContact from './CreateContact.svelte'
import { deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
@ -64,6 +70,8 @@
}
$: twoRows = $deviceInfo.twoRows
$: viewOptions = getViewOptions(viewlet)
</script>
<ActionContext
@ -95,7 +103,7 @@
size={'small'}
on:click={(ev) => showCreateDialog(ev)}
/>
<ViewletSettingButton {viewlet} />
<ViewletSettingButton bind:viewOptions {viewlet} />
</div>
</div>

View File

@ -18,7 +18,7 @@
import { createQuery, getClient, UsersPopup, IconMembersOutline } from '@hcengineering/presentation'
import { Button, IconAdd, Label, showPopup, Icon } from '@hcengineering/ui'
import view, { Viewlet, ViewletPreference } from '@hcengineering/view'
import { Table, ViewletSettingButton } from '@hcengineering/view-resources'
import { getViewOptions, Table, ViewletSettingButton } from '@hcengineering/view-resources'
import contact from '../plugin'
export let objectId: Ref<Doc>
@ -90,6 +90,7 @@
}
})
}
$: viewOptions = getViewOptions(descr)
</script>
<div class="antiSection">
@ -101,7 +102,7 @@
<Label label={contact.string.Members} />
</span>
<div class="buttons-group xsmall-gap">
<ViewletSettingButton viewlet={descr} />
<ViewletSettingButton bind:viewOptions viewlet={descr} />
<Button id={contact.string.AddMember} icon={IconAdd} kind={'transparent'} shape={'circle'} on:click={createApp} />
</div>
</div>
@ -118,6 +119,7 @@
<span class="dark-color">
<Label label={contact.string.NoMembers} />
</span>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span class="over-underline content-accent-color" on:click={createApp}>
<Label label={contact.string.AddMember} />
</span>

View File

@ -20,7 +20,13 @@
import { createQuery, getClient } from '@hcengineering/presentation'
import { deviceOptionsStore as deviceInfo, Icon, Label, Loading, SearchEdit } from '@hcengineering/ui'
import view, { Viewlet, ViewletPreference } from '@hcengineering/view'
import { ActionContext, FilterButton, TableBrowser, ViewletSettingButton } from '@hcengineering/view-resources'
import {
ActionContext,
FilterButton,
getViewOptions,
TableBrowser,
ViewletSettingButton
} from '@hcengineering/view-resources'
import document from '../plugin'
export let query: DocumentQuery<Document> = {}
@ -63,6 +69,8 @@
let twoRows: boolean
$: twoRows = $deviceInfo.docWidth <= 680
$: viewOptions = getViewOptions(viewlet)
</script>
<ActionContext
@ -87,7 +95,7 @@
/>
</div>
<div class="ac-header-full" class:secondRow={twoRows}>
<ViewletSettingButton {viewlet} />
<ViewletSettingButton bind:viewOptions {viewlet} />
</div>
</div>

View File

@ -19,7 +19,7 @@
import { createQuery, getClient, UsersPopup } from '@hcengineering/presentation'
import { Button, eventToHTMLElement, IconAdd, Label, Scroller, showPopup } from '@hcengineering/ui'
import view, { Viewlet, ViewletPreference } from '@hcengineering/view'
import { Table, ViewletSettingButton } from '@hcengineering/view-resources'
import { getViewOptions, Table, ViewletSettingButton } from '@hcengineering/view-resources'
import hr from '../plugin'
import { addMember } from '../utils'
@ -91,6 +91,8 @@
}
})
}
$: viewOptions = getViewOptions(descr)
</script>
<div class="antiSection">
@ -99,7 +101,7 @@
<Label label={hr.string.Members} />
</span>
<div class="buttons-group xsmall-gap">
<ViewletSettingButton viewlet={descr} />
<ViewletSettingButton bind:viewOptions viewlet={descr} />
<Button id={hr.string.AddEmployee} icon={IconAdd} kind={'transparent'} shape={'circle'} on:click={add} />
</div>
</div>

View File

@ -20,7 +20,7 @@
import { createQuery, getClient } from '@hcengineering/presentation'
import { Button, Label, Loading, Scroller, tableSP } from '@hcengineering/ui'
import view, { BuildModelKey, Viewlet, ViewletPreference } from '@hcengineering/view'
import { Table, ViewletSettingButton } from '@hcengineering/view-resources'
import { getViewOptions, Table, ViewletSettingButton } from '@hcengineering/view-resources'
import hr from '../../plugin'
import {
EmployeeReports,
@ -215,6 +215,8 @@
}
return result
}
$: viewOptions = getViewOptions(descr)
</script>
{#if departmentStaff.length}
@ -226,7 +228,7 @@
{:else}
<div class="flex-row-center flex-reverse">
<div class="ml-1">
<ViewletSettingButton viewlet={descr} />
<ViewletSettingButton bind:viewOptions viewlet={descr} />
</div>
<Button
label={getEmbeddedLabel('Export')}

View File

@ -22,7 +22,7 @@
import { Vacancy } from '@hcengineering/recruit'
import { FullDescriptionBox } from '@hcengineering/text-editor'
import tracker from '@hcengineering/tracker'
import { Button, Component, EditBox, Grid, Icon, IconAdd, IconMoreH, Label, showPopup } from '@hcengineering/ui'
import { Button, Component, EditBox, Grid, IconMoreH, showPopup } from '@hcengineering/ui'
import { ClassAttributeBar, ContextMenu } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import recruit from '../plugin'
@ -131,33 +131,7 @@
/>
<!-- <MembersBox label={recruit.string.Members} space={object} /> -->
<div class="antiSection">
<div class="antiSection-header">
<div class="antiSection-header__icon">
<Icon icon={tracker.icon.Issue} size={'small'} />
</div>
<span class="antiSection-header__title">
<Label label={recruit.string.RelatedIssues} />
</span>
<div class="buttons-group small-gap">
<Button
id="add-sub-issue"
width="min-content"
icon={IconAdd}
label={undefined}
labelParams={{ subIssues: 0 }}
kind={'transparent'}
size={'small'}
on:click={() => {
showPopup(tracker.component.CreateIssue, { relatedTo: object, space: object.space }, 'top')
}}
/>
</div>
</div>
<div class="flex-row">
<Component is={tracker.component.RelatedIssues} props={{ object }} />
</div>
</div></Grid
>
<Component is={tracker.component.RelatedIssuesSection} props={{ object, label: recruit.string.RelatedIssues }} />
</Grid>
</Panel>
{/if}

View File

@ -18,7 +18,7 @@
import { Vacancy } from '@hcengineering/recruit'
import { Button, Icon, IconAdd, Label, Loading, SearchEdit, showPopup } from '@hcengineering/ui'
import view, { BuildModelKey, Viewlet, ViewletPreference } from '@hcengineering/view'
import { FilterButton, TableBrowser, ViewletSettingButton } from '@hcengineering/view-resources'
import { FilterButton, getViewOptions, TableBrowser, ViewletSettingButton } from '@hcengineering/view-resources'
import recruit from '../plugin'
import CreateVacancy from './CreateVacancy.svelte'
import { deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
@ -132,6 +132,8 @@
}
$: twoRows = $deviceInfo.twoRows
$: viewOptions = getViewOptions(descr)
</script>
<div class="ac-header withSettings" class:full={!twoRows} class:mini={twoRows}>
@ -156,7 +158,7 @@
kind={'primary'}
on:click={showCreateDialog}
/>
<ViewletSettingButton viewlet={descr} />
<ViewletSettingButton bind:viewOptions viewlet={descr} />
</div>
</div>

View File

@ -9,6 +9,7 @@
"build:docs": "api-extractor run --local",
"lint": "svelte-check && eslint",
"lint:fix": "eslint --fix src",
"svelte-check": "svelte-check",
"format": "prettier --write --plugin-search-dir=. src && eslint --fix src"
},
"devDependencies": {

View File

@ -91,6 +91,7 @@
<div class="ap-scroll">
<div class="ap-box">
{#await getItems() then items}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="ap-menuItem flex-row-center"
on:click={() => {

View File

@ -27,7 +27,7 @@
refDocument: Doc,
type: keyof typeof relations,
operation: '$push' | '$pull',
placeholder: IntlString
_: IntlString
) {
const prop = type === 'isBlocking' ? 'blockedBy' : type
if (type !== 'isBlocking') {
@ -69,7 +69,7 @@
await update(value, type, docs, label)
}
const makeAddAction = (type: keyof typeof relations, placeholder: IntlString) => async (props: any, evt: Event) => {
const makeAddAction = (type: keyof typeof relations, placeholder: IntlString) => async () => {
closePopup('popup')
showPopup(
ObjectSearchPopup,
@ -81,7 +81,7 @@
}
)
}
async function removeRelation (evt: MouseEvent) {
async function removeRelation () {
closePopup('popup')
showPopup(
ObjectSearchPopup,

View File

@ -44,14 +44,6 @@
}
}
$: tooltipValue = new Date(value).toLocaleString('default', {
minute: '2-digit',
hour: 'numeric',
day: '2-digit',
month: 'short',
year: 'numeric'
})
$: formatTime(value)
</script>

View File

@ -1,22 +1,22 @@
<script lang="ts">
import { fade } from 'svelte/transition'
import {
NotificationSeverity,
Notification,
Button,
Icon,
IconClose,
IconInfo,
IconCheckCircle,
Label,
showPanel
} from '@hcengineering/ui'
import { copyTextToClipboard, createQuery } from '@hcengineering/presentation'
import { Issue, IssueStatus } from '@hcengineering/tracker'
import {
AnySvelteComponent,
Button,
Icon,
IconCheckCircle,
IconClose,
IconInfo,
Notification,
NotificationSeverity,
showPanel
} from '@hcengineering/ui'
import { fade } from 'svelte/transition'
import IssueStatusIcon from './IssueStatusIcon.svelte'
import IssuePresenter from './IssuePresenter.svelte'
import tracker from '../../plugin'
import IssuePresenter from './IssuePresenter.svelte'
import IssueStatusIcon from './IssueStatusIcon.svelte'
export let notification: Notification
export let onRemove: () => void
@ -31,7 +31,7 @@
$: issueQuery.query(
tracker.class.Issue,
{ _id: params.issueId },
{ _id: params?.issueId },
(res) => {
issue = res[0]
},
@ -49,7 +49,7 @@
)
}
const getIcon = () => {
const getIcon = (): AnySvelteComponent | undefined => {
switch (severity) {
case NotificationSeverity.Success:
return IconCheckCircle
@ -84,14 +84,17 @@
copyTextToClipboard(params?.issueUrl)
}
}
$: icon = getIcon()
</script>
<div class="root" in:fade out:fade>
<Icon icon={getIcon()} size="medium" fill={getIconColor()} />
{#if icon}
<Icon {icon} size="medium" fill={getIconColor()} />
{/if}
<div class="content">
<div class="title">
<Label label={title} />
{title}
</div>
<div class="row">
<div class="issue">
@ -105,7 +108,7 @@
{subTitle}
</div>
<div class="postfix">
{params.subTitlePostfix}
{params?.subTitlePostfix}
</div>
</div>
</div>

View File

@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { WithLookup } from '@hcengineering/core'
import { Ref, WithLookup } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import type { Issue, Team } from '@hcengineering/tracker'
import { showPanel } from '@hcengineering/ui'
@ -23,6 +23,9 @@
export let disableClick = false
export let onClick: (() => void) | undefined = undefined
// Extra properties
export let teams: Map<Ref<Team>, Team> | undefined = undefined
function handleIssueEditorOpened () {
if (disableClick) {
return
@ -38,16 +41,21 @@
const spaceQuery = createQuery()
let currentTeam: Team | undefined = value?.$lookup?.space
$: if (value && value?.$lookup?.space === undefined) {
spaceQuery.query(tracker.class.Team, { _id: value.space }, (res) => ([currentTeam] = res))
$: if (teams === undefined) {
if (value && value?.$lookup?.space === undefined) {
spaceQuery.query(tracker.class.Team, { _id: value.space }, (res) => ([currentTeam] = res))
} else {
spaceQuery.unsubscribe()
}
} else {
spaceQuery.unsubscribe()
currentTeam = teams.get(value.space)
}
$: title = currentTeam ? `${currentTeam.identifier}-${value?.number}` : `${value?.number}`
</script>
{#if value}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span
class="issuePresenterRoot"
class:noPointer={disableClick}

View File

@ -48,7 +48,7 @@
async function updateStatus (
txes: Tx[],
statuses: Map<Ref<IssueStatus>, WithLookup<IssueStatus>>,
now: number
_: number
): Promise<void> {
const result: WithTime[] = []

View File

@ -24,6 +24,8 @@
export let size: IconSize
export let fill: string | undefined = undefined
export let issueStatuses: IssueStatus[] | undefined = undefined
const dynamicFillCategories = [tracker.issueStatusCategory.Started]
const client = getClient()
@ -36,12 +38,19 @@
} = { index: undefined, count: undefined }
const categoriesQuery = createQuery()
categoriesQuery.query(
tracker.class.IssueStatus,
{ category: tracker.issueStatusCategory.Started },
(res) => (statuses = res),
{ sort: { rank: SortingOrder.Ascending } }
)
$: if (issueStatuses === undefined) {
categoriesQuery.query(
tracker.class.IssueStatus,
{ category: tracker.issueStatusCategory.Started },
(res) => (statuses = res),
{ sort: { rank: SortingOrder.Ascending } }
)
} else {
const _s = [...issueStatuses.filter((it) => it.category === tracker.issueStatusCategory.Started)]
_s.sort((a, b) => a.rank.localeCompare(b.rank))
categoriesQuery.unsubscribe()
}
async function updateCategory (status: WithLookup<IssueStatus>, statuses: IssueStatus[]) {
if (status.$lookup?.category) {

View File

@ -1,8 +1,8 @@
<script lang="ts">
import { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import { Issue } from '@hcengineering/tracker'
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { Component } from '@hcengineering/ui'
import { Viewlet } from '@hcengineering/view'
import { Viewlet, ViewOptions } from '@hcengineering/view'
import tracker from '../../plugin'
import CreateIssue from '../CreateIssue.svelte'
@ -10,6 +10,11 @@
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
const createItemLabel = tracker.string.AddIssueTooltip
</script>
@ -24,9 +29,11 @@
createItemDialog,
createItemLabel,
viewlet,
viewOptions: viewlet.viewOptions?.other,
viewOptions,
viewOptionsConfig: viewlet.viewOptions?.other,
space,
query
query,
props: { teams, issueStatuses }
}}
/>
{/if}

View File

@ -45,7 +45,7 @@
$: groupedByProject = getGroupedIssues('project', issues)
$: groupedBySprint = getGroupedIssues('sprint', issues)
const handleStatusFilterMenuSectionOpened = (event: MouseEvent | KeyboardEvent) => {
const handleStatusFilterMenuSectionOpened = () => {
const statusGroups: { [key: string]: number } = {}
for (const status of defaultStatuses) {
@ -66,7 +66,7 @@
)
}
const handlePriorityFilterMenuSectionOpened = (event: MouseEvent | KeyboardEvent) => {
const handlePriorityFilterMenuSectionOpened = () => {
const priorityGroups: { [key: string]: number } = {}
for (const priority of defaultPriorities) {
@ -86,7 +86,7 @@
)
}
const handleProjectFilterMenuSectionOpened = (event: MouseEvent | KeyboardEvent) => {
const handleProjectFilterMenuSectionOpened = () => {
const projectGroups: { [key: string]: number } = {}
for (const [project, value] of Object.entries(groupedByProject)) {
@ -105,7 +105,7 @@
)
}
const handleSprintFilterMenuSectionOpened = (event: MouseEvent | KeyboardEvent) => {
const handleSprintFilterMenuSectionOpened = () => {
const sprintGroups: { [key: string]: number } = {}
for (const [project, value] of Object.entries(groupedBySprint)) {

View File

@ -1,11 +1,11 @@
<script lang="ts">
import { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import { DocumentQuery, Ref, SortingOrder, Space, WithLookup } from '@hcengineering/core'
import { IntlString, translate } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { Issue } from '@hcengineering/tracker'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { Button, IconDetails, IconDetailsFilled } from '@hcengineering/ui'
import view, { Viewlet } from '@hcengineering/view'
import { FilterBar, getActiveViewletId } from '@hcengineering/view-resources'
import { FilterBar, getActiveViewletId, getViewOptions } from '@hcengineering/view-resources'
import ViewletSettingButton from '@hcengineering/view-resources/src/components/ViewletSettingButton.svelte'
import tracker from '../../plugin'
import IssuesContent from './IssuesContent.svelte'
@ -36,7 +36,7 @@
async function update (): Promise<void> {
viewlets = await client.findAll(
view.class.Viewlet,
{ attachTo: tracker.class.Issue },
{ attachTo: tracker.class.Issue, variant: { $ne: 'subissue' } },
{
lookup: {
descriptor: view.class.ViewletDescriptor
@ -63,6 +63,41 @@
let docSize: boolean = false
$: if (docWidth <= 900 && !docSize) docSize = true
$: if (docWidth > 900 && docSize) docSize = false
const teamQuery = createQuery()
let _teams: Map<Ref<Team>, Team> | undefined = undefined
let _result: any
$: teamQuery.query(tracker.class.Team, {}, (result) => {
_result = JSON.stringify(result, undefined, 2)
console.log('#RESULT 124', _result)
const t = new Map<Ref<Team>, Team>()
for (const r of result) {
t.set(r._id, r)
}
_teams = t
})
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>
<IssuesHeader {viewlets} {label} bind:viewlet bind:search showLabelSelector={$$slots.label_selector}>
@ -71,7 +106,7 @@
</svelte:fragment>
<svelte:fragment slot="extra">
{#if viewlet}
<ViewletSettingButton {viewlet} />
<ViewletSettingButton bind:viewOptions {viewlet} />
{/if}
{#if asideFloat && $$slots.aside}
<div class="buttons-divider" />
@ -90,8 +125,8 @@
<slot name="afterHeader" />
<FilterBar _class={tracker.class.Issue} query={searchQuery} on:change={(e) => (resultQuery = e.detail)} />
<div class="flex w-full h-full clear-mins">
{#if viewlet}
<IssuesContent {viewlet} query={resultQuery} {space} />
{#if viewlet && _teams && issueStatuses}
<IssuesContent {viewlet} query={resultQuery} {space} teams={_teams} {issueStatuses} {viewOptions} />
{/if}
{#if $$slots.aside !== undefined && asideShown}
<div class="popupPanel-body__aside flex" class:float={asideFloat} class:shown={asideShown}>

View File

@ -37,8 +37,7 @@
ListSelectionProvider,
noCategory,
SelectDirection,
selectionStore,
viewOptionsStore
selectionStore
} from '@hcengineering/view-resources'
import ActionContext from '@hcengineering/view-resources/src/components/ActionContext.svelte'
import Menu from '@hcengineering/view-resources/src/components/Menu.svelte'
@ -59,11 +58,12 @@
export let space: Ref<Team> | undefined = undefined
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
export let query: DocumentQuery<Issue> = {}
export let viewOptions: ViewOptionModel[] | undefined
export let viewOptionsConfig: ViewOptionModel[] | undefined
export let viewOptions: ViewOptions
$: currentSpace = space || tracker.team.DefaultTeam
$: groupBy = ($viewOptionsStore.groupBy ?? noCategory) as IssuesGrouping
$: orderBy = $viewOptionsStore.orderBy
$: groupBy = (viewOptions.groupBy ?? noCategory) as IssuesGrouping
$: orderBy = viewOptions.orderBy
$: sort = { [orderBy[0]]: orderBy[1] }
$: dontUpdateRank = orderBy[0] !== IssuesOrdering.Manual
@ -76,7 +76,7 @@
})
let resultQuery: DocumentQuery<any> = query
$: getResultQuery(query, viewOptions, $viewOptionsStore).then((p) => (resultQuery = p))
$: getResultQuery(query, viewOptionsConfig, viewOptions).then((p) => (resultQuery = p))
const client = getClient()
const hierarchy = client.getHierarchy()
@ -188,6 +188,7 @@
mode: 'browser'
}}
/>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<Kanban
bind:this={kanbanUI}
_class={tracker.class.Issue}

View File

@ -38,6 +38,7 @@
<Spinner size="small" />
{/if}
<span class="overflow-label issue-title">{issue.title}</span>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="button-close"
use:tooltip={{ label: tracker.string.RemoveParent, direction: 'bottom' }}

View File

@ -31,6 +31,7 @@
<div class="root" style:max-width={maxWidth}>
<span class="names">
{#each value.parents as parentInfo}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span class="name cursor-pointer" on:click={() => handleIssueEditorOpened(parentInfo)}
>{parentInfo.parentTitle}</span
>

View File

@ -73,6 +73,7 @@
{#if value}
{#if kind === 'list' || kind === 'list-header'}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="priority-container" on:click={handlePriorityEditorOpened}>
<div class="icon">
{#if issuePriorities[value.priority]?.icon}<Icon icon={issuePriorities[value.priority]?.icon} {size} />{/if}

View File

@ -15,7 +15,7 @@
<script lang="ts">
import { AttachedData, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Issue, IssueStatus } from '@hcengineering/tracker'
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import type { ButtonKind, ButtonSize } from '@hcengineering/ui'
import { Button, eventToHTMLElement, SelectPopup, showPopup, TooltipAlignment } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
@ -34,6 +34,9 @@
export let justify: 'left' | 'center' = 'left'
export let width: string | undefined = undefined
// Extra properties
export let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]> | undefined = undefined
const client = getClient()
const statusesQuery = createQuery()
const dispatch = createEventDispatcher()
@ -65,7 +68,7 @@
$: selectedStatus = statuses?.find((status) => status._id === value.status) ?? statuses?.[0]
$: selectedStatusLabel = shouldShowLabel ? selectedStatus?.name : undefined
$: statusesInfo = statuses?.map((s, i) => {
$: statusesInfo = statuses?.map((s) => {
return {
id: s._id,
component: StatusPresenter,
@ -74,18 +77,23 @@
}
})
$: if (!statuses) {
const query = '_id' in value ? { attachedTo: value.space } : {}
statusesQuery.query(
tracker.class.IssueStatus,
query,
(result) => {
statuses = result
},
{
lookup: { category: tracker.class.IssueStatusCategory },
sort: { rank: SortingOrder.Ascending }
}
)
statuses = '_id' in value ? issueStatuses?.get(value.space) : undefined
if (statuses === undefined) {
const query = '_id' in value ? { attachedTo: value.space } : {}
statusesQuery.query(
tracker.class.IssueStatus,
query,
(result) => {
statuses = result
},
{
lookup: { category: tracker.class.IssueStatusCategory },
sort: { rank: SortingOrder.Ascending }
}
)
} else {
statusesQuery.unsubscribe()
}
}
$: smallgap = size === 'inline' || size === 'small'
</script>
@ -95,7 +103,11 @@
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="flex-row-center flex-no-shrink" class:cursor-pointer={isEditable} on:click={handleStatusEditorOpened}>
<div class="flex-center flex-no-shrink square-4">
{#if selectedStatus}<IssueStatusIcon value={selectedStatus} size={kind === 'list' ? 'inline' : 'medium'} />{/if}
{#if selectedStatus}<IssueStatusIcon
value={selectedStatus}
issueStatuses={statuses}
size={kind === 'list' ? 'inline' : 'medium'}
/>{/if}
</div>
{#if selectedStatusLabel}
<span

View File

@ -14,12 +14,13 @@
-->
<script lang="ts">
import type { Issue } from '@hcengineering/tracker'
import ParentNamesPresenter from './ParentNamesPresenter.svelte'
import tracker from '../../plugin'
import { showPanel } from '@hcengineering/ui'
import tracker from '../../plugin'
import ParentNamesPresenter from './ParentNamesPresenter.svelte'
export let value: Issue
export let shouldUseMargin: boolean = false
export let showParent = true
function handleIssueEditorOpened () {
showPanel(tracker.component.EditIssue, value._id, value._class, 'content')
@ -28,12 +29,15 @@
{#if value}
<span class="titlePresenter-container" class:with-margin={shouldUseMargin} title={value.title}>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span
class="name overflow-label cursor-pointer"
style={`max-width: ${value.parents.length !== 0 ? 95 : 100}%`}
style:max-width={showParent ? `${value.parents.length !== 0 ? 95 : 100}%` : '100%'}
on:click={handleIssueEditorOpened}>{value.title}</span
>
<ParentNamesPresenter {value} />
{#if showParent}
<ParentNamesPresenter {value} />
{/if}
</span>
{/if}

View File

@ -241,6 +241,7 @@
<span class="title select-text">{title}</span>
<div class="mt-6 description-preview select-text">
{#if isDescriptionEmpty}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="placeholder" on:click={edit}>
<Label label={tracker.string.IssueDescriptionPlaceholder} />
</div>

View File

@ -13,65 +13,20 @@
// limitations under the License.
-->
<script lang="ts">
import { Ref, WithLookup } from '@hcengineering/core'
import { DocumentQuery, Ref, WithLookup } from '@hcengineering/core'
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { getEventPositionElement, showPanel, showPopup } from '@hcengineering/ui'
import { ActionContext, ContextMenu, FixedColumn } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import { flip } from 'svelte/animate'
import { getIssueId } from '../../../issues'
import { Viewlet, ViewOptions } from '@hcengineering/view'
import { ActionContext, List } from '@hcengineering/view-resources'
import tracker from '../../../plugin'
import { subIssueListProvider } from '../../../utils'
import Circles from '../../icons/Circles.svelte'
import AssigneeEditor from '../AssigneeEditor.svelte'
import DueDateEditor from '../DueDateEditor.svelte'
import PriorityEditor from '../PriorityEditor.svelte'
import StatusEditor from '../StatusEditor.svelte'
import EstimationEditor from '../timereport/EstimationEditor.svelte'
import SubIssuesSelector from './SubIssuesSelector.svelte'
export let issues: Issue[]
export let query: DocumentQuery<Issue> | undefined = undefined
export let issues: Issue[] | undefined = undefined
export let viewlet: Viewlet
export let viewOptions: ViewOptions
export let teams: Map<Ref<Team>, Team>
// Extra properties
export let teams: Map<Ref<Team>, Team> | undefined
export let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]>
const dispatch = createEventDispatcher()
let draggingIndex: number | null = null
let hoveringIndex: number | null = null
function openIssue (target: Issue) {
dispatch('issue-focus', target)
subIssueListProvider(issues, target._id)
showPanel(tracker.component.EditIssue, target._id, target._class, 'content')
}
function resetDrag () {
draggingIndex = null
hoveringIndex = null
}
function handleDragStart (ev: DragEvent, index: number) {
if (ev.dataTransfer) {
ev.dataTransfer.effectAllowed = 'move'
ev.dataTransfer.dropEffect = 'move'
draggingIndex = index
}
}
function handleDrop (ev: DragEvent, toIndex: number) {
if (ev.dataTransfer && draggingIndex !== null && toIndex !== draggingIndex) {
ev.dataTransfer.dropEffect = 'move'
dispatch('move', { fromIndex: draggingIndex, toIndex })
}
resetDrag()
}
function showContextMenu (ev: MouseEvent, object: Issue) {
showPopup(ContextMenu, { object }, getEventPositionElement(ev))
}
</script>
<ActionContext
@ -80,136 +35,15 @@
}}
/>
{#each issues as issue, index (issue._id)}
{@const currentTeam = teams.get(issue.space)}
{@const openIssueCall = () => openIssue(issue)}
<div
class="flex-between row"
class:is-dragging={index === draggingIndex}
class:is-dragged-over-up={draggingIndex !== null && index < draggingIndex && index === hoveringIndex}
class:is-dragged-over-down={draggingIndex !== null && index > draggingIndex && index === hoveringIndex}
animate:flip={{ duration: 400 }}
draggable={true}
on:click|self={openIssueCall}
on:contextmenu|preventDefault={(ev) => showContextMenu(ev, issue)}
on:dragstart={(ev) => handleDragStart(ev, index)}
on:dragover|preventDefault={() => false}
on:dragenter={() => (hoveringIndex = index)}
on:drop|preventDefault={(ev) => handleDrop(ev, index)}
on:dragend={resetDrag}
>
<div class="draggable-container">
<div class="draggable-mark"><Circles /></div>
</div>
<div class="flex-row-center ml-6 clear-mins gap-2">
<PriorityEditor value={issue} isEditable kind={'list'} size={'small'} justify={'center'} />
<span class="issuePresenter" on:click={openIssueCall}>
<FixedColumn key={'subissue_issue'} justify={'left'}>
{#if currentTeam}
{getIssueId(currentTeam, issue)}
{/if}
</FixedColumn>
</span>
<StatusEditor
value={issue}
statuses={issueStatuses.get(issue.space)}
justify="center"
kind={'list'}
size={'small'}
tooltipAlignment="bottom"
/>
<span class="text name" title={issue.title} on:click={openIssueCall}>
{issue.title}
</span>
{#if issue.subIssues > 0}
<SubIssuesSelector value={issue} {currentTeam} />
{/if}
</div>
<div class="flex-center flex-no-shrink">
<EstimationEditor value={issue} kind={'list'} />
{#if issue.dueDate !== null}
<DueDateEditor value={issue} />
{/if}
<AssigneeEditor value={issue} />
</div>
</div>
{/each}
<style lang="scss">
.row {
position: relative;
border-bottom: 1px solid var(--divider-color);
.text {
font-weight: 500;
color: var(--caption-color);
}
.issuePresenter {
flex-shrink: 0;
min-width: 0;
min-height: 0;
font-weight: 500;
color: var(--content-color);
cursor: pointer;
&:hover {
color: var(--caption-color);
text-decoration: underline;
}
&:active {
color: var(--accent-color);
}
}
.name {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.draggable-container {
position: absolute;
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;
}
}
&.is-dragging::before {
position: absolute;
content: '';
background-color: var(--theme-bg-color);
opacity: 0.4;
inset: 0;
}
&.is-dragged-over-up::before {
position: absolute;
content: '';
inset: 0;
border-top: 1px solid var(--theme-bg-check);
}
&.is-dragged-over-down::before {
position: absolute;
content: '';
inset: 0;
border-bottom: 1px solid var(--theme-bg-check);
}
}
</style>
{#if viewlet}
<List
_class={tracker.class.Issue}
{viewOptions}
viewOptionsConfig={viewlet.viewOptions?.other}
config={viewlet.config}
documents={issues}
{query}
flatHeaders={true}
props={{ teams, issueStatuses }}
/>
{/if}

View File

@ -92,7 +92,7 @@
$: areSubIssuesLoading = !subIssues
$: parentIssue = issue.$lookup?.attachedTo ? (issue.$lookup?.attachedTo as Issue) : null
$: if (parentIssue) {
$: if (parentIssue && parentIssue.subIssues > 0) {
subIssuesQeury.query(
tracker.class.Issue,
{ space: issue.space, attachedTo: parentIssue._id },
@ -113,6 +113,7 @@
{#if parentIssue}
<div class="flex root">
<div class="item clear-mins">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="flex-center parent-issue cursor-pointer"
use:tooltip={{ label: tracker.string.OpenParent, direction: 'bottom' }}
@ -136,6 +137,7 @@
<Spinner size="small" />
</div>
{:else}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
bind:this={subIssuesElement}
class="flex-center sub-issues cursor-pointer"

View File

@ -14,9 +14,11 @@
-->
<script lang="ts">
import { Ref, SortingOrder, WithLookup } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { calcRank, Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { Button, Spinner, ExpandCollapse, closeTooltip, IconAdd, Chevron, Label } from '@hcengineering/ui'
import { createQuery } from '@hcengineering/presentation'
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { Button, Chevron, closeTooltip, ExpandCollapse, IconAdd, Label } from '@hcengineering/ui'
import view, { Viewlet } from '@hcengineering/view'
import { getViewOptions, ViewletSettingButton } from '@hcengineering/view-resources'
import tracker from '../../../plugin'
import CreateSubIssue from './CreateSubIssue.svelte'
import SubIssueList from './SubIssueList.svelte'
@ -25,35 +27,54 @@
export let teams: Map<Ref<Team>, Team>
export let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]>
const subIssuesQuery = createQuery()
const client = getClient()
let subIssues: Issue[] | undefined
let isCollapsed = false
let isCreating = false
async function handleIssueSwap (ev: CustomEvent<{ fromIndex: number; toIndex: number }>) {
if (subIssues) {
const { fromIndex, toIndex } = ev.detail
const [prev, next] = [
subIssues[fromIndex < toIndex ? toIndex : toIndex - 1],
subIssues[fromIndex < toIndex ? toIndex + 1 : toIndex]
]
const issue = subIssues[fromIndex]
$: hasSubIssues = issue.subIssues > 0
await client.update(issue, { rank: calcRank(prev, next) })
}
let viewlet: Viewlet | undefined
const query = createQuery()
$: query.query(view.class.Viewlet, { _id: tracker.viewlet.SubIssues }, (res) => {
;[viewlet] = res
})
let _teams = teams
let _issueStatuses = issueStatuses
const teamsQuery = createQuery()
$: if (teams === undefined) {
teamsQuery.query(tracker.class.Team, {}, async (result) => {
_teams = new Map(result.map((it) => [it._id, it]))
})
} else {
teamsQuery.unsubscribe()
}
$: hasSubIssues = issue.subIssues > 0
$: subIssuesQuery.query(tracker.class.Issue, { attachedTo: issue._id }, async (result) => (subIssues = result), {
sort: { rank: SortingOrder.Ascending },
lookup: {
_id: {
subIssues: tracker.class.Issue
const statusesQuery = createQuery()
$: if (issueStatuses === undefined) {
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 }
}
}
})
)
} else {
statusesQuery.unsubscribe()
}
$: viewOptions = viewlet !== undefined ? getViewOptions(viewlet) : undefined
</script>
<div class="flex-between">
@ -73,38 +94,42 @@
</svelte:fragment>
</Button>
{/if}
<Button
id="add-sub-issue"
width="min-content"
icon={hasSubIssues ? IconAdd : undefined}
label={hasSubIssues ? undefined : tracker.string.AddSubIssues}
labelParams={{ subIssues: 0 }}
kind={'transparent'}
size={'small'}
showTooltip={{ label: tracker.string.AddSubIssues, props: { subIssues: 1 }, direction: 'bottom' }}
on:click={() => {
closeTooltip()
isCreating = true
isCollapsed = false
}}
/>
<div class="flex-row-center">
{#if viewlet && hasSubIssues && viewOptions}
<ViewletSettingButton bind:viewOptions {viewlet} kind={'transparent'} />
{/if}
<Button
id="add-sub-issue"
width="min-content"
icon={hasSubIssues ? IconAdd : undefined}
label={hasSubIssues ? undefined : tracker.string.AddSubIssues}
labelParams={{ subIssues: 0 }}
kind={'transparent'}
size={'small'}
showTooltip={{ label: tracker.string.AddSubIssues, props: { subIssues: 1 }, direction: 'bottom' }}
on:click={() => {
closeTooltip()
isCreating = true
isCollapsed = false
}}
/>
</div>
</div>
<div class="mt-1">
{#if subIssues && issueStatuses}
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
{#if hasSubIssues}
{#if issueStatuses}
{#if hasSubIssues && viewOptions && viewlet}
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
<div class="list" class:collapsed={isCollapsed}>
<SubIssueList
issues={subIssues}
{issueStatuses}
{teams}
on:issue-focus={() => (isCreating = false)}
on:move={handleIssueSwap}
teams={_teams}
{viewlet}
{viewOptions}
issueStatuses={_issueStatuses}
query={{ attachedTo: issue._id }}
/>
</div>
{/if}
</ExpandCollapse>
</ExpandCollapse>
{/if}
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
{#if isCreating}
{@const team = teams.get(issue.space)}
@ -121,10 +146,6 @@
{/if}
{/if}
</ExpandCollapse>
{:else}
<div class="flex-center pt-3">
<Spinner />
</div>
{/if}
</div>

View File

@ -17,10 +17,13 @@
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
import { calcRank, Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { Label, Spinner } from '@hcengineering/ui'
import { Viewlet, ViewOptions } from '@hcengineering/view'
import tracker from '../../../plugin'
import SubIssueList from '../edit/SubIssueList.svelte'
export let object: Doc
export let viewlet: Viewlet
export let viewOptions: ViewOptions
let query: DocumentQuery<Issue>
$: query = { 'relations._id': object._id, 'relations._class': object._class }
@ -75,9 +78,9 @@
</script>
<div class="mt-1">
{#if subIssues !== undefined}
{#if subIssues !== undefined && viewlet !== undefined}
{#if issueStatuses.size > 0 && teams}
<SubIssueList issues={subIssues} {teams} {issueStatuses} on:move={handleIssueSwap} />
<SubIssueList bind:viewOptions {viewlet} issues={subIssues} {teams} {issueStatuses} on:move={handleIssueSwap} />
{:else}
<div class="p-1">
<Label label={presentation.string.NoMatchesFound} />

View File

@ -0,0 +1,54 @@
<script lang="ts">
import { Doc } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import { Button, Icon, IconAdd, Label, showPopup } from '@hcengineering/ui'
import view, { Viewlet } from '@hcengineering/view'
import { getViewOptions, ViewletSettingButton } from '@hcengineering/view-resources'
import tracker from '../../../plugin'
import RelatedIssues from './RelatedIssues.svelte'
export let object: Doc
export let label: IntlString
let viewlet: Viewlet | undefined
const vquery = createQuery()
$: vquery.query(view.class.Viewlet, { _id: tracker.viewlet.SubIssues }, (res) => {
;[viewlet] = res
})
let viewOptions = getViewOptions(viewlet)
</script>
<div class="antiSection">
<div class="antiSection-header">
<div class="antiSection-header__icon">
<Icon icon={tracker.icon.Issue} size={'small'} />
</div>
<span class="antiSection-header__title">
<Label {label} />
</span>
<div class="buttons-group small-gap">
{#if viewlet && viewOptions}
<ViewletSettingButton bind:viewOptions {viewlet} kind={'transparent'} />
{/if}
<Button
id="add-sub-issue"
width="min-content"
icon={IconAdd}
label={undefined}
labelParams={{ subIssues: 0 }}
kind={'transparent'}
size={'small'}
on:click={() => {
showPopup(tracker.component.CreateIssue, { relatedTo: object, space: object.space }, 'top')
}}
/>
</div>
</div>
<div class="flex-row">
{#if viewlet}
<RelatedIssues {object} {viewOptions} {viewlet} />
{/if}
</div>
</div>

View File

@ -139,7 +139,7 @@
<Button
icon={IconAdd}
size={'small'}
on:click={(event) => {
on:click={() => {
showPopup(
TimeSpendReportPopup,
{

View File

@ -14,11 +14,11 @@
-->
<script lang="ts">
import contact from '@hcengineering/contact'
import { Doc, Ref } from '@hcengineering/core'
import { Ref } from '@hcengineering/core'
import { UserBox } from '@hcengineering/presentation'
import { Issue, Team } from '@hcengineering/tracker'
import { getEventPositionElement, ListView, showPopup, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
import { ContextMenu, FixedColumn, ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
import { deviceOptionsStore as deviceInfo, getEventPositionElement, ListView, showPopup } from '@hcengineering/ui'
import { ContextMenu, FixedColumn, ListSelectionProvider } from '@hcengineering/view-resources'
import { getIssueId } from '../../../issues'
import tracker from '../../../plugin'
import EstimationEditor from './EstimationEditor.svelte'
@ -31,7 +31,7 @@
showPopup(ContextMenu, { object }, $deviceInfo.isMobile ? 'top' : getEventPositionElement(ev))
}
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {})
const listProvider = new ListSelectionProvider(() => {})
$: twoRows = $deviceInfo.twoRows
</script>

View File

@ -53,6 +53,7 @@
</script>
{#if kind === 'link'}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div id="ReportedTimeEditor" class="link-container flex-between" on:click={showReports}>
{#if value !== undefined}
<span class="overflow-label">

View File

@ -23,6 +23,7 @@
export let value: number
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span
{id}
class:link={kind === 'link'}

View File

@ -14,13 +14,18 @@
-->
<script lang="ts">
import contact from '@hcengineering/contact'
import { Doc, Ref, Space, WithLookup } from '@hcengineering/core'
import { Ref, Space, WithLookup } from '@hcengineering/core'
import UserBox from '@hcengineering/presentation/src/components/UserBox.svelte'
import { Team, TimeReportDayType, TimeSpendReport } from '@hcengineering/tracker'
import { eventToHTMLElement, getEventPositionElement, ListView, showPopup } from '@hcengineering/ui'
import { deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
import {
deviceOptionsStore as deviceInfo,
eventToHTMLElement,
getEventPositionElement,
ListView,
showPopup
} from '@hcengineering/ui'
import DatePresenter from '@hcengineering/ui/src/components/calendar/DatePresenter.svelte'
import { ContextMenu, FixedColumn, ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
import { ContextMenu, FixedColumn, ListSelectionProvider } from '@hcengineering/view-resources'
import { getIssueId } from '../../../issues'
import tracker from '../../../plugin'
import TimePresenter from './TimePresenter.svelte'
@ -34,7 +39,7 @@
showPopup(ContextMenu, { object }, getEventPositionElement(ev))
}
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {})
const listProvider = new ListSelectionProvider(() => {})
const toTeamId = (ref: Ref<Space>) => ref as Ref<Team>

View File

@ -25,9 +25,9 @@
import ModeSelector from '../ModeSelector.svelte'
const config: [string, IntlString, object][] = [
['assigned', tracker.string.Assigned],
['assigned', tracker.string.Assigned, {}],
['created', tracker.string.Created, { value: 0 }],
['subscribed', tracker.string.Subscribed]
['subscribed', tracker.string.Subscribed, {}]
]
const currentUser = getCurrentAccount() as EmployeeAccount
const assigned = { assignee: currentUser.employee }

View File

@ -86,12 +86,3 @@
<DatePresenter bind:value={object.targetDate} labelNull={tracker.string.TargetDate} editable />
</svelte:fragment>
</Card>
<style>
.label {
margin-bottom: 10px;
}
.description {
margin-bottom: 10px;
}
</style>

View File

@ -27,6 +27,7 @@
</script>
{#if value}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="flex-presenter flex-grow" on:click={navigateToProject}>
<span title={value.label} class="projectLabel flex-grow">{value.label}</span>
</div>

View File

@ -18,8 +18,8 @@
import { getClient } from '@hcengineering/presentation'
import { Issue, Project } from '@hcengineering/tracker'
import { CheckBox, Spinner, tooltip } from '@hcengineering/ui'
import { AttributeModel, BuildModelKey } from '@hcengineering/view'
import { buildModel, getObjectPresenter, LoadingProps } from '@hcengineering/view-resources'
import { BuildModelKey } from '@hcengineering/view'
import { buildModel, LoadingProps } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import tracker from '../../plugin'
@ -42,16 +42,10 @@
}
}
let personPresenter: AttributeModel
$: options = { ...baseOptions } as FindOptions<Project>
$: selectedObjectIdsSet = new Set<Ref<Doc>>(selectedObjectIds.map((it) => it._id))
$: objectRefs.length = projects?.length ?? 0
$: getObjectPresenter(client, contact.class.Person, { key: '' }).then((p) => {
personPresenter = p
})
export const onObjectChecked = (docs: Doc[], value: boolean) => {
dispatch('check', { docs, value })
}

View File

@ -22,7 +22,8 @@
await client.update(sprint, { [field]: value })
}
let container: HTMLElement
function selectSprint (evt: MouseEvent): void {
function selectSprint (): void {
showPopup(SprintPopup, { _class: tracker.class.Sprint }, container, (value) => {
if (value != null) {
sprint = value

View File

@ -104,12 +104,3 @@
/>
</svelte:fragment>
</Card>
<style>
.label {
margin-bottom: 10px;
}
.description {
margin-bottom: 10px;
}
</style>

View File

@ -123,9 +123,6 @@
</div>
<style lang="scss">
.showWarning {
color: var(--warning-color) !important;
}
.minus-margin {
margin-left: -0.5rem;
&-vSpace {

View File

@ -119,6 +119,7 @@
<div class="listRoot">
{#if sprints}
{#each Array.from(byProject?.entries() ?? []) as e}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="flex-between categoryHeader row" on:click={() => handleCollapseCategory(e[0])}>
<div class="flex-row-center gap-2 clear-mins">
<SprintProjectEditor

View File

@ -27,6 +27,7 @@
</script>
{#if value}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="flex-presenter flex-grow" on:click={navigateToSprint}>
<span title={value.label} class="projectLabel flex-grow">{value.label}</span>
</div>

View File

@ -82,10 +82,6 @@
resetDrag()
}
function showContextMenu (ev: MouseEvent, object: IssueTemplateChild) {
// showPopup(ContextMenu, { object }, getEventPositionElement(ev))
}
export function getIssueTemplateId (team: string, issue: IssueTemplateChild): string {
return `${team}-${issues.findIndex((it) => it.id === issue.id)}`
}
@ -107,7 +103,6 @@
animate:flip={{ duration: 400 }}
draggable={true}
on:click|self={(evt) => openIssue(evt, issue)}
on:contextmenu|preventDefault={(ev) => showContextMenu(ev, issue)}
on:dragstart={(ev) => handleDragStart(ev, index)}
on:dragover|preventDefault={() => false}
on:dragenter={() => (hoveringIndex = index)}

View File

@ -33,6 +33,7 @@
</script>
{#if value}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span class="issuePresenterRoot flex" class:noPointer={disableClick} on:click={handleIssueEditorOpened}>
<Icon icon={tracker.icon.Issues} size={'small'} />
<span class="ml-2">

View File

@ -4,11 +4,10 @@
import { getClient } from '@hcengineering/presentation'
import { IssueTemplate } from '@hcengineering/tracker'
import { Button, IconAdd, IconDetails, IconDetailsFilled, showPopup } from '@hcengineering/ui'
import view, { Viewlet, ViewOptionModel } from '@hcengineering/view'
import { FilterBar, getActiveViewletId } from '@hcengineering/view-resources'
import view, { Viewlet } from '@hcengineering/view'
import { FilterBar, getActiveViewletId, getViewOptions } from '@hcengineering/view-resources'
import ViewletSettingButton from '@hcengineering/view-resources/src/components/ViewletSettingButton.svelte'
import tracker from '../../plugin'
import { getDefaultViewOptionsTemplatesConfig } from '../../utils'
import IssuesHeader from '../issues/IssuesHeader.svelte'
import CreateIssueTemplate from './CreateIssueTemplate.svelte'
import IssueTemplatesContent from './IssueTemplatesContent.svelte'
@ -16,7 +15,6 @@
export let query: DocumentQuery<IssueTemplate> = {}
export let title: IntlString | undefined = undefined
export let label: string = ''
export let viewOptionsConfig: ViewOptionModel[] = getDefaultViewOptionsTemplatesConfig()
export let panelWidth: number = 0
@ -70,6 +68,8 @@
const showCreateDialog = async () => {
showPopup(CreateIssueTemplate, { targetElement: null }, 'top')
}
$: viewOptions = getViewOptions(viewlet)
</script>
<IssuesHeader {viewlets} {label} bind:viewlet bind:search showLabelSelector={$$slots.label_selector}>
@ -86,7 +86,7 @@
/>
{#if viewlet}
<ViewletSettingButton {viewlet} />
<ViewletSettingButton bind:viewOptions {viewlet} />
{/if}
{#if asideFloat && $$slots.aside}

View File

@ -13,8 +13,8 @@
// limitations under the License.
-->
<script lang="ts">
import { Doc, generateId, Ref, WithLookup } from '@hcengineering/core'
import { AttributeBarEditor, createQuery, getClient, KeyedAttribute } from '@hcengineering/presentation'
import { generateId, Ref, WithLookup } from '@hcengineering/core'
import { AttributeBarEditor, getClient, KeyedAttribute } from '@hcengineering/presentation'
import tags, { TagElement, TagReference } from '@hcengineering/tags'
import type { IssueTemplate } from '@hcengineering/tracker'
import { Component, Label } from '@hcengineering/ui'
@ -27,14 +27,6 @@
export let issue: WithLookup<IssueTemplate>
const query = createQuery()
let showIsBlocking = false
let blockedBy: Doc[]
$: query.query(tracker.class.Issue, { blockedBy: { _id: issue._id, _class: issue._class } }, (result) => {
blockedBy = result
showIsBlocking = result.length > 0
})
const client = getClient()
const hierarchy = client.getHierarchy()

View File

@ -41,6 +41,7 @@
<div class="flex-no-shrink draggable-mark">
{#if !isSingle}<Circles />{/if}
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="flex-no-shrink ml-2 color" on:click={pickColor}>
<div class="dot" style="background-color: {getPlatformColor(value.color ?? 0)}" />
</div>

View File

@ -23,6 +23,7 @@
export let value: IssueStatus
export let isDefault = false
export let isSingle = true
export let issueStatuses: IssueStatus[] | undefined = undefined
const dispatch = createEventDispatcher()
@ -37,7 +38,7 @@
<Circles />
</div>
<div class="flex-no-shrink ml-2">
<IssueStatusIcon {value} size="small" />
<IssueStatusIcon {value} size="small" {issueStatuses} />
</div>
<span class="content-accent-color ml-2">{value.name}</span>
{#if value.description}
@ -48,10 +49,12 @@
{#if isDefault}
<Label label={tracker.string.Default} />
{:else if value.category === tracker.issueStatusCategory.Backlog || value.category === tracker.issueStatusCategory.Unstarted}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="btn" on:click|preventDefault={() => dispatch('default-update', value._id)}>
<Label label={tracker.string.MakeDefault} />
</div>
{/if}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="btn"
use:tooltip={{ label: tracker.string.EditWorkflowStatus, direction: 'bottom' }}
@ -60,6 +63,7 @@
<Icon icon={IconEdit} size="small" />
</div>
{#if !isSingle}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="btn"
use:tooltip={{ label: tracker.string.DeleteWorkflowStatus, direction: 'bottom' }}

View File

@ -295,6 +295,7 @@
editingStatus = { ...detail, color: detail.color ?? category.color }
}}
on:delete={deleteStatus}
issueStatuses={workflowStatuses}
/>
{/if}
</div>

View File

@ -60,6 +60,7 @@ import SetDueDateActionPopup from './components/SetDueDateActionPopup.svelte'
import SetParentIssueActionPopup from './components/SetParentIssueActionPopup.svelte'
import Views from './components/views/Views.svelte'
import Statuses from './components/workflow/Statuses.svelte'
import RelatedIssuesSection from './components/issues/related/RelatedIssuesSection.svelte'
import {
getIssueId,
getIssueTitle,
@ -278,7 +279,8 @@ export default async (): Promise<Resources> => ({
CreateTeam,
TeamPresenter,
IssueStatistics,
StatusRefPresenter
StatusRefPresenter,
RelatedIssuesSection
},
completion: {
IssueQuery: async (client: Client, query: string, filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }) =>

View File

@ -15,12 +15,15 @@
import { Client, Doc, DocumentQuery, Ref } from '@hcengineering/core'
import type { IntlString, Metadata, Resource } from '@hcengineering/platform'
import { mergeIds } from '@hcengineering/platform'
import { AnyComponent } from '@hcengineering/ui'
import tracker, { trackerId } from '../../tracker/lib'
import { IssueDraft } from '@hcengineering/tracker'
import { SortFunc } from '@hcengineering/view'
import { AnyComponent } from '@hcengineering/ui'
import { SortFunc, Viewlet } from '@hcengineering/view'
import tracker, { trackerId } from '../../tracker/lib'
export default mergeIds(trackerId, tracker, {
viewlet: {
SubIssues: '' as Ref<Viewlet>
},
string: {
More: '' as IntlString,
Delete: '' as IntlString,

View File

@ -399,6 +399,7 @@ export default plugin(trackerId, {
Tracker: '' as AnyComponent,
TrackerApp: '' as AnyComponent,
RelatedIssues: '' as AnyComponent,
RelatedIssuesSection: '' as AnyComponent,
RelatedIssueTemplates: '' as AnyComponent,
EditIssue: '' as AnyComponent,
CreateIssue: '' as AnyComponent,

View File

@ -24,14 +24,16 @@
let prevKey = key
let element: HTMLDivElement | undefined
let cWidth: number = 0
let cWidth: number | undefined = undefined
afterUpdate(() => {
if (prevKey !== key) {
$fixedWidthStore[prevKey] = 0
$fixedWidthStore[key] = 0
prevKey = key
cWidth = 0
if (cWidth !== undefined) {
if (prevKey !== key) {
$fixedWidthStore[prevKey] = 0
$fixedWidthStore[key] = 0
prevKey = key
cWidth = undefined
}
}
})
@ -43,14 +45,18 @@
}
onDestroy(() => {
$fixedWidthStore[key] = 0
if (cWidth === $fixedWidthStore[key]) {
// If we are longest element
$fixedWidthStore[key] = 0
}
})
</script>
<div
bind:this={element}
class="flex-no-shrink{addClass ? ` ${addClass}` : ''}"
style="{justify !== '' ? `text-align: ${justify}; ` : ''} min-width: {$fixedWidthStore[key] ?? 0}px;"
style:text-align={justify !== '' ? justify : ''}
style:min-width={`${$fixedWidthStore[key] ?? 0}}px;`}
use:resizeObserver={resize}
>
<slot />

View File

@ -45,7 +45,10 @@
selected={viewOptions.groupBy}
width="10rem"
justify="left"
on:selected={(e) => dispatch('update', { key: 'groupBy', value: e.detail })}
on:selected={(e) => {
viewOptions.groupBy = e.detail
dispatch('update', { key: 'groupBy', value: e.detail })
}}
/>
</div>
<span class="label"><Label label={view.string.Ordering} /></span>
@ -60,6 +63,7 @@
const key = e.detail
const value = config.orderBy.find((p) => p[0] === key)
if (value !== undefined) {
viewOptions.orderBy = value
dispatch('update', { key: 'orderBy', value })
}
}}
@ -71,7 +75,10 @@
{#if isToggleType(model)}
<MiniToggle
on={viewOptions[model.key]}
on:change={() => dispatch('update', { key: model.key, value: !viewOptions[model.key] })}
on:change={() => {
viewOptions[model.key] = !viewOptions[model.key]
dispatch('update', { key: model.key, value: viewOptions[model.key] })
}}
/>
{:else if isDropdownType(model)}
{@const items = model.values.filter(({ hidden }) => !hidden?.(viewOptions))}
@ -81,7 +88,10 @@
selected={viewOptions[model.key]}
width="10rem"
justify="left"
on:selected={(e) => dispatch('update', { key: model.key, value: e.detail })}
on:selected={(e) => {
viewOptions[model.key] = e.detail
dispatch('update', { key: model.key, value: e.detail })
}}
/>
{/if}
</div>

View File

@ -13,36 +13,36 @@
// limitations under the License.
-->
<script lang="ts">
import { Button, eventToHTMLElement, IconDownOutline, Label, showPopup } from '@hcengineering/ui'
import { Viewlet, ViewOptionsModel } from '@hcengineering/view'
import { Button, ButtonKind, eventToHTMLElement, IconDownOutline, Label, showPopup } from '@hcengineering/ui'
import { Viewlet, ViewOptions } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import view from '../plugin'
import { defaulOptions, getViewOptions, setViewOptions, viewOptionsStore } from '../viewOptions'
import { setViewOptions } from '../viewOptions'
import ViewletSetting from './ViewletSetting.svelte'
import ViewOptions from './ViewOptions.svelte'
import ViewOptionsEditor from './ViewOptions.svelte'
export let viewlet: Viewlet | undefined
export let kind: ButtonKind = 'secondary'
export let viewOptions: ViewOptions
const dispatch = createEventDispatcher()
let btn: HTMLButtonElement
$: viewlet && loadViewOptionsStore(viewlet.viewOptions, viewlet._id)
function loadViewOptionsStore (config: ViewOptionsModel | undefined, key: string) {
if (!config) return
viewOptionsStore.set(getViewOptions(key) ?? defaulOptions)
}
function clickHandler (event: MouseEvent) {
if (viewlet?.viewOptions !== undefined) {
showPopup(
ViewOptions,
{ viewlet, config: viewlet.viewOptions, viewOptions: $viewOptionsStore },
ViewOptionsEditor,
{ viewlet, config: viewlet.viewOptions, viewOptions },
eventToHTMLElement(event),
undefined,
(result) => {
if (result?.key === undefined) return
$viewOptionsStore[result.key] = result.value
viewOptionsStore.set($viewOptionsStore)
setViewOptions(viewlet?._id ?? '', $viewOptionsStore)
if (viewlet) {
viewOptions = { ...viewOptions, [result.key]: result.value }
dispatch('viewOptions', viewOptions)
setViewOptions(viewlet, viewOptions)
}
}
)
} else {
@ -54,7 +54,7 @@
{#if viewlet}
<Button
icon={view.icon.ViewButton}
kind={'secondary'}
{kind}
size={'small'}
showTooltip={{ label: view.string.CustomizeView }}
bind:input={btn}

View File

@ -26,7 +26,7 @@
} from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import { buildConfigLookup, buildModel, getCategories, getPresenter, groupBy, LoadingProps } from '../../utils'
import { noCategory, viewOptionsStore } from '../../viewOptions'
import { noCategory } from '../../viewOptions'
import ListCategory from './ListCategory.svelte'
export let _class: Ref<Class<Doc>>
@ -38,33 +38,48 @@
export let selectedObjectIds: Doc[] = []
export let selectedRowIndex: number | undefined = undefined
export let loadingProps: LoadingProps | undefined = undefined
export let createItemDialog: AnyComponent | undefined
export let createItemLabel: IntlString | undefined
export let viewOptions: ViewOptionModel[] | undefined
export let createItemDialog: AnyComponent | undefined = undefined
export let createItemLabel: IntlString | undefined = undefined
export let viewOptionsConfig: ViewOptionModel[] | undefined
export let viewOptions: ViewOptions
export let flatHeaders = false
export let props: Record<string, any> = {}
export let documents: Doc[] | undefined = undefined
const objectRefs: HTMLElement[] = []
let docs: Doc[] = []
$: groupByKey = $viewOptionsStore.groupBy ?? noCategory
$: orderBy = $viewOptionsStore.orderBy
$: groupByKey = viewOptions.groupBy ?? noCategory
$: orderBy = viewOptions.orderBy
$: groupedDocs = groupBy(docs, groupByKey)
let categories: any[] = []
$: getCategories(client, _class, docs, groupByKey).then((p) => (categories = p))
$: getCategories(client, _class, docs, groupByKey).then((p) => {
categories = p
})
const docsQuery = createQuery()
$: resultOptions = { lookup, ...options, sort: { [orderBy[0]]: orderBy[1] } }
let resultQuery: DocumentQuery<Doc> = query
$: getResultQuery(query, viewOptions, $viewOptionsStore).then((p) => (resultQuery = p))
$: getResultQuery(query, viewOptionsConfig, viewOptions).then((p) => {
resultQuery = { ...p, ...query }
})
$: docsQuery.query(
_class,
resultQuery,
(res) => {
docs = res
dispatch('content', docs)
},
resultOptions
)
$: if (documents === undefined) {
docsQuery.query(
_class,
resultQuery,
(res) => {
docs = res
dispatch('content', docs)
},
resultOptions
)
} else {
docsQuery.unsubscribe()
docs = documents
}
const dispatch = createEventDispatcher()
@ -117,7 +132,9 @@
let headerComponent: AttributeModel | undefined
$: getHeader(_class, groupByKey)
$: buildModel({ client, _class, keys: config, lookup }).then((res) => (itemModels = res))
$: buildModel({ client, _class, keys: config, lookup }).then((res) => {
itemModels = res
})
function getInitIndex (categories: any, i: number): number {
let res = 0
@ -194,6 +211,8 @@
on:check
on:uncheckAll={uncheckAll}
on:row-focus
{flatHeaders}
{props}
/>
{/each}
</div>

View File

@ -45,6 +45,8 @@
export let selectedRowIndex: number | undefined
export let extraHeaders: AnyComponent[] | undefined
export let objectRefs: HTMLElement[] = []
export let flatHeaders = false
export let props: Record<string, any> = {}
const autoFoldLimit = 20
const defaultLimit = 20
@ -92,6 +94,8 @@
{createItemDialog}
{createItemLabel}
{extraHeaders}
flat={flatHeaders}
{props}
on:more={() => {
limit += 20
}}
@ -114,6 +118,7 @@
on:contextmenu={(event) => handleMenuOpened(event, docObject, initIndex + i)}
on:focus={() => {}}
on:mouseover={() => handleRowFocused(docObject)}
{props}
/>
{/each}
{/if}

View File

@ -41,6 +41,8 @@
export let limited: number
export let items: Doc[]
export let extraHeaders: AnyComponent[] | undefined
export let flat = false
export let props: Record<string, any> = {}
const dispatch = createEventDispatcher()
@ -52,7 +54,7 @@
{#if headerComponent || groupByKey === noCategory}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="flex-between categoryHeader row" on:click={() => dispatch('collapse')}>
<div class="flex-between categoryHeader row" class:flat on:click={() => dispatch('collapse')}>
<div class="flex-row-center gap-2 clear-mins caption-color">
<FixedColumn key={`list_groupBy_${groupByKey}`} justify={'left'}>
{#if groupByKey === noCategory}
@ -66,7 +68,7 @@
{#if extraHeaders}
{#each extraHeaders as extra}
<FixedColumn key={`list_groupBy_${groupByKey}_extra_${extra}`} justify={'left'}>
<Component is={extra} props={{ value: category, docs: items }} />
<Component is={extra} props={{ ...props, value: category, docs: items }} />
</FixedColumn>
{/each}
{/if}
@ -109,6 +111,14 @@
min-width: 0;
background: var(--header-bg-color);
z-index: 5;
&.flat {
background: var(--header-bg-color);
background-blend-mode: darken;
min-height: 2.25rem;
height: 2.25rem;
padding: 0 0.25rem 0 0.25rem;
}
}
.row:not(:last-child) {

View File

@ -28,6 +28,7 @@
export let groupByKey: string | undefined
export let checked: boolean
export let selected: boolean
export let props: Record<string, any> = {}
const dispatch = createEventDispatcher()
@ -69,6 +70,7 @@
<FixedColumn key={`list_item_${attributeModel.key}`} justify={attributeModel.props.fixed}>
<svelte:component
this={attributeModel.presenter}
{...props}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
object={docObject}
kind={'list'}
@ -78,6 +80,7 @@
{:else}
<svelte:component
this={attributeModel.presenter}
{...props}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
object={docObject}
kind={'list'}
@ -104,6 +107,7 @@
{#if attributeModel.props?.optional && attributeModel.props?.excludeByKey !== groupByKey && value !== undefined}
<svelte:component
this={attributeModel.presenter}
{...props}
value={value ?? ''}
objectId={docObject._id}
groupBy={groupByKey}

View File

@ -2,7 +2,7 @@
import { Class, Doc, DocumentQuery, FindOptions, Ref, Space } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { AnyComponent, issueSP, Scroller } from '@hcengineering/ui'
import { BuildModelKey, Viewlet } from '@hcengineering/view'
import { BuildModelKey, Viewlet, ViewOptions } from '@hcengineering/view'
import { onMount } from 'svelte'
import {
ActionContext,
@ -24,6 +24,8 @@
export let loadingProps: LoadingProps | undefined = undefined
export let createItemDialog: AnyComponent | undefined
export let createItemLabel: IntlString | undefined
export let viewOptions: ViewOptions
export let props: Record<string, any> = {}
let list: List
@ -57,7 +59,9 @@
{loadingProps}
{createItemDialog}
{createItemLabel}
viewOptions={viewlet.viewOptions?.other}
{viewOptions}
{props}
viewOptionsConfig={viewlet.viewOptions?.other}
selectedObjectIds={$selectionStore ?? []}
selectedRowIndex={listProvider.current($focusStore)}
on:row-focus={(event) => {

View File

@ -88,6 +88,8 @@ export { default as FixedColumn } from './components/FixedColumn.svelte'
export { default as ValueSelector } from './components/ValueSelector.svelte'
export { default as ObjectBox } from './components/ObjectBox.svelte'
export { default as ObjectPresenter } from './components/ObjectPresenter.svelte'
export { default as List } from './components/list/List.svelte'
export * from './context'
export * from './filter'
export * from './selection'

View File

@ -1,7 +1,6 @@
import { SortingOrder } from '@hcengineering/core'
import { getCurrentLocation, locationToUrl } from '@hcengineering/ui'
import { DropdownViewOption, ToggleViewOption, ViewOptionModel, ViewOptions } from '@hcengineering/view'
import { writable } from 'svelte/store'
import { DropdownViewOption, ToggleViewOption, Viewlet, ViewOptionModel, ViewOptions } from '@hcengineering/view'
export const noCategory = '#no_category'
@ -10,8 +9,6 @@ export const defaulOptions: ViewOptions = {
orderBy: ['modifiedBy', SortingOrder.Descending]
}
export const viewOptionsStore = writable<ViewOptions>(defaulOptions)
export function isToggleType (viewOption: ViewOptionModel): viewOption is ToggleViewOption {
return viewOption.type === 'toggle'
}
@ -27,14 +24,27 @@ function makeViewOptionsKey (prefix: string): string {
return `viewOptions:${prefix}:${locationToUrl(loc)}`
}
export function setViewOptions (prefix: string, options: ViewOptions): void {
function _setViewOptions (prefix: string, options: ViewOptions): void {
const key = makeViewOptionsKey(prefix)
localStorage.setItem(key, JSON.stringify(options))
}
export function getViewOptions (prefix: string): ViewOptions | null {
export function setViewOptions (viewlet: Viewlet, options: ViewOptions): void {
const viewletKey = viewlet?._id + (viewlet?.variant !== undefined ? `-${viewlet.variant}` : '')
_setViewOptions(viewletKey, options)
}
function _getViewOptions (prefix: string): ViewOptions | null {
const key = makeViewOptionsKey(prefix)
const options = localStorage.getItem(key)
if (options === null) return null
return JSON.parse(options)
}
export function getViewOptions (viewlet: Viewlet | undefined, defaults = defaulOptions): ViewOptions {
if (viewlet === undefined) {
return { ...defaults }
}
const viewletKey = viewlet?._id + (viewlet?.variant !== undefined ? `-${viewlet.variant}` : '')
return _getViewOptions(viewletKey) ?? defaults
}

View File

@ -222,6 +222,7 @@ export interface Viewlet extends Doc {
config: (BuildModelKey | string)[]
hiddenKeys?: string[]
viewOptions?: ViewOptionsModel
variant?: string
}
/**
@ -553,7 +554,8 @@ const view = plugin(viewId, {
EditDoc: '' as AnyComponent,
SpacePresenter: '' as AnyComponent,
BooleanTruePresenter: '' as AnyComponent,
ValueSelector: '' as AnyComponent
ValueSelector: '' as AnyComponent,
GrowPresenter: '' as AnyComponent
},
string: {
CustomizeView: '' as IntlString,

View File

@ -19,7 +19,12 @@
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
import { AnyComponent, Button, IconAdd, SearchEdit, showPanel, showPopup, TabList } from '@hcengineering/ui'
import view, { Viewlet } from '@hcengineering/view'
import { getActiveViewletId, setActiveViewletId, ViewletSettingButton } from '@hcengineering/view-resources'
import {
getActiveViewletId,
getViewOptions,
setActiveViewletId,
ViewletSettingButton
} from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import plugin from '../plugin'
import { classIcon } from '../utils'
@ -84,6 +89,8 @@
})
$: twoRows = $deviceInfo.twoRows
$: viewOptions = getViewOptions(viewlet)
</script>
<div class="ac-header withSettings" class:full={!twoRows} class:mini={twoRows}>
@ -128,7 +135,7 @@
}}
/>
{/if}
<ViewletSettingButton {viewlet} />
<ViewletSettingButton bind:viewOptions {viewlet} />
</div>
{/if}
</div>

View File

@ -29,7 +29,7 @@
showPopup
} from '@hcengineering/ui'
import view, { Viewlet, ViewletDescriptor, ViewletPreference } from '@hcengineering/view'
import { FilterBar, FilterButton, ViewletSettingButton } from '@hcengineering/view-resources'
import { FilterBar, FilterButton, getViewOptions, ViewletSettingButton } from '@hcengineering/view-resources'
export let _class: Ref<Class<Doc>>
export let icon: Asset
@ -91,6 +91,8 @@
}
$: twoRows = $deviceInfo.twoRows
$: viewOptions = getViewOptions(viewlet)
</script>
<div class="ac-header withSettings" class:full={!twoRows} class:mini={twoRows}>
@ -107,7 +109,7 @@
{#if createLabel && createComponent}
<Button label={createLabel} icon={IconAdd} kind={'primary'} size={'small'} on:click={() => showCreateDialog()} />
{/if}
<ViewletSettingButton {viewlet} />
<ViewletSettingButton bind:viewOptions {viewlet} />
</div>
</div>

View File

@ -510,6 +510,7 @@
/>
<div class="workbench-container">
{#if currentApplication && navigatorModel && navigator && visibileNav}
<!-- svelte-ignore a11y-click-events-have-key-events -->
{#if visibileNav && navFloat}<div class="cover shown" on:click={() => (visibileNav = false)} />{/if}
<div
class="antiPanel-navigator {appsDirection === 'horizontal' ? 'portrait' : 'landscape'}"

File diff suppressed because one or more lines are too long