Fix TSK-101 navigation (#2068)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-06-14 23:54:25 +07:00 committed by GitHub
parent 5d33062176
commit 665ef78199
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 244 additions and 151 deletions

View File

@ -17,13 +17,9 @@
import { createQuery, getClient } from '@anticrm/presentation'
import { getPlatformColor, ScrollBox, Scroller } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import { slide } from 'svelte/transition'
import { DocWithRank, StateType, TypeState } from '../types'
import { CardDragEvent, ExtItem, Item, StateType, TypeState } from '../types'
import { calcRank } from '../utils'
type Item = DocWithRank & { state: StateType; doneState: StateType | null }
type ExtItem = { prev?: Item; it: Item; next?: Item; pos: number }
type CardDragEvent = DragEvent & { currentTarget: EventTarget & HTMLDivElement }
import KanbanRow from './KanbanRow.svelte'
export let _class: Ref<Class<Item>>
export let space: Ref<Space>
@ -144,7 +140,6 @@
return calcRank(object.it, object.next)
}
}
const slideD = (node: any, args: any) => (args.isDragging ? slide(node, args) : {})
function panelDragOver (event: Event, state: TypeState): void {
event.preventDefault()
@ -184,11 +179,14 @@
let stateObjects: ExtItem[]
const stateRefs: HTMLElement[] = []
const stateRows: KanbanRow[] = []
$: stateRefs.length = states.length
$: stateRows.length = states.length
function scrollInto (statePos: number): void {
function scrollInto (statePos: number, obj: Item): void {
stateRefs[statePos]?.scrollIntoView({ behavior: 'auto', block: 'nearest' })
stateRows[statePos]?.scroll(obj)
}
export function select (offset: 1 | -1 | 0, of?: Doc, dir?: 'vertical' | 'horizontal'): void {
@ -227,16 +225,18 @@
if (offset === -1) {
if (dir === undefined || dir === 'vertical') {
scrollInto(objState)
dispatch('obj-focus', (stateObjs[statePos - 1] ?? stateObjs[0]).it)
const obj = (stateObjs[statePos - 1] ?? stateObjs[0]).it
scrollInto(objState, obj)
dispatch('obj-focus', obj)
return
} else {
while (objState > 0) {
objState--
const nstateObjs = getStateObjects(objects, states[objState])
if (nstateObjs.length > 0) {
scrollInto(objState)
dispatch('obj-focus', (nstateObjs[statePos] ?? nstateObjs[nstateObjs.length - 1]).it)
const obj = (nstateObjs[statePos] ?? nstateObjs[nstateObjs.length - 1]).it
scrollInto(objState, obj)
dispatch('obj-focus', obj)
break
}
}
@ -244,23 +244,25 @@
}
if (offset === 1) {
if (dir === undefined || dir === 'vertical') {
scrollInto(objState)
dispatch('obj-focus', (stateObjs[statePos + 1] ?? stateObjs[stateObjs.length - 1]).it)
const obj = (stateObjs[statePos + 1] ?? stateObjs[stateObjs.length - 1]).it
scrollInto(objState, obj)
dispatch('obj-focus', obj)
return
} else {
while (objState < states.length - 1) {
objState++
const nstateObjs = getStateObjects(objects, states[objState])
if (nstateObjs.length > 0) {
scrollInto(objState)
dispatch('obj-focus', (nstateObjs[statePos] ?? nstateObjs[nstateObjs.length - 1]).it)
const obj = (nstateObjs[statePos] ?? nstateObjs[nstateObjs.length - 1]).it
scrollInto(objState, obj)
dispatch('obj-focus', obj)
break
}
}
}
}
if (offset === 0) {
scrollInto(objState)
scrollInto(objState, obj)
dispatch('obj-focus', obj)
}
}
@ -309,35 +311,25 @@
{/if}
<Scroller padding={'.5rem 0'} on:dragover on:drop>
<slot name="beforeCard" {state} />
{#each stateObjects as object}
{@const dragged = isDragging && object.it._id === dragCard?._id}
<div
transition:slideD|local={{ isDragging }}
class="step-tb75"
on:dragover|preventDefault={(evt) => cardDragOver(evt, object)}
on:drop|preventDefault={(evt) => cardDrop(evt, object)}
>
<div
class="card-container"
class:selection={selection !== undefined ? objects[selection]?._id === object.it._id : false}
class:checked={checkedSet.has(object.it._id)}
on:mouseover={() => dispatch('obj-focus', object.it)}
on:focus={() => {}}
on:contextmenu={(evt) => showMenu(evt, object)}
draggable={true}
class:draggable={true}
on:dragstart
on:dragend
class:dragged
on:dragstart={() => onDragStart(object, state)}
on:dragend={() => {
isDragging = false
}}
>
<slot name="card" object={toAny(object.it)} {dragged} />
</div>
</div>
{/each}
<KanbanRow
bind:this={stateRows[si]}
{stateObjects}
{isDragging}
{dragCard}
{objects}
{selection}
{checkedSet}
{state}
{cardDragOver}
{cardDrop}
{onDragStart}
{showMenu}
>
<svelte:fragment slot="card" let:object let:dragged>
<slot name="card" {object} {dragged} />
</svelte:fragment>
</KanbanRow>
<slot name="afterCard" {space} {state} />
</Scroller>
</div>
@ -361,42 +353,6 @@
padding: 1.5rem 2rem 0;
}
.card-container {
background-color: var(--board-card-bg-color);
border-radius: 0.25rem;
// transition: box-shadow .15s ease-in-out;
&:hover {
background-color: var(--board-card-bg-hover);
}
&.checked {
background-color: var(--highlight-select);
box-shadow: inset 0 0 1px 1px var(--highlight-select-border);
&:hover {
background-color: var(--highlight-select-hover);
}
}
&.selection,
&.checked.selection {
box-shadow: inset 0 0 1px 1px var(--primary-bg-color);
animation: anim-border 1s ease-in-out;
&:hover {
background-color: var(--highlight-hover);
}
}
&.checked.selection:hover {
background-color: var(--highlight-select-hover);
}
&.draggable {
cursor: grab;
}
&.dragged {
background-color: var(--board-bg-color);
}
}
@keyframes anim-border {
from {
box-shadow: inset 0 0 1px 1px var(--primary-edit-border-color);

View File

@ -0,0 +1,129 @@
<!--
// 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 { Doc, Ref } from '@anticrm/core'
import { createEventDispatcher } from 'svelte'
import { slide } from 'svelte/transition'
import { CardDragEvent, ExtItem, Item, TypeState } from '../types'
export let stateObjects: ExtItem[]
export let isDragging: boolean
export let dragCard: Item | undefined
export let objects: Item[]
export let selection: number | undefined = undefined
export let checkedSet: Set<Ref<Doc>>
export let state: TypeState
export let cardDragOver: (evt: CardDragEvent, object: ExtItem) => void
export let cardDrop: (evt: CardDragEvent, object: ExtItem) => void
export let onDragStart: (object: ExtItem, state: TypeState) => void
export let showMenu: (evt: MouseEvent, object: ExtItem) => void
const dispatch = createEventDispatcher()
function toAny (object: any): any {
return object
}
const slideD = (node: any, args: any) => (args.isDragging ? slide(node, args) : {})
const stateRefs: HTMLElement[] = []
$: stateRefs.length = stateObjects.length
export function scroll (item: Item): void {
const pos = stateObjects.findIndex((it) => it.it._id === item._id)
if (pos >= 0) {
stateRefs[pos]?.scrollIntoView({ behavior: 'auto', block: 'nearest' })
}
}
</script>
{#each stateObjects as object, i}
{@const dragged = isDragging && object.it._id === dragCard?._id}
<div
bind:this={stateRefs[i]}
transition:slideD|local={{ isDragging }}
class="step-tb75"
on:dragover|preventDefault={(evt) => cardDragOver(evt, object)}
on:drop|preventDefault={(evt) => cardDrop(evt, object)}
>
<div
class="card-container"
class:selection={selection !== undefined ? objects[selection]?._id === object.it._id : false}
class:checked={checkedSet.has(object.it._id)}
on:mouseover={() => dispatch('obj-focus', object.it)}
on:focus={() => {}}
on:contextmenu={(evt) => showMenu(evt, object)}
draggable={true}
class:draggable={true}
on:dragstart
on:dragend
class:dragged
on:dragstart={() => onDragStart(object, state)}
on:dragend={() => {
isDragging = false
}}
>
<slot name="card" object={toAny(object.it)} {dragged} />
</div>
</div>
{/each}
<style lang="scss">
.card-container {
background-color: var(--board-card-bg-color);
border-radius: 0.25rem;
// transition: box-shadow .15s ease-in-out;
&:hover {
background-color: var(--board-card-bg-hover);
}
&.checked {
background-color: var(--highlight-select);
box-shadow: inset 0 0 1px 1px var(--highlight-select-border);
&:hover {
background-color: var(--highlight-select-hover);
}
}
&.selection,
&.checked.selection {
box-shadow: inset 0 0 1px 1px var(--primary-bg-color);
animation: anim-border 1s ease-in-out;
&:hover {
background-color: var(--highlight-hover);
}
}
&.checked.selection:hover {
background-color: var(--highlight-select-hover);
}
&.draggable {
cursor: grab;
}
&.dragged {
background-color: var(--board-bg-color);
}
}
@keyframes anim-border {
from {
box-shadow: inset 0 0 1px 1px var(--primary-edit-border-color);
}
to {
box-shadow: inset 0 0 1px 1px var(--primary-bg-color);
}
}
</style>

View File

@ -19,3 +19,20 @@ export interface TypeState {
color: number
icon?: Asset
}
/**
* @public
*/
export type Item = DocWithRank & { state: StateType, doneState: StateType | null }
/**
* @public
*/
export interface ExtItem {
prev?: Item
it: Item
next?: Item
pos: number
}
/**
* @public
*/
export type CardDragEvent = DragEvent & { currentTarget: EventTarget & HTMLDivElement }

View File

@ -45,7 +45,6 @@
<Avatar size={'x-small'} avatar={employee.avatar} />
<div class="overflow-label user">{formatName(employee.name)}</div>
{:else}
{JSON.stringify(value)}
<div class="overflow-label user">{value.email}</div>
{/if}
</div>

View File

@ -14,11 +14,11 @@
-->
<script lang="ts">
import { Ref, SortingOrder } from '@anticrm/core'
import { Project } from '@anticrm/tracker'
import { IntlString, translate } from '@anticrm/platform'
import { createQuery, getClient } from '@anticrm/presentation'
import { Button, showPopup, SelectPopup, eventToHTMLElement, ButtonShape } from '@anticrm/ui'
import { getClient } from '@anticrm/presentation'
import { Project } from '@anticrm/tracker'
import type { ButtonKind, ButtonSize } from '@anticrm/ui'
import { Button, ButtonShape, eventToHTMLElement, SelectPopup, showPopup } from '@anticrm/ui'
import tracker from '../plugin'
export let value: Ref<Project> | null | undefined
@ -33,23 +33,10 @@
export let width: string | undefined = 'min-content'
const client = getClient()
const projectsQuery = createQuery()
let projects: Project[] = []
let selectedProject: Project | undefined
let defaultProjectLabel = ''
$: projectsQuery.query(
tracker.class.Project,
{},
(currentProjects) => {
projects = currentProjects
},
{
sort: { modifiedOn: SortingOrder.Ascending }
}
)
$: if (value !== undefined) {
handleSelectedProjectIdUpdated(value)
}
@ -58,15 +45,6 @@
$: projectIcon = selectedProject?.icon ?? tracker.icon.Projects
$: projectText = shouldShowLabel ? selectedProject?.label ?? defaultProjectLabel : undefined
$: projectsInfo = [
{ id: null, icon: tracker.icon.Projects, label: tracker.string.NoProject },
...projects.map((p) => ({
id: p._id,
icon: p.icon,
text: p.label
}))
]
const handleSelectedProjectIdUpdated = async (newProjectId: Ref<Project> | null) => {
if (newProjectId === null) {
selectedProject = undefined
@ -77,12 +55,28 @@
selectedProject = await client.findOne(tracker.class.Project, { _id: newProjectId })
}
const handleProjectEditorOpened = (event: MouseEvent) => {
const handleProjectEditorOpened = async (event: MouseEvent): Promise<void> => {
event.stopPropagation()
if (!isEditable) {
return
}
const projects = await client.findAll(
tracker.class.Project,
{},
{
sort: { modifiedOn: SortingOrder.Ascending }
}
)
const projectsInfo = [
{ id: null, icon: tracker.icon.Projects, label: tracker.string.NoProject },
...projects.map((p) => ({
id: p._id,
icon: p.icon,
text: p.label
}))
]
showPopup(
SelectPopup,
{ value: projectsInfo, placeholder: popupPlaceholder, searchable: true },

View File

@ -69,7 +69,11 @@
const options: FindOptions<Issue> = {
lookup: {
assignee: contact.class.Employee
assignee: contact.class.Employee,
space: tracker.class.Team,
_id: {
subIssues: tracker.class.Issue
}
}
}
@ -157,7 +161,7 @@
}}
>
<div class="flex-col mr-6">
<IssuePresenter value={object} {currentTeam} />
<IssuePresenter value={object} />
<span class="fs-bold caption-color mt-1 lines-limit-2">
{object.title}
</span>

View File

@ -13,40 +13,29 @@
// limitations under the License.
-->
<script lang="ts">
import { createQuery, getClient } from '@anticrm/presentation'
import { WithLookup } from '@anticrm/core'
import type { Issue, Team } from '@anticrm/tracker'
import { Icon, showPanel } from '@anticrm/ui'
import tracker from '../../plugin'
import { getIssueId } from '../../utils'
export let value: Issue
export let currentTeam: Team | undefined
export let value: WithLookup<Issue>
export let inline: boolean = false
const client = getClient()
const spaceQuery = createQuery()
const shortLabel = client.getHierarchy().getClass(value._class).shortLabel
function handleIssueEditorOpened () {
showPanel(tracker.component.EditIssue, value._id, value._class, 'content')
}
$: if (!currentTeam) {
spaceQuery.query(tracker.class.Team, { _id: value.space }, (res) => ([currentTeam] = res))
} else {
spaceQuery.unsubscribe()
}
$: issueName = currentTeam && getIssueId(currentTeam, value)
$: title = `${(value?.$lookup?.space as Team)?.identifier}-${value.number}`
</script>
{#if value && shortLabel}
{#if value}
<div class="flex-presenter issuePresenterRoot" class:inline-presenter={inline} on:click={handleIssueEditorOpened}>
<div class="icon">
<Icon icon={tracker.icon.Issue} size={'small'} />
</div>
{#if issueName !== undefined}
<span title={issueName} class="label nowrap issueLabel">{issueName}</span>
{/if}
<span title="title" class="label nowrap issueLabel">
{title}
</span>
</div>
{/if}

View File

@ -91,7 +91,11 @@
const options: FindOptions<Issue> = {
sort: { [issuesOrderKeyMap[orderingKey]]: issuesSortOrderMap[issuesOrderKeyMap[orderingKey]] },
limit: ENTRIES_LIMIT,
lookup: { assignee: contact.class.Employee, status: tracker.class.IssueStatus }
lookup: {
assignee: contact.class.Employee,
status: tracker.class.IssueStatus,
space: tracker.class.Team
}
}
$: baseQuery = {

View File

@ -57,7 +57,8 @@
const baseOptions: FindOptions<Issue> = {
lookup: {
assignee: contact.class.Employee,
status: tracker.class.IssueStatus
status: tracker.class.IssueStatus,
space: tracker.class.Team
}
}

View File

@ -54,7 +54,11 @@
{
sort: { [orderByKey]: issuesSortOrderMap[orderByKey] },
limit: 200,
lookup: { assignee: contact.class.Employee, status: tracker.class.IssueStatus }
lookup: {
assignee: contact.class.Employee,
status: tracker.class.IssueStatus,
space: tracker.class.Team
}
}
)
</script>

View File

@ -13,15 +13,14 @@
// limitations under the License.
-->
<script lang="ts">
import { SortingOrder, WithLookup, Ref, Doc } from '@anticrm/core'
import { createQuery } from '@anticrm/presentation'
import { Doc, Ref, WithLookup } from '@anticrm/core'
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
import { Button, ProgressCircle, showPopup, SelectPopup, closeTooltip, showPanel } from '@anticrm/ui'
import type { ButtonKind, ButtonSize } from '@anticrm/ui'
import { Button, closeTooltip, ProgressCircle, SelectPopup, showPanel, showPopup } from '@anticrm/ui'
import tracker from '../../../plugin'
import { getIssueId } from '../../../utils'
export let issue: Issue
export let issue: WithLookup<Issue>
export let currentTeam: Team | undefined
export let issueStatuses: WithLookup<IssueStatus>[] | undefined
@ -30,7 +29,6 @@
export let justify: 'left' | 'center' = 'left'
export let width: string | undefined = 'min-contet'
const subIssuesQuery = createQuery()
let btn: HTMLElement
let subIssues: Issue[] | undefined
@ -38,9 +36,10 @@
let countComplate: number = 0
$: hasSubIssues = issue.subIssues > 0
$: subIssuesQuery.query(tracker.class.Issue, { attachedTo: issue._id }, async (result) => (subIssues = result), {
sort: { rank: SortingOrder.Ascending }
})
$: if (issue.$lookup?.subIssues !== undefined) {
subIssues = issue.$lookup.subIssues as Issue[]
subIssues.sort((a, b) => a.rank.localeCompare(b.rank))
}
$: if (issueStatuses && subIssues) {
doneStatus = issueStatuses.find((s) => s.category === tracker.issueStatusCategory.Completed)?._id ?? undefined
if (doneStatus) countComplate = subIssues.filter((si) => si.status === doneStatus).length

View File

@ -54,8 +54,6 @@
}
</script>
{projectId}
{JSON.stringify(project)}
{#if project}
<EditProject {project} />
{:else}

View File

@ -69,13 +69,12 @@ test.describe('recruit tests', () => {
await page.click('button:has-text("Vacancy")')
await page.fill('[placeholder="Software\\ Engineer"]', vacancyId)
await page.click('button:has-text("Create")')
await page.locator(`text=${vacancyId}`).click()
await page.click(`tr > :has-text("${vacancyId}")`)
await page.click('text=Talents')
await page.click('text=Talents')
await page.click('text=Andrey P.')
// await page.locator('.mixin-selector').locator('text="Candidate"').click()
// Click on Add button
await page.click('.applications-container .flex-row-center .flex-center')
@ -87,7 +86,7 @@ test.describe('recruit tests', () => {
await page.click('button:has-text("Create")')
await page.locator(`tr:has-text("${vacancyId}") >> text=APP-`).click()
await page.click(`tr:has-text("${vacancyId}") >> text=APP-`)
await page.click('button:has-text("Assigned recruiter")')
await page.click('button:has-text("Rosamund Chen")')
})
@ -100,9 +99,9 @@ test.describe('recruit tests', () => {
await page.locator('text=Vacancies').click()
await page.click('button:has-text("Vacancy")')
await page.fill('[placeholder="Software\\ Engineer"]', vacancyId)
await page.click('button:has-text("Create")')
await page.locator(`text=${vacancyId}`).click()
await page.fill('form [placeholder="Software\\ Engineer"]', vacancyId)
await page.click('form button:has-text("Create")')
await page.click(`tr > :has-text("${vacancyId}")`)
// Create Applicatio n1
await page.click('button:has-text("Application")')

View File

@ -90,17 +90,17 @@ test.describe('recruit tests', () => {
// Click [placeholder="John"]
await page.click('[placeholder="John"]')
// Fill [placeholder="John"]
const first = 'first-' + generateId().slice(0, 4)
const first = 'first-' + generateId(4)
await page.fill('[placeholder="John"]', first)
// Click [placeholder="Appleseed"]
await page.click('[placeholder="Appleseed"]')
// Fill [placeholder="Appleseed"]
const last = 'last-' + generateId().slice(0, 4)
const last = 'last-' + generateId(4)
await page.fill('[placeholder="Appleseed"]', last)
// Click button:has-text("Create")
await page.click('button:has-text("Create")')
// Click text=q w
await page.click(`text=${first} ${last}`)
await page.click(`tr > :has-text("${first} ${last}")`)
// Click text=java
await expect(page.locator('text=java').first()).toBeVisible()
})