feat(planner): new layout for attached todos (#4995)

Signed-off-by: Eduard Aksamitov <e@euaaaio.ru>
This commit is contained in:
Eduard Aksamitov 2024-03-18 17:58:00 +03:00 committed by GitHub
parent 5630af508c
commit c9761b3502
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 171 additions and 227 deletions

View File

@ -41,6 +41,7 @@
"@hcengineering/recruit": "^0.6.21",
"@hcengineering/board": "^0.6.12",
"@hcengineering/calendar": "^0.6.17",
"@hcengineering/model-document": "^0.6.0",
"@hcengineering/model-workbench": "^0.6.1",
"@hcengineering/model-core": "^0.6.0",
"@hcengineering/model-view": "^0.6.0",

View File

@ -33,6 +33,7 @@ import { Collection, Mixin, Model, Prop, TypeRef, TypeString, UX, type Builder,
import { TEvent } from '@hcengineering/model-calendar'
import core, { TAttachedDoc, TClass, TDoc, TType } from '@hcengineering/model-core'
import tracker from '@hcengineering/model-tracker'
import document from '@hcengineering/model-document'
import view, { createAction } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench'
import notification from '@hcengineering/notification'
@ -138,6 +139,10 @@ export function createModel (builder: Builder): void {
presenter: time.component.IssuePresenter
})
builder.mixin(document.class.Document, core.class.Class, time.mixin.ItemPresenter, {
presenter: time.component.DocumentPresenter
})
builder.mixin(lead.class.Lead, core.class.Class, time.mixin.ItemPresenter, {
presenter: time.component.LeadPresenter
})

View File

@ -44,6 +44,7 @@ export default mergeIds(timeId, time, {
},
component: {
IssuePresenter: '' as AnyComponent,
DocumentPresenter: '' as AnyComponent,
ApplicantPresenter: '' as AnyComponent,
CardPresenter: '' as AnyComponent,
LeadPresenter: '' as AnyComponent,

View File

@ -70,6 +70,7 @@
--tag-on-subtle-PorpoiseText: #F2F4F6;
--tag-subtle-PorpoiseBackground: #343F49;
--tag-nuance-SkyBackground: #1F2737;
--icon-disabled-IconColor: #394358;
@ -144,6 +145,7 @@
--tag-on-subtle-PorpoiseText: #293139;
--tag-subtle-PorpoiseBackground: #C8D1D9;
--tag-nuance-SkyBackground: #EEF4FD;
--icon-disabled-IconColor: #B3BCCC;

View File

@ -618,6 +618,16 @@
height: var(--global-extra-small-Size);
}
}
.hulyToDoLine-reference {
padding: 0 var(--spacing-1) 0 var(--spacing-0_75);
box-shadow: inset 0 0 0 1px var(--global-subtle-ui-BorderColor);
border-radius: var(--extra-small-BorderRadius);
background-color: var(--tag-nuance-SkyBackground);
&:hover {
box-shadow: inset 0 0 0 1px var(--global-ui-BorderColor);
}
}
&.hovered,
&:hover {

View File

@ -62,6 +62,7 @@
export let _id: Ref<Document>
export let readonly: boolean = false
export let embedded: boolean = false
export let kind: 'default' | 'modern' = 'default'
$: readonly = $restrictionStore.readonly
@ -229,7 +230,7 @@
<Panel
object={doc}
withoutActivity
allowClose={false}
allowClose={!embedded}
isAside={true}
customAside={aside}
bind:selectedAside
@ -237,6 +238,7 @@
isCustomAttr={false}
isSub={false}
{embedded}
{kind}
bind:content
bind:innerWidth
bind:useMaxWidth
@ -251,7 +253,13 @@
<svelte:fragment slot="utils">
{#if !$restrictionStore.disableActions}
<Button icon={IconMoreH} iconProps={{ size: 'medium' }} kind={'icon'} on:click={showContextMenu} />
<Button
id="btn-doc-title-open-more"
icon={IconMoreH}
iconProps={{ size: 'medium' }}
kind={'icon'}
on:click={showContextMenu}
/>
{#each actions as action}
<Button
icon={action.icon}

View File

@ -44,6 +44,7 @@
"@hcengineering/calendar-resources": "^0.6.0",
"@hcengineering/presentation": "^0.6.2",
"@hcengineering/core": "^0.6.28",
"@hcengineering/document": "^0.6.0",
"@hcengineering/tracker": "^0.6.13",
"@hcengineering/tracker-resources": "^0.6.0",
"@hcengineering/task": "^0.6.13",

View File

@ -2,27 +2,15 @@
import { SortingOrder, WithLookup } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import tags from '@hcengineering/tags'
import {
CheckBox,
Component,
IconMoreH,
IconMoreV2,
Spinner,
eventToHTMLElement,
getEventPositionElement,
showPopup,
showPanel,
Icon
} from '@hcengineering/ui'
import { showMenu, Menu, FixedColumn } from '@hcengineering/view-resources'
import time, { ToDo, WorkSlot } from '@hcengineering/time'
import { Component, IconMoreV2, Spinner, showPanel, Icon } from '@hcengineering/ui'
import { showMenu } from '@hcengineering/view-resources'
import time, { ToDo, ToDoPriority, WorkSlot } from '@hcengineering/time'
import { createEventDispatcher } from 'svelte'
import plugin from '../plugin'
import EditToDo from './EditToDo.svelte'
import ToDoDuration from './ToDoDuration.svelte'
import WorkItemPresenter from './WorkItemPresenter.svelte'
import ToDoCheckbox from './ToDoCheckbox.svelte'
import ToDoPriority from './ToDoPriority.svelte'
import ToDoPriorityPresenter from './ToDoPriorityPresenter.svelte'
export let todo: WithLookup<ToDo>
export let size: 'small' | 'large' = 'small'
@ -75,11 +63,9 @@
}
function open (e: MouseEvent): void {
// hovered = true
showPanel(time.component.EditToDo, todo._id, todo._class, 'content')
}
$: isTodo = todo.attachedTo === time.ids.NotAttached
$: isDone = todo.doneOn != null
</script>
@ -116,45 +102,31 @@
{/if}
</div>
{#if size === 'small'}
<ToDoPriority value={todo.priority} muted={isDone} />
<ToDoPriorityPresenter value={todo.priority} muted={isDone} />
{/if}
</div>
</div>
{#if isTodo}
{#if size === 'small'}
<div class="hulyToDoLine-title hulyToDoLine-top-align top-12 text-left font-regular-14 overflow-label">
{todo.title}
</div>
{:else}
<div class="flex-col flex-gap-1 flex-grow text-left">
<div class="hulyToDoLine-title hulyToDoLine-top-align top-12 text-left font-regular-14">
{todo.title}
</div>
<div class="flex-row-center flex-grow flex-gap-2">
<Component is={tags.component.LabelsPresenter} props={{ object: todo, value: todo.labels, kind: 'todo' }} />
<ToDoPriority value={todo.priority} muted={isDone} showLabel />
</div>
</div>
{/if}
<WorkItemPresenter {todo} kind={'todo-line'} withoutSpace />
{#if size === 'small'}
<div class="hulyToDoLine-title hulyToDoLine-top-align top-12 text-left font-regular-14 overflow-label">
{todo.title}
</div>
{:else}
<div class="flex-col flex-gap-1 flex-grow text-left">
<div
class="hulyToDoLine-title hulyToDoLine-top-align text-left top-12 font-bold-12 secondary-textColor"
class:overflow-label={size === 'small'}
>
<div class="flex-col flex-gap-2 flex-grow text-left">
<div class="hulyToDoLine-title hulyToDoLine-top-align top-12 text-left font-regular-14">
{todo.title}
</div>
<WorkItemPresenter {todo} kind={'todo-line'} {size} withoutSpace>
{#if size === 'large'}
<div class="flex-row-top flex-grow flex-gap-2">
{#if todo.labels && todo.labels > 0 && todo.priority !== ToDoPriority.NoPriority}
<div class="flex-row-center flex-grow flex-gap-2">
{#if todo.labels && todo.labels > 0}
<Component
is={tags.component.LabelsPresenter}
props={{ object: todo, value: todo.labels, kind: 'todo' }}
/>
<ToDoPriority value={todo.priority} muted={isDone} showLabel />
</div>
{/if}
</WorkItemPresenter>
{/if}
<ToDoPriorityPresenter value={todo.priority} muted={isDone} showLabel />
</div>
{/if}
</div>
{/if}
</div>

View File

@ -0,0 +1,37 @@
<!--
// Copyright © 2024 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">
export let label: string
</script>
<button class="hulyToDoLine-reference flex-row-top flex-no-shrink flex-gap-2" on:click>
<div class="hulyToDoLine-icon">
<slot name="icon" />
</div>
<span class="hulyToDoLine-label overflow-label font-medium-12 text-left max-w-20 secondary-textColor">
{label}
</span>
<slot />
</button>
<style lang="scss">
button {
margin: 0;
padding: 0;
text-align: left;
border: none;
outline: none;
}
</style>

View File

@ -1,18 +1,29 @@
<!--
// Copyright © 2024 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 } from '@hcengineering/core'
import type { ItemPresenter, ToDo } from '@hcengineering/time'
import type { Class, Doc } from '@hcengineering/core'
import type { ObjectPanel } from '@hcengineering/view'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Component, navigate } from '@hcengineering/ui'
import view, { ObjectPanel } from '@hcengineering/view'
import { getObjectLinkFragment } from '@hcengineering/view-resources'
import { ItemPresenter, ToDo } from '@hcengineering/time'
import { Component, showPanel } from '@hcengineering/ui'
import view from '@hcengineering/view'
import time from '../plugin'
export let todo: ToDo
export let kind: 'default' | 'todo-line' = 'default'
export let size: 'small' | 'large' = 'small'
export let withoutSpace: boolean = false
export let isEditable: boolean = false
export let shouldShowAvatar: boolean = false
const client = getClient()
const hierarchy = client.getHierarchy()
@ -24,13 +35,12 @@
doc = res[0]
})
async function click (ev: MouseEvent) {
ev.stopPropagation()
async function click (event: MouseEvent) {
event.stopPropagation()
if (!doc) return
const panelComponent = hierarchy.classHierarchyMixin<Class<Doc>, ObjectPanel>(doc._class, view.mixin.ObjectPanel)
const component = panelComponent?.component ?? view.component.EditDoc
const loc = await getObjectLinkFragment(hierarchy, doc, {}, component)
navigate(loc)
showPanel(component, doc._id, doc._class, 'content')
}
</script>
@ -38,15 +48,11 @@
{#if kind === 'default'}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="cursor-pointer clear-mins" on:click|stopPropagation={click}>
<Component is={presenter.presenter} props={{ value: doc, withoutSpace, isEditable, shouldShowAvatar }} />
<div class="cursor-pointer clear-mins" on:click={click}>
<Component is={presenter.presenter} props={{ value: doc, withoutSpace }} />
</div>
{:else}
<Component
is={presenter.presenter}
props={{ value: doc, withoutSpace, isEditable, kind: size === 'large' ? 'todo-line-large' : 'todo-line' }}
on:click={click}
>
<Component is={presenter.presenter} props={{ value: doc, withoutSpace }} on:click={click}>
<slot />
</Component>
{/if}

View File

@ -0,0 +1,41 @@
<!--
// Copyright © 2024 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 type { Document } from '@hcengineering/document'
import { Icon, IconWithEmoji, getPlatformColorDef, themeStore } from '@hcengineering/ui'
import document from '@hcengineering/document'
import view from '@hcengineering/view'
import ToDoReference from '../ToDoReference.svelte'
export let value: Document
export let inline: boolean = false
export let disabled: boolean = false
export let withoutSpace: boolean = true
$: icon = value.icon === view.ids.IconWithEmoji ? IconWithEmoji : value.icon ?? document.icon.Document
$: iconProps =
value.icon === view.ids.IconWithEmoji
? { icon: value.color }
: {
fill: value.color !== undefined ? getPlatformColorDef(value.color, $themeStore.dark).icon : 'currentColor'
}
</script>
<ToDoReference label={value.name} on:click>
<svelte:fragment slot="icon">
<Icon size="small" {icon} {iconProps} />
</svelte:fragment>
<slot />
</ToDoReference>

View File

@ -1,162 +1,20 @@
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { getStates } from '@hcengineering/task'
import { typeStore } from '@hcengineering/task-resources'
import tracker, { Issue, IssueStatus, Project } from '@hcengineering/tracker'
import { AssigneeEditor, IssueStatusIcon, StatusPresenter } from '@hcengineering/tracker-resources'
import { activeProjects } from '@hcengineering/tracker-resources/src/utils'
import { Label, SelectPopup, eventToHTMLElement, showPopup } from '@hcengineering/ui'
import { Issue } from '@hcengineering/tracker'
import { IssueStatusIcon } from '@hcengineering/tracker-resources'
import { statusStore } from '@hcengineering/view-resources'
import ToDoReference from '../ToDoReference.svelte'
export let value: Issue
export let withoutSpace: boolean
export let isEditable: boolean = false
export let shouldShowAvatar: boolean = false
export let kind: 'todo-line' | 'todo-line-large' | undefined = undefined
export let inline: boolean = false
export let disabled: boolean = false
export let withoutSpace: boolean = true
let space: Project | undefined = undefined
const defaultIssueStatus: Ref<IssueStatus> | undefined = undefined
const client = getClient()
$: space = $activeProjects.get(value.space)
$: st = $statusStore.byId.get(value.status)
const changeStatus = async (newStatus: Ref<IssueStatus> | undefined, refocus: boolean = true) => {
if (!isEditable || newStatus === undefined || value.status === newStatus) {
return
}
if ('_class' in value) {
await client.update(value, { status: newStatus })
}
}
function getSelectedStatus (
statuses: IssueStatus[] | undefined,
value: Issue,
defaultStatus: Ref<IssueStatus> | undefined
): IssueStatus | undefined {
if (value.status !== undefined) {
const current = statuses?.find((status) => status._id === value.status)
if (current) return current
}
if (defaultIssueStatus !== undefined) {
const res = statuses?.find((status) => status._id === defaultStatus)
changeStatus(res?._id, false)
return res
}
}
$: selectedStatus = getSelectedStatus(statuses, value, defaultIssueStatus)
$: statuses = getStates(space, $typeStore, $statusStore.byId)
$: statusesInfo = statuses?.map((s) => {
return {
id: s._id,
component: StatusPresenter,
props: { value: s, size: 'small', space: value.space },
isSelected: selectedStatus?._id === s._id ?? false
}
})
const handleStatusEditorOpened = (event: MouseEvent) => {
if (!isEditable) {
return
}
showPopup(SelectPopup, { value: statusesInfo }, eventToHTMLElement(event), changeStatus)
}
$: status = $statusStore.byId.get(value.status)
</script>
{#if kind === 'todo-line'}
<button class="flex-row-top flex-grow relative" on:click>
{#if st}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="hulyToDoLine-icon"
class:cursor-pointer={isEditable}
on:click|stopPropagation={handleStatusEditorOpened}
>
<IssueStatusIcon value={st} size={'small'} space={value.space} />
</div>
{/if}
<span class="hulyToDoLine-label overflow-label font-regular-14 text-left secondary-textColor ml-2">
{value.title}
</span>
</button>
{:else if kind === 'todo-line-large'}
<button class="flex-row-top flex-grow relative" on:click>
{#if st}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="hulyToDoLine-icon"
class:cursor-pointer={isEditable}
on:click|stopPropagation={handleStatusEditorOpened}
>
<IssueStatusIcon value={st} size={'small'} space={value.space} />
</div>
{/if}
<div class="flex-col flex-gap-1 flex-grow text-left ml-2">
<div class="hulyToDoLine-label large font-regular-14 secondary-textColor">
{value.title}
</div>
<slot />
</div>
</button>
{:else if shouldShowAvatar}
<div class="flex-between">
<div class="flex-col flex-grow mr-3">
{#if !withoutSpace}
<div class="flex-row-center">
<Label label={tracker.string.ConfigLabel} />
/
{space?.name}
</div>
{/if}
<div class="flex-row-center">
{#if st}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="flex-no-shrink" class:cursor-pointer={isEditable} on:click={handleStatusEditorOpened}>
<IssueStatusIcon value={st} size={'small'} space={value.space} />
</div>
{/if}
<span class="ml-1-5 overflow-label">{value.title}</span>
</div>
</div>
<div class="hideOnDrag flex-no-shrink">
<AssigneeEditor object={value} avatarSize={'smaller'} shouldShowName={false} />
</div>
</div>
{:else}
{#if !withoutSpace}
<div class="flex-row-center">
<Label label={tracker.string.ConfigLabel} />
/
{space?.name}
</div>
{/if}
<div class="flex-row-center">
{#if st}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="flex-no-shrink mr-1-5" class:cursor-pointer={isEditable} on:click={handleStatusEditorOpened}>
<IssueStatusIcon value={st} size={'small'} space={value.space} />
</div>
{/if}
<span class="overflow-label">{value.title}</span>
</div>
{/if}
<style lang="scss">
button {
margin: 0;
padding: 0;
text-align: left;
border: none;
outline: none;
}
</style>
<ToDoReference label={value.identifier} on:click>
<svelte:fragment slot="icon">
<IssueStatusIcon size="small" value={status} space={value.space} />
</svelte:fragment>
<slot />
</ToDoReference>

View File

@ -13,12 +13,13 @@
// limitations under the License.
//
import { type Resources } from '@hcengineering/platform'
import type { Resources } from '@hcengineering/platform'
import Me from './components/Me.svelte'
import Team from './components/team/Team.svelte'
import IssuePresenter from './components/presenters/IssuePresenter.svelte'
import CardPresenter from './components/presenters/CardPresenter.svelte'
import LeadPresenter from './components/presenters/LeadPresenter.svelte'
import DocumentPresenter from './components/presenters/DocumentPresenter.svelte'
import ApplicantPresenter from './components/presenters/ApplicantPresenter.svelte'
import WorkSlotElement from './components/WorkSlotElement.svelte'
import EditWorkSlot from './components/EditWorkSlot.svelte'
@ -37,6 +38,7 @@ export default async (): Promise<Resources> => ({
IssuePresenter,
CardPresenter,
LeadPresenter,
DocumentPresenter,
ApplicantPresenter,
EditWorkSlot,
WorkSlotElement,

View File

@ -447,10 +447,10 @@ async function getIssueToDoData (
const data: AttachedData<ProjectToDo> = {
attachedSpace: issue.space,
workslots: 0,
description: issue.title,
description: '',
priority: ToDoPriority.NoPriority,
visibility: 'public',
title: issue.identifier,
title: issue.title,
user: acc.person
}
return data

View File

@ -18,7 +18,7 @@ export class DocumentContentPage extends CommonPage {
this.buttonToolbarLink = page.locator('div.text-editor-toolbar button:nth-child(10)')
this.inputFormLink = page.locator('form[id="text-editor:string:Link"] input')
this.buttonFormLinkSave = page.locator('form[id="text-editor:string:Link"] button[type="submit"]')
this.buttonMoreActions = page.locator('div.popupPanel-title button:first-child')
this.buttonMoreActions = page.locator('div.popupPanel-title button#btn-doc-title-open-more')
}
async checkDocumentTitle (title: string): Promise<void> {