mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-25 09:13:07 +03:00
Use list for sub issues (#2514)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
1e14668439
commit
28f3e6169c
1
.gitignore
vendored
1
.gitignore
vendored
@ -78,3 +78,4 @@ tsconfig.tsbuildinfo
|
||||
ingest-attachment-*.zip
|
||||
tsdoc-metadata.json
|
||||
pods/front/dist
|
||||
*.cpuprofile
|
||||
|
@ -550,6 +550,74 @@ export function createModel (builder: Builder): void {
|
||||
]
|
||||
})
|
||||
|
||||
const subIssuesOptions: ViewOptionsModel = {
|
||||
groupBy: ['status', 'assignee', 'priority', 'sprint'],
|
||||
orderBy: [
|
||||
['status', SortingOrder.Ascending],
|
||||
['priority', SortingOrder.Ascending],
|
||||
['modifiedOn', SortingOrder.Descending],
|
||||
['dueDate', SortingOrder.Descending],
|
||||
['rank', SortingOrder.Ascending]
|
||||
],
|
||||
other: []
|
||||
}
|
||||
|
||||
builder.createDoc(
|
||||
view.class.Viewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
attachTo: tracker.class.Issue,
|
||||
descriptor: view.viewlet.List,
|
||||
viewOptions: subIssuesOptions,
|
||||
variant: 'subissue',
|
||||
config: [
|
||||
{
|
||||
key: '',
|
||||
presenter: tracker.component.PriorityEditor,
|
||||
props: { type: 'priority', kind: 'list', size: 'small' }
|
||||
},
|
||||
{ key: '', presenter: tracker.component.IssuePresenter, props: { type: 'issue', fixed: 'left' } },
|
||||
{
|
||||
key: '',
|
||||
presenter: tracker.component.StatusEditor,
|
||||
props: { kind: 'list', size: 'small', justify: 'center' }
|
||||
},
|
||||
{ key: '', presenter: tracker.component.TitlePresenter, props: { shouldUseMargin: true, showParent: false } },
|
||||
{ key: '', presenter: tracker.component.SubIssuesSelector, props: {} },
|
||||
{ key: '', presenter: view.component.GrowPresenter, props: { type: 'grow' } },
|
||||
{ key: '', presenter: tracker.component.DueDatePresenter, props: { kind: 'list' } },
|
||||
{
|
||||
key: '',
|
||||
presenter: tracker.component.SprintEditor,
|
||||
props: {
|
||||
kind: 'list',
|
||||
size: 'small',
|
||||
shape: 'round',
|
||||
shouldShowPlaceholder: false,
|
||||
excludeByKey: 'sprint',
|
||||
optional: true
|
||||
}
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
presenter: tracker.component.EstimationEditor,
|
||||
props: { kind: 'list', size: 'small', optional: true }
|
||||
},
|
||||
{
|
||||
key: 'modifiedOn',
|
||||
presenter: tracker.component.ModificationDatePresenter,
|
||||
props: { fixed: 'right', optional: true }
|
||||
},
|
||||
{
|
||||
key: '$lookup.assignee',
|
||||
presenter: tracker.component.AssigneePresenter,
|
||||
props: { issueClass: tracker.class.Issue, defaultClass: contact.class.Employee, shouldShowLabel: false }
|
||||
}
|
||||
]
|
||||
},
|
||||
tracker.viewlet.SubIssues
|
||||
)
|
||||
|
||||
builder.createDoc(view.class.Viewlet, core.space.Model, {
|
||||
attachTo: tracker.class.IssueTemplate,
|
||||
descriptor: view.viewlet.List,
|
||||
|
@ -63,8 +63,7 @@ export default mergeIds(viewId, view, {
|
||||
HTMLEditor: '' as AnyComponent,
|
||||
MarkupEditor: '' as AnyComponent,
|
||||
MarkupEditorPopup: '' as AnyComponent,
|
||||
ListView: '' as AnyComponent,
|
||||
GrowPresenter: '' as AnyComponent
|
||||
ListView: '' as AnyComponent
|
||||
},
|
||||
string: {
|
||||
Table: '' as IntlString,
|
||||
|
@ -20,6 +20,7 @@ import core from './component'
|
||||
import { _createMixinProxy, _mixinClass, _toDoc } from './proxy'
|
||||
import type { Tx, TxCreateDoc, TxMixin, TxRemoveDoc, TxUpdateDoc } from './tx'
|
||||
import { TxProcessor } from './tx'
|
||||
import getTypeOf from './typeof'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -463,11 +464,12 @@ export class Hierarchy {
|
||||
if (typeof obj === 'function') {
|
||||
return obj
|
||||
}
|
||||
const result: any = Array.isArray(obj) ? [] : {}
|
||||
const isArray = Array.isArray(obj)
|
||||
const result: any = isArray ? [] : Object.assign({}, obj)
|
||||
for (const key in obj) {
|
||||
// include prototype properties
|
||||
const value = obj[key]
|
||||
const type = {}.toString.call(value).slice(8, -1)
|
||||
const type = getTypeOf(value)
|
||||
if (type === 'Array') {
|
||||
result[key] = this.clone(value)
|
||||
} else if (type === 'Object') {
|
||||
@ -477,7 +479,9 @@ export class Hierarchy {
|
||||
} else if (type === 'Date') {
|
||||
result[key] = new Date(value.getTime())
|
||||
} else {
|
||||
result[key] = value
|
||||
if (isArray) {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
35
packages/core/src/typeof.ts
Normal file
35
packages/core/src/typeof.ts
Normal 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)
|
||||
}
|
@ -14,7 +14,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Status, OK, unknownError } from './status'
|
||||
import { Status, OK, unknownError, Severity } from './status'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -68,7 +68,9 @@ async function broadcastEvent (event: string, data: any): Promise<void> {
|
||||
* @returns
|
||||
*/
|
||||
export async function setPlatformStatus (status: Status): Promise<void> {
|
||||
// console.log(await translate(status.code, status.params))
|
||||
if (status.severity === Severity.ERROR) {
|
||||
console.trace('Platform Error Status', status)
|
||||
}
|
||||
return await broadcastEvent(PlatformEvent, status)
|
||||
}
|
||||
|
||||
|
@ -12,11 +12,26 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts" context="module">
|
||||
const providers = new Map<string, AvatarProvider | null>()
|
||||
|
||||
async function getProvider (client: Client, providerId: Ref<AvatarProvider>): Promise<AvatarProvider | undefined> {
|
||||
const p = providers.get(providerId)
|
||||
if (p !== undefined) {
|
||||
return p ?? undefined
|
||||
}
|
||||
const res = await getClient().findOne(contact.class.AvatarProvider, { _id: providerId })
|
||||
providers.set(providerId, res ?? null)
|
||||
return res
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import contact, { AvatarType, AvatarProvider } from '@hcengineering/contact'
|
||||
import contact, { AvatarProvider, AvatarType } from '@hcengineering/contact'
|
||||
import { Client, Ref } from '@hcengineering/core'
|
||||
import { Asset, getResource } from '@hcengineering/platform'
|
||||
import { AnySvelteComponent, Icon, IconSize } from '@hcengineering/ui'
|
||||
import { getBlobURL, getAvatarProviderId, getClient } from '../utils'
|
||||
import { getAvatarProviderId, getBlobURL, getClient } from '../utils'
|
||||
import AvatarIcon from './icons/Avatar.svelte'
|
||||
|
||||
export let avatar: string | null | undefined = undefined
|
||||
@ -35,8 +50,7 @@
|
||||
})
|
||||
} else if (avatar) {
|
||||
const avatarProviderId = getAvatarProviderId(avatar)
|
||||
avatarProvider =
|
||||
avatarProviderId && (await getClient().findOne(contact.class.AvatarProvider, { _id: avatarProviderId }))
|
||||
avatarProvider = avatarProviderId && (await getProvider(getClient(), avatarProviderId))
|
||||
|
||||
if (!avatarProvider || avatarProvider.type === AvatarType.COLOR) {
|
||||
url = undefined
|
||||
|
@ -19,11 +19,19 @@
|
||||
export let label: IntlString
|
||||
export let params: Record<string, any> = {}
|
||||
|
||||
$: translation = translate(label, params)
|
||||
let _value: string | undefined = undefined
|
||||
|
||||
$: if (label !== undefined) {
|
||||
translate(label, params ?? {}).then((r) => {
|
||||
_value = r
|
||||
})
|
||||
} else {
|
||||
_value = label
|
||||
}
|
||||
</script>
|
||||
|
||||
{#await translation}
|
||||
{#if _value}
|
||||
{_value}
|
||||
{:else}
|
||||
{label}
|
||||
{:then text}
|
||||
{text}
|
||||
{/await}
|
||||
{/if}
|
||||
|
@ -23,7 +23,6 @@ export default mergeIds(attachmentId, attachment, {
|
||||
string: {
|
||||
NoAttachments: '' as IntlString,
|
||||
UploadDropFilesHere: '' as IntlString,
|
||||
Attachments: '' as IntlString,
|
||||
Photos: '' as IntlString,
|
||||
FileBrowser: '' as IntlString,
|
||||
FileBrowserFileCounter: '' as IntlString,
|
||||
|
@ -86,6 +86,7 @@ export default plugin(attachmentId, {
|
||||
FileBrowserTypeFilterAudio: '' as IntlString,
|
||||
FileBrowserTypeFilterVideos: '' as IntlString,
|
||||
FileBrowserTypeFilterPDFs: '' as IntlString,
|
||||
DeleteFile: '' as IntlString
|
||||
DeleteFile: '' as IntlString,
|
||||
Attachments: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
@ -30,7 +30,7 @@
|
||||
TabList
|
||||
} from '@hcengineering/ui'
|
||||
import view, { Viewlet, ViewletPreference } from '@hcengineering/view'
|
||||
import { FilterButton, ViewletSettingButton } from '@hcengineering/view-resources'
|
||||
import { FilterButton, getViewOptions, ViewletSettingButton } from '@hcengineering/view-resources'
|
||||
import calendar from '../plugin'
|
||||
import { deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
|
||||
|
||||
@ -101,6 +101,8 @@
|
||||
})
|
||||
|
||||
$: twoRows = $deviceInfo.twoRows
|
||||
|
||||
$: viewOptions = getViewOptions(selectedViewlet)
|
||||
</script>
|
||||
|
||||
<div class="ac-header withSettings" class:full={!twoRows} class:mini={twoRows}>
|
||||
@ -133,7 +135,7 @@
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<ViewletSettingButton viewlet={selectedViewlet} />
|
||||
<ViewletSettingButton bind:viewOptions viewlet={selectedViewlet} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -148,6 +150,7 @@
|
||||
space,
|
||||
options: selectedViewlet.options,
|
||||
config: preference?.config ?? selectedViewlet.config,
|
||||
viewOptions,
|
||||
viewlet: selectedViewlet,
|
||||
query: resultQuery,
|
||||
search,
|
||||
|
@ -18,7 +18,13 @@
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Button, Icon, IconAdd, Label, Loading, SearchEdit, showPopup } from '@hcengineering/ui'
|
||||
import view, { Viewlet, ViewletPreference } from '@hcengineering/view'
|
||||
import { ActionContext, FilterButton, TableBrowser, ViewletSettingButton } from '@hcengineering/view-resources'
|
||||
import {
|
||||
ActionContext,
|
||||
FilterButton,
|
||||
getViewOptions,
|
||||
TableBrowser,
|
||||
ViewletSettingButton
|
||||
} from '@hcengineering/view-resources'
|
||||
import contact from '../plugin'
|
||||
import CreateContact from './CreateContact.svelte'
|
||||
import { deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
|
||||
@ -64,6 +70,8 @@
|
||||
}
|
||||
|
||||
$: twoRows = $deviceInfo.twoRows
|
||||
|
||||
$: viewOptions = getViewOptions(viewlet)
|
||||
</script>
|
||||
|
||||
<ActionContext
|
||||
@ -95,7 +103,7 @@
|
||||
size={'small'}
|
||||
on:click={(ev) => showCreateDialog(ev)}
|
||||
/>
|
||||
<ViewletSettingButton {viewlet} />
|
||||
<ViewletSettingButton bind:viewOptions {viewlet} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -18,7 +18,7 @@
|
||||
import { createQuery, getClient, UsersPopup, IconMembersOutline } from '@hcengineering/presentation'
|
||||
import { Button, IconAdd, Label, showPopup, Icon } from '@hcengineering/ui'
|
||||
import view, { Viewlet, ViewletPreference } from '@hcengineering/view'
|
||||
import { Table, ViewletSettingButton } from '@hcengineering/view-resources'
|
||||
import { getViewOptions, Table, ViewletSettingButton } from '@hcengineering/view-resources'
|
||||
import contact from '../plugin'
|
||||
|
||||
export let objectId: Ref<Doc>
|
||||
@ -90,6 +90,7 @@
|
||||
}
|
||||
})
|
||||
}
|
||||
$: viewOptions = getViewOptions(descr)
|
||||
</script>
|
||||
|
||||
<div class="antiSection">
|
||||
@ -101,7 +102,7 @@
|
||||
<Label label={contact.string.Members} />
|
||||
</span>
|
||||
<div class="buttons-group xsmall-gap">
|
||||
<ViewletSettingButton viewlet={descr} />
|
||||
<ViewletSettingButton bind:viewOptions viewlet={descr} />
|
||||
<Button id={contact.string.AddMember} icon={IconAdd} kind={'transparent'} shape={'circle'} on:click={createApp} />
|
||||
</div>
|
||||
</div>
|
||||
@ -118,6 +119,7 @@
|
||||
<span class="dark-color">
|
||||
<Label label={contact.string.NoMembers} />
|
||||
</span>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<span class="over-underline content-accent-color" on:click={createApp}>
|
||||
<Label label={contact.string.AddMember} />
|
||||
</span>
|
||||
|
@ -20,7 +20,13 @@
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { deviceOptionsStore as deviceInfo, Icon, Label, Loading, SearchEdit } from '@hcengineering/ui'
|
||||
import view, { Viewlet, ViewletPreference } from '@hcengineering/view'
|
||||
import { ActionContext, FilterButton, TableBrowser, ViewletSettingButton } from '@hcengineering/view-resources'
|
||||
import {
|
||||
ActionContext,
|
||||
FilterButton,
|
||||
getViewOptions,
|
||||
TableBrowser,
|
||||
ViewletSettingButton
|
||||
} from '@hcengineering/view-resources'
|
||||
import document from '../plugin'
|
||||
|
||||
export let query: DocumentQuery<Document> = {}
|
||||
@ -63,6 +69,8 @@
|
||||
|
||||
let twoRows: boolean
|
||||
$: twoRows = $deviceInfo.docWidth <= 680
|
||||
|
||||
$: viewOptions = getViewOptions(viewlet)
|
||||
</script>
|
||||
|
||||
<ActionContext
|
||||
@ -87,7 +95,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="ac-header-full" class:secondRow={twoRows}>
|
||||
<ViewletSettingButton {viewlet} />
|
||||
<ViewletSettingButton bind:viewOptions {viewlet} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
||||
import { createQuery, getClient, UsersPopup } from '@hcengineering/presentation'
|
||||
import { Button, eventToHTMLElement, IconAdd, Label, Scroller, showPopup } from '@hcengineering/ui'
|
||||
import view, { Viewlet, ViewletPreference } from '@hcengineering/view'
|
||||
import { Table, ViewletSettingButton } from '@hcengineering/view-resources'
|
||||
import { getViewOptions, Table, ViewletSettingButton } from '@hcengineering/view-resources'
|
||||
import hr from '../plugin'
|
||||
import { addMember } from '../utils'
|
||||
|
||||
@ -91,6 +91,8 @@
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
$: viewOptions = getViewOptions(descr)
|
||||
</script>
|
||||
|
||||
<div class="antiSection">
|
||||
@ -99,7 +101,7 @@
|
||||
<Label label={hr.string.Members} />
|
||||
</span>
|
||||
<div class="buttons-group xsmall-gap">
|
||||
<ViewletSettingButton viewlet={descr} />
|
||||
<ViewletSettingButton bind:viewOptions viewlet={descr} />
|
||||
<Button id={hr.string.AddEmployee} icon={IconAdd} kind={'transparent'} shape={'circle'} on:click={add} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -20,7 +20,7 @@
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Button, Label, Loading, Scroller, tableSP } from '@hcengineering/ui'
|
||||
import view, { BuildModelKey, Viewlet, ViewletPreference } from '@hcengineering/view'
|
||||
import { Table, ViewletSettingButton } from '@hcengineering/view-resources'
|
||||
import { getViewOptions, Table, ViewletSettingButton } from '@hcengineering/view-resources'
|
||||
import hr from '../../plugin'
|
||||
import {
|
||||
EmployeeReports,
|
||||
@ -215,6 +215,8 @@
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
$: viewOptions = getViewOptions(descr)
|
||||
</script>
|
||||
|
||||
{#if departmentStaff.length}
|
||||
@ -226,7 +228,7 @@
|
||||
{:else}
|
||||
<div class="flex-row-center flex-reverse">
|
||||
<div class="ml-1">
|
||||
<ViewletSettingButton viewlet={descr} />
|
||||
<ViewletSettingButton bind:viewOptions viewlet={descr} />
|
||||
</div>
|
||||
<Button
|
||||
label={getEmbeddedLabel('Export')}
|
||||
|
@ -22,7 +22,7 @@
|
||||
import { Vacancy } from '@hcengineering/recruit'
|
||||
import { FullDescriptionBox } from '@hcengineering/text-editor'
|
||||
import tracker from '@hcengineering/tracker'
|
||||
import { Button, Component, EditBox, Grid, Icon, IconAdd, IconMoreH, Label, showPopup } from '@hcengineering/ui'
|
||||
import { Button, Component, EditBox, Grid, IconMoreH, showPopup } from '@hcengineering/ui'
|
||||
import { ClassAttributeBar, ContextMenu } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import recruit from '../plugin'
|
||||
@ -131,33 +131,7 @@
|
||||
/>
|
||||
<!-- <MembersBox label={recruit.string.Members} space={object} /> -->
|
||||
|
||||
<div class="antiSection">
|
||||
<div class="antiSection-header">
|
||||
<div class="antiSection-header__icon">
|
||||
<Icon icon={tracker.icon.Issue} size={'small'} />
|
||||
</div>
|
||||
<span class="antiSection-header__title">
|
||||
<Label label={recruit.string.RelatedIssues} />
|
||||
</span>
|
||||
<div class="buttons-group small-gap">
|
||||
<Button
|
||||
id="add-sub-issue"
|
||||
width="min-content"
|
||||
icon={IconAdd}
|
||||
label={undefined}
|
||||
labelParams={{ subIssues: 0 }}
|
||||
kind={'transparent'}
|
||||
size={'small'}
|
||||
on:click={() => {
|
||||
showPopup(tracker.component.CreateIssue, { relatedTo: object, space: object.space }, 'top')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<Component is={tracker.component.RelatedIssues} props={{ object }} />
|
||||
</div>
|
||||
</div></Grid
|
||||
>
|
||||
<Component is={tracker.component.RelatedIssuesSection} props={{ object, label: recruit.string.RelatedIssues }} />
|
||||
</Grid>
|
||||
</Panel>
|
||||
{/if}
|
||||
|
@ -18,7 +18,7 @@
|
||||
import { Vacancy } from '@hcengineering/recruit'
|
||||
import { Button, Icon, IconAdd, Label, Loading, SearchEdit, showPopup } from '@hcengineering/ui'
|
||||
import view, { BuildModelKey, Viewlet, ViewletPreference } from '@hcengineering/view'
|
||||
import { FilterButton, TableBrowser, ViewletSettingButton } from '@hcengineering/view-resources'
|
||||
import { FilterButton, getViewOptions, TableBrowser, ViewletSettingButton } from '@hcengineering/view-resources'
|
||||
import recruit from '../plugin'
|
||||
import CreateVacancy from './CreateVacancy.svelte'
|
||||
import { deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
|
||||
@ -132,6 +132,8 @@
|
||||
}
|
||||
|
||||
$: twoRows = $deviceInfo.twoRows
|
||||
|
||||
$: viewOptions = getViewOptions(descr)
|
||||
</script>
|
||||
|
||||
<div class="ac-header withSettings" class:full={!twoRows} class:mini={twoRows}>
|
||||
@ -156,7 +158,7 @@
|
||||
kind={'primary'}
|
||||
on:click={showCreateDialog}
|
||||
/>
|
||||
<ViewletSettingButton viewlet={descr} />
|
||||
<ViewletSettingButton bind:viewOptions viewlet={descr} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -9,6 +9,7 @@
|
||||
"build:docs": "api-extractor run --local",
|
||||
"lint": "svelte-check && eslint",
|
||||
"lint:fix": "eslint --fix src",
|
||||
"svelte-check": "svelte-check",
|
||||
"format": "prettier --write --plugin-search-dir=. src && eslint --fix src"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -91,6 +91,7 @@
|
||||
<div class="ap-scroll">
|
||||
<div class="ap-box">
|
||||
{#await getItems() then items}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="ap-menuItem flex-row-center"
|
||||
on:click={() => {
|
||||
|
@ -27,7 +27,7 @@
|
||||
refDocument: Doc,
|
||||
type: keyof typeof relations,
|
||||
operation: '$push' | '$pull',
|
||||
placeholder: IntlString
|
||||
_: IntlString
|
||||
) {
|
||||
const prop = type === 'isBlocking' ? 'blockedBy' : type
|
||||
if (type !== 'isBlocking') {
|
||||
@ -69,7 +69,7 @@
|
||||
await update(value, type, docs, label)
|
||||
}
|
||||
|
||||
const makeAddAction = (type: keyof typeof relations, placeholder: IntlString) => async (props: any, evt: Event) => {
|
||||
const makeAddAction = (type: keyof typeof relations, placeholder: IntlString) => async () => {
|
||||
closePopup('popup')
|
||||
showPopup(
|
||||
ObjectSearchPopup,
|
||||
@ -81,7 +81,7 @@
|
||||
}
|
||||
)
|
||||
}
|
||||
async function removeRelation (evt: MouseEvent) {
|
||||
async function removeRelation () {
|
||||
closePopup('popup')
|
||||
showPopup(
|
||||
ObjectSearchPopup,
|
||||
|
@ -44,14 +44,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: tooltipValue = new Date(value).toLocaleString('default', {
|
||||
minute: '2-digit',
|
||||
hour: 'numeric',
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
})
|
||||
|
||||
$: formatTime(value)
|
||||
</script>
|
||||
|
||||
|
@ -1,22 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition'
|
||||
import {
|
||||
NotificationSeverity,
|
||||
Notification,
|
||||
Button,
|
||||
Icon,
|
||||
IconClose,
|
||||
IconInfo,
|
||||
IconCheckCircle,
|
||||
Label,
|
||||
showPanel
|
||||
} from '@hcengineering/ui'
|
||||
import { copyTextToClipboard, createQuery } from '@hcengineering/presentation'
|
||||
import { Issue, IssueStatus } from '@hcengineering/tracker'
|
||||
import {
|
||||
AnySvelteComponent,
|
||||
Button,
|
||||
Icon,
|
||||
IconCheckCircle,
|
||||
IconClose,
|
||||
IconInfo,
|
||||
Notification,
|
||||
NotificationSeverity,
|
||||
showPanel
|
||||
} from '@hcengineering/ui'
|
||||
import { fade } from 'svelte/transition'
|
||||
|
||||
import IssueStatusIcon from './IssueStatusIcon.svelte'
|
||||
import IssuePresenter from './IssuePresenter.svelte'
|
||||
import tracker from '../../plugin'
|
||||
import IssuePresenter from './IssuePresenter.svelte'
|
||||
import IssueStatusIcon from './IssueStatusIcon.svelte'
|
||||
|
||||
export let notification: Notification
|
||||
export let onRemove: () => void
|
||||
@ -31,7 +31,7 @@
|
||||
|
||||
$: issueQuery.query(
|
||||
tracker.class.Issue,
|
||||
{ _id: params.issueId },
|
||||
{ _id: params?.issueId },
|
||||
(res) => {
|
||||
issue = res[0]
|
||||
},
|
||||
@ -49,7 +49,7 @@
|
||||
)
|
||||
}
|
||||
|
||||
const getIcon = () => {
|
||||
const getIcon = (): AnySvelteComponent | undefined => {
|
||||
switch (severity) {
|
||||
case NotificationSeverity.Success:
|
||||
return IconCheckCircle
|
||||
@ -84,14 +84,17 @@
|
||||
copyTextToClipboard(params?.issueUrl)
|
||||
}
|
||||
}
|
||||
$: icon = getIcon()
|
||||
</script>
|
||||
|
||||
<div class="root" in:fade out:fade>
|
||||
<Icon icon={getIcon()} size="medium" fill={getIconColor()} />
|
||||
{#if icon}
|
||||
<Icon {icon} size="medium" fill={getIconColor()} />
|
||||
{/if}
|
||||
|
||||
<div class="content">
|
||||
<div class="title">
|
||||
<Label label={title} />
|
||||
{title}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="issue">
|
||||
@ -105,7 +108,7 @@
|
||||
{subTitle}
|
||||
</div>
|
||||
<div class="postfix">
|
||||
{params.subTitlePostfix}
|
||||
{params?.subTitlePostfix}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { WithLookup } from '@hcengineering/core'
|
||||
import { Ref, WithLookup } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import type { Issue, Team } from '@hcengineering/tracker'
|
||||
import { showPanel } from '@hcengineering/ui'
|
||||
@ -23,6 +23,9 @@
|
||||
export let disableClick = false
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
|
||||
// Extra properties
|
||||
export let teams: Map<Ref<Team>, Team> | undefined = undefined
|
||||
|
||||
function handleIssueEditorOpened () {
|
||||
if (disableClick) {
|
||||
return
|
||||
@ -38,16 +41,21 @@
|
||||
const spaceQuery = createQuery()
|
||||
let currentTeam: Team | undefined = value?.$lookup?.space
|
||||
|
||||
$: if (value && value?.$lookup?.space === undefined) {
|
||||
spaceQuery.query(tracker.class.Team, { _id: value.space }, (res) => ([currentTeam] = res))
|
||||
$: if (teams === undefined) {
|
||||
if (value && value?.$lookup?.space === undefined) {
|
||||
spaceQuery.query(tracker.class.Team, { _id: value.space }, (res) => ([currentTeam] = res))
|
||||
} else {
|
||||
spaceQuery.unsubscribe()
|
||||
}
|
||||
} else {
|
||||
spaceQuery.unsubscribe()
|
||||
currentTeam = teams.get(value.space)
|
||||
}
|
||||
|
||||
$: title = currentTeam ? `${currentTeam.identifier}-${value?.number}` : `${value?.number}`
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<span
|
||||
class="issuePresenterRoot"
|
||||
class:noPointer={disableClick}
|
||||
|
@ -48,7 +48,7 @@
|
||||
async function updateStatus (
|
||||
txes: Tx[],
|
||||
statuses: Map<Ref<IssueStatus>, WithLookup<IssueStatus>>,
|
||||
now: number
|
||||
_: number
|
||||
): Promise<void> {
|
||||
const result: WithTime[] = []
|
||||
|
||||
|
@ -24,6 +24,8 @@
|
||||
export let size: IconSize
|
||||
export let fill: string | undefined = undefined
|
||||
|
||||
export let issueStatuses: IssueStatus[] | undefined = undefined
|
||||
|
||||
const dynamicFillCategories = [tracker.issueStatusCategory.Started]
|
||||
|
||||
const client = getClient()
|
||||
@ -36,12 +38,19 @@
|
||||
} = { index: undefined, count: undefined }
|
||||
|
||||
const categoriesQuery = createQuery()
|
||||
categoriesQuery.query(
|
||||
tracker.class.IssueStatus,
|
||||
{ category: tracker.issueStatusCategory.Started },
|
||||
(res) => (statuses = res),
|
||||
{ sort: { rank: SortingOrder.Ascending } }
|
||||
)
|
||||
|
||||
$: if (issueStatuses === undefined) {
|
||||
categoriesQuery.query(
|
||||
tracker.class.IssueStatus,
|
||||
{ category: tracker.issueStatusCategory.Started },
|
||||
(res) => (statuses = res),
|
||||
{ sort: { rank: SortingOrder.Ascending } }
|
||||
)
|
||||
} else {
|
||||
const _s = [...issueStatuses.filter((it) => it.category === tracker.issueStatusCategory.Started)]
|
||||
_s.sort((a, b) => a.rank.localeCompare(b.rank))
|
||||
categoriesQuery.unsubscribe()
|
||||
}
|
||||
|
||||
async function updateCategory (status: WithLookup<IssueStatus>, statuses: IssueStatus[]) {
|
||||
if (status.$lookup?.category) {
|
||||
|
@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
|
||||
import { Issue } from '@hcengineering/tracker'
|
||||
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
||||
import { Component } from '@hcengineering/ui'
|
||||
import { Viewlet } from '@hcengineering/view'
|
||||
import { Viewlet, ViewOptions } from '@hcengineering/view'
|
||||
import tracker from '../../plugin'
|
||||
import CreateIssue from '../CreateIssue.svelte'
|
||||
|
||||
@ -10,6 +10,11 @@
|
||||
export let query: DocumentQuery<Issue> = {}
|
||||
export let space: Ref<Space> | undefined
|
||||
|
||||
// Extra properties
|
||||
export let teams: Map<Ref<Team>, Team> | undefined
|
||||
export let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]>
|
||||
export let viewOptions: ViewOptions
|
||||
|
||||
const createItemDialog = CreateIssue
|
||||
const createItemLabel = tracker.string.AddIssueTooltip
|
||||
</script>
|
||||
@ -24,9 +29,11 @@
|
||||
createItemDialog,
|
||||
createItemLabel,
|
||||
viewlet,
|
||||
viewOptions: viewlet.viewOptions?.other,
|
||||
viewOptions,
|
||||
viewOptionsConfig: viewlet.viewOptions?.other,
|
||||
space,
|
||||
query
|
||||
query,
|
||||
props: { teams, issueStatuses }
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
@ -45,7 +45,7 @@
|
||||
$: groupedByProject = getGroupedIssues('project', issues)
|
||||
$: groupedBySprint = getGroupedIssues('sprint', issues)
|
||||
|
||||
const handleStatusFilterMenuSectionOpened = (event: MouseEvent | KeyboardEvent) => {
|
||||
const handleStatusFilterMenuSectionOpened = () => {
|
||||
const statusGroups: { [key: string]: number } = {}
|
||||
|
||||
for (const status of defaultStatuses) {
|
||||
@ -66,7 +66,7 @@
|
||||
)
|
||||
}
|
||||
|
||||
const handlePriorityFilterMenuSectionOpened = (event: MouseEvent | KeyboardEvent) => {
|
||||
const handlePriorityFilterMenuSectionOpened = () => {
|
||||
const priorityGroups: { [key: string]: number } = {}
|
||||
|
||||
for (const priority of defaultPriorities) {
|
||||
@ -86,7 +86,7 @@
|
||||
)
|
||||
}
|
||||
|
||||
const handleProjectFilterMenuSectionOpened = (event: MouseEvent | KeyboardEvent) => {
|
||||
const handleProjectFilterMenuSectionOpened = () => {
|
||||
const projectGroups: { [key: string]: number } = {}
|
||||
|
||||
for (const [project, value] of Object.entries(groupedByProject)) {
|
||||
@ -105,7 +105,7 @@
|
||||
)
|
||||
}
|
||||
|
||||
const handleSprintFilterMenuSectionOpened = (event: MouseEvent | KeyboardEvent) => {
|
||||
const handleSprintFilterMenuSectionOpened = () => {
|
||||
const sprintGroups: { [key: string]: number } = {}
|
||||
|
||||
for (const [project, value] of Object.entries(groupedBySprint)) {
|
||||
|
@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
|
||||
import { DocumentQuery, Ref, SortingOrder, Space, WithLookup } from '@hcengineering/core'
|
||||
import { IntlString, translate } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Issue } from '@hcengineering/tracker'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
||||
import { Button, IconDetails, IconDetailsFilled } from '@hcengineering/ui'
|
||||
import view, { Viewlet } from '@hcengineering/view'
|
||||
import { FilterBar, getActiveViewletId } from '@hcengineering/view-resources'
|
||||
import { FilterBar, getActiveViewletId, getViewOptions } from '@hcengineering/view-resources'
|
||||
import ViewletSettingButton from '@hcengineering/view-resources/src/components/ViewletSettingButton.svelte'
|
||||
import tracker from '../../plugin'
|
||||
import IssuesContent from './IssuesContent.svelte'
|
||||
@ -36,7 +36,7 @@
|
||||
async function update (): Promise<void> {
|
||||
viewlets = await client.findAll(
|
||||
view.class.Viewlet,
|
||||
{ attachTo: tracker.class.Issue },
|
||||
{ attachTo: tracker.class.Issue, variant: { $ne: 'subissue' } },
|
||||
{
|
||||
lookup: {
|
||||
descriptor: view.class.ViewletDescriptor
|
||||
@ -63,6 +63,41 @@
|
||||
let docSize: boolean = false
|
||||
$: if (docWidth <= 900 && !docSize) docSize = true
|
||||
$: if (docWidth > 900 && docSize) docSize = false
|
||||
|
||||
const teamQuery = createQuery()
|
||||
|
||||
let _teams: Map<Ref<Team>, Team> | undefined = undefined
|
||||
let _result: any
|
||||
$: teamQuery.query(tracker.class.Team, {}, (result) => {
|
||||
_result = JSON.stringify(result, undefined, 2)
|
||||
console.log('#RESULT 124', _result)
|
||||
const t = new Map<Ref<Team>, Team>()
|
||||
for (const r of result) {
|
||||
t.set(r._id, r)
|
||||
}
|
||||
_teams = t
|
||||
})
|
||||
|
||||
let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]>
|
||||
|
||||
const statusesQuery = createQuery()
|
||||
statusesQuery.query(
|
||||
tracker.class.IssueStatus,
|
||||
{},
|
||||
(statuses) => {
|
||||
const st = new Map<Ref<Team>, WithLookup<IssueStatus>[]>()
|
||||
for (const s of statuses) {
|
||||
const id = s.attachedTo as Ref<Team>
|
||||
st.set(id, [...(st.get(id) ?? []), s])
|
||||
}
|
||||
issueStatuses = st
|
||||
},
|
||||
{
|
||||
lookup: { category: tracker.class.IssueStatusCategory },
|
||||
sort: { rank: SortingOrder.Ascending }
|
||||
}
|
||||
)
|
||||
$: viewOptions = getViewOptions(viewlet)
|
||||
</script>
|
||||
|
||||
<IssuesHeader {viewlets} {label} bind:viewlet bind:search showLabelSelector={$$slots.label_selector}>
|
||||
@ -71,7 +106,7 @@
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="extra">
|
||||
{#if viewlet}
|
||||
<ViewletSettingButton {viewlet} />
|
||||
<ViewletSettingButton bind:viewOptions {viewlet} />
|
||||
{/if}
|
||||
{#if asideFloat && $$slots.aside}
|
||||
<div class="buttons-divider" />
|
||||
@ -90,8 +125,8 @@
|
||||
<slot name="afterHeader" />
|
||||
<FilterBar _class={tracker.class.Issue} query={searchQuery} on:change={(e) => (resultQuery = e.detail)} />
|
||||
<div class="flex w-full h-full clear-mins">
|
||||
{#if viewlet}
|
||||
<IssuesContent {viewlet} query={resultQuery} {space} />
|
||||
{#if viewlet && _teams && issueStatuses}
|
||||
<IssuesContent {viewlet} query={resultQuery} {space} teams={_teams} {issueStatuses} {viewOptions} />
|
||||
{/if}
|
||||
{#if $$slots.aside !== undefined && asideShown}
|
||||
<div class="popupPanel-body__aside flex" class:float={asideFloat} class:shown={asideShown}>
|
||||
|
@ -37,8 +37,7 @@
|
||||
ListSelectionProvider,
|
||||
noCategory,
|
||||
SelectDirection,
|
||||
selectionStore,
|
||||
viewOptionsStore
|
||||
selectionStore
|
||||
} from '@hcengineering/view-resources'
|
||||
import ActionContext from '@hcengineering/view-resources/src/components/ActionContext.svelte'
|
||||
import Menu from '@hcengineering/view-resources/src/components/Menu.svelte'
|
||||
@ -59,11 +58,12 @@
|
||||
export let space: Ref<Team> | undefined = undefined
|
||||
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
|
||||
export let query: DocumentQuery<Issue> = {}
|
||||
export let viewOptions: ViewOptionModel[] | undefined
|
||||
export let viewOptionsConfig: ViewOptionModel[] | undefined
|
||||
export let viewOptions: ViewOptions
|
||||
|
||||
$: currentSpace = space || tracker.team.DefaultTeam
|
||||
$: groupBy = ($viewOptionsStore.groupBy ?? noCategory) as IssuesGrouping
|
||||
$: orderBy = $viewOptionsStore.orderBy
|
||||
$: groupBy = (viewOptions.groupBy ?? noCategory) as IssuesGrouping
|
||||
$: orderBy = viewOptions.orderBy
|
||||
$: sort = { [orderBy[0]]: orderBy[1] }
|
||||
$: dontUpdateRank = orderBy[0] !== IssuesOrdering.Manual
|
||||
|
||||
@ -76,7 +76,7 @@
|
||||
})
|
||||
|
||||
let resultQuery: DocumentQuery<any> = query
|
||||
$: getResultQuery(query, viewOptions, $viewOptionsStore).then((p) => (resultQuery = p))
|
||||
$: getResultQuery(query, viewOptionsConfig, viewOptions).then((p) => (resultQuery = p))
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
@ -188,6 +188,7 @@
|
||||
mode: 'browser'
|
||||
}}
|
||||
/>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<Kanban
|
||||
bind:this={kanbanUI}
|
||||
_class={tracker.class.Issue}
|
||||
|
@ -38,6 +38,7 @@
|
||||
<Spinner size="small" />
|
||||
{/if}
|
||||
<span class="overflow-label issue-title">{issue.title}</span>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="button-close"
|
||||
use:tooltip={{ label: tracker.string.RemoveParent, direction: 'bottom' }}
|
||||
|
@ -31,6 +31,7 @@
|
||||
<div class="root" style:max-width={maxWidth}>
|
||||
<span class="names">
|
||||
{#each value.parents as parentInfo}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<span class="name cursor-pointer" on:click={() => handleIssueEditorOpened(parentInfo)}
|
||||
>{parentInfo.parentTitle}</span
|
||||
>
|
||||
|
@ -73,6 +73,7 @@
|
||||
|
||||
{#if value}
|
||||
{#if kind === 'list' || kind === 'list-header'}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="priority-container" on:click={handlePriorityEditorOpened}>
|
||||
<div class="icon">
|
||||
{#if issuePriorities[value.priority]?.icon}<Icon icon={issuePriorities[value.priority]?.icon} {size} />{/if}
|
||||
|
@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import { AttachedData, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Issue, IssueStatus } from '@hcengineering/tracker'
|
||||
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
||||
import type { ButtonKind, ButtonSize } from '@hcengineering/ui'
|
||||
import { Button, eventToHTMLElement, SelectPopup, showPopup, TooltipAlignment } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
@ -34,6 +34,9 @@
|
||||
export let justify: 'left' | 'center' = 'left'
|
||||
export let width: string | undefined = undefined
|
||||
|
||||
// Extra properties
|
||||
export let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]> | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
const statusesQuery = createQuery()
|
||||
const dispatch = createEventDispatcher()
|
||||
@ -65,7 +68,7 @@
|
||||
|
||||
$: selectedStatus = statuses?.find((status) => status._id === value.status) ?? statuses?.[0]
|
||||
$: selectedStatusLabel = shouldShowLabel ? selectedStatus?.name : undefined
|
||||
$: statusesInfo = statuses?.map((s, i) => {
|
||||
$: statusesInfo = statuses?.map((s) => {
|
||||
return {
|
||||
id: s._id,
|
||||
component: StatusPresenter,
|
||||
@ -74,18 +77,23 @@
|
||||
}
|
||||
})
|
||||
$: if (!statuses) {
|
||||
const query = '_id' in value ? { attachedTo: value.space } : {}
|
||||
statusesQuery.query(
|
||||
tracker.class.IssueStatus,
|
||||
query,
|
||||
(result) => {
|
||||
statuses = result
|
||||
},
|
||||
{
|
||||
lookup: { category: tracker.class.IssueStatusCategory },
|
||||
sort: { rank: SortingOrder.Ascending }
|
||||
}
|
||||
)
|
||||
statuses = '_id' in value ? issueStatuses?.get(value.space) : undefined
|
||||
if (statuses === undefined) {
|
||||
const query = '_id' in value ? { attachedTo: value.space } : {}
|
||||
statusesQuery.query(
|
||||
tracker.class.IssueStatus,
|
||||
query,
|
||||
(result) => {
|
||||
statuses = result
|
||||
},
|
||||
{
|
||||
lookup: { category: tracker.class.IssueStatusCategory },
|
||||
sort: { rank: SortingOrder.Ascending }
|
||||
}
|
||||
)
|
||||
} else {
|
||||
statusesQuery.unsubscribe()
|
||||
}
|
||||
}
|
||||
$: smallgap = size === 'inline' || size === 'small'
|
||||
</script>
|
||||
@ -95,7 +103,11 @@
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="flex-row-center flex-no-shrink" class:cursor-pointer={isEditable} on:click={handleStatusEditorOpened}>
|
||||
<div class="flex-center flex-no-shrink square-4">
|
||||
{#if selectedStatus}<IssueStatusIcon value={selectedStatus} size={kind === 'list' ? 'inline' : 'medium'} />{/if}
|
||||
{#if selectedStatus}<IssueStatusIcon
|
||||
value={selectedStatus}
|
||||
issueStatuses={statuses}
|
||||
size={kind === 'list' ? 'inline' : 'medium'}
|
||||
/>{/if}
|
||||
</div>
|
||||
{#if selectedStatusLabel}
|
||||
<span
|
||||
|
@ -14,12 +14,13 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Issue } from '@hcengineering/tracker'
|
||||
import ParentNamesPresenter from './ParentNamesPresenter.svelte'
|
||||
import tracker from '../../plugin'
|
||||
import { showPanel } from '@hcengineering/ui'
|
||||
import tracker from '../../plugin'
|
||||
import ParentNamesPresenter from './ParentNamesPresenter.svelte'
|
||||
|
||||
export let value: Issue
|
||||
export let shouldUseMargin: boolean = false
|
||||
export let showParent = true
|
||||
|
||||
function handleIssueEditorOpened () {
|
||||
showPanel(tracker.component.EditIssue, value._id, value._class, 'content')
|
||||
@ -28,12 +29,15 @@
|
||||
|
||||
{#if value}
|
||||
<span class="titlePresenter-container" class:with-margin={shouldUseMargin} title={value.title}>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<span
|
||||
class="name overflow-label cursor-pointer"
|
||||
style={`max-width: ${value.parents.length !== 0 ? 95 : 100}%`}
|
||||
style:max-width={showParent ? `${value.parents.length !== 0 ? 95 : 100}%` : '100%'}
|
||||
on:click={handleIssueEditorOpened}>{value.title}</span
|
||||
>
|
||||
<ParentNamesPresenter {value} />
|
||||
{#if showParent}
|
||||
<ParentNamesPresenter {value} />
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
|
@ -241,6 +241,7 @@
|
||||
<span class="title select-text">{title}</span>
|
||||
<div class="mt-6 description-preview select-text">
|
||||
{#if isDescriptionEmpty}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="placeholder" on:click={edit}>
|
||||
<Label label={tracker.string.IssueDescriptionPlaceholder} />
|
||||
</div>
|
||||
|
@ -13,65 +13,20 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref, WithLookup } from '@hcengineering/core'
|
||||
import { DocumentQuery, Ref, WithLookup } from '@hcengineering/core'
|
||||
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
||||
import { getEventPositionElement, showPanel, showPopup } from '@hcengineering/ui'
|
||||
import { ActionContext, ContextMenu, FixedColumn } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { flip } from 'svelte/animate'
|
||||
import { getIssueId } from '../../../issues'
|
||||
import { Viewlet, ViewOptions } from '@hcengineering/view'
|
||||
import { ActionContext, List } from '@hcengineering/view-resources'
|
||||
import tracker from '../../../plugin'
|
||||
import { subIssueListProvider } from '../../../utils'
|
||||
import Circles from '../../icons/Circles.svelte'
|
||||
import AssigneeEditor from '../AssigneeEditor.svelte'
|
||||
import DueDateEditor from '../DueDateEditor.svelte'
|
||||
import PriorityEditor from '../PriorityEditor.svelte'
|
||||
import StatusEditor from '../StatusEditor.svelte'
|
||||
import EstimationEditor from '../timereport/EstimationEditor.svelte'
|
||||
import SubIssuesSelector from './SubIssuesSelector.svelte'
|
||||
|
||||
export let issues: Issue[]
|
||||
export let query: DocumentQuery<Issue> | undefined = undefined
|
||||
export let issues: Issue[] | undefined = undefined
|
||||
export let viewlet: Viewlet
|
||||
export let viewOptions: ViewOptions
|
||||
|
||||
export let teams: Map<Ref<Team>, Team>
|
||||
// Extra properties
|
||||
export let teams: Map<Ref<Team>, Team> | undefined
|
||||
export let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]>
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let draggingIndex: number | null = null
|
||||
let hoveringIndex: number | null = null
|
||||
|
||||
function openIssue (target: Issue) {
|
||||
dispatch('issue-focus', target)
|
||||
subIssueListProvider(issues, target._id)
|
||||
showPanel(tracker.component.EditIssue, target._id, target._class, 'content')
|
||||
}
|
||||
|
||||
function resetDrag () {
|
||||
draggingIndex = null
|
||||
hoveringIndex = null
|
||||
}
|
||||
|
||||
function handleDragStart (ev: DragEvent, index: number) {
|
||||
if (ev.dataTransfer) {
|
||||
ev.dataTransfer.effectAllowed = 'move'
|
||||
ev.dataTransfer.dropEffect = 'move'
|
||||
draggingIndex = index
|
||||
}
|
||||
}
|
||||
|
||||
function handleDrop (ev: DragEvent, toIndex: number) {
|
||||
if (ev.dataTransfer && draggingIndex !== null && toIndex !== draggingIndex) {
|
||||
ev.dataTransfer.dropEffect = 'move'
|
||||
|
||||
dispatch('move', { fromIndex: draggingIndex, toIndex })
|
||||
}
|
||||
|
||||
resetDrag()
|
||||
}
|
||||
|
||||
function showContextMenu (ev: MouseEvent, object: Issue) {
|
||||
showPopup(ContextMenu, { object }, getEventPositionElement(ev))
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionContext
|
||||
@ -80,136 +35,15 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
{#each issues as issue, index (issue._id)}
|
||||
{@const currentTeam = teams.get(issue.space)}
|
||||
{@const openIssueCall = () => openIssue(issue)}
|
||||
<div
|
||||
class="flex-between row"
|
||||
class:is-dragging={index === draggingIndex}
|
||||
class:is-dragged-over-up={draggingIndex !== null && index < draggingIndex && index === hoveringIndex}
|
||||
class:is-dragged-over-down={draggingIndex !== null && index > draggingIndex && index === hoveringIndex}
|
||||
animate:flip={{ duration: 400 }}
|
||||
draggable={true}
|
||||
on:click|self={openIssueCall}
|
||||
on:contextmenu|preventDefault={(ev) => showContextMenu(ev, issue)}
|
||||
on:dragstart={(ev) => handleDragStart(ev, index)}
|
||||
on:dragover|preventDefault={() => false}
|
||||
on:dragenter={() => (hoveringIndex = index)}
|
||||
on:drop|preventDefault={(ev) => handleDrop(ev, index)}
|
||||
on:dragend={resetDrag}
|
||||
>
|
||||
<div class="draggable-container">
|
||||
<div class="draggable-mark"><Circles /></div>
|
||||
</div>
|
||||
<div class="flex-row-center ml-6 clear-mins gap-2">
|
||||
<PriorityEditor value={issue} isEditable kind={'list'} size={'small'} justify={'center'} />
|
||||
<span class="issuePresenter" on:click={openIssueCall}>
|
||||
<FixedColumn key={'subissue_issue'} justify={'left'}>
|
||||
{#if currentTeam}
|
||||
{getIssueId(currentTeam, issue)}
|
||||
{/if}
|
||||
</FixedColumn>
|
||||
</span>
|
||||
<StatusEditor
|
||||
value={issue}
|
||||
statuses={issueStatuses.get(issue.space)}
|
||||
justify="center"
|
||||
kind={'list'}
|
||||
size={'small'}
|
||||
tooltipAlignment="bottom"
|
||||
/>
|
||||
<span class="text name" title={issue.title} on:click={openIssueCall}>
|
||||
{issue.title}
|
||||
</span>
|
||||
{#if issue.subIssues > 0}
|
||||
<SubIssuesSelector value={issue} {currentTeam} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-center flex-no-shrink">
|
||||
<EstimationEditor value={issue} kind={'list'} />
|
||||
{#if issue.dueDate !== null}
|
||||
<DueDateEditor value={issue} />
|
||||
{/if}
|
||||
<AssigneeEditor value={issue} />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<style lang="scss">
|
||||
.row {
|
||||
position: relative;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
|
||||
.text {
|
||||
font-weight: 500;
|
||||
color: var(--caption-color);
|
||||
}
|
||||
|
||||
.issuePresenter {
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
font-weight: 500;
|
||||
color: var(--content-color);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--caption-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
&:active {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.draggable-container {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 1.5rem;
|
||||
cursor: grabbing;
|
||||
|
||||
.draggable-mark {
|
||||
opacity: 0;
|
||||
width: 0.375rem;
|
||||
height: 1rem;
|
||||
margin-left: 0.75rem;
|
||||
transition: opacity 0.1s;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.draggable-mark {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-dragging::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
background-color: var(--theme-bg-color);
|
||||
opacity: 0.4;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
&.is-dragged-over-up::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
inset: 0;
|
||||
border-top: 1px solid var(--theme-bg-check);
|
||||
}
|
||||
&.is-dragged-over-down::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
inset: 0;
|
||||
border-bottom: 1px solid var(--theme-bg-check);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{#if viewlet}
|
||||
<List
|
||||
_class={tracker.class.Issue}
|
||||
{viewOptions}
|
||||
viewOptionsConfig={viewlet.viewOptions?.other}
|
||||
config={viewlet.config}
|
||||
documents={issues}
|
||||
{query}
|
||||
flatHeaders={true}
|
||||
props={{ teams, issueStatuses }}
|
||||
/>
|
||||
{/if}
|
||||
|
@ -92,7 +92,7 @@
|
||||
|
||||
$: areSubIssuesLoading = !subIssues
|
||||
$: parentIssue = issue.$lookup?.attachedTo ? (issue.$lookup?.attachedTo as Issue) : null
|
||||
$: if (parentIssue) {
|
||||
$: if (parentIssue && parentIssue.subIssues > 0) {
|
||||
subIssuesQeury.query(
|
||||
tracker.class.Issue,
|
||||
{ space: issue.space, attachedTo: parentIssue._id },
|
||||
@ -113,6 +113,7 @@
|
||||
{#if parentIssue}
|
||||
<div class="flex root">
|
||||
<div class="item clear-mins">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="flex-center parent-issue cursor-pointer"
|
||||
use:tooltip={{ label: tracker.string.OpenParent, direction: 'bottom' }}
|
||||
@ -136,6 +137,7 @@
|
||||
<Spinner size="small" />
|
||||
</div>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
bind:this={subIssuesElement}
|
||||
class="flex-center sub-issues cursor-pointer"
|
||||
|
@ -14,9 +14,11 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref, SortingOrder, WithLookup } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { calcRank, Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
||||
import { Button, Spinner, ExpandCollapse, closeTooltip, IconAdd, Chevron, Label } from '@hcengineering/ui'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
||||
import { Button, Chevron, closeTooltip, ExpandCollapse, IconAdd, Label } from '@hcengineering/ui'
|
||||
import view, { Viewlet } from '@hcengineering/view'
|
||||
import { getViewOptions, ViewletSettingButton } from '@hcengineering/view-resources'
|
||||
import tracker from '../../../plugin'
|
||||
import CreateSubIssue from './CreateSubIssue.svelte'
|
||||
import SubIssueList from './SubIssueList.svelte'
|
||||
@ -25,35 +27,54 @@
|
||||
export let teams: Map<Ref<Team>, Team>
|
||||
export let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]>
|
||||
|
||||
const subIssuesQuery = createQuery()
|
||||
const client = getClient()
|
||||
|
||||
let subIssues: Issue[] | undefined
|
||||
let isCollapsed = false
|
||||
let isCreating = false
|
||||
|
||||
async function handleIssueSwap (ev: CustomEvent<{ fromIndex: number; toIndex: number }>) {
|
||||
if (subIssues) {
|
||||
const { fromIndex, toIndex } = ev.detail
|
||||
const [prev, next] = [
|
||||
subIssues[fromIndex < toIndex ? toIndex : toIndex - 1],
|
||||
subIssues[fromIndex < toIndex ? toIndex + 1 : toIndex]
|
||||
]
|
||||
const issue = subIssues[fromIndex]
|
||||
$: hasSubIssues = issue.subIssues > 0
|
||||
|
||||
await client.update(issue, { rank: calcRank(prev, next) })
|
||||
}
|
||||
let viewlet: Viewlet | undefined
|
||||
|
||||
const query = createQuery()
|
||||
$: query.query(view.class.Viewlet, { _id: tracker.viewlet.SubIssues }, (res) => {
|
||||
;[viewlet] = res
|
||||
})
|
||||
|
||||
let _teams = teams
|
||||
let _issueStatuses = issueStatuses
|
||||
|
||||
const teamsQuery = createQuery()
|
||||
|
||||
$: if (teams === undefined) {
|
||||
teamsQuery.query(tracker.class.Team, {}, async (result) => {
|
||||
_teams = new Map(result.map((it) => [it._id, it]))
|
||||
})
|
||||
} else {
|
||||
teamsQuery.unsubscribe()
|
||||
}
|
||||
|
||||
$: hasSubIssues = issue.subIssues > 0
|
||||
$: subIssuesQuery.query(tracker.class.Issue, { attachedTo: issue._id }, async (result) => (subIssues = result), {
|
||||
sort: { rank: SortingOrder.Ascending },
|
||||
lookup: {
|
||||
_id: {
|
||||
subIssues: tracker.class.Issue
|
||||
const statusesQuery = createQuery()
|
||||
$: if (issueStatuses === undefined) {
|
||||
statusesQuery.query(
|
||||
tracker.class.IssueStatus,
|
||||
{},
|
||||
(statuses) => {
|
||||
const st = new Map<Ref<Team>, WithLookup<IssueStatus>[]>()
|
||||
for (const s of statuses) {
|
||||
const id = s.attachedTo as Ref<Team>
|
||||
st.set(id, [...(st.get(id) ?? []), s])
|
||||
}
|
||||
_issueStatuses = st
|
||||
},
|
||||
{
|
||||
lookup: { category: tracker.class.IssueStatusCategory },
|
||||
sort: { rank: SortingOrder.Ascending }
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
} else {
|
||||
statusesQuery.unsubscribe()
|
||||
}
|
||||
|
||||
$: viewOptions = viewlet !== undefined ? getViewOptions(viewlet) : undefined
|
||||
</script>
|
||||
|
||||
<div class="flex-between">
|
||||
@ -73,38 +94,42 @@
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
id="add-sub-issue"
|
||||
width="min-content"
|
||||
icon={hasSubIssues ? IconAdd : undefined}
|
||||
label={hasSubIssues ? undefined : tracker.string.AddSubIssues}
|
||||
labelParams={{ subIssues: 0 }}
|
||||
kind={'transparent'}
|
||||
size={'small'}
|
||||
showTooltip={{ label: tracker.string.AddSubIssues, props: { subIssues: 1 }, direction: 'bottom' }}
|
||||
on:click={() => {
|
||||
closeTooltip()
|
||||
isCreating = true
|
||||
isCollapsed = false
|
||||
}}
|
||||
/>
|
||||
<div class="flex-row-center">
|
||||
{#if viewlet && hasSubIssues && viewOptions}
|
||||
<ViewletSettingButton bind:viewOptions {viewlet} kind={'transparent'} />
|
||||
{/if}
|
||||
<Button
|
||||
id="add-sub-issue"
|
||||
width="min-content"
|
||||
icon={hasSubIssues ? IconAdd : undefined}
|
||||
label={hasSubIssues ? undefined : tracker.string.AddSubIssues}
|
||||
labelParams={{ subIssues: 0 }}
|
||||
kind={'transparent'}
|
||||
size={'small'}
|
||||
showTooltip={{ label: tracker.string.AddSubIssues, props: { subIssues: 1 }, direction: 'bottom' }}
|
||||
on:click={() => {
|
||||
closeTooltip()
|
||||
isCreating = true
|
||||
isCollapsed = false
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
{#if subIssues && issueStatuses}
|
||||
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
|
||||
{#if hasSubIssues}
|
||||
{#if issueStatuses}
|
||||
{#if hasSubIssues && viewOptions && viewlet}
|
||||
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
|
||||
<div class="list" class:collapsed={isCollapsed}>
|
||||
<SubIssueList
|
||||
issues={subIssues}
|
||||
{issueStatuses}
|
||||
{teams}
|
||||
on:issue-focus={() => (isCreating = false)}
|
||||
on:move={handleIssueSwap}
|
||||
teams={_teams}
|
||||
{viewlet}
|
||||
{viewOptions}
|
||||
issueStatuses={_issueStatuses}
|
||||
query={{ attachedTo: issue._id }}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</ExpandCollapse>
|
||||
</ExpandCollapse>
|
||||
{/if}
|
||||
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
|
||||
{#if isCreating}
|
||||
{@const team = teams.get(issue.space)}
|
||||
@ -121,10 +146,6 @@
|
||||
{/if}
|
||||
{/if}
|
||||
</ExpandCollapse>
|
||||
{:else}
|
||||
<div class="flex-center pt-3">
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
@ -17,10 +17,13 @@
|
||||
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { calcRank, Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
||||
import { Label, Spinner } from '@hcengineering/ui'
|
||||
import { Viewlet, ViewOptions } from '@hcengineering/view'
|
||||
import tracker from '../../../plugin'
|
||||
import SubIssueList from '../edit/SubIssueList.svelte'
|
||||
|
||||
export let object: Doc
|
||||
export let viewlet: Viewlet
|
||||
export let viewOptions: ViewOptions
|
||||
|
||||
let query: DocumentQuery<Issue>
|
||||
$: query = { 'relations._id': object._id, 'relations._class': object._class }
|
||||
@ -75,9 +78,9 @@
|
||||
</script>
|
||||
|
||||
<div class="mt-1">
|
||||
{#if subIssues !== undefined}
|
||||
{#if subIssues !== undefined && viewlet !== undefined}
|
||||
{#if issueStatuses.size > 0 && teams}
|
||||
<SubIssueList issues={subIssues} {teams} {issueStatuses} on:move={handleIssueSwap} />
|
||||
<SubIssueList bind:viewOptions {viewlet} issues={subIssues} {teams} {issueStatuses} on:move={handleIssueSwap} />
|
||||
{:else}
|
||||
<div class="p-1">
|
||||
<Label label={presentation.string.NoMatchesFound} />
|
||||
|
@ -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>
|
@ -139,7 +139,7 @@
|
||||
<Button
|
||||
icon={IconAdd}
|
||||
size={'small'}
|
||||
on:click={(event) => {
|
||||
on:click={() => {
|
||||
showPopup(
|
||||
TimeSpendReportPopup,
|
||||
{
|
||||
|
@ -14,11 +14,11 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact from '@hcengineering/contact'
|
||||
import { Doc, Ref } from '@hcengineering/core'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { UserBox } from '@hcengineering/presentation'
|
||||
import { Issue, Team } from '@hcengineering/tracker'
|
||||
import { getEventPositionElement, ListView, showPopup, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
|
||||
import { ContextMenu, FixedColumn, ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
|
||||
import { deviceOptionsStore as deviceInfo, getEventPositionElement, ListView, showPopup } from '@hcengineering/ui'
|
||||
import { ContextMenu, FixedColumn, ListSelectionProvider } from '@hcengineering/view-resources'
|
||||
import { getIssueId } from '../../../issues'
|
||||
import tracker from '../../../plugin'
|
||||
import EstimationEditor from './EstimationEditor.svelte'
|
||||
@ -31,7 +31,7 @@
|
||||
showPopup(ContextMenu, { object }, $deviceInfo.isMobile ? 'top' : getEventPositionElement(ev))
|
||||
}
|
||||
|
||||
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {})
|
||||
const listProvider = new ListSelectionProvider(() => {})
|
||||
$: twoRows = $deviceInfo.twoRows
|
||||
</script>
|
||||
|
||||
|
@ -53,6 +53,7 @@
|
||||
</script>
|
||||
|
||||
{#if kind === 'link'}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div id="ReportedTimeEditor" class="link-container flex-between" on:click={showReports}>
|
||||
{#if value !== undefined}
|
||||
<span class="overflow-label">
|
||||
|
@ -23,6 +23,7 @@
|
||||
export let value: number
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<span
|
||||
{id}
|
||||
class:link={kind === 'link'}
|
||||
|
@ -14,13 +14,18 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact from '@hcengineering/contact'
|
||||
import { Doc, Ref, Space, WithLookup } from '@hcengineering/core'
|
||||
import { Ref, Space, WithLookup } from '@hcengineering/core'
|
||||
import UserBox from '@hcengineering/presentation/src/components/UserBox.svelte'
|
||||
import { Team, TimeReportDayType, TimeSpendReport } from '@hcengineering/tracker'
|
||||
import { eventToHTMLElement, getEventPositionElement, ListView, showPopup } from '@hcengineering/ui'
|
||||
import { deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
|
||||
import {
|
||||
deviceOptionsStore as deviceInfo,
|
||||
eventToHTMLElement,
|
||||
getEventPositionElement,
|
||||
ListView,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import DatePresenter from '@hcengineering/ui/src/components/calendar/DatePresenter.svelte'
|
||||
import { ContextMenu, FixedColumn, ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
|
||||
import { ContextMenu, FixedColumn, ListSelectionProvider } from '@hcengineering/view-resources'
|
||||
import { getIssueId } from '../../../issues'
|
||||
import tracker from '../../../plugin'
|
||||
import TimePresenter from './TimePresenter.svelte'
|
||||
@ -34,7 +39,7 @@
|
||||
showPopup(ContextMenu, { object }, getEventPositionElement(ev))
|
||||
}
|
||||
|
||||
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {})
|
||||
const listProvider = new ListSelectionProvider(() => {})
|
||||
|
||||
const toTeamId = (ref: Ref<Space>) => ref as Ref<Team>
|
||||
|
||||
|
@ -25,9 +25,9 @@
|
||||
import ModeSelector from '../ModeSelector.svelte'
|
||||
|
||||
const config: [string, IntlString, object][] = [
|
||||
['assigned', tracker.string.Assigned],
|
||||
['assigned', tracker.string.Assigned, {}],
|
||||
['created', tracker.string.Created, { value: 0 }],
|
||||
['subscribed', tracker.string.Subscribed]
|
||||
['subscribed', tracker.string.Subscribed, {}]
|
||||
]
|
||||
const currentUser = getCurrentAccount() as EmployeeAccount
|
||||
const assigned = { assignee: currentUser.employee }
|
||||
|
@ -86,12 +86,3 @@
|
||||
<DatePresenter bind:value={object.targetDate} labelNull={tracker.string.TargetDate} editable />
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
|
||||
<style>
|
||||
.label {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.description {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
|
@ -27,6 +27,7 @@
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="flex-presenter flex-grow" on:click={navigateToProject}>
|
||||
<span title={value.label} class="projectLabel flex-grow">{value.label}</span>
|
||||
</div>
|
||||
|
@ -18,8 +18,8 @@
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Issue, Project } from '@hcengineering/tracker'
|
||||
import { CheckBox, Spinner, tooltip } from '@hcengineering/ui'
|
||||
import { AttributeModel, BuildModelKey } from '@hcengineering/view'
|
||||
import { buildModel, getObjectPresenter, LoadingProps } from '@hcengineering/view-resources'
|
||||
import { BuildModelKey } from '@hcengineering/view'
|
||||
import { buildModel, LoadingProps } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
|
||||
@ -42,16 +42,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
let personPresenter: AttributeModel
|
||||
|
||||
$: options = { ...baseOptions } as FindOptions<Project>
|
||||
$: selectedObjectIdsSet = new Set<Ref<Doc>>(selectedObjectIds.map((it) => it._id))
|
||||
$: objectRefs.length = projects?.length ?? 0
|
||||
|
||||
$: getObjectPresenter(client, contact.class.Person, { key: '' }).then((p) => {
|
||||
personPresenter = p
|
||||
})
|
||||
|
||||
export const onObjectChecked = (docs: Doc[], value: boolean) => {
|
||||
dispatch('check', { docs, value })
|
||||
}
|
||||
|
@ -22,7 +22,8 @@
|
||||
await client.update(sprint, { [field]: value })
|
||||
}
|
||||
let container: HTMLElement
|
||||
function selectSprint (evt: MouseEvent): void {
|
||||
|
||||
function selectSprint (): void {
|
||||
showPopup(SprintPopup, { _class: tracker.class.Sprint }, container, (value) => {
|
||||
if (value != null) {
|
||||
sprint = value
|
||||
|
@ -104,12 +104,3 @@
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
|
||||
<style>
|
||||
.label {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.description {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
|
@ -123,9 +123,6 @@
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.showWarning {
|
||||
color: var(--warning-color) !important;
|
||||
}
|
||||
.minus-margin {
|
||||
margin-left: -0.5rem;
|
||||
&-vSpace {
|
||||
|
@ -119,6 +119,7 @@
|
||||
<div class="listRoot">
|
||||
{#if sprints}
|
||||
{#each Array.from(byProject?.entries() ?? []) as e}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="flex-between categoryHeader row" on:click={() => handleCollapseCategory(e[0])}>
|
||||
<div class="flex-row-center gap-2 clear-mins">
|
||||
<SprintProjectEditor
|
||||
|
@ -27,6 +27,7 @@
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="flex-presenter flex-grow" on:click={navigateToSprint}>
|
||||
<span title={value.label} class="projectLabel flex-grow">{value.label}</span>
|
||||
</div>
|
||||
|
@ -82,10 +82,6 @@
|
||||
resetDrag()
|
||||
}
|
||||
|
||||
function showContextMenu (ev: MouseEvent, object: IssueTemplateChild) {
|
||||
// showPopup(ContextMenu, { object }, getEventPositionElement(ev))
|
||||
}
|
||||
|
||||
export function getIssueTemplateId (team: string, issue: IssueTemplateChild): string {
|
||||
return `${team}-${issues.findIndex((it) => it.id === issue.id)}`
|
||||
}
|
||||
@ -107,7 +103,6 @@
|
||||
animate:flip={{ duration: 400 }}
|
||||
draggable={true}
|
||||
on:click|self={(evt) => openIssue(evt, issue)}
|
||||
on:contextmenu|preventDefault={(ev) => showContextMenu(ev, issue)}
|
||||
on:dragstart={(ev) => handleDragStart(ev, index)}
|
||||
on:dragover|preventDefault={() => false}
|
||||
on:dragenter={() => (hoveringIndex = index)}
|
||||
|
@ -33,6 +33,7 @@
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<span class="issuePresenterRoot flex" class:noPointer={disableClick} on:click={handleIssueEditorOpened}>
|
||||
<Icon icon={tracker.icon.Issues} size={'small'} />
|
||||
<span class="ml-2">
|
||||
|
@ -4,11 +4,10 @@
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { IssueTemplate } from '@hcengineering/tracker'
|
||||
import { Button, IconAdd, IconDetails, IconDetailsFilled, showPopup } from '@hcengineering/ui'
|
||||
import view, { Viewlet, ViewOptionModel } from '@hcengineering/view'
|
||||
import { FilterBar, getActiveViewletId } from '@hcengineering/view-resources'
|
||||
import view, { Viewlet } from '@hcengineering/view'
|
||||
import { FilterBar, getActiveViewletId, getViewOptions } from '@hcengineering/view-resources'
|
||||
import ViewletSettingButton from '@hcengineering/view-resources/src/components/ViewletSettingButton.svelte'
|
||||
import tracker from '../../plugin'
|
||||
import { getDefaultViewOptionsTemplatesConfig } from '../../utils'
|
||||
import IssuesHeader from '../issues/IssuesHeader.svelte'
|
||||
import CreateIssueTemplate from './CreateIssueTemplate.svelte'
|
||||
import IssueTemplatesContent from './IssueTemplatesContent.svelte'
|
||||
@ -16,7 +15,6 @@
|
||||
export let query: DocumentQuery<IssueTemplate> = {}
|
||||
export let title: IntlString | undefined = undefined
|
||||
export let label: string = ''
|
||||
export let viewOptionsConfig: ViewOptionModel[] = getDefaultViewOptionsTemplatesConfig()
|
||||
|
||||
export let panelWidth: number = 0
|
||||
|
||||
@ -70,6 +68,8 @@
|
||||
const showCreateDialog = async () => {
|
||||
showPopup(CreateIssueTemplate, { targetElement: null }, 'top')
|
||||
}
|
||||
|
||||
$: viewOptions = getViewOptions(viewlet)
|
||||
</script>
|
||||
|
||||
<IssuesHeader {viewlets} {label} bind:viewlet bind:search showLabelSelector={$$slots.label_selector}>
|
||||
@ -86,7 +86,7 @@
|
||||
/>
|
||||
|
||||
{#if viewlet}
|
||||
<ViewletSettingButton {viewlet} />
|
||||
<ViewletSettingButton bind:viewOptions {viewlet} />
|
||||
{/if}
|
||||
|
||||
{#if asideFloat && $$slots.aside}
|
||||
|
@ -13,8 +13,8 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Doc, generateId, Ref, WithLookup } from '@hcengineering/core'
|
||||
import { AttributeBarEditor, createQuery, getClient, KeyedAttribute } from '@hcengineering/presentation'
|
||||
import { generateId, Ref, WithLookup } from '@hcengineering/core'
|
||||
import { AttributeBarEditor, getClient, KeyedAttribute } from '@hcengineering/presentation'
|
||||
import tags, { TagElement, TagReference } from '@hcengineering/tags'
|
||||
import type { IssueTemplate } from '@hcengineering/tracker'
|
||||
import { Component, Label } from '@hcengineering/ui'
|
||||
@ -27,14 +27,6 @@
|
||||
|
||||
export let issue: WithLookup<IssueTemplate>
|
||||
|
||||
const query = createQuery()
|
||||
let showIsBlocking = false
|
||||
let blockedBy: Doc[]
|
||||
$: query.query(tracker.class.Issue, { blockedBy: { _id: issue._id, _class: issue._class } }, (result) => {
|
||||
blockedBy = result
|
||||
showIsBlocking = result.length > 0
|
||||
})
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
|
@ -41,6 +41,7 @@
|
||||
<div class="flex-no-shrink draggable-mark">
|
||||
{#if !isSingle}<Circles />{/if}
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="flex-no-shrink ml-2 color" on:click={pickColor}>
|
||||
<div class="dot" style="background-color: {getPlatformColor(value.color ?? 0)}" />
|
||||
</div>
|
||||
|
@ -23,6 +23,7 @@
|
||||
export let value: IssueStatus
|
||||
export let isDefault = false
|
||||
export let isSingle = true
|
||||
export let issueStatuses: IssueStatus[] | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@ -37,7 +38,7 @@
|
||||
<Circles />
|
||||
</div>
|
||||
<div class="flex-no-shrink ml-2">
|
||||
<IssueStatusIcon {value} size="small" />
|
||||
<IssueStatusIcon {value} size="small" {issueStatuses} />
|
||||
</div>
|
||||
<span class="content-accent-color ml-2">{value.name}</span>
|
||||
{#if value.description}
|
||||
@ -48,10 +49,12 @@
|
||||
{#if isDefault}
|
||||
<Label label={tracker.string.Default} />
|
||||
{:else if value.category === tracker.issueStatusCategory.Backlog || value.category === tracker.issueStatusCategory.Unstarted}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="btn" on:click|preventDefault={() => dispatch('default-update', value._id)}>
|
||||
<Label label={tracker.string.MakeDefault} />
|
||||
</div>
|
||||
{/if}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="btn"
|
||||
use:tooltip={{ label: tracker.string.EditWorkflowStatus, direction: 'bottom' }}
|
||||
@ -60,6 +63,7 @@
|
||||
<Icon icon={IconEdit} size="small" />
|
||||
</div>
|
||||
{#if !isSingle}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="btn"
|
||||
use:tooltip={{ label: tracker.string.DeleteWorkflowStatus, direction: 'bottom' }}
|
||||
|
@ -295,6 +295,7 @@
|
||||
editingStatus = { ...detail, color: detail.color ?? category.color }
|
||||
}}
|
||||
on:delete={deleteStatus}
|
||||
issueStatuses={workflowStatuses}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -60,6 +60,7 @@ import SetDueDateActionPopup from './components/SetDueDateActionPopup.svelte'
|
||||
import SetParentIssueActionPopup from './components/SetParentIssueActionPopup.svelte'
|
||||
import Views from './components/views/Views.svelte'
|
||||
import Statuses from './components/workflow/Statuses.svelte'
|
||||
import RelatedIssuesSection from './components/issues/related/RelatedIssuesSection.svelte'
|
||||
import {
|
||||
getIssueId,
|
||||
getIssueTitle,
|
||||
@ -278,7 +279,8 @@ export default async (): Promise<Resources> => ({
|
||||
CreateTeam,
|
||||
TeamPresenter,
|
||||
IssueStatistics,
|
||||
StatusRefPresenter
|
||||
StatusRefPresenter,
|
||||
RelatedIssuesSection
|
||||
},
|
||||
completion: {
|
||||
IssueQuery: async (client: Client, query: string, filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }) =>
|
||||
|
@ -15,12 +15,15 @@
|
||||
import { Client, Doc, DocumentQuery, Ref } from '@hcengineering/core'
|
||||
import type { IntlString, Metadata, Resource } from '@hcengineering/platform'
|
||||
import { mergeIds } from '@hcengineering/platform'
|
||||
import { AnyComponent } from '@hcengineering/ui'
|
||||
import tracker, { trackerId } from '../../tracker/lib'
|
||||
import { IssueDraft } from '@hcengineering/tracker'
|
||||
import { SortFunc } from '@hcengineering/view'
|
||||
import { AnyComponent } from '@hcengineering/ui'
|
||||
import { SortFunc, Viewlet } from '@hcengineering/view'
|
||||
import tracker, { trackerId } from '../../tracker/lib'
|
||||
|
||||
export default mergeIds(trackerId, tracker, {
|
||||
viewlet: {
|
||||
SubIssues: '' as Ref<Viewlet>
|
||||
},
|
||||
string: {
|
||||
More: '' as IntlString,
|
||||
Delete: '' as IntlString,
|
||||
|
@ -399,6 +399,7 @@ export default plugin(trackerId, {
|
||||
Tracker: '' as AnyComponent,
|
||||
TrackerApp: '' as AnyComponent,
|
||||
RelatedIssues: '' as AnyComponent,
|
||||
RelatedIssuesSection: '' as AnyComponent,
|
||||
RelatedIssueTemplates: '' as AnyComponent,
|
||||
EditIssue: '' as AnyComponent,
|
||||
CreateIssue: '' as AnyComponent,
|
||||
|
@ -24,14 +24,16 @@
|
||||
let prevKey = key
|
||||
let element: HTMLDivElement | undefined
|
||||
|
||||
let cWidth: number = 0
|
||||
let cWidth: number | undefined = undefined
|
||||
|
||||
afterUpdate(() => {
|
||||
if (prevKey !== key) {
|
||||
$fixedWidthStore[prevKey] = 0
|
||||
$fixedWidthStore[key] = 0
|
||||
prevKey = key
|
||||
cWidth = 0
|
||||
if (cWidth !== undefined) {
|
||||
if (prevKey !== key) {
|
||||
$fixedWidthStore[prevKey] = 0
|
||||
$fixedWidthStore[key] = 0
|
||||
prevKey = key
|
||||
cWidth = undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -43,14 +45,18 @@
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
$fixedWidthStore[key] = 0
|
||||
if (cWidth === $fixedWidthStore[key]) {
|
||||
// If we are longest element
|
||||
$fixedWidthStore[key] = 0
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={element}
|
||||
class="flex-no-shrink{addClass ? ` ${addClass}` : ''}"
|
||||
style="{justify !== '' ? `text-align: ${justify}; ` : ''} min-width: {$fixedWidthStore[key] ?? 0}px;"
|
||||
style:text-align={justify !== '' ? justify : ''}
|
||||
style:min-width={`${$fixedWidthStore[key] ?? 0}}px;`}
|
||||
use:resizeObserver={resize}
|
||||
>
|
||||
<slot />
|
||||
|
@ -45,7 +45,10 @@
|
||||
selected={viewOptions.groupBy}
|
||||
width="10rem"
|
||||
justify="left"
|
||||
on:selected={(e) => dispatch('update', { key: 'groupBy', value: e.detail })}
|
||||
on:selected={(e) => {
|
||||
viewOptions.groupBy = e.detail
|
||||
dispatch('update', { key: 'groupBy', value: e.detail })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span class="label"><Label label={view.string.Ordering} /></span>
|
||||
@ -60,6 +63,7 @@
|
||||
const key = e.detail
|
||||
const value = config.orderBy.find((p) => p[0] === key)
|
||||
if (value !== undefined) {
|
||||
viewOptions.orderBy = value
|
||||
dispatch('update', { key: 'orderBy', value })
|
||||
}
|
||||
}}
|
||||
@ -71,7 +75,10 @@
|
||||
{#if isToggleType(model)}
|
||||
<MiniToggle
|
||||
on={viewOptions[model.key]}
|
||||
on:change={() => dispatch('update', { key: model.key, value: !viewOptions[model.key] })}
|
||||
on:change={() => {
|
||||
viewOptions[model.key] = !viewOptions[model.key]
|
||||
dispatch('update', { key: model.key, value: viewOptions[model.key] })
|
||||
}}
|
||||
/>
|
||||
{:else if isDropdownType(model)}
|
||||
{@const items = model.values.filter(({ hidden }) => !hidden?.(viewOptions))}
|
||||
@ -81,7 +88,10 @@
|
||||
selected={viewOptions[model.key]}
|
||||
width="10rem"
|
||||
justify="left"
|
||||
on:selected={(e) => dispatch('update', { key: model.key, value: e.detail })}
|
||||
on:selected={(e) => {
|
||||
viewOptions[model.key] = e.detail
|
||||
dispatch('update', { key: model.key, value: e.detail })
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -13,36 +13,36 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Button, eventToHTMLElement, IconDownOutline, Label, showPopup } from '@hcengineering/ui'
|
||||
import { Viewlet, ViewOptionsModel } from '@hcengineering/view'
|
||||
import { Button, ButtonKind, eventToHTMLElement, IconDownOutline, Label, showPopup } from '@hcengineering/ui'
|
||||
import { Viewlet, ViewOptions } from '@hcengineering/view'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import view from '../plugin'
|
||||
import { defaulOptions, getViewOptions, setViewOptions, viewOptionsStore } from '../viewOptions'
|
||||
import { setViewOptions } from '../viewOptions'
|
||||
import ViewletSetting from './ViewletSetting.svelte'
|
||||
import ViewOptions from './ViewOptions.svelte'
|
||||
import ViewOptionsEditor from './ViewOptions.svelte'
|
||||
|
||||
export let viewlet: Viewlet | undefined
|
||||
export let kind: ButtonKind = 'secondary'
|
||||
export let viewOptions: ViewOptions
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let btn: HTMLButtonElement
|
||||
|
||||
$: viewlet && loadViewOptionsStore(viewlet.viewOptions, viewlet._id)
|
||||
|
||||
function loadViewOptionsStore (config: ViewOptionsModel | undefined, key: string) {
|
||||
if (!config) return
|
||||
viewOptionsStore.set(getViewOptions(key) ?? defaulOptions)
|
||||
}
|
||||
|
||||
function clickHandler (event: MouseEvent) {
|
||||
if (viewlet?.viewOptions !== undefined) {
|
||||
showPopup(
|
||||
ViewOptions,
|
||||
{ viewlet, config: viewlet.viewOptions, viewOptions: $viewOptionsStore },
|
||||
ViewOptionsEditor,
|
||||
{ viewlet, config: viewlet.viewOptions, viewOptions },
|
||||
eventToHTMLElement(event),
|
||||
undefined,
|
||||
(result) => {
|
||||
if (result?.key === undefined) return
|
||||
$viewOptionsStore[result.key] = result.value
|
||||
viewOptionsStore.set($viewOptionsStore)
|
||||
setViewOptions(viewlet?._id ?? '', $viewOptionsStore)
|
||||
if (viewlet) {
|
||||
viewOptions = { ...viewOptions, [result.key]: result.value }
|
||||
dispatch('viewOptions', viewOptions)
|
||||
setViewOptions(viewlet, viewOptions)
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
@ -54,7 +54,7 @@
|
||||
{#if viewlet}
|
||||
<Button
|
||||
icon={view.icon.ViewButton}
|
||||
kind={'secondary'}
|
||||
{kind}
|
||||
size={'small'}
|
||||
showTooltip={{ label: view.string.CustomizeView }}
|
||||
bind:input={btn}
|
||||
|
@ -26,7 +26,7 @@
|
||||
} from '@hcengineering/view'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { buildConfigLookup, buildModel, getCategories, getPresenter, groupBy, LoadingProps } from '../../utils'
|
||||
import { noCategory, viewOptionsStore } from '../../viewOptions'
|
||||
import { noCategory } from '../../viewOptions'
|
||||
import ListCategory from './ListCategory.svelte'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
@ -38,33 +38,48 @@
|
||||
export let selectedObjectIds: Doc[] = []
|
||||
export let selectedRowIndex: number | undefined = undefined
|
||||
export let loadingProps: LoadingProps | undefined = undefined
|
||||
export let createItemDialog: AnyComponent | undefined
|
||||
export let createItemLabel: IntlString | undefined
|
||||
export let viewOptions: ViewOptionModel[] | undefined
|
||||
export let createItemDialog: AnyComponent | undefined = undefined
|
||||
export let createItemLabel: IntlString | undefined = undefined
|
||||
export let viewOptionsConfig: ViewOptionModel[] | undefined
|
||||
export let viewOptions: ViewOptions
|
||||
export let flatHeaders = false
|
||||
export let props: Record<string, any> = {}
|
||||
|
||||
export let documents: Doc[] | undefined = undefined
|
||||
|
||||
const objectRefs: HTMLElement[] = []
|
||||
let docs: Doc[] = []
|
||||
$: groupByKey = $viewOptionsStore.groupBy ?? noCategory
|
||||
$: orderBy = $viewOptionsStore.orderBy
|
||||
|
||||
$: groupByKey = viewOptions.groupBy ?? noCategory
|
||||
$: orderBy = viewOptions.orderBy
|
||||
$: groupedDocs = groupBy(docs, groupByKey)
|
||||
let categories: any[] = []
|
||||
$: getCategories(client, _class, docs, groupByKey).then((p) => (categories = p))
|
||||
$: getCategories(client, _class, docs, groupByKey).then((p) => {
|
||||
categories = p
|
||||
})
|
||||
|
||||
const docsQuery = createQuery()
|
||||
$: resultOptions = { lookup, ...options, sort: { [orderBy[0]]: orderBy[1] } }
|
||||
|
||||
let resultQuery: DocumentQuery<Doc> = query
|
||||
$: getResultQuery(query, viewOptions, $viewOptionsStore).then((p) => (resultQuery = p))
|
||||
$: getResultQuery(query, viewOptionsConfig, viewOptions).then((p) => {
|
||||
resultQuery = { ...p, ...query }
|
||||
})
|
||||
|
||||
$: docsQuery.query(
|
||||
_class,
|
||||
resultQuery,
|
||||
(res) => {
|
||||
docs = res
|
||||
dispatch('content', docs)
|
||||
},
|
||||
resultOptions
|
||||
)
|
||||
$: if (documents === undefined) {
|
||||
docsQuery.query(
|
||||
_class,
|
||||
resultQuery,
|
||||
(res) => {
|
||||
docs = res
|
||||
dispatch('content', docs)
|
||||
},
|
||||
resultOptions
|
||||
)
|
||||
} else {
|
||||
docsQuery.unsubscribe()
|
||||
docs = documents
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@ -117,7 +132,9 @@
|
||||
|
||||
let headerComponent: AttributeModel | undefined
|
||||
$: getHeader(_class, groupByKey)
|
||||
$: buildModel({ client, _class, keys: config, lookup }).then((res) => (itemModels = res))
|
||||
$: buildModel({ client, _class, keys: config, lookup }).then((res) => {
|
||||
itemModels = res
|
||||
})
|
||||
|
||||
function getInitIndex (categories: any, i: number): number {
|
||||
let res = 0
|
||||
@ -194,6 +211,8 @@
|
||||
on:check
|
||||
on:uncheckAll={uncheckAll}
|
||||
on:row-focus
|
||||
{flatHeaders}
|
||||
{props}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
@ -45,6 +45,8 @@
|
||||
export let selectedRowIndex: number | undefined
|
||||
export let extraHeaders: AnyComponent[] | undefined
|
||||
export let objectRefs: HTMLElement[] = []
|
||||
export let flatHeaders = false
|
||||
export let props: Record<string, any> = {}
|
||||
|
||||
const autoFoldLimit = 20
|
||||
const defaultLimit = 20
|
||||
@ -92,6 +94,8 @@
|
||||
{createItemDialog}
|
||||
{createItemLabel}
|
||||
{extraHeaders}
|
||||
flat={flatHeaders}
|
||||
{props}
|
||||
on:more={() => {
|
||||
limit += 20
|
||||
}}
|
||||
@ -114,6 +118,7 @@
|
||||
on:contextmenu={(event) => handleMenuOpened(event, docObject, initIndex + i)}
|
||||
on:focus={() => {}}
|
||||
on:mouseover={() => handleRowFocused(docObject)}
|
||||
{props}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
|
@ -41,6 +41,8 @@
|
||||
export let limited: number
|
||||
export let items: Doc[]
|
||||
export let extraHeaders: AnyComponent[] | undefined
|
||||
export let flat = false
|
||||
export let props: Record<string, any> = {}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@ -52,7 +54,7 @@
|
||||
|
||||
{#if headerComponent || groupByKey === noCategory}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="flex-between categoryHeader row" on:click={() => dispatch('collapse')}>
|
||||
<div class="flex-between categoryHeader row" class:flat on:click={() => dispatch('collapse')}>
|
||||
<div class="flex-row-center gap-2 clear-mins caption-color">
|
||||
<FixedColumn key={`list_groupBy_${groupByKey}`} justify={'left'}>
|
||||
{#if groupByKey === noCategory}
|
||||
@ -66,7 +68,7 @@
|
||||
{#if extraHeaders}
|
||||
{#each extraHeaders as extra}
|
||||
<FixedColumn key={`list_groupBy_${groupByKey}_extra_${extra}`} justify={'left'}>
|
||||
<Component is={extra} props={{ value: category, docs: items }} />
|
||||
<Component is={extra} props={{ ...props, value: category, docs: items }} />
|
||||
</FixedColumn>
|
||||
{/each}
|
||||
{/if}
|
||||
@ -109,6 +111,14 @@
|
||||
min-width: 0;
|
||||
background: var(--header-bg-color);
|
||||
z-index: 5;
|
||||
|
||||
&.flat {
|
||||
background: var(--header-bg-color);
|
||||
background-blend-mode: darken;
|
||||
min-height: 2.25rem;
|
||||
height: 2.25rem;
|
||||
padding: 0 0.25rem 0 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.row:not(:last-child) {
|
||||
|
@ -28,6 +28,7 @@
|
||||
export let groupByKey: string | undefined
|
||||
export let checked: boolean
|
||||
export let selected: boolean
|
||||
export let props: Record<string, any> = {}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@ -69,6 +70,7 @@
|
||||
<FixedColumn key={`list_item_${attributeModel.key}`} justify={attributeModel.props.fixed}>
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
{...props}
|
||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||
object={docObject}
|
||||
kind={'list'}
|
||||
@ -78,6 +80,7 @@
|
||||
{:else}
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
{...props}
|
||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||
object={docObject}
|
||||
kind={'list'}
|
||||
@ -104,6 +107,7 @@
|
||||
{#if attributeModel.props?.optional && attributeModel.props?.excludeByKey !== groupByKey && value !== undefined}
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
{...props}
|
||||
value={value ?? ''}
|
||||
objectId={docObject._id}
|
||||
groupBy={groupByKey}
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { Class, Doc, DocumentQuery, FindOptions, Ref, Space } from '@hcengineering/core'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { AnyComponent, issueSP, Scroller } from '@hcengineering/ui'
|
||||
import { BuildModelKey, Viewlet } from '@hcengineering/view'
|
||||
import { BuildModelKey, Viewlet, ViewOptions } from '@hcengineering/view'
|
||||
import { onMount } from 'svelte'
|
||||
import {
|
||||
ActionContext,
|
||||
@ -24,6 +24,8 @@
|
||||
export let loadingProps: LoadingProps | undefined = undefined
|
||||
export let createItemDialog: AnyComponent | undefined
|
||||
export let createItemLabel: IntlString | undefined
|
||||
export let viewOptions: ViewOptions
|
||||
export let props: Record<string, any> = {}
|
||||
|
||||
let list: List
|
||||
|
||||
@ -57,7 +59,9 @@
|
||||
{loadingProps}
|
||||
{createItemDialog}
|
||||
{createItemLabel}
|
||||
viewOptions={viewlet.viewOptions?.other}
|
||||
{viewOptions}
|
||||
{props}
|
||||
viewOptionsConfig={viewlet.viewOptions?.other}
|
||||
selectedObjectIds={$selectionStore ?? []}
|
||||
selectedRowIndex={listProvider.current($focusStore)}
|
||||
on:row-focus={(event) => {
|
||||
|
@ -88,6 +88,8 @@ export { default as FixedColumn } from './components/FixedColumn.svelte'
|
||||
export { default as ValueSelector } from './components/ValueSelector.svelte'
|
||||
export { default as ObjectBox } from './components/ObjectBox.svelte'
|
||||
export { default as ObjectPresenter } from './components/ObjectPresenter.svelte'
|
||||
|
||||
export { default as List } from './components/list/List.svelte'
|
||||
export * from './context'
|
||||
export * from './filter'
|
||||
export * from './selection'
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { SortingOrder } from '@hcengineering/core'
|
||||
import { getCurrentLocation, locationToUrl } from '@hcengineering/ui'
|
||||
import { DropdownViewOption, ToggleViewOption, ViewOptionModel, ViewOptions } from '@hcengineering/view'
|
||||
import { writable } from 'svelte/store'
|
||||
import { DropdownViewOption, ToggleViewOption, Viewlet, ViewOptionModel, ViewOptions } from '@hcengineering/view'
|
||||
|
||||
export const noCategory = '#no_category'
|
||||
|
||||
@ -10,8 +9,6 @@ export const defaulOptions: ViewOptions = {
|
||||
orderBy: ['modifiedBy', SortingOrder.Descending]
|
||||
}
|
||||
|
||||
export const viewOptionsStore = writable<ViewOptions>(defaulOptions)
|
||||
|
||||
export function isToggleType (viewOption: ViewOptionModel): viewOption is ToggleViewOption {
|
||||
return viewOption.type === 'toggle'
|
||||
}
|
||||
@ -27,14 +24,27 @@ function makeViewOptionsKey (prefix: string): string {
|
||||
return `viewOptions:${prefix}:${locationToUrl(loc)}`
|
||||
}
|
||||
|
||||
export function setViewOptions (prefix: string, options: ViewOptions): void {
|
||||
function _setViewOptions (prefix: string, options: ViewOptions): void {
|
||||
const key = makeViewOptionsKey(prefix)
|
||||
localStorage.setItem(key, JSON.stringify(options))
|
||||
}
|
||||
|
||||
export function getViewOptions (prefix: string): ViewOptions | null {
|
||||
export function setViewOptions (viewlet: Viewlet, options: ViewOptions): void {
|
||||
const viewletKey = viewlet?._id + (viewlet?.variant !== undefined ? `-${viewlet.variant}` : '')
|
||||
_setViewOptions(viewletKey, options)
|
||||
}
|
||||
|
||||
function _getViewOptions (prefix: string): ViewOptions | null {
|
||||
const key = makeViewOptionsKey(prefix)
|
||||
const options = localStorage.getItem(key)
|
||||
if (options === null) return null
|
||||
return JSON.parse(options)
|
||||
}
|
||||
|
||||
export function getViewOptions (viewlet: Viewlet | undefined, defaults = defaulOptions): ViewOptions {
|
||||
if (viewlet === undefined) {
|
||||
return { ...defaults }
|
||||
}
|
||||
const viewletKey = viewlet?._id + (viewlet?.variant !== undefined ? `-${viewlet.variant}` : '')
|
||||
return _getViewOptions(viewletKey) ?? defaults
|
||||
}
|
||||
|
@ -222,6 +222,7 @@ export interface Viewlet extends Doc {
|
||||
config: (BuildModelKey | string)[]
|
||||
hiddenKeys?: string[]
|
||||
viewOptions?: ViewOptionsModel
|
||||
variant?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@ -553,7 +554,8 @@ const view = plugin(viewId, {
|
||||
EditDoc: '' as AnyComponent,
|
||||
SpacePresenter: '' as AnyComponent,
|
||||
BooleanTruePresenter: '' as AnyComponent,
|
||||
ValueSelector: '' as AnyComponent
|
||||
ValueSelector: '' as AnyComponent,
|
||||
GrowPresenter: '' as AnyComponent
|
||||
},
|
||||
string: {
|
||||
CustomizeView: '' as IntlString,
|
||||
|
@ -19,7 +19,12 @@
|
||||
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { AnyComponent, Button, IconAdd, SearchEdit, showPanel, showPopup, TabList } from '@hcengineering/ui'
|
||||
import view, { Viewlet } from '@hcengineering/view'
|
||||
import { getActiveViewletId, setActiveViewletId, ViewletSettingButton } from '@hcengineering/view-resources'
|
||||
import {
|
||||
getActiveViewletId,
|
||||
getViewOptions,
|
||||
setActiveViewletId,
|
||||
ViewletSettingButton
|
||||
} from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import plugin from '../plugin'
|
||||
import { classIcon } from '../utils'
|
||||
@ -84,6 +89,8 @@
|
||||
})
|
||||
|
||||
$: twoRows = $deviceInfo.twoRows
|
||||
|
||||
$: viewOptions = getViewOptions(viewlet)
|
||||
</script>
|
||||
|
||||
<div class="ac-header withSettings" class:full={!twoRows} class:mini={twoRows}>
|
||||
@ -128,7 +135,7 @@
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<ViewletSettingButton {viewlet} />
|
||||
<ViewletSettingButton bind:viewOptions {viewlet} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -29,7 +29,7 @@
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import view, { Viewlet, ViewletDescriptor, ViewletPreference } from '@hcengineering/view'
|
||||
import { FilterBar, FilterButton, ViewletSettingButton } from '@hcengineering/view-resources'
|
||||
import { FilterBar, FilterButton, getViewOptions, ViewletSettingButton } from '@hcengineering/view-resources'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let icon: Asset
|
||||
@ -91,6 +91,8 @@
|
||||
}
|
||||
|
||||
$: twoRows = $deviceInfo.twoRows
|
||||
|
||||
$: viewOptions = getViewOptions(viewlet)
|
||||
</script>
|
||||
|
||||
<div class="ac-header withSettings" class:full={!twoRows} class:mini={twoRows}>
|
||||
@ -107,7 +109,7 @@
|
||||
{#if createLabel && createComponent}
|
||||
<Button label={createLabel} icon={IconAdd} kind={'primary'} size={'small'} on:click={() => showCreateDialog()} />
|
||||
{/if}
|
||||
<ViewletSettingButton {viewlet} />
|
||||
<ViewletSettingButton bind:viewOptions {viewlet} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -510,6 +510,7 @@
|
||||
/>
|
||||
<div class="workbench-container">
|
||||
{#if currentApplication && navigatorModel && navigator && visibileNav}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
{#if visibileNav && navFloat}<div class="cover shown" on:click={() => (visibileNav = false)} />{/if}
|
||||
<div
|
||||
class="antiPanel-navigator {appsDirection === 'horizontal' ? 'portrait' : 'landscape'}"
|
||||
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user