Uber 189 Uber 190 (#3300)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-05-31 14:40:47 +06:00 committed by GitHub
parent 5009a5b518
commit 0d6aa799f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 814 additions and 560 deletions

View File

@ -120,7 +120,9 @@ export function createModel (builder: Builder): void {
descriptor: calendar.viewlet.Calendar,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
config: [''],
configOptions: {
hiddenKeys: ['title', 'date']
}
},
calendar.viewlet.CalendarEvent
)

View File

@ -253,7 +253,9 @@ export function createModel (builder: Builder): void {
},
'modifiedOn'
],
configOptions: {
hiddenKeys: ['name', 'contact']
}
},
contact.viewlet.TableMember
)
@ -282,7 +284,9 @@ export function createModel (builder: Builder): void {
sortingKey: ['$lookup.channels.lastMessage', 'channels']
}
],
configOptions: {
hiddenKeys: ['name']
}
},
contact.viewlet.TableContact
)

View File

@ -391,8 +391,7 @@ export function createModel (builder: Builder): void {
sortingKey: ['$lookup.channels.lastMessage', 'channels']
},
'modifiedOn'
],
hiddenKeys: []
]
},
hr.viewlet.TableMember
)
@ -403,8 +402,7 @@ export function createModel (builder: Builder): void {
{
attachTo: hr.mixin.Staff,
descriptor: view.viewlet.Table,
config: [''],
hiddenKeys: []
config: ['']
},
hr.viewlet.StaffStats
)

View File

@ -208,7 +208,9 @@ export function createModel (builder: Builder): void {
sortingKey: ['$lookup.channels.lastMessage', 'channels']
}
],
hiddenKeys: ['name'],
configOptions: {
hiddenKeys: ['name']
},
options: {
lookup: {
_id: {
@ -288,50 +290,56 @@ export function createModel (builder: Builder): void {
{
attachTo: lead.class.Lead,
descriptor: view.viewlet.List,
configOptions: {
hiddenKeys: ['title'],
extraProps: {
displayProps: {
optional: true
}
}
},
config: [
{ key: '', props: { listProps: { fixed: 'left', key: 'lead' } } },
{ key: '', displayProps: { fixed: 'left', key: 'lead' } },
{
key: '',
presenter: lead.component.TitlePresenter,
props: { listProps: { fixed: 'left', key: 'title' }, maxWidth: '10rem' }
label: lead.string.Title,
displayProps: { fixed: 'left', key: 'title' },
props: { maxWidth: '10rem' }
},
{
key: '$lookup.attachedTo',
presenter: contact.component.PersonPresenter,
label: lead.string.Customer,
sortingKey: '$lookup.attachedTo.name',
displayProps: { fixed: 'left', key: 'talent' },
props: {
_class: lead.mixin.Customer,
listProps: { fixed: 'left', key: 'talent' },
inline: true,
maxWidth: '10rem'
}
},
{ key: 'state', props: { listProps: { fixed: 'left', key: 'state' } } },
{ key: 'state', displayProps: { fixed: 'left', key: 'state' } },
{
key: '',
presenter: tracker.component.RelatedIssueSelector,
label: tracker.string.Relations,
props: { listProps: { fixed: 'left', key: 'issues' } }
displayProps: { fixed: 'left', key: 'issues' }
},
{ key: 'attachments', props: { listProps: { fixed: 'left', key: 'attachments' } } },
{ key: 'comments', props: { listProps: { fixed: 'left' }, key: 'comments' } },
{ key: '', presenter: view.component.GrowPresenter, props: { type: 'grow' } },
{ key: '', presenter: view.component.DividerPresenter, props: { type: 'divider' } },
{ key: 'attachments', displayProps: { fixed: 'left', key: 'attachments' } },
{ key: 'comments', displayProps: { fixed: 'left', key: 'comments' } },
{
key: '$lookup.attachedTo.$lookup.channels',
label: contact.string.ContactInfo,
sortingKey: ['$lookup.attachedTo.$lookup.channels.lastMessage', '$lookup.attachedTo.channels'],
props: {
listProps: {
displayProps: {
fixed: 'left',
key: 'channels'
}
key: 'channels',
dividerBefore: true
}
},
{ key: '', presenter: view.component.DividerPresenter, props: { type: 'divider' } },
{ key: 'modifiedOn', props: { listProps: { key: 'modified', fixed: 'left' } } },
{ key: 'assignee', props: { listProps: { key: 'assignee', fixed: 'right' }, shouldShowLabel: false } }
{ key: 'modifiedOn', displayProps: { key: 'modified', fixed: 'left', dividerBefore: true } },
{ key: 'assignee', displayProps: { key: 'assignee', fixed: 'right' }, props: { shouldShowLabel: false } }
],
viewOptions: leadViewOptions
},
@ -399,7 +407,10 @@ export function createModel (builder: Builder): void {
groupDepth: 1
},
options: lookupLeadOptions,
config: []
config: ['attachedTo', 'attachments', 'comments', 'dueDate', 'assignee'],
configOptions: {
strict: true
}
},
lead.viewlet.KanbanLead
)

View File

@ -399,7 +399,9 @@ export function createModel (builder: Builder): void {
sortingKey: ['$lookup.channels.lastMessage', 'channels']
}
],
hiddenKeys: ['name'],
configOptions: {
hiddenKeys: ['name']
},
options: {
lookup: {
_id: {
@ -457,7 +459,9 @@ export function createModel (builder: Builder): void {
label: core.string.ModifiedDate
}
],
configOptions: {
hiddenKeys: ['name', 'space', 'modifiedOn']
}
},
recruit.viewlet.TableVacancy
)
@ -487,7 +491,9 @@ export function createModel (builder: Builder): void {
label: core.string.ModifiedDate
}
],
configOptions: {
hiddenKeys: ['name', 'space', 'modifiedOn']
}
},
recruit.viewlet.TableVacancyList
)
@ -526,7 +532,9 @@ export function createModel (builder: Builder): void {
sortingKey: ['$lookup.attachedTo.$lookup.channels.lastMessage', '$lookup.attachedTo.channels']
}
],
hiddenKeys: ['name', 'attachedTo'],
configOptions: {
hiddenKeys: ['name', 'attachedTo']
},
options: {
lookup: {
_id: {
@ -579,7 +587,9 @@ export function createModel (builder: Builder): void {
space: recruit.class.Vacancy
}
},
hiddenKeys: ['name', 'attachedTo'],
configOptions: {
hiddenKeys: ['name', 'attachedTo']
},
baseQuery: {
doneState: null,
'$lookup.space.archived': false
@ -595,7 +605,6 @@ export function createModel (builder: Builder): void {
attachTo: recruit.class.ApplicantMatch,
descriptor: view.viewlet.Table,
config: ['', 'response', 'attachedTo', 'space', 'modifiedOn'],
hiddenKeys: [],
options: {
lookup: {
space: recruit.class.Vacancy
@ -657,23 +666,23 @@ export function createModel (builder: Builder): void {
attachTo: recruit.class.Applicant,
descriptor: view.viewlet.List,
config: [
{ key: '', props: { listProps: { fixed: 'left', key: 'app' } } },
{ key: '', displayProps: { fixed: 'left', key: 'app' } },
{
key: '$lookup.attachedTo',
presenter: contact.component.PersonPresenter,
label: recruit.string.Talent,
sortingKey: '$lookup.attachedTo.name',
displayProps: { fixed: 'left', key: 'talent' },
props: {
_class: recruit.mixin.Candidate,
listProps: { fixed: 'left', key: 'talent' },
inline: true
}
},
{ key: 'state', props: { listProps: { fixed: 'left', key: 'state' }, inline: true, showLabel: false } },
{ key: 'state', displayProps: { fixed: 'left', key: 'state' }, props: { inline: true, showLabel: false } },
{
key: '$lookup.space.company',
displayProps: { fixed: 'left', key: 'company' },
props: {
listProps: { fixed: 'left', key: 'company' },
inline: true,
maxWidth: '10rem'
}
@ -682,26 +691,26 @@ export function createModel (builder: Builder): void {
key: '',
presenter: tracker.component.RelatedIssueSelector,
label: tracker.string.Issues,
props: { listProps: { fixed: 'left', key: 'issues' } }
displayProps: { fixed: 'left', key: 'issues' }
},
{ key: 'attachments', props: { listProps: { fixed: 'left', key: 'attachments' } } },
{ key: 'comments', props: { listProps: { fixed: 'left' }, key: 'comments' } },
{ key: '', presenter: view.component.GrowPresenter, props: { type: 'grow' } },
{ key: '', presenter: view.component.DividerPresenter, props: { type: 'divider' } },
{ key: 'attachments', displayProps: { fixed: 'left', key: 'attachments' } },
{ key: 'comments', displayProps: { fixed: 'left', key: 'comments' } },
{
key: '$lookup.attachedTo.$lookup.channels',
label: contact.string.ContactInfo,
sortingKey: ['$lookup.attachedTo.$lookup.channels.lastMessage', '$lookup.attachedTo.channels'],
props: {
listProps: {
length: 'full',
size: 'inline'
},
displayProps: {
fixed: 'left',
key: 'channels'
}
key: 'channels',
dividerBefore: true
}
},
{ key: '', presenter: view.component.DividerPresenter, props: { type: 'divider' } },
{ key: 'modifiedOn', props: { listProps: { key: 'modified', fixed: 'left' } } },
{ key: 'assignee', props: { listProps: { key: 'assignee', fixed: 'right' }, shouldShowLabel: false } }
{ key: 'modifiedOn', displayProps: { key: 'modified', fixed: 'left', dividerBefore: true } },
{ key: 'assignee', displayProps: { key: 'assignee', fixed: 'right' }, props: { shouldShowLabel: false } }
],
options: {
lookup: {
@ -711,7 +720,14 @@ export function createModel (builder: Builder): void {
space: recruit.class.Vacancy
}
},
configOptions: {
hiddenKeys: ['name', 'attachedTo'],
extraProps: {
displayProps: {
optional: true
}
}
},
baseQuery: {
doneState: null,
'$lookup.space.archived': false
@ -739,7 +755,25 @@ export function createModel (builder: Builder): void {
options: {
lookup: applicantKanbanLookup
},
config: []
configOptions: {
strict: true
},
config: [
'space',
'assignee',
'state',
'attachments',
'dueDate',
'comments',
{
key: 'company',
label: recruit.string.Company
},
{
key: 'channels',
label: contact.string.ContactInfo
}
]
},
recruit.viewlet.ApplicantKanban
)

View File

@ -487,86 +487,125 @@ export function createModel (builder: Builder): void {
attachTo: tracker.class.Issue,
descriptor: view.viewlet.List,
viewOptions: issuesOptions(false),
configOptions: {
hiddenKeys: [
'title',
'blockedBy',
'relations',
'description',
'number',
'titile',
'reportedTime',
'reports',
'priority',
'component',
'milestone',
'estimation',
'status',
'dueDate',
'attachedTo'
],
extraProps: {
displayProps: {
optional: true
}
}
},
config: [
{
key: '',
label: tracker.string.Priority,
presenter: tracker.component.PriorityEditor,
props: { type: 'priority', kind: 'list', size: 'small' }
props: { type: 'priority', kind: 'list', size: 'small' },
displayProps: { key: 'priority' }
},
{
key: '',
label: tracker.string.Identifier,
presenter: tracker.component.IssuePresenter,
props: { type: 'issue', listProps: { key: 'issue', fixed: 'left' } }
displayProps: { key: 'issue', fixed: 'left' }
},
{
key: '',
label: tracker.string.Status,
presenter: tracker.component.StatusEditor,
props: { kind: 'list', size: 'small', justify: 'center' }
props: { kind: 'list', size: 'small', justify: 'center' },
displayProps: {
key: 'status'
}
},
{ key: '', presenter: tracker.component.TitlePresenter, props: {} },
{ key: '', presenter: tracker.component.SubIssuesSelector, props: {} },
{ key: '', presenter: view.component.GrowPresenter, props: { type: 'grow' } },
{
key: '',
label: tracker.string.Title,
presenter: tracker.component.TitlePresenter,
props: {},
displayProps: { key: 'title' }
},
{ key: '', label: tracker.string.SubIssues, presenter: tracker.component.SubIssuesSelector, props: {} },
{
key: 'labels',
presenter: tags.component.LabelsPresenter,
props: { kind: 'list', full: false, listProps: { optional: true, compression: true } }
displayProps: { optional: true, compression: true },
props: { kind: 'list', full: false }
},
{
key: '',
label: tracker.string.DueDate,
presenter: tracker.component.DueDatePresenter,
props: { kind: 'list', listProps: { optional: true, compression: true } }
displayProps: { key: 'dueDate', optional: true, compression: true },
props: { kind: 'list' }
},
{
key: '',
label: tracker.string.Component,
presenter: tracker.component.ComponentEditor,
props: {
kind: 'list',
size: 'small',
shape: 'round',
shouldShowPlaceholder: false,
listProps: {
shouldShowPlaceholder: false
},
displayProps: {
key: 'component',
excludeByKey: 'component',
compression: true,
optional: true
}
}
},
{
key: '',
label: tracker.string.Milestone,
presenter: tracker.component.MilestoneEditor,
props: {
kind: 'list',
size: 'small',
shape: 'round',
shouldShowPlaceholder: false,
listProps: {
shouldShowPlaceholder: false
},
displayProps: {
key: 'milestone',
excludeByKey: 'milestone',
compression: true,
optional: true
}
}
},
{
key: '',
presenter: view.component.DividerPresenter,
props: { type: 'divider', listProps: { compression: true } }
},
{
key: '',
label: tracker.string.Estimation,
presenter: tracker.component.EstimationEditor,
props: { kind: 'list', size: 'small', listProps: { key: 'estimation', fixed: 'left', compression: true } }
props: { kind: 'list', size: 'small' },
displayProps: { key: 'estimation', fixed: 'left', compression: true, dividerBefore: true }
},
{ key: '', presenter: view.component.DividerPresenter, props: { type: 'divider' } },
{
key: 'modifiedOn',
presenter: tracker.component.ModificationDatePresenter,
props: { listProps: { key: 'modified', fixed: 'left' } }
displayProps: { key: 'modified', fixed: 'left', dividerBefore: true }
},
{
key: 'assignee',
presenter: tracker.component.AssigneePresenter,
displayProps: { key: 'assigee', fixed: 'right' },
props: {
listProps: { key: 'assigee', fixed: 'right' },
key: 'assignee',
defaultClass: contact.class.Employee,
shouldShowLabel: false
@ -613,7 +652,8 @@ export function createModel (builder: Builder): void {
{
key: '',
presenter: tracker.component.IssuePresenter,
props: { type: 'issue', listProps: { fixed: 'left' } }
props: { type: 'issue' },
displayProps: { fixed: 'left' }
},
{
key: '',
@ -622,8 +662,11 @@ export function createModel (builder: Builder): void {
},
{ 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.DueDatePresenter,
props: { kind: 'list' }
},
{
key: '',
presenter: tracker.component.MilestoneEditor,
@ -631,22 +674,23 @@ export function createModel (builder: Builder): void {
kind: 'list',
size: 'small',
shape: 'round',
shouldShowPlaceholder: false,
listProps: {
shouldShowPlaceholder: false
},
displayProps: {
excludeByKey: 'milestone',
optional: true
}
}
},
{
key: '',
presenter: tracker.component.EstimationEditor,
props: { kind: 'list', size: 'small', listProps: { optional: true } }
props: { kind: 'list', size: 'small' },
displayProps: { optional: true }
},
{
key: 'modifiedOn',
presenter: tracker.component.ModificationDatePresenter,
props: { listProps: { fixed: 'right', optional: true } }
displayProps: { fixed: 'right', optional: true }
},
{
key: 'assignee',
@ -674,6 +718,10 @@ export function createModel (builder: Builder): void {
],
other: [showColorsViewOption]
},
configOptions: {
hiddenKeys: ['milestone', 'estimation', 'component', 'title', 'description'],
extraProps: { displayProps: { optional: true } }
},
config: [
// { key: '', presenter: tracker.component.PriorityEditor, props: { kind: 'list', size: 'small' } },
{
@ -681,42 +729,43 @@ export function createModel (builder: Builder): void {
presenter: tracker.component.IssueTemplatePresenter,
props: { type: 'issue', shouldUseMargin: true }
},
{ key: '', presenter: view.component.GrowPresenter, props: { type: 'grow' } },
// { key: '', presenter: tracker.component.DueDatePresenter, props: { kind: 'list' } },
{
key: '',
presenter: tracker.component.ComponentEditor,
label: tracker.string.Component,
props: {
kind: 'list',
size: 'small',
shouldShowPlaceholder: false,
listProps: { optional: true, compression: true }
}
shouldShowPlaceholder: false
},
displayProps: { key: 'component', optional: true, compression: true }
},
{
key: '',
label: tracker.string.Milestone,
presenter: tracker.component.MilestoneEditor,
props: {
kind: 'list',
size: 'small',
shouldShowPlaceholder: false,
listProps: { optional: true, compression: true }
}
shouldShowPlaceholder: false
},
displayProps: { key: 'milestone', optional: true, compression: true }
},
{
key: '',
label: tracker.string.Estimation,
presenter: tracker.component.TemplateEstimationEditor,
props: {
kind: 'list',
size: 'small',
listProps: { optional: true, compression: true }
}
size: 'small'
},
displayProps: { key: 'estimation', optional: true, compression: true }
},
{ key: '', presenter: view.component.DividerPresenter, props: { type: 'divider' } },
{
key: 'modifiedOn',
presenter: tracker.component.ModificationDatePresenter,
props: { listProps: { fixed: 'right' } }
displayProps: { fixed: 'right', dividerBefore: true }
},
{
key: 'assignee',
@ -738,7 +787,10 @@ export function createModel (builder: Builder): void {
...issuesOptions(true),
groupDepth: 1
},
config: []
configOptions: {
strict: true
},
config: ['subIssues', 'priority', 'component', 'dueDate', 'labels', 'estimation', 'attachments', 'comments']
},
tracker.viewlet.IssueKanban
)
@ -1807,14 +1859,22 @@ export function createModel (builder: Builder): void {
attachTo: tracker.class.Milestone,
descriptor: view.viewlet.List,
viewOptions: milestoneOptions,
configOptions: {
hiddenKeys: ['targetDate', 'label', 'description'],
extraProps: { displayProps: { optional: true } }
},
config: [
{
key: 'status',
props: { width: '1rem', kind: 'list', size: 'small', justify: 'center' }
},
{ key: '', presenter: tracker.component.MilestonePresenter, props: { shouldUseMargin: true } },
{ key: '', presenter: view.component.GrowPresenter, props: { type: 'grow' } },
{ key: '', presenter: tracker.component.MilestoneDatePresenter, props: { field: 'targetDate' } }
{
key: '',
label: tracker.string.TargetDate,
presenter: tracker.component.MilestoneDatePresenter,
props: { field: 'targetDate' }
}
]
},
tracker.viewlet.MilestoneList
@ -1879,23 +1939,24 @@ export function createModel (builder: Builder): void {
attachTo: tracker.class.Component,
descriptor: view.viewlet.List,
viewOptions: componentListViewOptions,
configOptions: {
hiddenKeys: ['label', 'description'],
extraProps: { displayProps: { optional: true } }
},
config: [
{
key: '',
presenter: tracker.component.ComponentPresenter,
props: { kind: 'list' }
},
{ key: '', presenter: view.component.GrowPresenter, props: { type: 'grow' } },
{ key: '', presenter: view.component.DividerPresenter, props: { type: 'divider' } },
{
key: '$lookup.lead',
presenter: tracker.component.LeadPresenter,
props: { _class: tracker.class.Component, defaultClass: contact.class.Employee, shouldShowLabel: false }
displayProps: {
dividerBefore: true,
key: 'lead'
},
{
key: '',
presenter: tracker.component.DeleteComponentPresenter,
props: { kind: 'transparent', size: 'small' }
props: { _class: tracker.class.Component, defaultClass: contact.class.Employee, shouldShowLabel: false }
}
]
},

View File

@ -244,6 +244,9 @@
color: var(--theme-content-color);
}
}
&.small {
padding: 0 0.25rem;
}
}
&.link-bordered {
padding: 0 0.5rem;

View File

@ -134,6 +134,7 @@
}
&.small {
height: 1.5rem;
padding: 0 0.25rem;
}
&.medium {
height: 2rem;
@ -239,6 +240,10 @@
color: var(--theme-dark-color);
border-radius: 0.25rem;
&.small {
padding: 0 0.25rem;
}
.btn-icon {
color: var(--theme-darker-color);
}

View File

@ -28,9 +28,9 @@
export let inline: boolean = false
export let accent: boolean = false
$: employee = $employeeByIdStore.get((value as EmployeeAccount).employee)
$: employee = $employeeByIdStore.get((value as EmployeeAccount)?.employee)
const valueLabel = value.email === systemAccountEmail ? core.string.System : getEmbeddedLabel(value.email)
const valueLabel = value?.email === systemAccountEmail ? core.string.System : getEmbeddedLabel(value?.email)
</script>
{#if value}

View File

@ -21,8 +21,8 @@
import type { WithLookup } from '@hcengineering/core'
import type { Lead } from '@hcengineering/lead'
import { ActionIcon, Component, DueDatePresenter, IconMoreH, showPanel, showPopup } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { ContextMenu } from '@hcengineering/view-resources'
import view, { BuildModelKey } from '@hcengineering/view'
import { ContextMenu, enabledConfig } from '@hcengineering/view-resources'
import lead from '../plugin'
import notification from '@hcengineering/notification'
import { getClient } from '@hcengineering/presentation'
@ -30,6 +30,8 @@
import LeadPresenter from './LeadPresenter.svelte'
export let object: WithLookup<Lead>
export let config: (string | BuildModelKey)[]
const client = getClient()
const assigneeAttribute = client.getHierarchy().getAttribute(lead.class.Lead, 'assignee')
@ -63,31 +65,23 @@
</div>
<div class="flex-col">
<div class="flex-between">
{#if object.$lookup?.attachedTo}
{#if enabledConfig(config, 'attachedTo') && object.$lookup?.attachedTo}
<ContactPresenter value={object.$lookup.attachedTo} />
{/if}
<div class="flex-row-center">
{#if (object.attachments ?? 0) > 0}
<div class="step-lr75">
<div class="flex-row-center gap-3">
{#if enabledConfig(config, 'attachments') && (object.attachments ?? 0) > 0}
<AttachmentsPresenter value={object.attachments} {object} />
</div>
{/if}
{#if (object.comments ?? 0) > 0}
<div class="step-lr75">
{#if enabledConfig(config, 'comments') && (object.comments ?? 0) > 0}
<CommentsPresenter value={object.comments} {object} />
</div>
{/if}
</div>
</div>
<div class="flex-row-reverse flex-between mt-2">
<AssigneePresenter
value={object.assignee}
issueId={object._id}
defaultClass={contact.class.Employee}
currentSpace={object.space}
placeholderLabel={assigneeAttribute.label}
/>
<div class="flex-row-center flex-between mt-2">
<LeadPresenter value={object} />
{#if enabledConfig(config, 'dueDate')}
<DueDatePresenter
size={'small'}
value={object.dueDate}
shouldRender={object.dueDate !== null && object.dueDate !== undefined}
shouldIgnoreOverdue={object.doneState !== null}
@ -95,7 +89,16 @@
await client.update(object, { dueDate: e })
}}
/>
<LeadPresenter value={object} />
{/if}
{#if enabledConfig(config, 'assignee')}
<AssigneePresenter
value={object.assignee}
issueId={object._id}
defaultClass={contact.class.Employee}
currentSpace={object.space}
placeholderLabel={assigneeAttribute.label}
/>
{/if}
</div>
</div>
</div>

View File

@ -24,13 +24,14 @@
import { AssigneePresenter, StateRefPresenter } from '@hcengineering/task-resources'
import tracker from '@hcengineering/tracker'
import { Component, DueDatePresenter, showPanel } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { ObjectPresenter } from '@hcengineering/view-resources'
import view, { BuildModelKey } from '@hcengineering/view'
import { ObjectPresenter, enabledConfig } from '@hcengineering/view-resources'
import ApplicationPresenter from './ApplicationPresenter.svelte'
export let object: WithLookup<Applicant>
export let dragged: boolean
export let groupByKey: string
export let config: (string | BuildModelKey)[]
const client = getClient()
const hierarchy = client.getHierarchy()
@ -48,14 +49,16 @@
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="flex-col pt-2 pb-2 pr-4 pl-4 cursor-pointer" on:click={showCandidate}>
<div class="p-1 flex-between">
{#if enabledConfig(config, 'space') || enabledConfig(config, 'company')}
<div class="p-1 flex-between gap-2">
{#if enabledConfig(config, 'space')}
<ObjectPresenter _class={recruit.class.Vacancy} objectId={object.space} value={object.$lookup?.space} />
{#if company}
<div class="ml-2">
{/if}
{#if company && enabledConfig(config, 'company')}
<ObjectPresenter _class={contact.class.Organization} objectId={company} />
</div>
{/if}
</div>
{/if}
<div class="flex-between mb-3">
<div class="flex-row-center">
<Avatar avatar={object.$lookup?.attachedTo?.avatar} size={'medium'} />
@ -63,7 +66,7 @@
<div class="fs-title over-underline lines-limit-2">
{object.$lookup?.attachedTo ? getName(object.$lookup.attachedTo) : ''}
</div>
{#if !isTitleHidden}
{#if !isTitleHidden && enabledConfig(config, 'title')}
<div class="text-sm lines-limit-2">{object.$lookup?.attachedTo?.title ?? ''}</div>
{/if}
</div>
@ -75,13 +78,13 @@
</div>
{/if}
</div>
{#if channels && channels.length > 0}
{#if channels && channels.length > 0 && enabledConfig(config, 'channels')}
<div class="tool mr-1 flex-row-center">
<div class="step-lr75">
<Component
showLoading={false}
is={contact.component.ChannelsPresenter}
props={{ value: channels, object: object.$lookup?.attachedTo, length: 'tiny' }}
props={{ value: channels, object: object.$lookup?.attachedTo, length: 'tiny', size: 'inline' }}
/>
</div>
</div>
@ -91,11 +94,13 @@
<div class="flex-row-center">
<div class="sm-tool-icon step-lr75">
<div class="mr-2">
<ApplicationPresenter value={object} />
<ApplicationPresenter value={object} inline />
</div>
<Component showLoading={false} is={tracker.component.RelatedIssueSelector} props={{ object }} />
</div>
{#if enabledConfig(config, 'dueDate')}
<DueDatePresenter
size={'small'}
value={object.dueDate}
shouldRender={object.dueDate !== null && object.dueDate !== undefined}
shouldIgnoreOverdue={object.doneState !== null}
@ -103,13 +108,12 @@
await client.update(object, { dueDate: e })
}}
/>
{#if (object.attachments ?? 0) > 0}
<div class="step-lr75">
<AttachmentsPresenter value={object.attachments} {object} />
</div>
{/if}
{#if (object.comments ?? 0) > 0 || (object.$lookup?.attachedTo !== undefined && (object.$lookup.attachedTo.comments ?? 0) > 0)}
<div class="step-lr75">
<div class="flex-row-center gap-3">
{#if (object.attachments ?? 0) > 0 && enabledConfig(config, 'attachments')}
<AttachmentsPresenter value={object.attachments} {object} />
{/if}
{#if enabledConfig(config, 'comments')}
{#if (object.comments ?? 0) > 0}
<CommentsPresenter value={object.comments} {object} />
{/if}
@ -120,9 +124,10 @@
withInput={false}
/>
{/if}
</div>
{/if}
</div>
</div>
{#if enabledConfig(config, 'assignee')}
<AssigneePresenter
value={object.assignee}
issueId={object._id}
@ -130,9 +135,11 @@
currentSpace={object.space}
placeholderLabel={assigneeAttribute.label}
/>
{/if}
</div>
{#if groupByKey !== 'state'}
{#if groupByKey !== 'state' && enabledConfig(config, 'state')}
<StateRefPresenter
size={'small'}
value={object.state}
onChange={(state) => {
client.update(object, { state })

View File

@ -38,6 +38,7 @@
} from '@hcengineering/ui'
import {
AttributeModel,
BuildModelKey,
CategoryOption,
Viewlet,
ViewOptionModel,
@ -70,6 +71,7 @@
export let viewOptionsConfig: ViewOptionModel[] | undefined
export let viewOptions: ViewOptions
export let viewlet: Viewlet
export let config: (string | BuildModelKey)[]
export let options: FindOptions<Task> | undefined
@ -296,7 +298,7 @@
</div>
</svelte:fragment>
<svelte:fragment slot="card" let:object let:dragged>
<svelte:component this={presenter} {object} {dragged} {groupByKey} />
<svelte:component this={presenter} {object} {dragged} {groupByKey} {config} />
</svelte:fragment>
<!-- eslint-disable-next-line no-undef -->
<svelte:fragment slot="doneBar" let:onDone>

View File

@ -16,6 +16,7 @@
<script lang="ts">
import { Ref, StatusValue } from '@hcengineering/core'
import { statusStore } from '@hcengineering/presentation'
import type { ButtonSize } from '@hcengineering/ui'
import { State } from '@hcengineering/task'
import StateEditor from './StateEditor.svelte'
import StatePresenter from './StatePresenter.svelte'
@ -24,12 +25,13 @@
export let onChange: ((value: Ref<State>) => void) | undefined = undefined
export let colorInherit: boolean = false
export let accent: boolean = false
export let size: ButtonSize = 'medium'
</script>
{#if value}
{@const state = $statusStore.get(typeof value === 'string' ? value : value.values?.[0]?._id)}
{#if onChange !== undefined && state !== undefined}
<StateEditor value={state._id} space={state.space} {onChange} kind="link" size="medium" />
<StateEditor value={state._id} space={state.space} {onChange} kind="link" {size} />
{:else}
<StatePresenter value={state} {colorInherit} {accent} on:accent-color />
{/if}

View File

@ -15,26 +15,47 @@
<script lang="ts">
import { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import { Component } from '@hcengineering/tracker'
import { Component as ViewComponent } from '@hcengineering/ui'
import { Viewlet, ViewOptions } from '@hcengineering/view'
import { Loading, Component as ViewComponent } from '@hcengineering/ui'
import view, { Viewlet, ViewletPreference, ViewOptions } from '@hcengineering/view'
import tracker from '../../plugin'
import CreateComponent from './NewComponent.svelte'
import { createQuery } from '@hcengineering/presentation'
export let viewlet: WithLookup<Viewlet>
export let viewOptions: ViewOptions
export let query: DocumentQuery<Component> = {}
export let space: Ref<Space> | undefined
const preferenceQuery = createQuery()
let preference: ViewletPreference | undefined
let loading = true
$: viewlet &&
preferenceQuery.query(
view.class.ViewletPreference,
{
attachedTo: viewlet._id
},
(res) => {
preference = res[0]
loading = false
},
{ limit: 1 }
)
const createItemDialog = CreateComponent
const createItemLabel = tracker.string.Component
</script>
{#if viewlet?.$lookup?.descriptor?.component}
{#if loading}
<Loading />
{:else}
<ViewComponent
is={viewlet.$lookup.descriptor.component}
props={{
_class: tracker.class.Component,
config: viewlet.config,
config: preference?.config ?? viewlet.config,
options: viewlet.options,
createItemDialog,
createItemLabel,
@ -45,4 +66,5 @@
query
}}
/>
{/if}
{/if}

View File

@ -1,10 +1,11 @@
<script lang="ts">
import { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import { Issue } from '@hcengineering/tracker'
import { Component } from '@hcengineering/ui'
import { Viewlet, ViewOptions } from '@hcengineering/view'
import { Component, Loading } from '@hcengineering/ui'
import view, { Viewlet, ViewletPreference, ViewOptions } from '@hcengineering/view'
import tracker from '../../plugin'
import CreateIssue from '../CreateIssue.svelte'
import { createQuery } from '@hcengineering/presentation'
export let viewlet: WithLookup<Viewlet>
export let query: DocumentQuery<Issue> = {}
@ -12,16 +13,36 @@
export let viewOptions: ViewOptions
const preferenceQuery = createQuery()
let preference: ViewletPreference | undefined
let loading = true
$: viewlet &&
preferenceQuery.query(
view.class.ViewletPreference,
{
attachedTo: viewlet._id
},
(res) => {
preference = res[0]
loading = false
},
{ limit: 1 }
)
const createItemDialog = CreateIssue
const createItemLabel = tracker.string.AddIssueTooltip
</script>
{#if viewlet?.$lookup?.descriptor?.component}
{#if loading}
<Loading />
{:else}
<Component
is={viewlet.$lookup.descriptor.component}
props={{
_class: tracker.class.Issue,
config: viewlet.config,
config: preference?.config ?? viewlet.config,
options: viewlet.options,
createItemDialog,
createItemLabel,
@ -32,4 +53,5 @@
query
}}
/>
{/if}
{/if}

View File

@ -50,6 +50,7 @@
} from '@hcengineering/ui'
import {
AttributeModel,
BuildModelKey,
CategoryOption,
Viewlet,
ViewOptionModel,
@ -57,6 +58,7 @@
ViewQueryOption
} from '@hcengineering/view'
import {
enabledConfig,
focusStore,
getCategories,
getCategorySpaces,
@ -90,6 +92,7 @@
export let viewOptionsConfig: ViewOptionModel[] | undefined
export let viewOptions: ViewOptions
export let viewlet: Viewlet
export let config: (string | BuildModelKey)[]
$: currentSpace = space || tracker.project.DefaultProject
$: groupByKey = (viewOptions.groupBy[0] ?? noCategory) as IssuesGrouping
@ -251,6 +254,21 @@
space: doc.space
}
}
function shouldShowFooter (
config: (string | BuildModelKey)[],
reports: number,
estimations: number,
issue: WithLookup<Issue>
): boolean {
if (enabledConfig(config, 'estimation') && (reports > 0 || estimations > 0)) return true
if (enabledConfig(config, 'comments')) {
if ((issue.comments ?? 0) > 0) return true
if ((issue.$lookup?.attachedTo?.comments ?? 0) > 0) return true
}
if (enabledConfig(config, 'attachments') && (issue.attachments ?? 0) > 0) return true
return false
}
</script>
{#if categories.length === 0}
@ -361,10 +379,19 @@
{object.title}
</div>
<div class="card-labels">
{#if issue && issue.subIssues > 0}
{#if enabledConfig(config, 'subIssues') && issue && issue.subIssues > 0}
<SubIssuesSelector value={issue} {currentProject} size={'small'} />
{/if}
<PriorityEditor value={issue} isEditable={true} kind={'link-bordered'} size={'small'} justify={'center'} />
{#if enabledConfig(config, 'priority')}
<PriorityEditor
value={issue}
isEditable={true}
kind={'link-bordered'}
size={'small'}
justify={'center'}
/>
{/if}
{#if enabledConfig(config, 'component')}
<ComponentEditor
value={issue}
isEditable={true}
@ -374,8 +401,12 @@
width={''}
bind:onlyIcon={fullFilled[issueId]}
/>
{/if}
{#if enabledConfig(config, 'dueDate')}
<DueDatePresenter value={issue} size={'small'} kind={'link-bordered'} />
{/if}
</div>
{#if enabledConfig(config, 'labels')}
<div
class="card-labels labels"
use:tooltip={{
@ -391,14 +422,17 @@
}}
/>
</div>
{#if reports > 0 || estimations > 0 || (object.comments ?? 0) > 0 || (object.$lookup?.attachedTo !== undefined && (object.$lookup.attachedTo.comments ?? 0) > 0)}
{/if}
{#if shouldShowFooter(config, reports, estimations, object)}
<div class="card-footer flex-between">
{#if enabledConfig(config, 'estimation')}
<EstimationEditor kind={'list'} size={'small'} value={issue} />
<!-- {@debug issue} -->
{/if}
<div class="flex-row-center gap-3 reverse">
{#if (object.attachments ?? 0) > 0}
{#if enabledConfig(config, 'attachments') && (object.attachments ?? 0) > 0}
<AttachmentsPresenter value={object.attachments} {object} />
{/if}
{#if enabledConfig(config, 'comments')}
{#if (object.comments ?? 0) > 0}
<CommentsPresenter value={object.comments} {object} />
{/if}
@ -409,6 +443,7 @@
withInput={false}
/>
{/if}
{/if}
</div>
</div>
{:else}

View File

@ -28,11 +28,13 @@
$: text = project ? `${getIssueId(project, issue)} ${issue.title}` : issue.title
</script>
{#if status}
<div class="flex-row-center">
{#if status}
<div class="icon mr-2">
<IssueStatusIcon value={status} {size} />
</div>
{/if}
<span class="label" class:text-base={huge}>
{/if}
<span class="label" class:text-base={huge}>
<span>{text}</span>
</span>
</span>
</div>

View File

@ -1,10 +1,11 @@
<script lang="ts">
import { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import { Milestone } from '@hcengineering/tracker'
import { Component } from '@hcengineering/ui'
import { Viewlet, ViewOptions } from '@hcengineering/view'
import { Component, Loading } from '@hcengineering/ui'
import view, { Viewlet, ViewletPreference, ViewOptions } from '@hcengineering/view'
import tracker from '../../plugin'
import NewMilestone from './NewMilestone.svelte'
import { createQuery } from '@hcengineering/presentation'
export let viewlet: WithLookup<Viewlet>
export let query: DocumentQuery<Milestone> = {}
@ -13,16 +14,36 @@
// Extra properties
export let viewOptions: ViewOptions
const preferenceQuery = createQuery()
let preference: ViewletPreference | undefined
let loading = true
$: viewlet &&
preferenceQuery.query(
view.class.ViewletPreference,
{
attachedTo: viewlet._id
},
(res) => {
preference = res[0]
loading = false
},
{ limit: 1 }
)
const createItemDialog = NewMilestone
const createItemLabel = tracker.string.CreateMilestone
</script>
{#if viewlet?.$lookup?.descriptor?.component}
{#if loading}
<Loading />
{:else}
<Component
is={viewlet.$lookup.descriptor.component}
props={{
_class: tracker.class.Milestone,
config: viewlet.config,
config: preference?.config ?? viewlet.config,
options: viewlet.options,
createItemDialog,
createItemLabel,
@ -34,4 +55,5 @@
props: {}
}}
/>
{/if}
{/if}

View File

@ -15,7 +15,6 @@
<script lang="ts">
import core, { AttachedDoc, Doc, SortingOrder, TxCollectionCUD, TxCUD } from '@hcengineering/core'
import { Issue } from '@hcengineering/tracker'
import view from '@hcengineering/view'
import { groupBy, List } from '@hcengineering/view-resources'
import tracker from '../../plugin'
import ChangedObjectPresenter from './ChangedObjectPresenter.svelte'
@ -101,7 +100,6 @@
presenter: ChangedObjectPresenter,
props: { onNavigate }
},
{ key: '', presenter: view.component.GrowPresenter, props: { type: 'grow' } },
{
key: 'modifiedOn',
presenter: tracker.component.ModificationDatePresenter,

View File

@ -13,13 +13,12 @@
// limitations under the License.
-->
<script lang="ts">
import { createQuery } from '@hcengineering/presentation'
import core, { Doc, Ref, SortingOrder, TxCollectionCUD, TxCreateDoc, TxCUD, TxUpdateDoc } from '@hcengineering/core'
import { Issue, TimeSpendReport } from '@hcengineering/tracker'
import tracker from '../../plugin'
import { Employee } from '@hcengineering/contact'
import view from '@hcengineering/view'
import core, { Doc, Ref, SortingOrder, TxCollectionCUD, TxCreateDoc, TxCUD, TxUpdateDoc } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { Issue, TimeSpendReport } from '@hcengineering/tracker'
import { List } from '@hcengineering/view-resources'
import tracker from '../../plugin'
type TimeSpendByEmployee = { [key: Ref<Employee>]: number | undefined }
type TimeSpendByIssue = { [key: Ref<Issue>]: TimeSpendByEmployee | undefined }
@ -165,19 +164,22 @@
props: { shouldUseMargin: true, showParent: false, onClick: onNavigate }
},
{ key: '', presenter: tracker.component.SubIssuesSelector, props: {} },
{ key: '', presenter: view.component.GrowPresenter, props: { type: 'grow' } },
{ key: '', presenter: tracker.component.DueDatePresenter, props: { kind: 'list', isEditable: false } },
{
key: '',
presenter: tracker.component.DueDatePresenter,
props: { kind: 'list', isEditable: false }
},
{
key: '',
presenter: tracker.component.MilestoneEditor,
displayProps: {
excludeByKey: 'milestone'
},
props: {
kind: 'list',
size: 'small',
shape: 'round',
shouldShowPlaceholder: false,
listProps: {
excludeByKey: 'milestone'
},
isEditable: false
}
},

View File

@ -2,15 +2,33 @@
import { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import { IssueTemplate } from '@hcengineering/tracker'
import { Component } from '@hcengineering/ui'
import { Viewlet, ViewOptions } from '@hcengineering/view'
import view, { Viewlet, ViewletPreference, ViewOptions } from '@hcengineering/view'
import tracker from '../../plugin'
import CreateIssueTemplate from './CreateIssueTemplate.svelte'
import { createQuery } from '@hcengineering/presentation'
export let viewlet: WithLookup<Viewlet>
export let viewOptions: ViewOptions
export let query: DocumentQuery<IssueTemplate> = {}
export let space: Ref<Space> | undefined
const preferenceQuery = createQuery()
let preference: ViewletPreference | undefined
let loading = true
$: viewlet &&
preferenceQuery.query(
view.class.ViewletPreference,
{
attachedTo: viewlet._id
},
(res) => {
preference = res[0]
loading = false
},
{ limit: 1 }
)
const createItemDialog = CreateIssueTemplate
const createItemLabel = tracker.string.IssueTemplate
</script>
@ -20,7 +38,7 @@
is={viewlet.$lookup.descriptor.component}
props={{
_class: tracker.class.IssueTemplate,
config: viewlet.config,
config: preference?.config ?? viewlet.config,
options: viewlet.options,
createItemDialog,
createItemLabel,

View File

@ -136,6 +136,7 @@ export interface Milestone extends Doc {
* @public
*/
export interface Issue extends AttachedDoc {
attachedTo: Ref<Issue>
title: string
description: Markup
status: Ref<IssueStatus>

View File

@ -96,4 +96,8 @@
<symbol id="filter" viewBox="0 0 24 24">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.3,4.2C2.4,3.9,2.7,3.8,3,3.8h18c0.3,0,0.6,0.2,0.7,0.4c0.1,0.3,0.1,0.6-0.1,0.8l-7,8.2V21c0,0.3-0.1,0.5-0.4,0.6c-0.2,0.1-0.5,0.2-0.7,0l-3.6-1.8c-0.3-0.1-0.4-0.4-0.4-0.7v-6L2.4,5C2.2,4.8,2.2,4.5,2.3,4.2z M4.6,5.2l6.1,7.2c0.1,0.1,0.2,0.3,0.2,0.5v5.8l2.1,1v-6.9c0-0.2,0.1-0.4,0.2-0.5l6.1-7.2H4.6z" />
</symbol>
<symbol id="configure" viewBox="0 0 16 16">
<path d="M15 4.5C15 4.22386 14.7761 4 14.5 4H12.95C12.7 2.85 11.7 2 10.5 2C9.3 2 8.3 2.85 8.05 4H1.5C1.22386 4 1 4.22386 1 4.5C1 4.77614 1.22386 5 1.5 5H8.05C8.3 6.15 9.3 7 10.5 7C11.7 7 12.7 6.15 12.95 5H14.5C14.7761 5 15 4.77614 15 4.5ZM10.5 6C9.65 6 9 5.35 9 4.5C9 3.65 9.65 3 10.5 3C11.35 3 12 3.65 12 4.5C12 5.35 11.35 6 10.5 6Z"/>
<path d="M1 11.5C1 11.7761 1.22386 12 1.5 12H3.05C3.3 13.15 4.3 14 5.5 14C6.7 14 7.7 13.15 7.95 12H14.5C14.7761 12 15 11.7761 15 11.5C15 11.2239 14.7761 11 14.5 11H7.95C7.7 9.85 6.7 9 5.5 9C4.3 9 3.3 9.85 3.05 11H1.5C1.22386 11 1 11.2239 1 11.5ZM5.5 10C6.35 10 7 10.65 7 11.5C7 12.35 6.35 13 5.5 13C4.65 13 4 12.35 4 11.5C4 10.65 4.65 10 5.5 10Z"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -97,6 +97,7 @@
"SaveAs": "Save as",
"And": "and",
"Between": "is between",
"ShowColors": "Use colors"
"ShowColors": "Use colors",
"Show": "Show"
}
}

View File

@ -93,6 +93,7 @@
"SaveAs": "Сохранить как",
"And": "и",
"Between": "между",
"ShowColors": "Использовать цвета"
"ShowColors": "Использовать цвета",
"Show": "Отображение"
}
}

View File

@ -38,7 +38,8 @@ loadMetadata(view.icon, {
Model: `${icons}#model`,
DevModel: `${icons}#devmodel`,
ViewButton: `${icons}#viewButton`,
Filter: `${icons}#filter`
Filter: `${icons}#filter`,
Configure: `${icons}#configure`
})
addStringsLoader(viewId, async (lang: string) => await import(`../lang/${lang}.json`))

View File

@ -0,0 +1,63 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Button, ButtonKind, eventToHTMLElement, showPopup } from '@hcengineering/ui'
import { ViewOptions, Viewlet } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import view from '../plugin'
import { focusStore } from '../selection'
import { setViewOptions } from '../viewOptions'
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
function clickHandler (event: MouseEvent) {
showPopup(
ViewOptionsEditor,
{ viewlet, config: viewlet?.viewOptions, viewOptions },
eventToHTMLElement(event),
undefined,
(result) => {
if (result?.key === undefined) return
if (viewlet) {
viewOptions = { ...viewOptions, [result.key]: result.value }
// Clear selection on view settings change.
focusStore.set({})
dispatch('viewOptions', viewOptions)
setViewOptions(viewlet, viewOptions)
}
}
)
}
</script>
{#if viewlet?.viewOptions !== undefined}
<Button
icon={view.icon.ViewButton}
label={view.string.View}
{kind}
showTooltip={{ label: view.string.CustomizeView }}
bind:input={btn}
on:click={clickHandler}
/>
{/if}

View File

@ -16,26 +16,10 @@
import core, { AnyAttribute, ArrOf, Class, Doc, Ref, Type } from '@hcengineering/core'
import { Asset, IntlString } from '@hcengineering/platform'
import preferencePlugin from '@hcengineering/preference'
import presentation, {
Card,
createQuery,
getAttributePresenterClass,
getClient,
hasResource
} from '@hcengineering/presentation'
import {
Button,
getEventPositionElement,
getPlatformColorForText,
Loading,
SelectPopup,
showPopup,
themeStore,
ToggleButton
} from '@hcengineering/ui'
import { createQuery, getAttributePresenterClass, getClient, hasResource } from '@hcengineering/presentation'
import { Loading, ToggleWithLabel } from '@hcengineering/ui'
import { BuildModelKey, Viewlet, ViewletPreference } from '@hcengineering/view'
import { deepEqual } from 'fast-equals'
import { createEventDispatcher } from 'svelte'
import view from '../plugin'
import { buildConfigLookup, getKeyLabel } from '../utils'
@ -53,7 +37,7 @@
(res) => {
preference = res[0]
attributes = getConfig(viewlet, preference)
enabled = attributes.filter((p) => p.enabled)
classes = groupByClasses(attributes)
loading = false
},
{ limit: 1 }
@ -64,9 +48,7 @@
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
let attributes: AttributeConfig[] = []
let enabled: AttributeConfig[] = []
let loading = true
interface AttributeConfig {
@ -94,6 +76,7 @@
const clazz = hierarchy.getClass(viewlet.attachTo)
for (const param of viewlet.config) {
if (typeof param === 'string') {
if (viewlet.configOptions?.hiddenKeys?.includes(param)) continue
if (param.length === 0) {
result.push(getObjectConfig(viewlet.attachTo, param))
} else {
@ -106,9 +89,10 @@
})
}
} else {
if (viewlet.configOptions?.hiddenKeys?.includes(param.key)) continue
result.push({
value: param,
label: param.label as IntlString,
label: param.label ?? getKeyLabel(client, viewlet.attachTo, param.key, lookup),
enabled: true,
_class: viewlet.attachTo,
icon: clazz.icon
@ -130,11 +114,14 @@
function processAttribute (attribute: AnyAttribute, result: AttributeConfig[], useMixinProxy = false): void {
if (attribute.hidden === true || attribute.label === undefined) return
if (viewlet.hiddenKeys?.includes(attribute.name)) return
if (viewlet.configOptions?.hiddenKeys?.includes(attribute.name)) return
if (hierarchy.isDerived(attribute.type._class, core.class.Collection)) return
const value = getValue(attribute.name, attribute.type)
if (result.findIndex((p) => p.value === attribute.name) !== -1) return
if (result.findIndex((p) => p.value === value) !== -1) return
for (const res of result) {
const key = typeof res.value === 'string' ? res.value : res.value.key
if (key === attribute.name) return
if (key === value) return
}
const { attrClass, category } = getAttributePresenterClass(hierarchy, attribute)
const mixin =
category === 'object'
@ -145,7 +132,7 @@
const presenter = hierarchy.classHierarchyMixin(attrClass, mixin, (m) => hasResource(m.presenter))?.presenter
if (presenter === undefined) return
const clazz = hierarchy.getClass(attribute.attributeOf)
const extraProps = viewlet.configOptions?.extraProps
if (useMixinProxy) {
const newValue = {
value: attribute.attributeOf + '.' + attribute.name,
@ -159,7 +146,7 @@
}
} else {
const newValue = {
value,
value: extraProps ? { ...extraProps, key: value } : value,
label: attribute.label,
enabled: false,
_class: attribute.attributeOf,
@ -184,6 +171,7 @@
function getConfig (viewlet: Viewlet, preference: ViewletPreference | undefined): AttributeConfig[] {
const result = getBaseConfig(viewlet)
if (viewlet.configOptions?.strict !== true) {
const allAttributes = hierarchy.getAllAttributes(viewlet.attachTo)
for (const [, attribute] of allAttributes) {
processAttribute(attribute, result)
@ -207,12 +195,16 @@
processAttribute(attr, result, true)
})
})
}
return preference === undefined ? result : setStatus(result, preference)
}
async function save (): Promise<void> {
const config = enabled.map((p) => p.value)
const config = Array.from(classes.values())
.flat()
.filter((p) => p.enabled)
.map((p) => p.value)
if (preference !== undefined) {
await client.update(preference, {
config
@ -225,69 +217,21 @@
}
}
function restoreDefault (): void {
attributes = getConfig(viewlet, undefined)
enabled = attributes.filter((p) => p.enabled)
}
// function restoreDefault (): void {
// attributes = getConfig(viewlet, undefined)
// classes = groupByClasses(attributes)
// }
function setStatus (result: AttributeConfig[], preference: ViewletPreference): AttributeConfig[] {
for (const key of result) {
key.enabled = preference.config.findIndex((p) => deepEqual(p, key.value)) !== -1
}
result.sort((a, b) => {
if (a.enabled !== b.enabled) {
return a.enabled ? -1 : 1
}
return (
preference.config.findIndex((p) => deepEqual(p, a.value)) -
preference.config.findIndex((p) => deepEqual(p, b.value))
)
})
return result
}
const elements: HTMLElement[] = []
let selected: number | undefined
function dragswap (ev: MouseEvent, i: number, s: number): boolean {
if (i < s) {
if (elements[i].offsetTop !== elements[s].offsetTop) {
return ev.offsetY < elements[i].offsetHeight / 2
} else {
return ev.offsetX < elements[i].offsetWidth / 2
}
} else if (i > s) {
if (elements[i].offsetTop !== elements[s].offsetTop) {
return ev.offsetY > elements[i].offsetHeight / 2
} else {
return ev.offsetX > elements[i].offsetWidth / 2
}
}
return false
}
function dragover (ev: MouseEvent, i: number) {
const s = selected as number
if (dragswap(ev, i, s)) {
;[enabled[i], enabled[s]] = [enabled[s], enabled[i]]
selected = i
}
}
function getColor (attribute: AttributeConfig, black: boolean): string {
const color = getPlatformColorForText(attribute._class, black)
return `${color + (attribute.enabled ? 'cc' : '33')};`
}
function getStyle (attribute: AttributeConfig, black: boolean): string {
const color = getPlatformColorForText(attribute._class, black)
return `border: 1px solid ${color + (attribute.enabled ? 'ff' : 'cc')};`
}
function groupByClasses (attributes: AttributeConfig[]): Map<Ref<Class<Doc>>, AttributeConfig[]> {
const res = new Map()
for (const attribute of attributes) {
if (attribute.enabled) continue
const arr = res.get(attribute._class) ?? []
arr.push(attribute)
res.set(attribute._class, arr)
@ -295,88 +239,42 @@
return res
}
$: classes = groupByClasses(attributes)
function getClassLabel (_class: Ref<Class<Doc>>): IntlString {
return hierarchy.getClass(_class).label
}
let classes: Map<Ref<Class<Doc>>, AttributeConfig[]> = new Map()
</script>
<Card
label={view.string.CustomizeView}
okAction={save}
okLabel={presentation.string.Save}
canSave={true}
gap={'gapV-4'}
on:close={() => {
dispatch('close')
}}
on:changeContent
>
<div class="selectPopup p-2">
<div class="scroll">
{#if loading}
<Loading />
{:else}
<div class="flex-row-stretch flex-wrap">
{#each enabled as attribute, i}
<div
class="m-0-5 border-radius-1 overflow-label"
style={getStyle(attribute, $themeStore.dark)}
bind:this={elements[i]}
draggable={true}
on:dragover|preventDefault={(ev) => {
dragover(ev, i)
}}
on:drop|preventDefault
on:dragstart={() => {
selected = i
}}
on:dragend={() => {
selected = undefined
}}
>
<ToggleButton
backgroundColor={getColor(attribute, $themeStore.dark)}
icon={attribute.icon}
label={attribute.label}
bind:value={attribute.enabled}
on:change={() => {
enabled.splice(i, 1)
enabled = enabled
}}
/>
</div>
{/each}
</div>
<div class="flex-row-stretch flex-wrap">
{#each Array.from(classes.keys()) as _class}
<div class="m-0-5">
<Button
label={getClassLabel(_class)}
on:click={(e) => {
showPopup(
SelectPopup,
{
value: classes.get(_class)?.map((it) => ({ id: it.value, label: it.label }))
},
getEventPositionElement(e),
(val) => {
if (val !== undefined) {
const value = classes.get(_class)?.find((it) => it.value === val)
if (value) {
value.enabled = true
enabled.push(value)
enabled = enabled
}
}
}
)
}}
/>
</div>
{/each}
</div>
{#each Array.from(classes.keys()) as _class, i}
{@const items = classes.get(_class) ?? []}
{#if i !== 0}
<div class="menu-separator" />
{/if}
<svelte:fragment slot="footer">
<Button label={view.string.RestoreDefaults} on:click={restoreDefault} />
</svelte:fragment>
</Card>
{#each items as item}
<div class="item">
<ToggleWithLabel
on={item.enabled}
label={item.label}
on:change={(e) => {
item.enabled = e.detail
save()
}}
/>
</div>
{/each}
{/each}
{/if}
</div>
</div>
<style lang="scss">
.item {
padding: 0.5rem;
border-radius: 0.25rem;
&:hover {
background-color: var(--theme-button-hovered);
}
}
</style>

View File

@ -13,58 +13,33 @@
// limitations under the License.
-->
<script lang="ts">
import { Button, ButtonKind, eventToHTMLElement, showPopup } from '@hcengineering/ui'
import { Button, ButtonKind, showPopup } from '@hcengineering/ui'
import { ViewOptions, Viewlet } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import view from '../plugin'
import { focusStore } from '../selection'
import { setViewOptions } from '../viewOptions'
import ViewOptionsEditor from './ViewOptions.svelte'
import ViewOptionsButton from './ViewOptionsButton.svelte'
import ViewletSetting from './ViewletSetting.svelte'
import IconArrowDown from './icons/ArrowDown.svelte'
export let viewlet: Viewlet | undefined
export let kind: ButtonKind = 'secondary'
export let viewOptions: ViewOptions
const dispatch = createEventDispatcher()
let btn: HTMLButtonElement
function clickHandler (event: MouseEvent) {
if (viewlet?.viewOptions !== undefined) {
showPopup(
ViewOptionsEditor,
{ viewlet, config: viewlet.viewOptions, viewOptions },
eventToHTMLElement(event),
undefined,
(result) => {
if (result?.key === undefined) return
if (viewlet) {
viewOptions = { ...viewOptions, [result.key]: result.value }
// Clear selection on view settings change.
focusStore.set({})
dispatch('viewOptions', viewOptions)
setViewOptions(viewlet, viewOptions)
}
}
)
} else {
showPopup(ViewletSetting, { viewlet }, btn)
}
}
</script>
{#if viewlet}
<div class="flex-row-center">
<div class="mr-3"><ViewOptionsButton {viewlet} {kind} {viewOptions} /></div>
<Button
icon={view.icon.ViewButton}
label={view.string.View}
iconRight={IconArrowDown}
icon={view.icon.Configure}
label={view.string.Show}
{kind}
showTooltip={{ label: view.string.CustomizeView }}
bind:input={btn}
on:click={clickHandler}
/>
</div>
{/if}

View File

@ -115,9 +115,9 @@
$: buildModel({ client, _class, keys: config, lookup }).then((res) => {
itemModels = res
res.forEach((m) => {
if (m.props?.listProps?.key !== undefined) {
const key = `list_item_${m.props.listProps.key}`
if (m.props.listProps.fixed) {
if (m.displayProps?.key !== undefined) {
const key = `list_item_${m.displayProps.key}`
if (m.displayProps.fixed) {
$fixedWidthStore[key] = 0
}
}

View File

@ -21,6 +21,8 @@
import { createEventDispatcher, onMount } from 'svelte'
import { FixedColumn } from '../..'
import view from '../../plugin'
import GrowPresenter from './GrowPresenter.svelte'
import DividerPresenter from './DividerPresenter.svelte'
export let docObject: Doc
export let model: AttributeModel[]
@ -64,11 +66,7 @@
}
function joinProps (attribute: AttributeModel, object: Doc, props: Record<string, any>) {
let clearAttributeProps = attribute.props
if (attribute.props?.listProps !== undefined) {
const { listProps, ...other } = attribute.props as any
clearAttributeProps = other
}
const clearAttributeProps = attribute.props
if (attribute.attribute?.type._class === core.class.EnumOf) {
return { ...clearAttributeProps, type: attribute.attribute.type, ...props }
}
@ -79,12 +77,14 @@
$: if (model) {
noCompressed = -1
model.forEach((m, i) => {
if (m.props?.listProps?.compression) noCompressed = i
if (m.displayProps?.compression) noCompressed = i
})
}
onMount(() => {
dispatch('on-mount')
})
$: growBefore = Math.ceil(model.filter((p) => p.displayProps?.optional !== true).length / 2)
</script>
<div
@ -131,36 +131,22 @@
/>
</div>
</div>
{#each model.filter((m) => !m.props?.listProps?.optional) as attributeModel, i}
{@const listProps = attributeModel.props?.listProps}
{#if attributeModel.props?.type === 'grow'}
<svelte:component this={attributeModel.presenter} />
{#if !compactMode}
<div class="optional-bar">
{#each model.filter((m) => m.props?.listProps?.optional) as attrModel, j}
{@const lp = attrModel.props?.listProps}
{@const v = getObjectValue(attrModel.key, docObject)}
{#if lp?.excludeByKey !== groupByKey && v !== undefined}
<svelte:component
this={attrModel.presenter}
value={getObjectValue(attrModel.key, docObject) ?? ''}
onChange={getOnChange(docObject, attrModel)}
kind={'list'}
compression
{...joinProps(attrModel, docObject, props)}
/>
{#each model.filter((p) => !p.displayProps?.optional) as attributeModel, i}
{@const displayProps = attributeModel.displayProps}
{#if !groupByKey || displayProps?.excludeByKey !== groupByKey}
{#if !(compactMode && displayProps?.compression)}
{#if i === growBefore}
<GrowPresenter />
{#each model.filter((p) => p.displayProps?.optional) as attributeModel, i}
{@const dp = attributeModel.displayProps}
{#if dp?.dividerBefore === true}
<DividerPresenter />
{/if}
{/each}
</div>
{/if}
{:else if (!groupByKey || listProps?.excludeByKey !== groupByKey) && !listProps?.optional}
{#if !(compactMode && listProps?.compression)}
{#if listProps?.fixed}
<FixedColumn key={`list_item_${attributeModel.props?.listProps.key}`} justify={listProps.fixed}>
{#if dp?.fixed}
<FixedColumn key={`list_item_${dp.key}`} justify={dp.fixed}>
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
value={getObjectValue(attributeModel.key, docObject)}
kind={'list'}
onChange={getOnChange(docObject, attributeModel)}
{...joinProps(attributeModel, docObject, props)}
@ -169,10 +155,35 @@
{:else}
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
value={getObjectValue(attributeModel.key, docObject)}
onChange={getOnChange(docObject, attributeModel)}
kind={'list'}
compression={listProps?.compression && i !== noCompressed}
compression={dp?.compression && i !== noCompressed}
{...joinProps(attributeModel, docObject, props)}
/>
{/if}
{/each}
{/if}
{#if i !== 0 && displayProps?.dividerBefore === true}
<DividerPresenter />
{/if}
{#if displayProps?.fixed}
<FixedColumn key={`list_item_${displayProps.key}`} justify={displayProps.fixed}>
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, docObject)}
kind={'list'}
onChange={getOnChange(docObject, attributeModel)}
{...joinProps(attributeModel, docObject, props)}
/>
</FixedColumn>
{:else}
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, docObject)}
onChange={getOnChange(docObject, attributeModel)}
kind={'list'}
compression={displayProps?.compression && i !== noCompressed}
{...joinProps(attributeModel, docObject, props)}
/>
{/if}
@ -193,13 +204,16 @@
<IconCircles />
</div>
<div class="scroll-box gap-2">
{#each model.filter((m) => m.props?.listProps?.optional || m.props?.listProps?.compression) as attributeModel}
{@const listProps = attributeModel.props?.listProps}
{#each model.filter((m) => m.displayProps?.optional || m.displayProps?.compression) as attributeModel, j}
{@const displayProps = attributeModel.displayProps}
{@const value = getObjectValue(attributeModel.key, docObject)}
{#if listProps?.excludeByKey !== groupByKey && value !== undefined}
{#if displayProps?.excludeByKey !== groupByKey && value !== undefined}
{#if j !== 0 && displayProps?.dividerBefore === true}
<DividerPresenter />
{/if}
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
value={getObjectValue(attributeModel.key, docObject)}
onChange={getOnChange(docObject, attributeModel)}
kind={'list'}
{...joinProps(attributeModel, docObject, props)}

View File

@ -88,7 +88,8 @@ export default mergeIds(viewId, view, {
SaveAs: '' as IntlString,
And: '' as IntlString,
Between: '' as IntlString,
ShowColors: '' as IntlString
ShowColors: '' as IntlString,
Show: '' as IntlString
},
function: {
StatusSort: '' as SortFunc

View File

@ -107,6 +107,7 @@ export async function getObjectPresenter (
_class,
label: preserveKey.label ?? clazz.label,
presenter,
displayProps: preserveKey.displayProps,
props: preserveKey.props,
sortingKey,
collectionAttr: isCollectionAttr,
@ -167,6 +168,7 @@ async function getAttributePresenter (
label: preserveKey.label ?? attribute.shortLabel ?? attribute.label,
presenter,
props: preserveKey.props,
displayProps: preserveKey.displayProps,
icon: presenterMixin.icon,
attribute,
collectionAttr: isCollectionAttr,
@ -191,6 +193,7 @@ export async function getPresenter<T extends Doc> (
label: label as IntlString,
presenter: typeof presenter === 'string' ? await getResource(presenter) : presenter,
props: preserveKey.props,
displayProps: preserveKey.displayProps,
collectionAttr: isCollectionAttr,
isLookup: false
}
@ -796,6 +799,9 @@ export function getKeyLabel<T extends Doc> (
const lookupProperty = getLookupProperty(key)
const lookupKey = { key: lookupProperty[0] }
return getLookupLabel(client, lookupClass[1], lookupClass[0], lookupKey, lookupProperty[1])
} else if (key.length === 0) {
const clazz = client.getHierarchy().getClass(_class)
return clazz.label
} else {
const attribute = client.getHierarchy().getAttribute(_class, key)
return attribute.label
@ -953,3 +959,14 @@ export async function statusSort (
export function isAttachedDoc (doc: Doc | AttachedDoc): doc is AttachedDoc {
return 'attachedTo' in doc
}
export function enabledConfig (config: Array<string | BuildModelKey>, key: string): boolean {
for (const value of config) {
if (typeof value === 'string') {
if (value === key) return true
} else {
if (value.key === key) return true
}
}
return false
}

View File

@ -317,11 +317,20 @@ export interface Viewlet extends Doc {
descriptor: Ref<ViewletDescriptor>
options?: FindOptions<Doc>
config: (BuildModelKey | string)[]
hiddenKeys?: string[]
configOptions?: ViewletConfigOptions
viewOptions?: ViewOptionsModel
variant?: string
}
/**
* @public
*/
export interface ViewletConfigOptions {
hiddenKeys?: string[]
strict?: boolean
extraProps?: Omit<BuildModelKey, 'key'>
}
/**
* @public
*/
@ -456,6 +465,18 @@ export interface PreviewPresenter extends Class<Doc> {
*/
export const viewId = 'view' as Plugin
/**
* @public
*/
export interface DisplayProps {
key?: string
excludeByKey?: string
fixed?: 'left' | 'right' // using for align items in row
optional?: boolean
compression?: boolean
dividerBefore?: boolean // should show divider before
}
/**
* @public
*/
@ -464,6 +485,8 @@ export interface BuildModelKey {
presenter?: AnyComponent | AnySvelteComponent
// A set of extra props passed to presenter.
props?: Record<string, any>
// A set of extra props which using for display.
displayProps?: DisplayProps
label?: IntlString
sortingKey?: string | string[]
@ -482,6 +505,7 @@ export interface AttributeModel {
presenter: AnySvelteComponent
// Extra properties for component
props?: Record<string, any>
displayProps?: DisplayProps
sortingKey: string | string[]
// Extra icon if applicable
icon?: Asset
@ -752,7 +776,8 @@ const view = plugin(viewId, {
Model: '' as Asset,
DevModel: '' as Asset,
ViewButton: '' as Asset,
Filter: '' as Asset
Filter: '' as Asset,
Configure: '' as Asset
},
category: {
General: '' as Ref<ActionCategory>,