UBER-799: Allow extensions to tracker for github (#3727)

This commit is contained in:
Andrey Sobolev
2023-09-21 19:20:17 +07:00
committed by GitHub
parent b18c7ab158
commit c7bb5b9cab
28 changed files with 613 additions and 467 deletions
@@ -2,9 +2,10 @@
import { AttachmentStyleBoxEditor } from '@hcengineering/attachment-resources'
import { getClient } from '@hcengineering/presentation'
import { Component } from '@hcengineering/tracker'
import { EditBox } from '@hcengineering/ui'
import { EditBox, Label } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
import tracker from '../../plugin'
import QueryIssuesList from '../issues/edit/QueryIssuesList.svelte'
export let object: Component
@@ -53,3 +54,20 @@
placeholder={tracker.string.IssueDescriptionPlaceholder}
/>
</div>
<div class="w-full mt-6">
<QueryIssuesList
focusIndex={50}
{object}
query={{ component: object._id }}
shouldSaveDraft
hasSubIssues={true}
viewletId={tracker.viewlet.ComponentIssuesList}
createParams={{ component: object._id }}
on:docs
>
<svelte:fragment slot="header">
<Label label={tracker.string.Issues} />
</svelte:fragment>
</QueryIssuesList>
</div>
@@ -7,6 +7,7 @@
import { FilterBar, SpaceHeader, ViewletContentView, ViewletSettingButton } from '@hcengineering/view-resources'
import tracker from '../../plugin'
import CreateIssue from '../CreateIssue.svelte'
import { ComponentExtensions } from '@hcengineering/presentation'
export let space: Ref<Space> | undefined = undefined
export let query: DocumentQuery<Issue> = {}
@@ -51,7 +52,7 @@
bind:viewlet
bind:search
showLabelSelector={$$slots.label_selector}
viewletQuery={{ attachTo: tracker.class.Issue, variant: { $ne: 'subissue' } }}
viewletQuery={{ attachTo: tracker.class.Issue, variant: { $nin: ['subissue', 'component', 'milestone'] } }}
{viewlets}
{label}
{space}
@@ -61,6 +62,11 @@
<slot name="label_selector" />
</svelte:fragment>
<svelte:fragment slot="extra">
<ComponentExtensions
extension={tracker.extensions.IssueListHeader}
props={{ size: 'medium', kind: 'ghost', space }}
/>
<ViewletSettingButton bind:viewOptions bind:viewlet />
{#if asideFloat && $$slots.aside}
<div class="buttons-divider" />
@@ -0,0 +1,205 @@
<!--
// 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 { Class, Doc, DocumentQuery, Ref, toIdMap } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Issue, Project } from '@hcengineering/tracker'
import { Button, Chevron, ExpandCollapse, IconAdd, closeTooltip, showPopup } from '@hcengineering/ui'
import view, { ViewOptions, Viewlet, ViewletPreference } from '@hcengineering/view'
import { ViewletsSettingButton } from '@hcengineering/view-resources'
import { afterUpdate } from 'svelte'
import tracker from '../../../plugin'
import CreateIssue from '../../CreateIssue.svelte'
import SubIssueList from './SubIssueList.svelte'
export let projects: Map<Ref<Project>, Project> | undefined = undefined
export let shouldSaveDraft: boolean = false
export let object: Doc
export let query: DocumentQuery<Issue> = {}
export let createParams: Record<string, any> = {}
export let viewletId = tracker.viewlet.SubIssues
export let createLabel = tracker.string.CreatedIssue
export let hasSubIssues = false
let isCollapsed = false
let listWidth: number
let viewlet: Viewlet | undefined
let viewOptions: ViewOptions | undefined
let _projects = projects
export let focusIndex = -1
const projectsQuery = createQuery()
function openNewIssueDialog (): void {
showPopup(tracker.component.CreateIssue, { space: object.space, ...createParams, shouldSaveDraft }, 'top')
}
$: if (projects === undefined) {
projectsQuery.query(tracker.class.Project, { archived: false }, async (result) => {
_projects = toIdMap(result)
})
} else {
projectsQuery.unsubscribe()
}
let lastIssueId: Ref<Doc>
afterUpdate(() => {
if (lastIssueId !== object._id) {
lastIssueId = object._id
}
})
const preferenceQuery = createQuery()
const objectConfigurations = createQuery()
let preference: ViewletPreference[] = []
let loading = true
let configurationRaw: Viewlet[] = []
let configurations: Record<Ref<Class<Doc>>, Viewlet['config']> = {}
const client = getClient()
$: viewlet &&
objectConfigurations.query(
view.class.Viewlet,
{
attachTo: { $in: client.getHierarchy().getDescendants(viewlet.attachTo) },
descriptor: viewlet.descriptor,
variant: viewlet.variant ? viewlet.variant : { $exists: false }
},
(res) => {
configurationRaw = res
loading = false
}
)
$: viewlet &&
preferenceQuery.query(
view.class.ViewletPreference,
{
attachedTo: { $in: configurationRaw.map((it) => it._id) }
},
(res) => {
preference = res
loading = false
}
)
function updateConfiguration (configurationRaw: Viewlet[], preference: ViewletPreference[]): void {
const newConfigurations: Record<Ref<Class<Doc>>, Viewlet['config']> = {}
for (const v of configurationRaw) {
newConfigurations[v.attachTo] = v.config
}
// Add viewlet configurations.
for (const pref of preference) {
if (pref.config.length > 0) {
const viewlet = configurationRaw.find((it) => it._id === pref.attachedTo)
if (viewlet !== undefined) {
newConfigurations[viewlet.attachTo] = pref.config
}
}
}
configurations = newConfigurations
}
$: updateConfiguration(configurationRaw, preference)
</script>
<div class="flex-between mb-1">
{#if hasSubIssues}
{#if $$slots.header}
<slot name="header" />
{:else}
<Button
width="min-content"
kind="ghost"
on:click={() => {
isCollapsed = !isCollapsed
}}
>
<svelte:fragment slot="content">
<Chevron
size={'small'}
expanded={!isCollapsed}
outline
fill={'var(--caption-color)'}
marginRight={'.375rem'}
/>
<slot name="chevron" />
</svelte:fragment>
</Button>
{/if}
{/if}
<div class="flex-row-center gap-2">
{#if hasSubIssues}
<ViewletsSettingButton bind:viewOptions viewletQuery={{ _id: viewletId }} kind={'ghost'} bind:viewlet />
{/if}
{#if hasSubIssues}
<slot name="buttons" />
{/if}
<Button
id="add-sub-issue"
icon={IconAdd}
label={hasSubIssues ? undefined : createLabel}
labelParams={{ subIssues: 0 }}
kind={'ghost'}
showTooltip={{ label: createLabel, direction: 'bottom' }}
on:click={() => {
isCollapsed = false
closeTooltip()
openNewIssueDialog()
}}
/>
</div>
</div>
{#if hasSubIssues && viewOptions && viewlet}
{#if !isCollapsed}
<ExpandCollapse isExpanded={!isCollapsed}>
<div class="list" class:collapsed={isCollapsed} bind:clientWidth={listWidth}>
<SubIssueList
createItemDialog={CreateIssue}
createItemLabel={tracker.string.AddIssueTooltip}
createItemDialogProps={{ space: object.space, ...createParams, shouldSaveDraft }}
focusIndex={focusIndex === -1 ? -1 : focusIndex + 1}
projects={_projects}
{configurations}
{preference}
{viewlet}
{viewOptions}
{query}
compactMode={listWidth <= 600}
on:docs
/>
</div>
</ExpandCollapse>
{/if}
{/if}
<style lang="scss">
.list {
padding-top: 0.75rem;
border-top: 1px solid var(--divider-color);
&.collapsed {
padding-top: 1px;
border-top: none;
}
}
</style>
@@ -21,6 +21,7 @@
import { ViewOptions, Viewlet, ViewletPreference } from '@hcengineering/view'
import { List, ListSelectionProvider, SelectDirection, selectionStore } from '@hcengineering/view-resources'
import tracker from '../../../plugin'
import { createEventDispatcher } from 'svelte'
export let query: DocumentQuery<Issue> | undefined = undefined
export let viewlet: Viewlet
@@ -66,6 +67,8 @@
export let createItemDialog: AnySvelteComponent | AnyComponent | undefined = undefined
export let createItemLabel: IntlString | undefined = undefined
export let createItemDialogProps: Record<string, any> | undefined = undefined
const dispatch = createEventDispatcher()
</script>
<ActionContext
@@ -100,6 +103,7 @@
on:content={(evt) => {
docs = evt.detail
listProvider.update(evt.detail)
dispatch('docs', docs)
}}
/>
{/if}
@@ -13,213 +13,58 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Doc, Ref, toIdMap } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Ref } from '@hcengineering/core'
import { Issue, Project, trackerId } from '@hcengineering/tracker'
import {
Button,
Chevron,
ExpandCollapse,
IconAdd,
IconScaleFull,
Label,
closeTooltip,
getCurrentResolvedLocation,
navigate,
showPopup
} from '@hcengineering/ui'
import view, { ViewOptions, Viewlet, ViewletPreference } from '@hcengineering/view'
import { ViewletsSettingButton, createFilter, setFilters } from '@hcengineering/view-resources'
import { afterUpdate } from 'svelte'
import { Button, IconScaleFull, Label, closeTooltip, getCurrentResolvedLocation, navigate } from '@hcengineering/ui'
import { createFilter, setFilters } from '@hcengineering/view-resources'
import tracker from '../../../plugin'
import CreateIssue from '../../CreateIssue.svelte'
import SubIssueList from './SubIssueList.svelte'
import QueryIssuesList from './QueryIssuesList.svelte'
export let issue: Issue
export let projects: Map<Ref<Project>, Project>
export let projects: Map<Ref<Project>, Project> | undefined
export let shouldSaveDraft: boolean = false
let isCollapsed = false
let listWidth: number
$: hasSubIssues = issue.subIssues > 0
let viewlet: Viewlet | undefined
let viewOptions: ViewOptions | undefined
let _projects = projects
const projectsQuery = createQuery()
function openNewIssueDialog (): void {
showPopup(tracker.component.CreateIssue, { space: issue.space, parentIssue: issue, shouldSaveDraft }, 'top')
}
$: if (projects === undefined) {
projectsQuery.query(tracker.class.Project, {}, async (result) => {
_projects = toIdMap(result)
})
} else {
projectsQuery.unsubscribe()
}
// showPopup(tracker.component.CreateIssue, { space: issue.space, parentIssue: issue, shouldSaveDraft }, 'top')
export let focusIndex = -1
let lastIssueId: Ref<Issue>
afterUpdate(() => {
if (lastIssueId !== issue._id) {
lastIssueId = issue._id
}
})
const preferenceQuery = createQuery()
const objectConfigurations = createQuery()
let preference: ViewletPreference[] = []
let loading = true
let configurationRaw: Viewlet[] = []
let configurations: Record<Ref<Class<Doc>>, Viewlet['config']> = {}
const client = getClient()
$: viewlet &&
objectConfigurations.query(
view.class.Viewlet,
{
attachTo: { $in: client.getHierarchy().getDescendants(viewlet.attachTo) },
descriptor: viewlet.descriptor,
variant: viewlet.variant ? viewlet.variant : { $exists: false }
},
(res) => {
configurationRaw = res
loading = false
}
)
$: viewlet &&
preferenceQuery.query(
view.class.ViewletPreference,
{
attachedTo: { $in: configurationRaw.map((it) => it._id) }
},
(res) => {
preference = res
loading = false
}
)
function updateConfiguration (configurationRaw: Viewlet[], preference: ViewletPreference[]): void {
const newConfigurations: Record<Ref<Class<Doc>>, Viewlet['config']> = {}
for (const v of configurationRaw) {
newConfigurations[v.attachTo] = v.config
}
// Add viewlet configurations.
for (const pref of preference) {
if (pref.config.length > 0) {
const viewlet = configurationRaw.find((it) => it._id === pref.attachedTo)
if (viewlet !== undefined) {
newConfigurations[viewlet.attachTo] = pref.config
}
}
}
configurations = newConfigurations
}
$: updateConfiguration(configurationRaw, preference)
let size = issue.subIssues
</script>
<div class="flex-between mb-1">
{#if hasSubIssues}
<QueryIssuesList
object={issue}
query={{ attachedTo: issue._id }}
createParams={{ space: issue.space, parentIssue: issue }}
createLabel={tracker.string.AddSubIssues}
hasSubIssues={issue.subIssues > 0}
{focusIndex}
{projects}
{shouldSaveDraft}
on:docs={(evt) => {
size = evt.detail.length
}}
>
<svelte:fragment slot="chevron">
<Label label={tracker.string.SubIssuesList} params={{ subIssues: size }} />
</svelte:fragment>
<svelte:fragment slot="buttons">
<Button
width="min-content"
kind="ghost"
on:click={() => {
isCollapsed = !isCollapsed
}}
>
<svelte:fragment slot="content">
<Chevron size={'small'} expanded={!isCollapsed} outline fill={'var(--caption-color)'} marginRight={'.375rem'} />
<Label label={tracker.string.SubIssuesList} params={{ subIssues: issue.subIssues }} />
</svelte:fragment>
</Button>
{/if}
<div class="flex-row-center gap-2">
{#if hasSubIssues}
<ViewletsSettingButton
bind:viewOptions
viewletQuery={{ _id: tracker.viewlet.SubIssues }}
kind={'ghost'}
bind:viewlet
/>
{/if}
{#if hasSubIssues}
<Button
icon={IconScaleFull}
kind={'ghost'}
showTooltip={{ label: tracker.string.OpenSubIssues, direction: 'bottom' }}
on:click={() => {
const filter = createFilter(tracker.class.Issue, 'attachedTo', [issue._id])
if (filter !== undefined) {
closeTooltip()
const loc = getCurrentResolvedLocation()
loc.fragment = undefined
loc.query = undefined
loc.path[2] = trackerId
loc.path[3] = issue.space
loc.path[4] = 'issues'
navigate(loc)
setFilters([filter])
}
}}
/>
{/if}
<Button
id="add-sub-issue"
icon={hasSubIssues ? IconAdd : undefined}
label={hasSubIssues ? undefined : tracker.string.AddSubIssues}
labelParams={{ subIssues: 0 }}
icon={IconScaleFull}
kind={'ghost'}
showTooltip={{ label: tracker.string.AddSubIssues, props: { subIssues: 1 }, direction: 'bottom' }}
showTooltip={{ label: tracker.string.OpenSubIssues, direction: 'bottom' }}
on:click={() => {
isCollapsed = false
closeTooltip()
openNewIssueDialog()
const filter = createFilter(tracker.class.Issue, 'attachedTo', [issue._id])
if (filter !== undefined) {
closeTooltip()
const loc = getCurrentResolvedLocation()
loc.fragment = undefined
loc.query = undefined
loc.path[2] = trackerId
loc.path[3] = issue.space
loc.path[4] = 'issues'
navigate(loc)
setFilters([filter])
}
}}
/>
</div>
</div>
{#if hasSubIssues && viewOptions && viewlet}
{#if !isCollapsed}
<ExpandCollapse isExpanded={!isCollapsed}>
<div class="list" class:collapsed={isCollapsed} bind:clientWidth={listWidth}>
<SubIssueList
createItemDialog={CreateIssue}
createItemLabel={tracker.string.AddIssueTooltip}
createItemDialogProps={{ space: issue.space, parentIssue: issue, shouldSaveDraft }}
focusIndex={focusIndex === -1 ? -1 : focusIndex + 1}
projects={_projects}
{configurations}
{preference}
{viewlet}
{viewOptions}
query={{ attachedTo: issue._id }}
compactMode={listWidth <= 600}
/>
</div>
</ExpandCollapse>
{/if}
{/if}
<style lang="scss">
.list {
padding-top: 0.75rem;
border-top: 1px solid var(--divider-color);
&.collapsed {
padding-top: 1px;
border-top: none;
}
}
</style>
</svelte:fragment>
</QueryIssuesList>
@@ -39,7 +39,7 @@
const projectsQuery = createQuery()
$: projectsQuery.query(tracker.class.Project, {}, async (result) => {
$: projectsQuery.query(tracker.class.Project, { archived: false }, async (result) => {
projects = new Map(result.map((it) => [it._id, it]))
})
</script>
@@ -1,86 +1,29 @@
<script lang="ts">
import { Doc, DocumentQuery } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { configurationStore, createQuery, getClient } from '@hcengineering/presentation'
import { Issue, trackerId } from '@hcengineering/tracker'
import { Button, Component, Icon, IconAdd, Label, showPopup } from '@hcengineering/ui'
import { ViewOptions, Viewlet } from '@hcengineering/view'
import { ViewletsSettingButton, getAdditionalHeader } from '@hcengineering/view-resources'
import viewplg from '@hcengineering/view-resources/src/plugin'
import { fade } from 'svelte/transition'
import tracker from '../../../plugin'
import RelatedIssues from './RelatedIssues.svelte'
import { configurationStore } from '@hcengineering/presentation'
import tracker, { Issue, trackerId } from '@hcengineering/tracker'
import { Icon, Label } from '@hcengineering/ui'
import QueryIssuesList from '../edit/QueryIssuesList.svelte'
export let object: Doc
export let label: IntlString
const client = getClient()
let viewlet: Viewlet | undefined
let listWidth: number
let viewOptions: ViewOptions | undefined
const createIssue = () => showPopup(tracker.component.CreateIssue, { relatedTo: object, space: object.space }, 'top')
let query: DocumentQuery<Issue>
$: query = { 'relations._id': object._id, 'relations._class': object._class }
const subIssuesQuery = createQuery()
let subIssues: Issue[] = []
$: subIssuesQuery.query(tracker.class.Issue, query, async (result) => (subIssues = result))
$: headerRemoval = viewOptions?.groupBy?.length === 0 || viewOptions?.groupBy?.[0] === '#no_category'
$: extraHeaders = headerRemoval ? getAdditionalHeader(client, tracker.class.Issue) : undefined
</script>
{#if $configurationStore.has(trackerId)}
<div class="antiSection" bind:clientWidth={listWidth}>
<div class="antiSection-header mb-3">
<div class="antiSection-header__icon">
<Icon icon={tracker.icon.Issue} size={'small'} />
</div>
<span class="antiSection-header__title short overflow-label">
<Label {label} />
</span>
{#if headerRemoval}
<div in:fade|local={{ duration: 150 }} class="antiSection-header__header flex-between">
<span class="content-dark-color"><Label label={viewplg.string.NoGrouping} /></span>
<div class="buttons-group font-normal text-normal">
{#if extraHeaders}
{#each extraHeaders as extra}
<Component is={extra} props={{ docs: subIssues }} />
{/each}
{/if}
<span class="antiSection-header__counter">{subIssues.length}</span>
</div>
<QueryIssuesList {object} {query} createParams={{ relatedTo: object }} hasSubIssues>
<svelte:fragment slot="header">
<div class="flex-row-center mb-3">
<div class="antiSection-header__icon">
<Icon icon={tracker.icon.Issue} size={'small'} />
</div>
{:else}
<span class="flex-grow" />
{/if}
<div class="flex-row-center gap-2">
<ViewletsSettingButton
bind:viewOptions
viewletQuery={{ _id: tracker.viewlet.SubIssues }}
kind={'ghost'}
bind:viewlet
/>
<Button
id="add-sub-issue"
icon={IconAdd}
label={undefined}
labelParams={{ subIssues: 0 }}
kind={'ghost'}
on:click={createIssue}
/>
<span class="antiSection-header__title short">
<Label {label} />
</span>
</div>
</div>
{#if viewlet && viewOptions}
<RelatedIssues
{object}
{viewOptions}
{viewlet}
on:add-issue={createIssue}
disableHeader={headerRemoval}
compactMode={listWidth <= 600}
/>
{/if}
</div>
</svelte:fragment>
</QueryIssuesList>
{/if}
@@ -13,12 +13,13 @@
// limitations under the License.
-->
<script lang="ts">
import { AttachmentStyleBoxEditor } from '@hcengineering/attachment-resources'
import { getClient } from '@hcengineering/presentation'
import { Milestone } from '@hcengineering/tracker'
import { EditBox } from '@hcengineering/ui'
import { EditBox, Label } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
import tracker from '../../plugin'
import { AttachmentStyleBoxEditor } from '@hcengineering/attachment-resources'
import QueryIssuesList from '../issues/edit/QueryIssuesList.svelte'
export let object: Milestone
@@ -66,3 +67,19 @@
placeholder={tracker.string.IssueDescriptionPlaceholder}
/>
</div>
<div class="w-full mt-6">
<QueryIssuesList
focusIndex={50}
{object}
query={{ milestone: object._id }}
shouldSaveDraft
hasSubIssues={true}
viewletId={tracker.viewlet.MilestoneIssuesList}
createParams={{ milestone: object._id }}
>
<svelte:fragment slot="header">
<Label label={tracker.string.Issues} />
</svelte:fragment>
</QueryIssuesList>
</div>
@@ -75,10 +75,9 @@
width="min-content"
icon={hasSubIssues ? IconAdd : undefined}
label={hasSubIssues ? undefined : tracker.string.AddSubIssues}
labelParams={{ subIssues: 0 }}
kind={'ghost'}
size={'small'}
showTooltip={{ label: tracker.string.AddSubIssues, props: { subIssues: 1 } }}
showTooltip={{ label: tracker.string.AddSubIssues }}
on:click={() => {
closeTooltip()
isCreating = true
+3 -1
View File
@@ -32,7 +32,9 @@ export default mergeIds(trackerId, tracker, {
viewlet: {
SubIssues: '' as Ref<Viewlet>,
List: '' as Ref<ViewletDescriptor>,
Kanban: '' as Ref<ViewletDescriptor>
Kanban: '' as Ref<ViewletDescriptor>,
MilestoneIssuesList: '' as Ref<Viewlet>,
ComponentIssuesList: '' as Ref<Viewlet>
},
string: {
More: '' as IntlString,