mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-15 04:49:00 +00:00
Tracker: Project - Editors (#1779)
Signed-off-by: Artyom Grigorovich <grigorovichartyom@gmail.com>
This commit is contained in:
parent
c6b757d67c
commit
ae13a1ff2f
@ -58,6 +58,8 @@
|
||||
let search: string = ''
|
||||
let objects: Doc[] = []
|
||||
|
||||
$: selectedElements = new Set(selectedObjects)
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const query = createQuery()
|
||||
$: query.query<Doc>(
|
||||
@ -73,15 +75,18 @@
|
||||
{ ...(options ?? {}), limit: 200 }
|
||||
)
|
||||
|
||||
const isSelected = (person: Doc): boolean => {
|
||||
if (selectedObjects.filter((p) => p === person._id).length > 0) return true
|
||||
return false
|
||||
}
|
||||
const checkSelected = (person: Doc, objects: Doc[]): void => {
|
||||
selectedObjects = isSelected(person)
|
||||
? selectedObjects.filter((p) => p !== person._id)
|
||||
: [...selectedObjects, person._id]
|
||||
if (selectedElements.has(person._id)) {
|
||||
selectedElements.delete(person._id)
|
||||
} else {
|
||||
selectedElements.add(person._id)
|
||||
}
|
||||
|
||||
selectedObjects = Array.from(selectedElements)
|
||||
|
||||
dispatch('update', selectedObjects)
|
||||
|
||||
selectedElements = selectedElements
|
||||
}
|
||||
|
||||
const client = getClient()
|
||||
@ -168,7 +173,7 @@
|
||||
>
|
||||
{#if multiSelect}
|
||||
<div class="check pointer-events-none">
|
||||
<CheckBox checked={isSelected(obj)} primary />
|
||||
<CheckBox checked={selectedElements.has(obj._id)} primary />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
@ -231,6 +231,16 @@
|
||||
background-color: var(--button-disabled-color);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: var(--button-bg-hover);
|
||||
border-color: var(--button-border-hover);
|
||||
color: var(--caption-color);
|
||||
|
||||
.btn-icon {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
&.no-border {
|
||||
font-weight: 400;
|
||||
@ -275,10 +285,10 @@
|
||||
}
|
||||
&.link-bordered {
|
||||
padding: 0 0.375rem;
|
||||
color: var(--acctent-color);
|
||||
color: var(--accent-color);
|
||||
border-color: var(--button-border-color);
|
||||
&:hover {
|
||||
color: var(--acctent-color);
|
||||
color: var(--accent-color);
|
||||
border-color: var(--button-border-hover);
|
||||
.btn-icon {
|
||||
color: var(--accent-color);
|
||||
|
@ -1,11 +1,11 @@
|
||||
// This file is read by tools that parse documentation comments conforming to the TSDoc standard.
|
||||
// It should be published with your NPM package. It should not be tracked by Git.
|
||||
{
|
||||
"tsdocVersion": "0.12",
|
||||
"toolPackages": [
|
||||
{
|
||||
"packageName": "@microsoft/api-extractor",
|
||||
"packageVersion": "7.23.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
// This file is read by tools that parse documentation comments conforming to the TSDoc standard.
|
||||
// It should be published with your NPM package. It should not be tracked by Git.
|
||||
{
|
||||
"tsdocVersion": "0.12",
|
||||
"toolPackages": [
|
||||
{
|
||||
"packageName": "@microsoft/api-extractor",
|
||||
"packageVersion": "7.23.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -8,10 +8,12 @@
|
||||
|
||||
export let value: WithLookup<Employee>
|
||||
export let shouldShowAvatar: boolean = true
|
||||
export let shouldShowName: boolean = true
|
||||
export let onEmployeeEdit: ((event: MouseEvent) => void) | undefined = undefined
|
||||
|
||||
let container: HTMLElement
|
||||
|
||||
function onEdit () {
|
||||
const onEdit = () => {
|
||||
showPopup(
|
||||
EmployeePreviewPopup,
|
||||
{
|
||||
@ -20,11 +22,13 @@
|
||||
container
|
||||
)
|
||||
}
|
||||
|
||||
$: handlePersonEdit = onEmployeeEdit ?? onEdit
|
||||
</script>
|
||||
|
||||
<div bind:this={container} class="flex-center container">
|
||||
<div class="pr-2 over-underline">
|
||||
<PersonPresenter {value} {onEdit} {shouldShowAvatar} />
|
||||
<div class="over-underline" class:pr-2={shouldShowName}>
|
||||
<PersonPresenter {value} onEdit={handlePersonEdit} {shouldShowAvatar} {shouldShowName} />
|
||||
</div>
|
||||
{#if value.$lookup?.statuses?.length}
|
||||
<div class="status content-color">
|
||||
|
@ -100,7 +100,7 @@
|
||||
}
|
||||
}
|
||||
.eContentPresenterIcon {
|
||||
margin-right: 0.5rem;
|
||||
margin-right: 0.25rem;
|
||||
color: var(--theme-content-dark-color);
|
||||
}
|
||||
.eContentPresenterLabel {
|
||||
|
@ -94,4 +94,20 @@
|
||||
<rect x="6" y="5" width="3" height="9" rx="1" fill-opacity=".4" />
|
||||
<rect x="11" y="2" width="3" height="12" rx="1" fill-opacity=".4" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="list" viewBox="0 0 16 16">
|
||||
<path d="M1 1.8C1 1.51997 1 1.37996 1.0545 1.273C1.10243 1.17892 1.17892 1.10243 1.273 1.0545C1.37996 1 1.51997 1 1.8 1H14.2C14.48 1 14.62 1 14.727 1.0545C14.8211 1.10243 14.8976 1.17892 14.9455 1.273C15 1.37996 15 1.51997 15 1.8V2.2C15 2.48003 15 2.62004 14.9455 2.727C14.8976 2.82108 14.8211 2.89757 14.727 2.9455C14.62 3 14.48 3 14.2 3H1.8C1.51997 3 1.37996 3 1.273 2.9455C1.17892 2.89757 1.10243 2.82108 1.0545 2.727C1 2.62004 1 2.48003 1 2.2V1.8Z" />
|
||||
<path d="M1 13.8C1 13.52 1 13.38 1.0545 13.273C1.10243 13.1789 1.17892 13.1024 1.273 13.0545C1.37996 13 1.51997 13 1.8 13H14.2C14.48 13 14.62 13 14.727 13.0545C14.8211 13.1024 14.8976 13.1789 14.9455 13.273C15 13.38 15 13.52 15 13.8V14.2C15 14.48 15 14.62 14.9455 14.727C14.8976 14.8211 14.8211 14.8976 14.727 14.9455C14.62 15 14.48 15 14.2 15H1.8C1.51997 15 1.37996 15 1.273 14.9455C1.17892 14.8976 1.10243 14.8211 1.0545 14.727C1 14.62 1 14.48 1 14.2V13.8Z" />
|
||||
<path d="M1 9.8C1 9.51997 1 9.37996 1.0545 9.273C1.10243 9.17892 1.17892 9.10243 1.273 9.0545C1.37996 9 1.51997 9 1.8 9H14.2C14.48 9 14.62 9 14.727 9.0545C14.8211 9.10243 14.8976 9.17892 14.9455 9.273C15 9.37996 15 9.51997 15 9.8V10.2C15 10.48 15 10.62 14.9455 10.727C14.8976 10.8211 14.8211 10.8976 14.727 10.9455C14.62 11 14.48 11 14.2 11H1.8C1.51997 11 1.37996 11 1.273 10.9455C1.17892 10.8976 1.10243 10.8211 1.0545 10.727C1 10.62 1 10.48 1 10.2V9.8Z" />
|
||||
<path d="M1 5.8C1 5.51997 1 5.37996 1.0545 5.273C1.10243 5.17892 1.17892 5.10243 1.273 5.0545C1.37996 5 1.51997 5 1.8 5H14.2C14.48 5 14.62 5 14.727 5.0545C14.8211 5.10243 14.8976 5.17892 14.9455 5.273C15 5.37996 15 5.51997 15 5.8V6.2C15 6.48003 15 6.62004 14.9455 6.727C14.8976 6.82108 14.8211 6.89757 14.727 6.9455C14.62 7 14.48 7 14.2 7H1.8C1.51997 7 1.37996 7 1.273 6.9455C1.17892 6.89757 1.10243 6.82108 1.0545 6.727C1 6.62004 1 6.48003 1 6.2V5.8Z" />
|
||||
</symbol>
|
||||
<symbol id="timeline" viewBox="0 0 14 14">
|
||||
<rect x="6" y="1" width="7" height="2.5" rx="0.5" />
|
||||
<rect x="1" y="5.75" width="9" height="2.5" rx="0.5" />
|
||||
<rect x="4" y="10.5" width="9" height="2.5" rx="0.5" />
|
||||
</symbol>
|
||||
<symbol id="projectMembers" viewBox="0 0 16 16">
|
||||
<path d="M1 3C1 1.89543 1.89543 1 3 1H9C10.1046 1 11 1.89543 11 3V3.5H6C4.61929 3.5 3.5 4.61929 3.5 6V11H3C1.89543 11 1 10.1046 1 9V3Z" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 5C5.89543 5 5 5.89543 5 7V13C5 14.1046 5.89543 15 7 15H13C14.1046 15 15 14.1046 15 13V7C15 5.89543 14.1046 5 13 5H7ZM10 10C10.9665 10 11.5 9.2165 11.5 8.25C11.5 7.2835 10.9665 6.5 10 6.5C9.0335 6.5 8.5 7.2835 8.5 8.25C8.5 9.2165 9.0335 10 10 10ZM7 12.5616C7 11.5144 7.9841 10.746 9 11C9.47572 11.7136 10.5243 11.7136 11 11C12.0159 10.746 13 11.5144 13 12.5616V13.0101C13 13.2806 12.7806 13.5 12.5101 13.5H7.48995C7.21936 13.5 7 13.2806 7 13.0101V12.5616Z" />
|
||||
</symbol>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 18 KiB |
@ -17,6 +17,10 @@
|
||||
"Backlog": "Backlog",
|
||||
"Board": "Board",
|
||||
"Projects": "Projects",
|
||||
"AllProjects": "All",
|
||||
"BacklogProjects": "Backlog",
|
||||
"ActiveProjects": "Active",
|
||||
"ClosedProjects": "Closed",
|
||||
"NewProject": "New project",
|
||||
"CreateProject": "Create project",
|
||||
"ProjectNamePlaceholder": "Project name",
|
||||
@ -101,6 +105,10 @@
|
||||
"AddToProject": "Add to project\u2026",
|
||||
"MoveToProject": "Move to project\u2026",
|
||||
"NoProject": "No project",
|
||||
"ProjectLeadTitle": "Project lead",
|
||||
"ProjectMembersTitle": "Project members",
|
||||
"ProjectLeadSearchPlaceholder": "Set project lead\u2026",
|
||||
"ProjectMembersSearchPlaceholder": "Change project members\u2026",
|
||||
|
||||
"GotoIssues": "Go to issues",
|
||||
"GotoActive": "Go to active issues",
|
||||
|
@ -45,7 +45,11 @@ loadMetadata(tracker.icon, {
|
||||
PriorityUrgent: `${icons}#priority-urgent`,
|
||||
PriorityHigh: `${icons}#priority-high`,
|
||||
PriorityMedium: `${icons}#priority-medium`,
|
||||
PriorityLow: `${icons}#priority-low`
|
||||
PriorityLow: `${icons}#priority-low`,
|
||||
|
||||
ProjectsList: `${icons}#list`,
|
||||
ProjectsTimeline: `${icons}#timeline`,
|
||||
ProjectMembers: `${icons}#projectMembers`
|
||||
})
|
||||
|
||||
addStringsLoader(trackerId, async (lang: string) => await import(`../lang/${lang}.json`))
|
||||
|
@ -0,0 +1,72 @@
|
||||
<!--
|
||||
// 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 { Timestamp } from '@anticrm/core'
|
||||
import { DatePresenter, Tooltip, getDaysDifference } from '@anticrm/ui'
|
||||
import DueDatePopup from './DueDatePopup.svelte'
|
||||
import { getDueDateIconModifier } from '../utils'
|
||||
|
||||
export let dateMs: number | null = null
|
||||
export let shouldRender: boolean = true
|
||||
export let onDateChange: (newDate: number | null) => void
|
||||
|
||||
$: today = new Date(new Date(Date.now()).setHours(0, 0, 0, 0))
|
||||
$: isOverdue = dateMs !== null && dateMs < today.getTime()
|
||||
$: dueDate = dateMs === null ? null : new Date(dateMs)
|
||||
$: daysDifference = dueDate === null ? null : getDaysDifference(today, dueDate)
|
||||
$: iconModifier = getDueDateIconModifier(isOverdue, daysDifference)
|
||||
$: formattedDate = !dateMs ? '' : new Date(dateMs).toLocaleString('default', { month: 'short', day: 'numeric' })
|
||||
|
||||
const handleDueDateChanged = async (event: CustomEvent<Timestamp>) => {
|
||||
const newDate = event.detail
|
||||
|
||||
if (newDate === undefined || dateMs === newDate) {
|
||||
return
|
||||
}
|
||||
|
||||
onDateChange(newDate)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if shouldRender}
|
||||
{#if formattedDate}
|
||||
<Tooltip
|
||||
direction={'top'}
|
||||
component={DueDatePopup}
|
||||
props={{
|
||||
formattedDate: formattedDate,
|
||||
daysDifference: daysDifference,
|
||||
isOverdue: isOverdue,
|
||||
iconModifier: iconModifier
|
||||
}}
|
||||
>
|
||||
<DatePresenter
|
||||
value={dateMs}
|
||||
editable={true}
|
||||
shouldShowLabel={false}
|
||||
icon={iconModifier}
|
||||
on:change={handleDueDateChanged}
|
||||
/>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<DatePresenter
|
||||
value={dateMs}
|
||||
editable={true}
|
||||
shouldShowLabel={false}
|
||||
icon={iconModifier}
|
||||
on:change={handleDueDateChanged}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
@ -14,7 +14,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Icon, Label, IconDPCalendarOver, IconDPCalendar } from '@anticrm/ui'
|
||||
import tracker from '../../plugin'
|
||||
import tracker from '../plugin'
|
||||
|
||||
export let formattedDate: string = ''
|
||||
export let daysDifference: number = 0
|
@ -14,7 +14,6 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref, WithLookup } from '@anticrm/core'
|
||||
|
||||
import { IssueStatus } from '@anticrm/tracker'
|
||||
import { Button, showPopup, SelectPopup, eventToHTMLElement } from '@anticrm/ui'
|
||||
import type { ButtonKind, ButtonSize } from '@anticrm/ui'
|
||||
|
@ -67,6 +67,7 @@
|
||||
if (!isEditable) {
|
||||
return
|
||||
}
|
||||
|
||||
showPopup(
|
||||
UsersPopup,
|
||||
{
|
||||
@ -90,7 +91,7 @@
|
||||
isInteractive={true}
|
||||
shouldShowPlaceholder={true}
|
||||
shouldShowName={shouldShowLabel}
|
||||
onEdit={handleAssigneeEditorOpened}
|
||||
onEmployeeEdit={handleAssigneeEditorOpened}
|
||||
tooltipLabels={{ personLabel: tracker.string.AssignedTo, placeholderLabel: tracker.string.AssignTo }}
|
||||
/>
|
||||
{/if}
|
||||
|
@ -13,59 +13,30 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Timestamp, WithLookup } from '@anticrm/core'
|
||||
import { WithLookup } from '@anticrm/core'
|
||||
import { Issue } from '@anticrm/tracker'
|
||||
import { DatePresenter, Tooltip, getDaysDifference } from '@anticrm/ui'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import DueDatePopup from './DueDatePopup.svelte'
|
||||
import CommonTrackerDatePresenter from '../CommonTrackerDatePresenter.svelte'
|
||||
import tracker from '../../plugin'
|
||||
import { getDueDateIconModifier } from '../../utils'
|
||||
|
||||
export let value: WithLookup<Issue>
|
||||
|
||||
const client = getClient()
|
||||
|
||||
$: today = new Date(new Date(Date.now()).setHours(0, 0, 0, 0))
|
||||
$: dueDateMs = value.dueDate
|
||||
$: isOverdue = dueDateMs !== null && dueDateMs < today.getTime()
|
||||
$: dueDate = dueDateMs === null ? null : new Date(dueDateMs)
|
||||
$: daysDifference = dueDate === null ? null : getDaysDifference(today, dueDate)
|
||||
$: iconModifier = getDueDateIconModifier(isOverdue, daysDifference)
|
||||
$: formattedDate = !dueDateMs ? '' : new Date(dueDateMs).toLocaleString('default', { month: 'short', day: 'numeric' })
|
||||
|
||||
const handleDueDateChanged = async (event: CustomEvent<Timestamp>) => {
|
||||
const newDate = event.detail
|
||||
|
||||
if (newDate === undefined || value.dueDate === newDate) {
|
||||
return
|
||||
}
|
||||
|
||||
const handleDueDateChanged = async (newDate: number | null) => {
|
||||
await client.update(value, { dueDate: newDate })
|
||||
}
|
||||
|
||||
$: shouldRenderPresenter =
|
||||
dueDateMs &&
|
||||
dueDateMs !== null &&
|
||||
value.$lookup?.status?.category !== tracker.issueStatusCategory.Completed &&
|
||||
value.$lookup?.status?.category !== tracker.issueStatusCategory.Canceled
|
||||
</script>
|
||||
|
||||
{#if shouldRenderPresenter}
|
||||
<Tooltip
|
||||
direction={'top'}
|
||||
component={DueDatePopup}
|
||||
props={{
|
||||
formattedDate: formattedDate,
|
||||
daysDifference: daysDifference,
|
||||
isOverdue: isOverdue,
|
||||
iconModifier: iconModifier
|
||||
}}
|
||||
>
|
||||
<DatePresenter
|
||||
value={dueDateMs}
|
||||
editable={true}
|
||||
shouldShowLabel={false}
|
||||
icon={iconModifier}
|
||||
on:change={handleDueDateChanged}
|
||||
/>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
<CommonTrackerDatePresenter
|
||||
dateMs={dueDateMs}
|
||||
shouldRender={shouldRenderPresenter}
|
||||
onDateChange={handleDueDateChanged}
|
||||
/>
|
||||
|
@ -0,0 +1,23 @@
|
||||
<!--
|
||||
// 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 { WithLookup } from '@anticrm/core'
|
||||
import { Project } from '@anticrm/tracker'
|
||||
import { Button } from '@anticrm/ui'
|
||||
|
||||
export let value: WithLookup<Project>
|
||||
</script>
|
||||
|
||||
<Button size="small" kind="link" icon={value.icon} />
|
@ -0,0 +1,71 @@
|
||||
<!--
|
||||
// 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 { Employee } from '@anticrm/contact'
|
||||
import { Avatar } from '@anticrm/presentation'
|
||||
import { Label } from '@anticrm/ui'
|
||||
import tracker from '../../plugin'
|
||||
|
||||
export let lead: Employee
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<div class="icon">
|
||||
<Avatar avatar={lead.avatar} size="medium" />
|
||||
</div>
|
||||
<div class="textContainer">
|
||||
<div class="title">
|
||||
<Label label={tracker.string.ProjectLeadTitle} />
|
||||
</div>
|
||||
<div class="description">
|
||||
{lead.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
display: flex;
|
||||
width: 20rem;
|
||||
background-color: var(--board-card-bg-color);
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
line-height: 0;
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.textContainer {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
margin-top: -0.125rem;
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--content-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.description {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
min-width: 0;
|
||||
margin-top: 0.375rem;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,111 @@
|
||||
<!--
|
||||
// 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 contact, { Employee } from '@anticrm/contact'
|
||||
import { Class, Doc, Ref } from '@anticrm/core'
|
||||
import { Project, Team } from '@anticrm/tracker'
|
||||
import { UsersPopup, getClient } from '@anticrm/presentation'
|
||||
import { AttributeModel } from '@anticrm/view'
|
||||
import { eventToHTMLElement, showPopup, Tooltip } from '@anticrm/ui'
|
||||
import { getObjectPresenter } from '@anticrm/view-resources'
|
||||
import { IntlString } from '@anticrm/platform'
|
||||
import tracker from '../../plugin'
|
||||
import LeadPopup from './LeadPopup.svelte'
|
||||
|
||||
export let value: Employee | null
|
||||
export let projectId: Ref<Project>
|
||||
export let defaultClass: Ref<Class<Doc>> | undefined = undefined
|
||||
export let currentSpace: Ref<Team> | undefined = undefined
|
||||
export let isEditable: boolean = true
|
||||
export let shouldShowLabel: boolean = false
|
||||
export let defaultName: IntlString | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let presenter: AttributeModel | undefined
|
||||
|
||||
$: if (value || defaultClass) {
|
||||
if (value) {
|
||||
getObjectPresenter(client, value._class, { key: '' }).then((p) => {
|
||||
presenter = p
|
||||
})
|
||||
} else if (defaultClass) {
|
||||
getObjectPresenter(client, defaultClass, { key: '' }).then((p) => {
|
||||
presenter = p
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleLeadChanged = async (result: Employee | null | undefined) => {
|
||||
if (!isEditable || result === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentProject = await client.findOne(tracker.class.Project, { space: currentSpace, _id: projectId })
|
||||
|
||||
if (currentProject === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const newLead = result === null ? null : result._id
|
||||
|
||||
await client.update(currentProject, { lead: newLead })
|
||||
}
|
||||
|
||||
const handleLeadEditorOpened = async (event: MouseEvent) => {
|
||||
if (!isEditable) {
|
||||
return
|
||||
}
|
||||
showPopup(
|
||||
UsersPopup,
|
||||
{
|
||||
_class: contact.class.Employee,
|
||||
selected: value?._id,
|
||||
allowDeselect: true,
|
||||
placeholder: tracker.string.ProjectLeadSearchPlaceholder
|
||||
},
|
||||
eventToHTMLElement(event),
|
||||
handleLeadChanged
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if value && presenter}
|
||||
<Tooltip component={LeadPopup} props={{ lead: value }}>
|
||||
<svelte:component
|
||||
this={presenter.presenter}
|
||||
{value}
|
||||
{defaultName}
|
||||
avatarSize={'tiny'}
|
||||
isInteractive={true}
|
||||
shouldShowPlaceholder={true}
|
||||
shouldShowName={shouldShowLabel}
|
||||
onEmployeeEdit={handleLeadEditorOpened}
|
||||
tooltipLabels={{ personLabel: tracker.string.AssignedTo, placeholderLabel: tracker.string.AssignTo }}
|
||||
/>
|
||||
</Tooltip>
|
||||
{:else if presenter}
|
||||
<svelte:component
|
||||
this={presenter.presenter}
|
||||
{value}
|
||||
{defaultName}
|
||||
avatarSize={'tiny'}
|
||||
isInteractive={true}
|
||||
shouldShowPlaceholder={true}
|
||||
shouldShowName={shouldShowLabel}
|
||||
onEmployeeEdit={handleLeadEditorOpened}
|
||||
tooltipLabels={{ personLabel: tracker.string.AssignedTo, placeholderLabel: tracker.string.AssignTo }}
|
||||
/>
|
||||
{/if}
|
@ -44,6 +44,14 @@
|
||||
async function onSave () {
|
||||
await client.createDoc(tracker.class.Project, space, object)
|
||||
}
|
||||
|
||||
const handleProjectStatusChanged = (newProjectStatus: ProjectStatus | undefined) => {
|
||||
if (newProjectStatus === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
object.status = newProjectStatus
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card
|
||||
@ -74,12 +82,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div slot="pool" class="flex-row-center text-sm gap-1-5">
|
||||
<ProjectStatusSelector
|
||||
bind:status={object.status}
|
||||
onStatusChange={(newStatus) => {
|
||||
newStatus !== undefined && (object.status = newStatus)
|
||||
}}
|
||||
/>
|
||||
<ProjectStatusSelector selectedProjectStatus={object.status} onProjectStatusChange={handleProjectStatusChanged} />
|
||||
<UserBox
|
||||
_class={contact.class.Employee}
|
||||
label={tracker.string.ProjectLead}
|
||||
|
@ -0,0 +1,72 @@
|
||||
<!--
|
||||
// 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 { Ref } from '@anticrm/core'
|
||||
import { Project } from '@anticrm/tracker'
|
||||
import { Button, showPopup, eventToHTMLElement } from '@anticrm/ui'
|
||||
import type { ButtonKind, ButtonSize } from '@anticrm/ui'
|
||||
import contact, { Employee } from '@anticrm/contact'
|
||||
import { getClient, UsersPopup } from '@anticrm/presentation'
|
||||
import { translate } from '@anticrm/platform'
|
||||
import tracker from '../../plugin'
|
||||
|
||||
export let value: Project
|
||||
export let kind: ButtonKind = 'no-border'
|
||||
export let size: ButtonSize = 'small'
|
||||
export let justify: 'left' | 'center' = 'center'
|
||||
export let width: string | undefined = 'min-content'
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let buttonTitle = ''
|
||||
|
||||
$: translate(tracker.string.ProjectMembersTitle, {}).then((res) => {
|
||||
buttonTitle = res
|
||||
})
|
||||
|
||||
const handleProjectMembersChanged = async (result: Ref<Employee>[] | undefined) => {
|
||||
if (result === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
await client.update(value, { members: result })
|
||||
}
|
||||
|
||||
const handleProjectMembersEditorOpened = async (event: MouseEvent) => {
|
||||
showPopup(
|
||||
UsersPopup,
|
||||
{
|
||||
_class: contact.class.Employee,
|
||||
selectedUsers: value.members,
|
||||
allowDeselect: true,
|
||||
multiSelect: true,
|
||||
placeholder: tracker.string.ProjectMembersSearchPlaceholder
|
||||
},
|
||||
eventToHTMLElement(event),
|
||||
undefined,
|
||||
handleProjectMembersChanged
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Button
|
||||
{kind}
|
||||
{size}
|
||||
{width}
|
||||
{justify}
|
||||
title={buttonTitle}
|
||||
icon={tracker.icon.ProjectMembers}
|
||||
on:click={handleProjectMembersEditorOpened}
|
||||
/>
|
@ -13,61 +13,31 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref, WithLookup } from '@anticrm/core'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { Icon } from '@anticrm/ui'
|
||||
import contact from '@anticrm/contact'
|
||||
import ObjectPresenter from '@anticrm/view-resources/src/components/ObjectPresenter.svelte'
|
||||
import { Project, ProjectStatus, Team } from '@anticrm/tracker'
|
||||
import ProjectStatusSelector from './ProjectStatusSelector.svelte'
|
||||
import tracker from '../../plugin'
|
||||
import { WithLookup } from '@anticrm/core'
|
||||
import { Project } from '@anticrm/tracker'
|
||||
|
||||
export let value: WithLookup<Project>
|
||||
export let space: Ref<Team>
|
||||
|
||||
const client = getClient()
|
||||
const lead = value.$lookup?.lead
|
||||
|
||||
async function updateStatus (status: ProjectStatus) {
|
||||
await client.updateDoc(tracker.class.Project, space, value._id, { status })
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex-presenter">
|
||||
<div class="icon">
|
||||
<Icon icon={value.icon} size="small" />
|
||||
{#if value}
|
||||
<div class="flex-presenter projectPresenterRoot">
|
||||
<span title={value.label} class="projectLabel">{value.label}</span>
|
||||
</div>
|
||||
<span class="label nowrap project-label">{value.label}</span>
|
||||
{#if lead}
|
||||
<div class="lead-container">
|
||||
<ObjectPresenter value={lead} objectId={lead._id} _class={lead._class} props={{ shouldShowName: false }} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="lead-placeholder">
|
||||
<Icon icon={contact.icon.Person} size="large" />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="icon">
|
||||
<ProjectStatusSelector
|
||||
kind="icon"
|
||||
shouldShowLabel={false}
|
||||
status={value.status}
|
||||
onStatusChange={(status) => status !== undefined && updateStatus(status)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.project-label {
|
||||
width: 250px;
|
||||
<style lang="scss">
|
||||
.projectPresenterRoot {
|
||||
max-width: 5rem;
|
||||
}
|
||||
|
||||
.lead-container {
|
||||
padding-bottom: 5px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.lead-placeholder {
|
||||
margin-right: 20px;
|
||||
.projectLabel {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
color: var(--theme-caption-color);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
|
@ -0,0 +1,69 @@
|
||||
<!--
|
||||
// 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 { Project, ProjectStatus } from '@anticrm/tracker'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { Tooltip } from '@anticrm/ui'
|
||||
import type { ButtonKind, ButtonSize } from '@anticrm/ui'
|
||||
import tracker from '../../plugin'
|
||||
|
||||
import ProjectStatusSelector from './ProjectStatusSelector.svelte'
|
||||
|
||||
export let value: Project
|
||||
export let isEditable: boolean = true
|
||||
export let shouldShowLabel: boolean = false
|
||||
export let kind: ButtonKind = 'link'
|
||||
export let size: ButtonSize = 'large'
|
||||
export let justify: 'left' | 'center' = 'left'
|
||||
export let width: string | undefined = '100%'
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const handleProjectStatusChanged = async (newStatus: ProjectStatus | undefined) => {
|
||||
if (!isEditable || newStatus === undefined || value.status === newStatus) {
|
||||
return
|
||||
}
|
||||
|
||||
await client.update(value, { status: newStatus })
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
{#if isEditable}
|
||||
<Tooltip label={tracker.string.SetStatus} fill>
|
||||
<ProjectStatusSelector
|
||||
{kind}
|
||||
{size}
|
||||
{width}
|
||||
{justify}
|
||||
{isEditable}
|
||||
{shouldShowLabel}
|
||||
selectedProjectStatus={value.status}
|
||||
onProjectStatusChange={handleProjectStatusChanged}
|
||||
/>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<ProjectStatusSelector
|
||||
{kind}
|
||||
{size}
|
||||
{width}
|
||||
{justify}
|
||||
{isEditable}
|
||||
{shouldShowLabel}
|
||||
selectedProjectStatus={value.status}
|
||||
onProjectStatusChange={handleProjectStatusChanged}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
@ -13,26 +13,35 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Button, Icon, Label, showPopup, SelectPopup, eventToHTMLElement } from '@anticrm/ui'
|
||||
import { ProjectStatus } from '@anticrm/tracker'
|
||||
import { Button, showPopup, SelectPopup, eventToHTMLElement } from '@anticrm/ui'
|
||||
import type { ButtonKind, ButtonSize } from '@anticrm/ui'
|
||||
import tracker from '../../plugin'
|
||||
import { projectStatuses } from '../../utils'
|
||||
import { defaultProjectStatuses, projectStatusAssets } from '../../utils'
|
||||
|
||||
export let status: ProjectStatus
|
||||
export let kind: 'button' | 'icon' = 'button'
|
||||
export let selectedProjectStatus: ProjectStatus | undefined
|
||||
export let shouldShowLabel: boolean = true
|
||||
export let onStatusChange: ((newStatus: ProjectStatus | undefined) => void) | undefined = undefined
|
||||
export let onProjectStatusChange: ((newProjectStatus: ProjectStatus | undefined) => void) | undefined = undefined
|
||||
export let isEditable: boolean = true
|
||||
|
||||
const statusesInfo = [
|
||||
ProjectStatus.Planned,
|
||||
ProjectStatus.InProgress,
|
||||
ProjectStatus.Paused,
|
||||
ProjectStatus.Completed,
|
||||
ProjectStatus.Canceled
|
||||
].map((s) => ({ id: s, ...projectStatuses[s] }))
|
||||
export let kind: ButtonKind = 'no-border'
|
||||
export let size: ButtonSize = 'small'
|
||||
export let justify: 'left' | 'center' = 'center'
|
||||
export let width: string | undefined = 'min-content'
|
||||
|
||||
const handleStatusEditorOpened = (event: MouseEvent) => {
|
||||
$: selectedStatusIcon = selectedProjectStatus
|
||||
? projectStatusAssets[selectedProjectStatus].icon
|
||||
: tracker.icon.CategoryBacklog
|
||||
|
||||
$: selectedStatusLabel = shouldShowLabel
|
||||
? selectedProjectStatus
|
||||
? projectStatusAssets[selectedProjectStatus].label
|
||||
: tracker.string.Planned
|
||||
: undefined
|
||||
|
||||
$: statusesInfo = defaultProjectStatuses.map((s) => ({ id: s, ...projectStatusAssets[s] }))
|
||||
|
||||
const handleProjectStatusEditorOpened = (event: MouseEvent) => {
|
||||
if (!isEditable) {
|
||||
return
|
||||
}
|
||||
@ -40,42 +49,18 @@
|
||||
SelectPopup,
|
||||
{ value: statusesInfo, placeholder: tracker.string.SetStatus, searchable: true },
|
||||
eventToHTMLElement(event),
|
||||
onStatusChange
|
||||
onProjectStatusChange
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if kind === 'button'}
|
||||
<Button
|
||||
label={shouldShowLabel ? projectStatuses[status].label : undefined}
|
||||
icon={projectStatuses[status].icon}
|
||||
width="min-content"
|
||||
size="small"
|
||||
kind="no-border"
|
||||
on:click={handleStatusEditorOpened}
|
||||
/>
|
||||
{:else if kind === 'icon'}
|
||||
<div class={isEditable ? 'flex-presenter' : 'presenter'} on:click={handleStatusEditorOpened}>
|
||||
<div class="statusIcon">
|
||||
<Icon icon={projectStatuses[status].icon} size="small" />
|
||||
</div>
|
||||
{#if shouldShowLabel}
|
||||
<div class="label nowrap ml-2">
|
||||
<Label label={projectStatuses[status].label} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.presenter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.statusIcon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
</style>
|
||||
<Button
|
||||
icon={selectedStatusIcon}
|
||||
label={selectedStatusLabel}
|
||||
{justify}
|
||||
{width}
|
||||
{size}
|
||||
{kind}
|
||||
disabled={!isEditable}
|
||||
on:click={handleProjectStatusEditorOpened}
|
||||
/>
|
||||
|
@ -13,60 +13,156 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref } from '@anticrm/core'
|
||||
import { Team } from '@anticrm/tracker'
|
||||
import { Button, IconAdd, Label, showPopup } from '@anticrm/ui'
|
||||
import { Table } from '@anticrm/view-resources'
|
||||
import plugin from '../../plugin'
|
||||
import contact from '@anticrm/contact'
|
||||
import { DocumentQuery, FindOptions, Ref, SortingOrder } from '@anticrm/core'
|
||||
import { IntlString } from '@anticrm/platform'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import { Project, Team } from '@anticrm/tracker'
|
||||
import { Button, IconAdd, IconOptions, Label, showPopup } from '@anticrm/ui'
|
||||
|
||||
import tracker from '../../plugin'
|
||||
import NewProject from './NewProject.svelte'
|
||||
import ProjectsListBrowser from './ProjectsListBrowser.svelte'
|
||||
|
||||
export let space: Ref<Team>
|
||||
export let currentSpace: Ref<Team>
|
||||
export let title: IntlString = tracker.string.AllProjects
|
||||
export let query: DocumentQuery<Project> = {}
|
||||
export let search: string = ''
|
||||
|
||||
async function showCreateDialog () {
|
||||
showPopup(NewProject, { space, targetElement: null }, null)
|
||||
const ENTRIES_LIMIT = 200
|
||||
const resultProjectsQuery = createQuery()
|
||||
|
||||
const projectOptions: FindOptions<Project> = {
|
||||
sort: { modifiedOn: SortingOrder.Descending },
|
||||
limit: ENTRIES_LIMIT,
|
||||
lookup: { lead: contact.class.Employee, members: contact.class.Employee }
|
||||
}
|
||||
|
||||
let resultProjects: Project[] = []
|
||||
|
||||
$: baseQuery = {
|
||||
space: currentSpace,
|
||||
...query
|
||||
}
|
||||
|
||||
$: resultQuery = search === '' ? baseQuery : { $search: search, ...baseQuery }
|
||||
|
||||
$: resultProjectsQuery.query<Project>(
|
||||
tracker.class.Project,
|
||||
{ ...resultQuery },
|
||||
(result) => {
|
||||
resultProjects = result
|
||||
},
|
||||
projectOptions
|
||||
)
|
||||
|
||||
const showCreateDialog = async () => {
|
||||
showPopup(NewProject, { space: currentSpace, targetElement: null }, null)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<Label label={plugin.string.Projects} />
|
||||
<div class="fs-title flex-between header">
|
||||
<div class="flex-center">
|
||||
<Label label={tracker.string.Projects} />
|
||||
<div class="projectTitle">
|
||||
› <Label label={title} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<Button icon={IconAdd} label={plugin.string.Project} kind="secondary" on:click={showCreateDialog} />
|
||||
<Button size="small" icon={IconAdd} label={tracker.string.Project} kind="secondary" on:click={showCreateDialog} />
|
||||
</div>
|
||||
<div class="itemsContainer">
|
||||
<div class="flex-center">
|
||||
<div class="flex-center">
|
||||
<div class="buttonWrapper">
|
||||
<Button selected size="small" shape="rectangle-right" label={tracker.string.AllProjects} />
|
||||
</div>
|
||||
<div class="buttonWrapper">
|
||||
<Button size="small" shape="rectangle" label={tracker.string.BacklogProjects} />
|
||||
</div>
|
||||
<div class="buttonWrapper">
|
||||
<Button size="small" shape="rectangle" label={tracker.string.ActiveProjects} />
|
||||
</div>
|
||||
<div class="buttonWrapper">
|
||||
<Button size="small" shape="rectangle-left" label={tracker.string.ClosedProjects} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3 filterButton">
|
||||
<Button
|
||||
size="small"
|
||||
icon={IconAdd}
|
||||
kind={'link-bordered'}
|
||||
borderStyle={'dashed'}
|
||||
label={tracker.string.Filter}
|
||||
on:click={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-center">
|
||||
<div class="flex-center">
|
||||
<div class="buttonWrapper">
|
||||
<Button selected size="small" shape="rectangle-right" icon={tracker.icon.ProjectsList} />
|
||||
</div>
|
||||
<div class="buttonWrapper">
|
||||
<Button size="small" shape="rectangle-left" icon={tracker.icon.ProjectsTimeline} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<Button size="small" icon={IconOptions} on:click={() => {}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
_class={plugin.class.Project}
|
||||
config={[
|
||||
<ProjectsListBrowser
|
||||
_class={tracker.class.Project}
|
||||
itemsConfig={[
|
||||
{ key: '', presenter: tracker.component.IconPresenter },
|
||||
{ key: '', presenter: tracker.component.ProjectPresenter },
|
||||
{
|
||||
key: '',
|
||||
presenter: plugin.component.ProjectPresenter,
|
||||
label: plugin.string.Project,
|
||||
sortingKey: 'name',
|
||||
props: { space }
|
||||
}
|
||||
key: '$lookup.lead',
|
||||
presenter: tracker.component.LeadPresenter,
|
||||
props: { currentSpace, defaultClass: contact.class.Employee, shouldShowLabel: false }
|
||||
},
|
||||
{ key: '', presenter: tracker.component.ProjectMembersPresenter, props: { kind: 'link' } },
|
||||
{ key: '', presenter: tracker.component.TargetDatePresenter },
|
||||
{ key: '', presenter: tracker.component.ProjectStatusPresenter }
|
||||
]}
|
||||
query={{}}
|
||||
projects={resultProjects}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
<style lang="scss">
|
||||
.header {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
min-height: 3.5rem;
|
||||
padding-left: 2.25rem;
|
||||
padding-right: 0.5rem;
|
||||
border-bottom: 1px solid var(--theme-button-border-hovered);
|
||||
}
|
||||
|
||||
.projectTitle {
|
||||
display: flex;
|
||||
margin-left: 0.25rem;
|
||||
color: var(--content-color);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.itemsContainer {
|
||||
display: flex;
|
||||
border-bottom: 0.5px solid #666666;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.65rem 1.35rem 0.65rem 2.25rem;
|
||||
border-bottom: 1px solid var(--theme-button-border-hovered);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
margin-left: 10px;
|
||||
.buttonWrapper {
|
||||
margin-right: 1px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
margin-right: 10px;
|
||||
.filterButton {
|
||||
color: var(--caption-color);
|
||||
}
|
||||
</style>
|
||||
|
@ -0,0 +1,246 @@
|
||||
<!--
|
||||
// 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 contact from '@anticrm/contact'
|
||||
import { Class, Doc, FindOptions, getObjectValue, Ref } from '@anticrm/core'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { Issue, Project } from '@anticrm/tracker'
|
||||
import { CheckBox, Spinner, Tooltip } from '@anticrm/ui'
|
||||
import { AttributeModel, BuildModelKey } from '@anticrm/view'
|
||||
import { buildModel, getObjectPresenter, LoadingProps } from '@anticrm/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let itemsConfig: (BuildModelKey | string)[]
|
||||
export let selectedObjectIds: Doc[] = []
|
||||
export let selectedRowIndex: number | undefined = undefined
|
||||
export let projects: Project[] | undefined = undefined
|
||||
export let loadingProps: LoadingProps | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const client = getClient()
|
||||
const objectRefs: HTMLElement[] = []
|
||||
|
||||
const baseOptions: FindOptions<Issue> = {
|
||||
lookup: {
|
||||
assignee: contact.class.Employee,
|
||||
status: tracker.class.IssueStatus
|
||||
}
|
||||
}
|
||||
|
||||
let personPresenter: AttributeModel
|
||||
|
||||
$: options = { ...baseOptions } as FindOptions<Project>
|
||||
$: selectedObjectIdsSet = new Set<Ref<Doc>>(selectedObjectIds.map((it) => it._id))
|
||||
$: objectRefs.length = projects?.length ?? 0
|
||||
|
||||
$: getObjectPresenter(client, contact.class.Person, { key: '' }).then((p) => {
|
||||
personPresenter = p
|
||||
})
|
||||
|
||||
export const onObjectChecked = (docs: Doc[], value: boolean) => {
|
||||
dispatch('check', { docs, value })
|
||||
}
|
||||
|
||||
const handleRowFocused = (object: Doc) => {
|
||||
dispatch('row-focus', object)
|
||||
}
|
||||
|
||||
export const onElementSelected = (offset: 1 | -1 | 0, docObject?: Doc) => {
|
||||
if (!projects) {
|
||||
return
|
||||
}
|
||||
|
||||
let position =
|
||||
(docObject !== undefined ? projects?.findIndex((x) => x._id === docObject?._id) : selectedRowIndex) ?? -1
|
||||
|
||||
position += offset
|
||||
|
||||
if (position < 0) {
|
||||
position = 0
|
||||
}
|
||||
|
||||
if (position >= projects.length) {
|
||||
position = projects.length - 1
|
||||
}
|
||||
|
||||
const objectRef = objectRefs[position]
|
||||
|
||||
selectedRowIndex = position
|
||||
|
||||
handleRowFocused(projects[position])
|
||||
|
||||
if (objectRef) {
|
||||
objectRef.scrollIntoView({ behavior: 'auto', block: 'nearest' })
|
||||
}
|
||||
}
|
||||
|
||||
const getLoadingElementsLength = (props: LoadingProps, options?: FindOptions<Doc>) => {
|
||||
if (options?.limit && options?.limit > 0) {
|
||||
return Math.min(options.limit, props.length)
|
||||
}
|
||||
|
||||
return props.length
|
||||
}
|
||||
</script>
|
||||
|
||||
{#await buildModel({ client, _class, keys: itemsConfig, lookup: options.lookup }) then itemModels}
|
||||
<div class="listRoot">
|
||||
{#if projects}
|
||||
{#each projects as docObject (docObject._id)}
|
||||
<div
|
||||
bind:this={objectRefs[projects.findIndex((x) => x === docObject)]}
|
||||
class="listGrid"
|
||||
class:mListGridChecked={selectedObjectIdsSet.has(docObject._id)}
|
||||
class:mListGridFixed={selectedRowIndex === projects.findIndex((x) => x === docObject)}
|
||||
class:mListGridSelected={selectedRowIndex === projects.findIndex((x) => x === docObject)}
|
||||
on:focus={() => {}}
|
||||
on:mouseover={() => handleRowFocused(docObject)}
|
||||
>
|
||||
<div class="contentWrapper">
|
||||
{#each itemModels as attributeModel, attributeModelIndex}
|
||||
{#if attributeModelIndex === 0}
|
||||
<div class="gridElement">
|
||||
<Tooltip direction={'bottom'} label={tracker.string.SelectIssue}>
|
||||
<div class="eListGridCheckBox">
|
||||
<CheckBox
|
||||
checked={selectedObjectIdsSet.has(docObject._id)}
|
||||
on:value={(event) => {
|
||||
onObjectChecked([docObject], event.detail)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div class="iconPresenter">
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||
{...attributeModel.props}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if attributeModelIndex === 1}
|
||||
<div class="projectPresenter">
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||
{...attributeModel.props}
|
||||
/>
|
||||
</div>
|
||||
<div class="filler" />
|
||||
{:else}
|
||||
<div class="gridElement">
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||
issueId={docObject._id}
|
||||
{...attributeModel.props}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{:else if loadingProps !== undefined}
|
||||
{#each Array(getLoadingElementsLength(loadingProps, options)) as _, rowIndex}
|
||||
<div class="listGrid" class:fixed={rowIndex === selectedRowIndex}>
|
||||
<div class="contentWrapper">
|
||||
<div class="gridElement">
|
||||
<CheckBox checked={false} />
|
||||
<div class="ml-4">
|
||||
<Spinner size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/await}
|
||||
|
||||
<style lang="scss">
|
||||
.listRoot {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.contentWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 1.15rem;
|
||||
}
|
||||
|
||||
.listGrid {
|
||||
width: 100%;
|
||||
height: 3.25rem;
|
||||
color: var(--theme-caption-color);
|
||||
border-bottom: 1px solid var(--theme-button-border-hovered);
|
||||
|
||||
&.mListGridChecked {
|
||||
background-color: var(--theme-table-bg-hover);
|
||||
|
||||
.eListGridCheckBox {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.mListGridSelected {
|
||||
background-color: var(--menu-bg-select);
|
||||
}
|
||||
|
||||
.eListGridCheckBox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filler {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.gridElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
margin-left: 0.5rem;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.iconPresenter {
|
||||
padding-left: 0.45rem;
|
||||
}
|
||||
|
||||
.projectPresenter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
width: 5.5rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,72 @@
|
||||
<!--
|
||||
// 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 type { Class, Doc, Ref } from '@anticrm/core'
|
||||
import { BuildModelKey } from '@anticrm/view'
|
||||
import {
|
||||
ActionContext,
|
||||
focusStore,
|
||||
ListSelectionProvider,
|
||||
SelectDirection,
|
||||
selectionStore,
|
||||
LoadingProps
|
||||
} from '@anticrm/view-resources'
|
||||
import { Project } from '@anticrm/tracker'
|
||||
import { onMount } from 'svelte'
|
||||
import ProjectsList from './ProjectsList.svelte'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let itemsConfig: (BuildModelKey | string)[]
|
||||
export let loadingProps: LoadingProps | undefined = undefined
|
||||
export let projects: Project[] = []
|
||||
|
||||
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
|
||||
if (dir === 'vertical') {
|
||||
projectsList.onElementSelected(offset, of)
|
||||
}
|
||||
})
|
||||
|
||||
let projectsList: ProjectsList
|
||||
|
||||
$: if (projectsList !== undefined) {
|
||||
listProvider.update(projects)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
;(document.activeElement as HTMLElement)?.blur()
|
||||
})
|
||||
</script>
|
||||
|
||||
<ActionContext
|
||||
context={{
|
||||
mode: 'browser'
|
||||
}}
|
||||
/>
|
||||
|
||||
<ProjectsList
|
||||
bind:this={projectsList}
|
||||
{_class}
|
||||
{itemsConfig}
|
||||
{loadingProps}
|
||||
{projects}
|
||||
selectedObjectIds={$selectionStore ?? []}
|
||||
selectedRowIndex={listProvider.current($focusStore)}
|
||||
on:row-focus={(event) => {
|
||||
listProvider.updateFocus(event.detail ?? undefined)
|
||||
}}
|
||||
on:check={(event) => {
|
||||
listProvider.updateSelection(event.detail.docs, event.detail.value)
|
||||
}}
|
||||
/>
|
@ -0,0 +1,31 @@
|
||||
<!--
|
||||
// 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 { Project } from '@anticrm/tracker'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import CommonTrackerDatePresenter from '../CommonTrackerDatePresenter.svelte'
|
||||
|
||||
export let value: Project
|
||||
|
||||
const client = getClient()
|
||||
|
||||
$: dueDateMs = value.targetDate
|
||||
|
||||
const handleDueDateChanged = async (newDate: number | null) => {
|
||||
await client.update(value, { targetDate: newDate })
|
||||
}
|
||||
</script>
|
||||
|
||||
<CommonTrackerDatePresenter dateMs={dueDateMs} shouldRender={true} onDateChange={handleDueDateChanged} />
|
@ -37,6 +37,11 @@ import StatusEditor from './components/issues/StatusEditor.svelte'
|
||||
import DueDatePresenter from './components/issues/DueDatePresenter.svelte'
|
||||
import AssigneePresenter from './components/issues/AssigneePresenter.svelte'
|
||||
import ViewOptionsPopup from './components/issues/ViewOptionsPopup.svelte'
|
||||
import IconPresenter from './components/projects/IconPresenter.svelte'
|
||||
import LeadPresenter from './components/projects/LeadPresenter.svelte'
|
||||
import TargetDatePresenter from './components/projects/TargetDatePresenter.svelte'
|
||||
import ProjectMembersPresenter from './components/projects/ProjectMembersPresenter.svelte'
|
||||
import ProjectStatusPresenter from './components/projects/ProjectStatusPresenter.svelte'
|
||||
|
||||
import ModificationDatePresenter from './components/issues/ModificationDatePresenter.svelte'
|
||||
import EditIssue from './components/issues/edit/EditIssue.svelte'
|
||||
@ -67,6 +72,11 @@ export default async (): Promise<Resources> => ({
|
||||
DueDatePresenter,
|
||||
EditIssue,
|
||||
NewIssueHeader,
|
||||
ViewOptionsPopup
|
||||
ViewOptionsPopup,
|
||||
IconPresenter,
|
||||
LeadPresenter,
|
||||
TargetDatePresenter,
|
||||
ProjectMembersPresenter,
|
||||
ProjectStatusPresenter
|
||||
}
|
||||
})
|
||||
|
@ -36,6 +36,10 @@ export default mergeIds(trackerId, tracker, {
|
||||
Board: '' as IntlString,
|
||||
Project: '' as IntlString,
|
||||
Projects: '' as IntlString,
|
||||
AllProjects: '' as IntlString,
|
||||
BacklogProjects: '' as IntlString,
|
||||
ActiveProjects: '' as IntlString,
|
||||
ClosedProjects: '' as IntlString,
|
||||
NewProject: '' as IntlString,
|
||||
CreateProject: '' as IntlString,
|
||||
ProjectNamePlaceholder: '' as IntlString,
|
||||
@ -119,6 +123,10 @@ export default mergeIds(trackerId, tracker, {
|
||||
AddToProject: '' as IntlString,
|
||||
MoveToProject: '' as IntlString,
|
||||
NoProject: '' as IntlString,
|
||||
ProjectLeadTitle: '' as IntlString,
|
||||
ProjectMembersTitle: '' as IntlString,
|
||||
ProjectLeadSearchPlaceholder: '' as IntlString,
|
||||
ProjectMembersSearchPlaceholder: '' as IntlString,
|
||||
|
||||
IssueTitlePlaceholder: '' as IntlString,
|
||||
IssueDescriptionPlaceholder: '' as IntlString,
|
||||
@ -160,6 +168,11 @@ export default mergeIds(trackerId, tracker, {
|
||||
DueDatePresenter: '' as AnyComponent,
|
||||
EditIssue: '' as AnyComponent,
|
||||
CreateTeam: '' as AnyComponent,
|
||||
NewIssueHeader: '' as AnyComponent
|
||||
NewIssueHeader: '' as AnyComponent,
|
||||
IconPresenter: '' as AnyComponent,
|
||||
LeadPresenter: '' as AnyComponent,
|
||||
TargetDatePresenter: '' as AnyComponent,
|
||||
ProjectMembersPresenter: '' as AnyComponent,
|
||||
ProjectStatusPresenter: '' as AnyComponent
|
||||
}
|
||||
})
|
||||
|
@ -114,8 +114,16 @@ export const getIssuesModificationDatePeriodTime = (period: IssuesDateModificati
|
||||
}
|
||||
}
|
||||
|
||||
export const defaultProjectStatuses = [
|
||||
ProjectStatus.Planned,
|
||||
ProjectStatus.InProgress,
|
||||
ProjectStatus.Paused,
|
||||
ProjectStatus.Completed,
|
||||
ProjectStatus.Canceled
|
||||
]
|
||||
|
||||
// TODO: update icons
|
||||
export const projectStatuses: Record<ProjectStatus, { icon: Asset, label: IntlString }> = {
|
||||
export const projectStatusAssets: Record<ProjectStatus, { icon: Asset, label: IntlString }> = {
|
||||
[ProjectStatus.Planned]: { icon: tracker.icon.CategoryBacklog, label: tracker.string.Planned },
|
||||
[ProjectStatus.InProgress]: { icon: tracker.icon.CategoryStarted, label: tracker.string.InProgress },
|
||||
[ProjectStatus.Paused]: { icon: tracker.icon.CategoryUnstarted, label: tracker.string.Paused },
|
||||
|
@ -226,6 +226,10 @@ export default plugin(trackerId, {
|
||||
PriorityUrgent: '' as Asset,
|
||||
PriorityHigh: '' as Asset,
|
||||
PriorityMedium: '' as Asset,
|
||||
PriorityLow: '' as Asset
|
||||
PriorityLow: '' as Asset,
|
||||
|
||||
ProjectsList: '' as Asset,
|
||||
ProjectsTimeline: '' as Asset,
|
||||
ProjectMembers: '' as Asset
|
||||
}
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user