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

1
.gitignore vendored
View File

@ -78,3 +78,4 @@ tsconfig.tsbuildinfo
ingest-attachment-*.zip ingest-attachment-*.zip
tsdoc-metadata.json 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, { builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: tracker.class.IssueTemplate, attachTo: tracker.class.IssueTemplate,
descriptor: view.viewlet.List, descriptor: view.viewlet.List,

View File

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

View File

@ -20,6 +20,7 @@ import core from './component'
import { _createMixinProxy, _mixinClass, _toDoc } from './proxy' import { _createMixinProxy, _mixinClass, _toDoc } from './proxy'
import type { Tx, TxCreateDoc, TxMixin, TxRemoveDoc, TxUpdateDoc } from './tx' import type { Tx, TxCreateDoc, TxMixin, TxRemoveDoc, TxUpdateDoc } from './tx'
import { TxProcessor } from './tx' import { TxProcessor } from './tx'
import getTypeOf from './typeof'
/** /**
* @public * @public
@ -463,11 +464,12 @@ export class Hierarchy {
if (typeof obj === 'function') { if (typeof obj === 'function') {
return obj 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) { for (const key in obj) {
// include prototype properties // include prototype properties
const value = obj[key] const value = obj[key]
const type = {}.toString.call(value).slice(8, -1) const type = getTypeOf(value)
if (type === 'Array') { if (type === 'Array') {
result[key] = this.clone(value) result[key] = this.clone(value)
} else if (type === 'Object') { } else if (type === 'Object') {
@ -477,7 +479,9 @@ export class Hierarchy {
} else if (type === 'Date') { } else if (type === 'Date') {
result[key] = new Date(value.getTime()) result[key] = new Date(value.getTime())
} else { } else {
result[key] = value if (isArray) {
result[key] = value
}
} }
} }
return result 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. // limitations under the License.
// //
import { Status, OK, unknownError } from './status' import { Status, OK, unknownError, Severity } from './status'
/** /**
* @public * @public
@ -68,7 +68,9 @@ async function broadcastEvent (event: string, data: any): Promise<void> {
* @returns * @returns
*/ */
export async function setPlatformStatus (status: Status): Promise<void> { 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) return await broadcastEvent(PlatformEvent, status)
} }

View File

@ -12,11 +12,26 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // 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"> <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 { Asset, getResource } from '@hcengineering/platform'
import { AnySvelteComponent, Icon, IconSize } from '@hcengineering/ui' 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' import AvatarIcon from './icons/Avatar.svelte'
export let avatar: string | null | undefined = undefined export let avatar: string | null | undefined = undefined
@ -35,8 +50,7 @@
}) })
} else if (avatar) { } else if (avatar) {
const avatarProviderId = getAvatarProviderId(avatar) const avatarProviderId = getAvatarProviderId(avatar)
avatarProvider = avatarProvider = avatarProviderId && (await getProvider(getClient(), avatarProviderId))
avatarProviderId && (await getClient().findOne(contact.class.AvatarProvider, { _id: avatarProviderId }))
if (!avatarProvider || avatarProvider.type === AvatarType.COLOR) { if (!avatarProvider || avatarProvider.type === AvatarType.COLOR) {
url = undefined url = undefined

View File

@ -19,11 +19,19 @@
export let label: IntlString export let label: IntlString
export let params: Record<string, any> = {} 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> </script>
{#await translation} {#if _value}
{_value}
{:else}
{label} {label}
{:then text} {/if}
{text}
{/await}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,7 +22,7 @@
import { Vacancy } from '@hcengineering/recruit' import { Vacancy } from '@hcengineering/recruit'
import { FullDescriptionBox } from '@hcengineering/text-editor' import { FullDescriptionBox } from '@hcengineering/text-editor'
import tracker from '@hcengineering/tracker' 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 { ClassAttributeBar, ContextMenu } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import recruit from '../plugin' import recruit from '../plugin'
@ -131,33 +131,7 @@
/> />
<!-- <MembersBox label={recruit.string.Members} space={object} /> --> <!-- <MembersBox label={recruit.string.Members} space={object} /> -->
<div class="antiSection"> <Component is={tracker.component.RelatedIssuesSection} props={{ object, label: recruit.string.RelatedIssues }} />
<div class="antiSection-header"> </Grid>
<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
>
</Panel> </Panel>
{/if} {/if}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,65 +13,20 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Ref, WithLookup } from '@hcengineering/core' import { DocumentQuery, Ref, WithLookup } from '@hcengineering/core'
import { Issue, IssueStatus, Team } from '@hcengineering/tracker' import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { getEventPositionElement, showPanel, showPopup } from '@hcengineering/ui' import { Viewlet, ViewOptions } from '@hcengineering/view'
import { ActionContext, ContextMenu, FixedColumn } from '@hcengineering/view-resources' import { ActionContext, List } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import { flip } from 'svelte/animate'
import { getIssueId } from '../../../issues'
import tracker from '../../../plugin' 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>[]> 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> </script>
<ActionContext <ActionContext
@ -80,136 +35,15 @@
}} }}
/> />
{#each issues as issue, index (issue._id)} {#if viewlet}
{@const currentTeam = teams.get(issue.space)} <List
{@const openIssueCall = () => openIssue(issue)} _class={tracker.class.Issue}
<div {viewOptions}
class="flex-between row" viewOptionsConfig={viewlet.viewOptions?.other}
class:is-dragging={index === draggingIndex} config={viewlet.config}
class:is-dragged-over-up={draggingIndex !== null && index < draggingIndex && index === hoveringIndex} documents={issues}
class:is-dragged-over-down={draggingIndex !== null && index > draggingIndex && index === hoveringIndex} {query}
animate:flip={{ duration: 400 }} flatHeaders={true}
draggable={true} props={{ teams, issueStatuses }}
on:click|self={openIssueCall} />
on:contextmenu|preventDefault={(ev) => showContextMenu(ev, issue)} {/if}
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>

View File

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

View File

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

View File

@ -17,10 +17,13 @@
import presentation, { createQuery, getClient } from '@hcengineering/presentation' import presentation, { createQuery, getClient } from '@hcengineering/presentation'
import { calcRank, Issue, IssueStatus, Team } from '@hcengineering/tracker' import { calcRank, Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { Label, Spinner } from '@hcengineering/ui' import { Label, Spinner } from '@hcengineering/ui'
import { Viewlet, ViewOptions } from '@hcengineering/view'
import tracker from '../../../plugin' import tracker from '../../../plugin'
import SubIssueList from '../edit/SubIssueList.svelte' import SubIssueList from '../edit/SubIssueList.svelte'
export let object: Doc export let object: Doc
export let viewlet: Viewlet
export let viewOptions: ViewOptions
let query: DocumentQuery<Issue> let query: DocumentQuery<Issue>
$: query = { 'relations._id': object._id, 'relations._class': object._class } $: query = { 'relations._id': object._id, 'relations._class': object._class }
@ -75,9 +78,9 @@
</script> </script>
<div class="mt-1"> <div class="mt-1">
{#if subIssues !== undefined} {#if subIssues !== undefined && viewlet !== undefined}
{#if issueStatuses.size > 0 && teams} {#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} {:else}
<div class="p-1"> <div class="p-1">
<Label label={presentation.string.NoMatchesFound} /> <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 <Button
icon={IconAdd} icon={IconAdd}
size={'small'} size={'small'}
on:click={(event) => { on:click={() => {
showPopup( showPopup(
TimeSpendReportPopup, TimeSpendReportPopup,
{ {

View File

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

View File

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

View File

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

View File

@ -14,13 +14,18 @@
--> -->
<script lang="ts"> <script lang="ts">
import contact from '@hcengineering/contact' 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 UserBox from '@hcengineering/presentation/src/components/UserBox.svelte'
import { Team, TimeReportDayType, TimeSpendReport } from '@hcengineering/tracker' import { Team, TimeReportDayType, TimeSpendReport } from '@hcengineering/tracker'
import { eventToHTMLElement, getEventPositionElement, ListView, showPopup } from '@hcengineering/ui' import {
import { deviceOptionsStore as deviceInfo } from '@hcengineering/ui' deviceOptionsStore as deviceInfo,
eventToHTMLElement,
getEventPositionElement,
ListView,
showPopup
} from '@hcengineering/ui'
import DatePresenter from '@hcengineering/ui/src/components/calendar/DatePresenter.svelte' 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 { getIssueId } from '../../../issues'
import tracker from '../../../plugin' import tracker from '../../../plugin'
import TimePresenter from './TimePresenter.svelte' import TimePresenter from './TimePresenter.svelte'
@ -34,7 +39,7 @@
showPopup(ContextMenu, { object }, getEventPositionElement(ev)) 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> const toTeamId = (ref: Ref<Space>) => ref as Ref<Team>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Doc, generateId, Ref, WithLookup } from '@hcengineering/core' import { generateId, Ref, WithLookup } from '@hcengineering/core'
import { AttributeBarEditor, createQuery, getClient, KeyedAttribute } from '@hcengineering/presentation' import { AttributeBarEditor, getClient, KeyedAttribute } from '@hcengineering/presentation'
import tags, { TagElement, TagReference } from '@hcengineering/tags' import tags, { TagElement, TagReference } from '@hcengineering/tags'
import type { IssueTemplate } from '@hcengineering/tracker' import type { IssueTemplate } from '@hcengineering/tracker'
import { Component, Label } from '@hcengineering/ui' import { Component, Label } from '@hcengineering/ui'
@ -27,14 +27,6 @@
export let issue: WithLookup<IssueTemplate> 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 client = getClient()
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()

View File

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

View File

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

View File

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

View File

@ -60,6 +60,7 @@ import SetDueDateActionPopup from './components/SetDueDateActionPopup.svelte'
import SetParentIssueActionPopup from './components/SetParentIssueActionPopup.svelte' import SetParentIssueActionPopup from './components/SetParentIssueActionPopup.svelte'
import Views from './components/views/Views.svelte' import Views from './components/views/Views.svelte'
import Statuses from './components/workflow/Statuses.svelte' import Statuses from './components/workflow/Statuses.svelte'
import RelatedIssuesSection from './components/issues/related/RelatedIssuesSection.svelte'
import { import {
getIssueId, getIssueId,
getIssueTitle, getIssueTitle,
@ -278,7 +279,8 @@ export default async (): Promise<Resources> => ({
CreateTeam, CreateTeam,
TeamPresenter, TeamPresenter,
IssueStatistics, IssueStatistics,
StatusRefPresenter StatusRefPresenter,
RelatedIssuesSection
}, },
completion: { completion: {
IssueQuery: async (client: Client, query: string, filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }) => 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 { Client, Doc, DocumentQuery, Ref } from '@hcengineering/core'
import type { IntlString, Metadata, Resource } from '@hcengineering/platform' import type { IntlString, Metadata, Resource } from '@hcengineering/platform'
import { mergeIds } 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 { 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, { export default mergeIds(trackerId, tracker, {
viewlet: {
SubIssues: '' as Ref<Viewlet>
},
string: { string: {
More: '' as IntlString, More: '' as IntlString,
Delete: '' as IntlString, Delete: '' as IntlString,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,6 @@
import { SortingOrder } from '@hcengineering/core' import { SortingOrder } from '@hcengineering/core'
import { getCurrentLocation, locationToUrl } from '@hcengineering/ui' import { getCurrentLocation, locationToUrl } from '@hcengineering/ui'
import { DropdownViewOption, ToggleViewOption, ViewOptionModel, ViewOptions } from '@hcengineering/view' import { DropdownViewOption, ToggleViewOption, Viewlet, ViewOptionModel, ViewOptions } from '@hcengineering/view'
import { writable } from 'svelte/store'
export const noCategory = '#no_category' export const noCategory = '#no_category'
@ -10,8 +9,6 @@ export const defaulOptions: ViewOptions = {
orderBy: ['modifiedBy', SortingOrder.Descending] orderBy: ['modifiedBy', SortingOrder.Descending]
} }
export const viewOptionsStore = writable<ViewOptions>(defaulOptions)
export function isToggleType (viewOption: ViewOptionModel): viewOption is ToggleViewOption { export function isToggleType (viewOption: ViewOptionModel): viewOption is ToggleViewOption {
return viewOption.type === 'toggle' return viewOption.type === 'toggle'
} }
@ -27,14 +24,27 @@ function makeViewOptionsKey (prefix: string): string {
return `viewOptions:${prefix}:${locationToUrl(loc)}` return `viewOptions:${prefix}:${locationToUrl(loc)}`
} }
export function setViewOptions (prefix: string, options: ViewOptions): void { function _setViewOptions (prefix: string, options: ViewOptions): void {
const key = makeViewOptionsKey(prefix) const key = makeViewOptionsKey(prefix)
localStorage.setItem(key, JSON.stringify(options)) 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 key = makeViewOptionsKey(prefix)
const options = localStorage.getItem(key) const options = localStorage.getItem(key)
if (options === null) return null if (options === null) return null
return JSON.parse(options) 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)[] config: (BuildModelKey | string)[]
hiddenKeys?: string[] hiddenKeys?: string[]
viewOptions?: ViewOptionsModel viewOptions?: ViewOptionsModel
variant?: string
} }
/** /**
@ -553,7 +554,8 @@ const view = plugin(viewId, {
EditDoc: '' as AnyComponent, EditDoc: '' as AnyComponent,
SpacePresenter: '' as AnyComponent, SpacePresenter: '' as AnyComponent,
BooleanTruePresenter: '' as AnyComponent, BooleanTruePresenter: '' as AnyComponent,
ValueSelector: '' as AnyComponent ValueSelector: '' as AnyComponent,
GrowPresenter: '' as AnyComponent
}, },
string: { string: {
CustomizeView: '' as IntlString, CustomizeView: '' as IntlString,

View File

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

View File

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

View File

@ -510,6 +510,7 @@
/> />
<div class="workbench-container"> <div class="workbench-container">
{#if currentApplication && navigatorModel && navigator && visibileNav} {#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} {#if visibileNav && navFloat}<div class="cover shown" on:click={() => (visibileNav = false)} />{/if}
<div <div
class="antiPanel-navigator {appsDirection === 'horizontal' ? 'portrait' : 'landscape'}" class="antiPanel-navigator {appsDirection === 'horizontal' ? 'portrait' : 'landscape'}"

File diff suppressed because one or more lines are too long