mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-22 08:20:39 +00:00
Tracker: labels on the card. (#2221)
Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
parent
bbe6c12474
commit
2450fd0fa9
@ -253,6 +253,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
|
max-width: 50vw;
|
||||||
color: var(--caption-color);
|
color: var(--caption-color);
|
||||||
background-color: var(--accent-bg-color);
|
background-color: var(--accent-bg-color);
|
||||||
border: 1px solid var(--divider-color);
|
border: 1px solid var(--divider-color);
|
||||||
|
90
plugins/tags-resources/src/components/LabelsPresenter.svelte
Normal file
90
plugins/tags-resources/src/components/LabelsPresenter.svelte
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Doc } from '@anticrm/core'
|
||||||
|
import { createQuery } from '@anticrm/presentation'
|
||||||
|
import type { TagReference } from '@anticrm/tags'
|
||||||
|
import tags from '@anticrm/tags'
|
||||||
|
import { getEventPopupPositionElement, showPopup, resizeObserver } from '@anticrm/ui'
|
||||||
|
import TagReferencePresenter from './TagReferencePresenter.svelte'
|
||||||
|
import TagsEditorPopup from './TagsEditorPopup.svelte'
|
||||||
|
import { createEventDispatcher, afterUpdate } from 'svelte'
|
||||||
|
|
||||||
|
export let object: Doc
|
||||||
|
export let full: boolean
|
||||||
|
export let ckeckFilled: boolean = false
|
||||||
|
export let kind: 'short' | 'full' = 'short'
|
||||||
|
export let isEditable: boolean = false
|
||||||
|
export let action: (evt: MouseEvent) => Promise<void> | void = async () => {}
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
let items: TagReference[] = []
|
||||||
|
const query = createQuery()
|
||||||
|
|
||||||
|
$: query.query(tags.class.TagReference, { attachedTo: object._id }, (result) => {
|
||||||
|
items = result
|
||||||
|
})
|
||||||
|
async function tagsHandler (evt: MouseEvent): Promise<void> {
|
||||||
|
showPopup(TagsEditorPopup, { object }, getEventPopupPositionElement(evt))
|
||||||
|
}
|
||||||
|
|
||||||
|
let allWidth: number
|
||||||
|
const widths: number[] = []
|
||||||
|
|
||||||
|
afterUpdate(() => {
|
||||||
|
let count: number = 0
|
||||||
|
widths.forEach((i) => (count += i))
|
||||||
|
full = count > allWidth
|
||||||
|
dispatch('change', { full, ckeckFilled })
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="labels-container"
|
||||||
|
style:justify-content={kind === 'short' ? 'space-between' : 'flex-start'}
|
||||||
|
style:flex-wrap={kind === 'short' ? 'nowrap' : 'wrap'}
|
||||||
|
use:resizeObserver={(element) => {
|
||||||
|
allWidth = element.clientWidth
|
||||||
|
}}
|
||||||
|
on:click|stopPropagation={(evt) => {
|
||||||
|
if (isEditable) tagsHandler(evt)
|
||||||
|
else action(evt)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#each items as value, i}
|
||||||
|
<div class="label-box wrap-{kind}">
|
||||||
|
<TagReferencePresenter {value} kind={'kanban-labels'} bind:realWidth={widths[i]} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.labels-container {
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 10;
|
||||||
|
width: auto;
|
||||||
|
min-width: 0;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
transition: box-shadow 0.15s ease-in-out;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.wrap-short:not(:last-child) {
|
||||||
|
margin-right: 0.375rem;
|
||||||
|
}
|
||||||
|
.wrap-full {
|
||||||
|
margin: 0.125rem;
|
||||||
|
}
|
||||||
|
</style>
|
@ -14,13 +14,14 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { TagReference } from '@anticrm/tags'
|
import type { TagReference } from '@anticrm/tags'
|
||||||
import { getPlatformColor, IconClose, Icon } from '@anticrm/ui'
|
import { getPlatformColor, IconClose, Icon, resizeObserver } from '@anticrm/ui'
|
||||||
import TagItem from './TagItem.svelte'
|
import TagItem from './TagItem.svelte'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
|
||||||
export let value: TagReference
|
export let value: TagReference
|
||||||
export let isEditable: boolean = false
|
export let isEditable: boolean = false
|
||||||
export let kind: 'labels' | 'skills' = 'skills'
|
export let kind: 'labels' | 'kanban-labels' | 'skills' = 'skills'
|
||||||
|
export let realWidth: number | undefined = undefined
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
</script>
|
</script>
|
||||||
@ -28,8 +29,24 @@
|
|||||||
{#if value}
|
{#if value}
|
||||||
{#if kind === 'skills'}
|
{#if kind === 'skills'}
|
||||||
<TagItem tag={value} />
|
<TagItem tag={value} />
|
||||||
|
{:else if kind === 'kanban-labels'}
|
||||||
|
<button
|
||||||
|
class="label-container"
|
||||||
|
use:resizeObserver={(element) => {
|
||||||
|
realWidth = element.clientWidth
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="color" style:background-color={getPlatformColor(value.color ?? 0)} />
|
||||||
|
<span class="overflow-label ml-1 text-sm caption-color">{value.title}</span>
|
||||||
|
</button>
|
||||||
{:else if kind === 'labels'}
|
{:else if kind === 'labels'}
|
||||||
<div class="tag-container" style:padding-right={isEditable ? '0' : '0.5rem'}>
|
<div
|
||||||
|
class="tag-container"
|
||||||
|
style:padding-right={isEditable ? '0' : '0.5rem'}
|
||||||
|
use:resizeObserver={(element) => {
|
||||||
|
realWidth = element.clientWidth
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div class="color" style:background-color={getPlatformColor(value.color ?? 0)} />
|
<div class="color" style:background-color={getPlatformColor(value.color ?? 0)} />
|
||||||
<span class="overflow-label ml-1-5 caption-color">{value.title}</span>
|
<span class="overflow-label ml-1-5 caption-color">{value.title}</span>
|
||||||
{#if isEditable}
|
{#if isEditable}
|
||||||
@ -54,12 +71,6 @@
|
|||||||
border: 1px solid var(--divider-color);
|
border: 1px solid var(--divider-color);
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
|
|
||||||
.color {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 0.5rem;
|
|
||||||
height: 0.5rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
.btn-close {
|
.btn-close {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-left: 0.125rem;
|
margin-left: 0.125rem;
|
||||||
@ -74,4 +85,45 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0 0.375rem;
|
||||||
|
height: 1.375rem;
|
||||||
|
min-width: 1.375rem;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 0.75rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--accent-color);
|
||||||
|
background-color: var(--board-card-bg-color);
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
transition-property: border, background-color, color, box-shadow;
|
||||||
|
transition-duration: 0.15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--accent-color);
|
||||||
|
background-color: var(--button-bg-hover);
|
||||||
|
border-color: var(--button-border-hover);
|
||||||
|
transition-duration: 0;
|
||||||
|
}
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--primary-edit-border-color) !important;
|
||||||
|
}
|
||||||
|
&:disabled {
|
||||||
|
color: rgb(var(--caption-color) / 40%);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.color {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 0.5rem;
|
||||||
|
height: 0.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if items.length}
|
{#if items.length}
|
||||||
<div class="flex-row-center flex-wrap" on:click={tagsHandler}>
|
<div class="flex-row-center flex-wrap">
|
||||||
{#each items as value}
|
{#each items as value}
|
||||||
<div class="step-container">
|
<div class="step-container">
|
||||||
<TagReferencePresenter {value} {isEditable} kind={'labels'} on:remove={(res) => removeTag(res.detail)} />
|
<TagReferencePresenter {value} {isEditable} kind={'labels'} on:remove={(res) => removeTag(res.detail)} />
|
||||||
@ -38,7 +38,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
{#if isEditable}
|
{#if isEditable}
|
||||||
<div class="step-container">
|
<div class="step-container">
|
||||||
<button class="tag-button" on:click={tagsHandler}>
|
<button class="tag-button" on:click|stopPropagation={tagsHandler}>
|
||||||
<div class="icon"><Icon icon={IconAdd} size={'full'} /></div>
|
<div class="icon"><Icon icon={IconAdd} size={'full'} /></div>
|
||||||
<span class="overflow-label label"><Label {label} /></span>
|
<span class="overflow-label label"><Label {label} /></span>
|
||||||
</button>
|
</button>
|
||||||
@ -46,7 +46,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if isEditable}
|
{:else if isEditable}
|
||||||
<button class="tag-button" style="width: min-content" on:click={tagsHandler}>
|
<button class="tag-button" style="width: min-content" on:click|stopPropagation={tagsHandler}>
|
||||||
<div class="icon"><Icon icon={IconAdd} size={'full'} /></div>
|
<div class="icon"><Icon icon={IconAdd} size={'full'} /></div>
|
||||||
<span class="overflow-label label"><Label {label} /></span>
|
<span class="overflow-label label"><Label {label} /></span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -29,6 +29,7 @@ import TagElementCountPresenter from './components/TagElementCountPresenter.svel
|
|||||||
import TagsFilter from './components/TagsFilter.svelte'
|
import TagsFilter from './components/TagsFilter.svelte'
|
||||||
import TagsAttributeEditor from './components/TagsAttributeEditor.svelte'
|
import TagsAttributeEditor from './components/TagsAttributeEditor.svelte'
|
||||||
import TagsEditorPopup from './components/TagsEditorPopup.svelte'
|
import TagsEditorPopup from './components/TagsEditorPopup.svelte'
|
||||||
|
import LabelsPresenter from './components/LabelsPresenter.svelte'
|
||||||
import { ObjQueryType } from '@anticrm/core'
|
import { ObjQueryType } from '@anticrm/core'
|
||||||
import { getRefs } from './utils'
|
import { getRefs } from './utils'
|
||||||
import { Filter } from '@anticrm/view'
|
import { Filter } from '@anticrm/view'
|
||||||
@ -58,7 +59,8 @@ export default async (): Promise<Resources> => ({
|
|||||||
TagsCategoryBar,
|
TagsCategoryBar,
|
||||||
TagElementCountPresenter,
|
TagElementCountPresenter,
|
||||||
TagsAttributeEditor,
|
TagsAttributeEditor,
|
||||||
TagsEditorPopup
|
TagsEditorPopup,
|
||||||
|
LabelsPresenter
|
||||||
},
|
},
|
||||||
actionImpl: {
|
actionImpl: {
|
||||||
Open: (value: TagElement, evt: MouseEvent) => {
|
Open: (value: TagElement, evt: MouseEvent) => {
|
||||||
|
@ -84,7 +84,8 @@ const tagsPlugin = plugin(tagsId, {
|
|||||||
TagsDropdownEditor: '' as AnyComponent,
|
TagsDropdownEditor: '' as AnyComponent,
|
||||||
TagsCategoryBar: '' as AnyComponent,
|
TagsCategoryBar: '' as AnyComponent,
|
||||||
TagsAttributeEditor: '' as AnyComponent,
|
TagsAttributeEditor: '' as AnyComponent,
|
||||||
TagsPresenter: '' as AnyComponent
|
TagsPresenter: '' as AnyComponent,
|
||||||
|
LabelsPresenter: '' as AnyComponent
|
||||||
},
|
},
|
||||||
category: {
|
category: {
|
||||||
NoCategory: '' as Ref<TagCategory>
|
NoCategory: '' as Ref<TagCategory>
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
export let shape: ButtonShape = undefined
|
export let shape: ButtonShape = undefined
|
||||||
export let justify: 'left' | 'center' = 'center'
|
export let justify: 'left' | 'center' = 'center'
|
||||||
export let width: string | undefined = 'min-content'
|
export let width: string | undefined = 'min-content'
|
||||||
|
export let onlyIcon: boolean = false
|
||||||
|
|
||||||
let selectedProject: Project | undefined
|
let selectedProject: Project | undefined
|
||||||
let defaultProjectLabel = ''
|
let defaultProjectLabel = ''
|
||||||
@ -88,6 +89,18 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#if onlyIcon}
|
||||||
|
<Button
|
||||||
|
{kind}
|
||||||
|
{size}
|
||||||
|
{shape}
|
||||||
|
{width}
|
||||||
|
{justify}
|
||||||
|
icon={projectIcon}
|
||||||
|
disabled={!isEditable}
|
||||||
|
on:click={handleProjectEditorOpened}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
<Button
|
<Button
|
||||||
{kind}
|
{kind}
|
||||||
{size}
|
{size}
|
||||||
@ -104,3 +117,4 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</Button>
|
</Button>
|
||||||
|
{/if}
|
||||||
|
@ -19,7 +19,17 @@
|
|||||||
import notification from '@anticrm/notification'
|
import notification from '@anticrm/notification'
|
||||||
import { createQuery } from '@anticrm/presentation'
|
import { createQuery } from '@anticrm/presentation'
|
||||||
import { Issue, IssuesGrouping, IssuesOrdering, IssueStatus, Team, ViewOptions } from '@anticrm/tracker'
|
import { Issue, IssuesGrouping, IssuesOrdering, IssueStatus, Team, ViewOptions } from '@anticrm/tracker'
|
||||||
import { Button, Component, Icon, IconAdd, showPanel, showPopup, getPlatformColor, Loading } from '@anticrm/ui'
|
import {
|
||||||
|
Button,
|
||||||
|
Component,
|
||||||
|
Icon,
|
||||||
|
IconAdd,
|
||||||
|
showPanel,
|
||||||
|
showPopup,
|
||||||
|
getPlatformColor,
|
||||||
|
Loading,
|
||||||
|
tooltip
|
||||||
|
} from '@anticrm/ui'
|
||||||
import { focusStore, ListSelectionProvider, SelectDirection, selectionStore } from '@anticrm/view-resources'
|
import { focusStore, ListSelectionProvider, SelectDirection, selectionStore } from '@anticrm/view-resources'
|
||||||
import ActionContext from '@anticrm/view-resources/src/components/ActionContext.svelte'
|
import ActionContext from '@anticrm/view-resources/src/components/ActionContext.svelte'
|
||||||
import Menu from '@anticrm/view-resources/src/components/Menu.svelte'
|
import Menu from '@anticrm/view-resources/src/components/Menu.svelte'
|
||||||
@ -40,6 +50,7 @@
|
|||||||
import ParentNamesPresenter from './ParentNamesPresenter.svelte'
|
import ParentNamesPresenter from './ParentNamesPresenter.svelte'
|
||||||
import PriorityEditor from './PriorityEditor.svelte'
|
import PriorityEditor from './PriorityEditor.svelte'
|
||||||
import StatusEditor from './StatusEditor.svelte'
|
import StatusEditor from './StatusEditor.svelte'
|
||||||
|
import tags from '@anticrm/tags'
|
||||||
|
|
||||||
export let currentSpace: Ref<Team> = tracker.team.DefaultTeam
|
export let currentSpace: Ref<Team> = tracker.team.DefaultTeam
|
||||||
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
|
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
|
||||||
@ -146,6 +157,8 @@
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
$: states = getIssueStates(groupBy, shouldShowEmptyGroups, issueStates, issueStatusStates, priorityStates)
|
$: states = getIssueStates(groupBy, shouldShowEmptyGroups, issueStates, issueStatusStates, priorityStates)
|
||||||
|
|
||||||
|
const fullFilled: { [key: string]: boolean } = {}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !states?.length}
|
{#if !states?.length}
|
||||||
@ -203,13 +216,14 @@
|
|||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
<svelte:fragment slot="card" let:object>
|
<svelte:fragment slot="card" let:object>
|
||||||
{@const issue = toIssue(object)}
|
{@const issue = toIssue(object)}
|
||||||
|
{@const issueId = object._id}
|
||||||
<div
|
<div
|
||||||
class="tracker-card"
|
class="tracker-card"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showPanel(tracker.component.EditIssue, object._id, object._class, 'content')
|
showPanel(tracker.component.EditIssue, object._id, object._class, 'content')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex-col mr-8">
|
<div class="flex-col ml-4 mr-8">
|
||||||
<div class="flex clear-mins names">
|
<div class="flex clear-mins names">
|
||||||
<IssuePresenter value={issue} />
|
<IssuePresenter value={issue} />
|
||||||
<ParentNamesPresenter value={issue} />
|
<ParentNamesPresenter value={issue} />
|
||||||
@ -235,7 +249,7 @@
|
|||||||
<Component is={notification.component.NotificationPresenter} props={{ value: object }} />
|
<Component is={notification.component.NotificationPresenter} props={{ value: object }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons-group xsmall-gap mt-10px">
|
<div class="buttons-group xsmall-gap states-bar">
|
||||||
{#if issue && issueStatuses && issue.subIssues > 0}
|
{#if issue && issueStatuses && issue.subIssues > 0}
|
||||||
<SubIssuesSelector {issue} {currentTeam} {issueStatuses} />
|
<SubIssuesSelector {issue} {currentTeam} {issueStatuses} />
|
||||||
{/if}
|
{/if}
|
||||||
@ -247,7 +261,23 @@
|
|||||||
size={'inline'}
|
size={'inline'}
|
||||||
justify={'center'}
|
justify={'center'}
|
||||||
width={''}
|
width={''}
|
||||||
|
bind:onlyIcon={fullFilled[issueId]}
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
class="clear-mins"
|
||||||
|
use:tooltip={{
|
||||||
|
component: fullFilled[issueId] ? tags.component.LabelsPresenter : undefined,
|
||||||
|
props: { object: issue, kind: 'full' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Component
|
||||||
|
is={tags.component.LabelsPresenter}
|
||||||
|
props={{ object: issue, ckeckFilled: fullFilled[issueId] }}
|
||||||
|
on:change={(res) => {
|
||||||
|
if (res.detail.full) fullFilled[issueId] = true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
@ -275,7 +305,12 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0.5rem 1rem;
|
// padding: 0.5rem 1rem;
|
||||||
min-height: 6.5rem;
|
min-height: 6.5rem;
|
||||||
}
|
}
|
||||||
|
.states-bar {
|
||||||
|
flex-shrink: 10;
|
||||||
|
width: fit-content;
|
||||||
|
margin: 0.625rem 1rem 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
export let shape: ButtonShape = undefined
|
export let shape: ButtonShape = undefined
|
||||||
export let justify: 'left' | 'center' = 'left'
|
export let justify: 'left' | 'center' = 'left'
|
||||||
export let width: string | undefined = '100%'
|
export let width: string | undefined = '100%'
|
||||||
|
export let onlyIcon: boolean = false
|
||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
|
|
||||||
@ -65,6 +66,7 @@
|
|||||||
{isEditable}
|
{isEditable}
|
||||||
{shouldShowLabel}
|
{shouldShowLabel}
|
||||||
{popupPlaceholder}
|
{popupPlaceholder}
|
||||||
|
{onlyIcon}
|
||||||
value={value.project}
|
value={value.project}
|
||||||
onProjectIdChange={handleProjectIdChanged}
|
onProjectIdChange={handleProjectIdChanged}
|
||||||
/>
|
/>
|
||||||
|
Loading…
Reference in New Issue
Block a user