TSK-810 Rename Team -> Project, Project -> Component (#2756)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov
2023-03-17 12:17:53 +06:00
committed by GitHub
parent 26adf14966
commit 0f24fe80d6
110 changed files with 1646 additions and 1324 deletions
@@ -0,0 +1,227 @@
<!--
// 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 '@hcengineering/contact'
import { AccountArrayEditor } from '@hcengineering/contact-resources'
import core, { Account, generateId, getCurrentAccount, Ref, SortingOrder } from '@hcengineering/core'
import { Asset } from '@hcengineering/platform'
import presentation, { AssigneeBox, Card, getClient } from '@hcengineering/presentation'
import { StyledTextBox } from '@hcengineering/text-editor'
import { genRanks, IssueStatus, Project, TimeReportDayType, WorkDayLength } from '@hcengineering/tracker'
import {
Button,
DropdownIntlItem,
DropdownLabelsIntl,
EditBox,
eventToHTMLElement,
Label,
showPopup,
ToggleWithLabel
} from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import tracker from '../../plugin'
import TimeReportDayDropdown from '../issues/timereport/TimeReportDayDropdown.svelte'
import ProjectIconChooser from './ProjectIconChooser.svelte'
export let project: Project | undefined = undefined
let name: string = project?.name ?? ''
let description: string = project?.description ?? ''
let isPrivate: boolean = project?.private ?? false
let icon: Asset | undefined = project?.icon ?? undefined
let selectedWorkDayType: TimeReportDayType | undefined =
project?.defaultTimeReportDay ?? TimeReportDayType.PreviousWorkDay
let selectedWorkDayLength: WorkDayLength | undefined = project?.workDayLength ?? WorkDayLength.EIGHT_HOURS
let defaultAssignee: Ref<Employee> | null | undefined = null
let members: Ref<Account>[] = project?.members ?? [getCurrentAccount()._id]
const dispatch = createEventDispatcher()
const client = getClient()
const workDayLengthItems: DropdownIntlItem[] = [
{
id: WorkDayLength.SEVEN_HOURS,
label: tracker.string.SevenHoursLength
},
{
id: WorkDayLength.EIGHT_HOURS,
label: tracker.string.EightHoursLength
}
]
$: isNew = !project
async function handleSave () {
isNew ? createProject() : updateProject()
}
let identifier: string = 'TSK'
const defaultStatusId: Ref<IssueStatus> = generateId()
function getProjectData () {
return {
name,
description,
private: isPrivate,
members,
archived: false,
identifier,
sequence: 0,
issueStatuses: 0,
defaultIssueStatus: defaultStatusId,
defaultAssignee: defaultAssignee ?? undefined,
icon,
defaultTimeReportDay: selectedWorkDayType ?? TimeReportDayType.PreviousWorkDay,
workDayLength: selectedWorkDayLength ?? WorkDayLength.EIGHT_HOURS
}
}
async function updateProject () {
const { sequence, issueStatuses, defaultIssueStatus, identifier, ...projectData } = getProjectData()
await client.update(project!, projectData)
}
async function createProject () {
const id = await client.createDoc(tracker.class.Project, core.space.Space, getProjectData())
await createProjectIssueStatuses(id, defaultStatusId)
}
async function createProjectIssueStatuses (
projectId: Ref<Project>,
defaultStatusId: Ref<IssueStatus>,
defaultCategoryId = tracker.issueStatusCategory.Backlog
): Promise<void> {
const categories = await client.findAll(
tracker.class.IssueStatusCategory,
{},
{ sort: { order: SortingOrder.Ascending } }
)
const issueStatusRanks = [...genRanks(categories.length)]
for (const [i, statusCategory] of categories.entries()) {
const { _id: category, defaultStatusName } = statusCategory
const rank = issueStatusRanks[i]
await client.addCollection(
tracker.class.IssueStatus,
projectId,
projectId,
tracker.class.Project,
'issueStatuses',
{ name: defaultStatusName, category, rank },
category === defaultCategoryId ? defaultStatusId : undefined
)
}
}
function chooseIcon (ev: MouseEvent) {
showPopup(ProjectIconChooser, { icon }, eventToHTMLElement(ev), (result) => {
if (result !== undefined && result !== null) {
icon = result
}
})
}
</script>
<Card
label={isNew ? tracker.string.NewProject : tracker.string.EditProject}
okLabel={isNew ? presentation.string.Create : presentation.string.Save}
okAction={handleSave}
canSave={name.length > 0 && !!selectedWorkDayType && !!selectedWorkDayLength}
on:close={() => {
dispatch('close')
}}
>
<div class="flex-row-center flex-between">
<EditBox
bind:value={name}
placeholder={tracker.string.ProjectTitlePlaceholder}
kind={'large-style'}
focus
on:input={() => {
identifier = name.toLocaleUpperCase().replaceAll(' ', '_').substring(0, 5)
}}
/>
<EditBox
bind:value={identifier}
disabled={!isNew}
placeholder={tracker.string.ProjectIdentifierPlaceholder}
kind={'large-style'}
/>
</div>
<StyledTextBox
alwaysEdit
showButtons={false}
bind:content={description}
placeholder={tracker.string.IssueDescriptionPlaceholder}
/>
<ToggleWithLabel
label={presentation.string.MakePrivate}
description={presentation.string.MakePrivateDescription}
bind:on={isPrivate}
/>
<div class="flex-between">
<div class="caption">
<Label label={tracker.string.ChooseIcon} />
</div>
<Button icon={icon ?? tracker.icon.Home} kind="no-border" size="medium" on:click={chooseIcon} />
</div>
<div class="flex-between">
<div class="caption">
<Label label={tracker.string.DefaultTimeReportDay} />
</div>
<TimeReportDayDropdown bind:selected={selectedWorkDayType} label={tracker.string.DefaultTimeReportDay} />
</div>
<div class="flex-between">
<div class="caption">
<Label label={tracker.string.WorkDayLength} />
</div>
<DropdownLabelsIntl
kind="link-bordered"
label={tracker.string.WorkDayLength}
items={workDayLengthItems}
shouldUpdateUndefined={false}
bind:selected={selectedWorkDayLength}
/>
</div>
<div class="flex-between">
<div class="caption">
<Label label={tracker.string.Members} />
</div>
<AccountArrayEditor
value={members}
label={tracker.string.Members}
onChange={(refs) => (members = refs)}
kind="link-bordered"
/>
</div>
<div class="flex-between">
<div class="caption">
<Label label={tracker.string.DefaultAssignee} />
</div>
<AssigneeBox
label={tracker.string.Assignee}
placeholder={tracker.string.Assignee}
kind="link-bordered"
bind:value={defaultAssignee}
titleDeselect={tracker.string.Unassigned}
showTooltip={{ label: tracker.string.DefaultAssignee }}
/>
</div>
</Card>
@@ -1,57 +0,0 @@
<!--
// Copyright © 2023 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 view from '@hcengineering/view'
import { Button, ButtonSize, LabelAndProps, showPopup } from '@hcengineering/ui'
import { getClient, MessageBox } from '@hcengineering/presentation'
import type { Project } from '@hcengineering/tracker'
import tracker from '../../plugin'
import { Ref, Space } from '@hcengineering/core'
import { createEventDispatcher } from 'svelte'
export let space: Ref<Space>
export let value: Project
export let size: ButtonSize = 'medium'
export let justify: 'left' | 'center' = 'center'
export let width: string | undefined = 'min-content'
export let showTooltip: LabelAndProps | undefined = undefined
const client = getClient()
const dispatch = createEventDispatcher()
async function showConfirmationDialog () {
showPopup(
MessageBox,
{
label: tracker.string.RemoveProjectDialogClose,
message: tracker.string.RemoveProjectDialogCloseNote
},
'top',
(result?: boolean) => {
if (result === true) {
dispatch('close')
removeProject()
}
}
)
}
async function removeProject () {
await client.removeDoc(tracker.class.Project, space, value._id)
}
</script>
{#if value}
<Button {size} {width} {justify} {showTooltip} icon={view.icon.Delete} on:click={() => showConfirmationDialog()} />
{/if}
@@ -1,63 +0,0 @@
<script lang="ts">
import { getClient } from '@hcengineering/presentation'
import { StyledTextBox } from '@hcengineering/text-editor'
import { Project } from '@hcengineering/tracker'
import { Button, EditBox, Icon, showPopup } from '@hcengineering/ui'
import { DocAttributeBar } from '@hcengineering/view-resources'
import { createEventDispatcher, onDestroy } from 'svelte'
import { activeProject } from '../../issues'
import tracker from '../../plugin'
import IssuesView from '../issues/IssuesView.svelte'
import ProjectPopup from './ProjectPopup.svelte'
export let project: Project
const client = getClient()
const dispatch = createEventDispatcher()
async function change (field: string, value: any) {
await client.update(project, { [field]: value })
}
function selectProject (evt: MouseEvent): void {
showPopup(ProjectPopup, { _class: tracker.class.Project }, evt.target as HTMLElement, (value) => {
if (value != null) {
project = value
dispatch('project', project._id)
}
})
}
$: $activeProject = project?._id
onDestroy(() => {
$activeProject = undefined
})
</script>
<IssuesView query={{ project: project._id, space: project.space }} space={project.space} label={project.label}>
<svelte:fragment slot="label_selector">
<Button size={'small'} kind={'link'} on:click={selectProject}>
<svelte:fragment slot="content">
<div class="ac-header__icon"><Icon icon={tracker.icon.Issues} size={'small'} /></div>
<span class="ac-header__title">{project.label}</span>
</svelte:fragment>
</Button>
</svelte:fragment>
<svelte:fragment slot="aside">
<div class="flex-row p-4 w-60 left-divider">
<div class="fs-title text-xl">
<EditBox bind:value={project.label} on:change={() => change('label', project.label)} />
</div>
<div class="mt-2">
<StyledTextBox
alwaysEdit={true}
showButtons={false}
placeholder={tracker.string.Description}
content={project.description ?? ''}
on:value={(evt) => change('description', evt.detail)}
/>
</div>
<DocAttributeBar object={project} mixins={[]} ignoreKeys={['icon', 'label', 'description']} />
</div>
</svelte:fragment>
</IssuesView>
@@ -1,23 +0,0 @@
<!--
// 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 '@hcengineering/core'
import { Project } from '@hcengineering/tracker'
import { Button } from '@hcengineering/ui'
export let value: WithLookup<Project>
</script>
<Button size="small" kind="link" icon={value.icon} />
@@ -1,71 +0,0 @@
<!--
// 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 '@hcengineering/contact'
import { Avatar } from '@hcengineering/presentation'
import { Label } from '@hcengineering/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>
@@ -1,113 +0,0 @@
<!--
// 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 '@hcengineering/contact'
import { Class, Doc, Ref } from '@hcengineering/core'
import { Project, Sprint } from '@hcengineering/tracker'
import { UsersPopup, getClient } from '@hcengineering/presentation'
import { AttributeModel } from '@hcengineering/view'
import { eventToHTMLElement, IconSize, showPopup } from '@hcengineering/ui'
import { getObjectPresenter } from '@hcengineering/view-resources'
import { IntlString } from '@hcengineering/platform'
import tracker from '../../plugin'
import LeadPopup from './LeadPopup.svelte'
export let value: Employee | null
export let _class: Ref<Class<Project | Sprint>>
export let size: IconSize = 'x-small'
export let parentId: Ref<Doc>
export let defaultClass: Ref<Class<Doc>> | 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 currentParent = await client.findOne(_class, { _id: parentId as Ref<Project> })
if (currentParent === undefined) {
return
}
const newLead = result === null ? null : result._id
await client.update(currentParent, { lead: newLead })
}
const handleLeadEditorOpened = async (event: MouseEvent) => {
if (!isEditable) {
return
}
showPopup(
UsersPopup,
{
_class: contact.class.Employee,
selected: value?._id,
docQuery: {
active: true
},
allowDeselect: true,
placeholder: tracker.string.ProjectLeadSearchPlaceholder
},
eventToHTMLElement(event),
handleLeadChanged
)
}
</script>
{#if value && presenter}
<svelte:component
this={presenter.presenter}
{value}
{defaultName}
avatarSize={size}
isInteractive={true}
shouldShowPlaceholder={true}
shouldShowName={shouldShowLabel}
onEmployeeEdit={handleLeadEditorOpened}
tooltipLabels={{ component: LeadPopup, props: { lead: value } }}
/>
{:else if presenter}
<svelte:component
this={presenter.presenter}
{value}
{defaultName}
avatarSize={size}
isInteractive={true}
shouldShowPlaceholder={true}
shouldShowName={shouldShowLabel}
onEmployeeEdit={handleLeadEditorOpened}
tooltipLabels={{ personLabel: tracker.string.AssignedTo, placeholderLabel: tracker.string.AssignTo }}
/>
{/if}
@@ -1,87 +0,0 @@
<!--
// 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 { Data, Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { Card, getClient, SpaceSelector, EmployeeBox, UserBoxList } from '@hcengineering/presentation'
import { Project, ProjectStatus, Team } from '@hcengineering/tracker'
import { DatePresenter, EditBox } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import tracker from '../../plugin'
import ProjectStatusSelector from './ProjectStatusSelector.svelte'
import { StyledTextArea } from '@hcengineering/text-editor'
export let space: Ref<Team>
const dispatch = createEventDispatcher()
const client = getClient()
const object: Data<Project> = {
label: '' as IntlString,
description: '',
icon: tracker.icon.Projects,
status: ProjectStatus.Backlog,
lead: null,
members: [],
comments: 0,
attachments: 0,
startDate: null,
targetDate: null
}
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
label={tracker.string.NewProject}
okAction={onSave}
canSave={object.label !== ''}
okLabel={tracker.string.CreateProject}
on:close={() => dispatch('close')}
>
<svelte:fragment slot="header">
<SpaceSelector _class={tracker.class.Team} label={tracker.string.Team} bind:space />
</svelte:fragment>
<EditBox bind:value={object.label} placeholder={tracker.string.ProjectNamePlaceholder} kind={'large-style'} focus />
<StyledTextArea
bind:content={object.description}
placeholder={tracker.string.ProjectDescriptionPlaceholder}
emphasized
/>
<svelte:fragment slot="pool">
<ProjectStatusSelector selectedProjectStatus={object.status} onProjectStatusChange={handleProjectStatusChanged} />
<EmployeeBox
label={tracker.string.ProjectLead}
placeholder={tracker.string.AssignTo}
bind:value={object.lead}
allowDeselect
titleDeselect={tracker.string.Unassigned}
showNavigate={false}
/>
<UserBoxList bind:items={object.members} label={tracker.string.ProjectMembersSearchPlaceholder} />
<!-- TODO: add labels after customize IssueNeedsToBeCompletedByThisDate -->
<DatePresenter bind:value={object.startDate} labelNull={tracker.string.StartDate} editable />
<DatePresenter bind:value={object.targetDate} labelNull={tracker.string.TargetDate} editable />
</svelte:fragment>
</Card>
@@ -1,187 +0,0 @@
<!--
// 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 '@hcengineering/contact'
import { DocumentQuery, FindOptions, SortingOrder } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import { Project } from '@hcengineering/tracker'
import { Button, IconAdd, Label, showPopup, TabList } from '@hcengineering/ui'
import type { TabItem } from '@hcengineering/ui'
import tracker from '../../plugin'
import view from '@hcengineering/view'
import { getIncludedProjectStatuses, projectsTitleMap, ProjectsViewMode } from '../../utils'
import NewProject from './NewProject.svelte'
import ProjectsListBrowser from './ProjectsListBrowser.svelte'
export let label: IntlString
export let query: DocumentQuery<Project> = {}
export let search: string = ''
export let mode: ProjectsViewMode = 'all'
export let viewMode: 'list' | 'timeline' = 'list'
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[] = []
$: includedProjectStatuses = getIncludedProjectStatuses(mode)
$: title = projectsTitleMap[mode]
$: includedProjectsQuery = { status: { $in: includedProjectStatuses } }
$: baseQuery = {
...includedProjectsQuery,
...query
}
$: resultQuery = search === '' ? baseQuery : { $search: search, ...baseQuery }
$: resultProjectsQuery.query<Project>(
tracker.class.Project,
{ ...resultQuery },
(result) => {
resultProjects = result
},
projectOptions
)
const space = typeof query.space === 'string' ? query.space : tracker.team.DefaultTeam
const showCreateDialog = async () => {
showPopup(NewProject, { space, targetElement: null }, 'top')
}
const handleViewModeChanged = (newMode: ProjectsViewMode) => {
if (newMode === undefined || newMode === mode) {
return
}
mode = newMode
}
const modeList: TabItem[] = [
{ id: 'all', labelIntl: tracker.string.AllProjects, action: () => handleViewModeChanged('all') },
{ id: 'backlog', labelIntl: tracker.string.BacklogProjects, action: () => handleViewModeChanged('backlog') },
{ id: 'active', labelIntl: tracker.string.ActiveProjects, action: () => handleViewModeChanged('active') },
{ id: 'closed', labelIntl: tracker.string.ClosedProjects, action: () => handleViewModeChanged('closed') }
]
const viewList: TabItem[] = [
{ id: 'list', icon: view.icon.List, tooltip: view.string.List },
{ id: 'timeline', icon: view.icon.Timeline, tooltip: view.string.Timeline }
]
const retrieveMembers = (p: Project) => p.members
</script>
<div class="fs-title flex-between header">
<div class="flex-center">
<Label {label} />
<div class="projectTitle">
<Label label={title} />
</div>
</div>
<Button size="small" icon={IconAdd} label={tracker.string.Project} kind={'primary'} on:click={showCreateDialog} />
</div>
<div class="itemsContainer">
<div class="flex-row-center">
<TabList
items={modeList}
selected={mode}
kind={'normal'}
on:select={(result) => {
if (result.detail !== undefined && result.detail.action) result.detail.action()
}}
/>
<!-- <div class="ml-3 filterButton">
<Button
size="small"
icon={IconAdd}
kind={'link-bordered'}
borderStyle={'dashed'}
label={tracker.string.Filter}
on:click={() => {}}
/>
</div> -->
</div>
<TabList
items={viewList}
selected={viewMode}
kind={'secondary'}
size={'small'}
on:select={(result) => {
if (result.detail !== undefined && result.detail.id !== viewMode) viewMode = result.detail.id
}}
/>
</div>
<ProjectsListBrowser
_class={tracker.class.Project}
itemsConfig={[
{ key: '', presenter: tracker.component.IconPresenter },
{ key: '', presenter: tracker.component.ProjectPresenter, props: { kind: 'list' } },
{
key: '$lookup.lead',
presenter: tracker.component.LeadPresenter,
props: { _class: tracker.class.Project, defaultClass: contact.class.Employee, shouldShowLabel: false }
},
{
key: '',
presenter: contact.component.MembersPresenter,
props: {
kind: 'link',
intlTitle: tracker.string.ProjectMembersTitle,
intlSearchPh: tracker.string.ProjectMembersSearchPlaceholder,
retrieveMembers
}
},
{ key: '', presenter: tracker.component.TargetDatePresenter },
{ key: '', presenter: tracker.component.ProjectStatusPresenter },
{ key: '', presenter: tracker.component.DeleteProjectPresenter, props: { space } }
]}
projects={resultProjects}
{viewMode}
/>
<style lang="scss">
.header {
padding: 0.5rem 0.75rem 0.5rem 2.25rem;
}
.projectTitle {
display: flex;
margin-left: 0.25rem;
color: var(--content-color);
font-size: 0.8125rem;
font-weight: 500;
}
.itemsContainer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.65rem 0.75rem 0.65rem 2.25rem;
background-color: var(--board-bg-color);
border-top: 1px solid var(--divider-color);
border-bottom: 1px solid var(--divider-color);
}
// .filterButton {
// color: var(--caption-color);
// }
</style>
@@ -1,76 +0,0 @@
<!--
// 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 '@hcengineering/core'
import { Issue, IssueTemplate, Project } from '@hcengineering/tracker'
import { getClient } from '@hcengineering/presentation'
import { ButtonKind, ButtonShape, ButtonSize, tooltip } from '@hcengineering/ui'
import { IntlString } from '@hcengineering/platform'
import tracker from '../../plugin'
import ProjectSelector from '../ProjectSelector.svelte'
import { activeProject } from '../../issues'
export let value: Issue | IssueTemplate
export let isEditable: boolean = true
export let shouldShowLabel: boolean = true
export let popupPlaceholder: IntlString = tracker.string.MoveToProject
export let shouldShowPlaceholder = true
export let kind: ButtonKind = 'link'
export let size: ButtonSize = 'large'
export let shape: ButtonShape = undefined
export let justify: 'left' | 'center' = 'left'
export let width: string | undefined = '100%'
export let onlyIcon: boolean = false
export let groupBy: string | undefined = undefined
export let enlargedText = false
const client = getClient()
const handleProjectIdChanged = async (newProjectId: Ref<Project> | null | undefined) => {
if (!isEditable || newProjectId === undefined || value.project === newProjectId) {
return
}
await client.update(value, { project: newProjectId })
}
</script>
{#if (value.project && value.project !== $activeProject && groupBy !== 'project') || shouldShowPlaceholder}
<div
class:minus-margin={kind === 'list-header'}
use:tooltip={{ label: value.project ? tracker.string.MoveToProject : tracker.string.AddToProject }}
>
<ProjectSelector
{kind}
{size}
{shape}
{width}
{justify}
{isEditable}
{shouldShowLabel}
{popupPlaceholder}
{onlyIcon}
{enlargedText}
value={value.project}
onChange={handleProjectIdChanged}
/>
</div>
{/if}
<style lang="scss">
.minus-margin {
margin-left: -0.5rem;
}
</style>
@@ -0,0 +1,41 @@
<script lang="ts">
import { Metadata } from '@hcengineering/platform'
import presentation, { Card } from '@hcengineering/presentation'
import { Button } from '@hcengineering/ui'
import tracker from '../../plugin'
import { createEventDispatcher } from 'svelte'
export let icon: Metadata<string> | undefined = undefined
const dispatch = createEventDispatcher()
const icons = [tracker.icon.Home, tracker.icon.RedCircle]
function save () {
dispatch('close', icon)
}
</script>
<Card
label={tracker.string.ChooseIcon}
okLabel={presentation.string.Save}
okAction={save}
canSave={icon !== undefined}
on:close={() => {
dispatch('close')
}}
>
<div class="float-left-box">
{#each icons as obj}
<div class="float-left p-2">
<Button
icon={obj}
size="medium"
kind={obj === icon ? 'primary' : 'transparent'}
on:click={() => {
icon = obj
}}
/>
</div>
{/each}
</div>
</Card>
@@ -1,51 +0,0 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
// 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, DocumentQuery, Ref } from '@hcengineering/core'
import { ObjectCreate, ObjectPopup } from '@hcengineering/presentation'
import { Project } from '@hcengineering/tracker'
import ProjectTitlePresenter from './ProjectTitlePresenter.svelte'
export let _class: Ref<Class<Project>>
export let selected: Ref<Project> | undefined
export let sprintQuery: DocumentQuery<Project> = {}
export let create: ObjectCreate | undefined = undefined
export let allowDeselect = false
$: _create =
create !== undefined
? {
...create,
update: (doc: Doc) => (doc as Project).label
}
: undefined
</script>
<ObjectPopup
{_class}
{selected}
bind:docQuery={sprintQuery}
searchField={'label'}
multiSelect={false}
{allowDeselect}
shadows={true}
create={_create}
on:update
on:close
>
<svelte:fragment slot="item" let:item={sprint}>
<ProjectTitlePresenter value={sprint} />
</svelte:fragment>
</ObjectPopup>
@@ -13,43 +13,31 @@
// limitations under the License.
-->
<script lang="ts">
import { WithLookup } from '@hcengineering/core'
import { Ref, Space } from '@hcengineering/core'
import { Project } from '@hcengineering/tracker'
import { getCurrentLocation, Icon, navigate, tooltip } from '@hcengineering/ui'
import tracker from '../../plugin'
import { NavLink } from '@hcengineering/ui'
import { SpacesNavModel } from '@hcengineering/workbench'
import { SpecialElement } from '@hcengineering/workbench-resources'
import { TreeNode } from '@hcengineering/view-resources'
export let value: WithLookup<Project>
export let withIcon = false
export let onClick: () => void | undefined
export let isInteractive = true
function navigateToProject () {
if (!isInteractive) {
return
}
if (onClick) {
onClick()
}
const loc = getCurrentLocation()
loc.path[4] = 'projects'
loc.path[5] = value._id
loc.path.length = 6
loc.fragment = undefined
navigate(loc)
}
export let space: Project
export let model: SpacesNavModel
export let currentSpace: Ref<Space> | undefined
export let currentSpecial: string | undefined
export let getActions: Function
</script>
{#if value}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="flex" on:click={navigateToProject}>
{#if withIcon}
<div class="mr-2" use:tooltip={{ label: tracker.string.Project }}>
<Icon icon={tracker.icon.Projects} size={'small'} />
</div>
{/if}
<span title={value.label} class="fs-bold cursor-pointer caption-color overflow-label clear-mins">
{value.label}
</span>
</div>
{#if model.specials}
<TreeNode icon={space?.icon ?? model.icon} title={space.name} indent={'ml-2'} actions={() => getActions(space)}>
{#each model.specials as special}
<NavLink space={space._id} special={special.id}>
<SpecialElement
indent={'ml-4'}
label={special.label}
icon={special.icon}
selected={currentSpace === space._id && special.id === currentSpecial}
/>
</NavLink>
{/each}
</TreeNode>
{/if}
@@ -1,8 +0,0 @@
<script lang="ts">
import { Project } from '@hcengineering/tracker'
import ProjectStatusPresenter from './ProjectStatusPresenter.svelte'
export let object: Project
</script>
<ProjectStatusPresenter value={object} shouldShowLabel />
@@ -1,54 +0,0 @@
<!--
// 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 '@hcengineering/tracker'
import { getClient } from '@hcengineering/presentation'
import type { ButtonKind, ButtonSize } from '@hcengineering/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}
<ProjectStatusSelector
{kind}
{size}
{width}
{justify}
{isEditable}
{shouldShowLabel}
showTooltip={isEditable ? { label: tracker.string.SetStatus } : undefined}
selectedProjectStatus={value.status}
onProjectStatusChange={handleProjectStatusChanged}
/>
{/if}
@@ -1,68 +0,0 @@
<!--
// 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 { ProjectStatus } from '@hcengineering/tracker'
import { Button, showPopup, SelectPopup, eventToHTMLElement } from '@hcengineering/ui'
import type { ButtonKind, ButtonSize, LabelAndProps } from '@hcengineering/ui'
import tracker from '../../plugin'
import { defaultProjectStatuses, projectStatusAssets } from '../../utils'
export let selectedProjectStatus: ProjectStatus | undefined
export let shouldShowLabel: boolean = true
export let onProjectStatusChange: ((newProjectStatus: ProjectStatus | undefined) => void) | undefined = undefined
export let isEditable: boolean = true
export let kind: ButtonKind = 'no-border'
export let size: ButtonSize = 'small'
export let justify: 'left' | 'center' = 'center'
export let width: string | undefined = 'min-content'
export let showTooltip: LabelAndProps | undefined = undefined
$: selectedStatusIcon = selectedProjectStatus
? projectStatusAssets[selectedProjectStatus].icon
: tracker.icon.ProjectStatusBacklog
$: selectedStatusLabel = shouldShowLabel
? selectedProjectStatus
? projectStatusAssets[selectedProjectStatus].label
: tracker.string.Backlog
: undefined
$: statusesInfo = defaultProjectStatuses.map((s) => ({ id: s, ...projectStatusAssets[s] }))
const handleProjectStatusEditorOpened = (event: MouseEvent) => {
if (!isEditable) {
return
}
showPopup(
SelectPopup,
{ value: statusesInfo, placeholder: tracker.string.SetStatus, searchable: true },
eventToHTMLElement(event),
onProjectStatusChange
)
}
</script>
<Button
{kind}
{size}
{width}
{justify}
disabled={!isEditable}
icon={selectedStatusIcon}
label={selectedStatusLabel}
{showTooltip}
on:click={handleProjectStatusEditorOpened}
/>
@@ -1,30 +0,0 @@
<!--
// 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 '@hcengineering/tracker'
import { Icon } from '@hcengineering/ui'
import tracker from '../../plugin'
export let value: Project | undefined
</script>
{#if value}
<span class="overflow-label flex">
<Icon icon={value.icon ?? tracker.icon.Project} size={'small'} />
<div class="ml-2 mr-2">
{value.label}
</div></span
>
{/if}
@@ -1,66 +0,0 @@
<!--
// 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 { DocumentQuery, Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import { Project } from '@hcengineering/tracker'
import { closePopup, closeTooltip, getCurrentLocation, location, navigate } from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import tracker from '../../plugin'
import { ProjectsViewMode } from '../../utils'
import EditProject from './EditProject.svelte'
import ProjectBrowser from './ProjectBrowser.svelte'
export let label: IntlString = tracker.string.Projects
export let query: DocumentQuery<Project> = {}
export let search: string = ''
export let mode: ProjectsViewMode = 'all'
let projectId: Ref<Project> | undefined
let project: Project | undefined
onDestroy(
location.subscribe(async (loc) => {
closeTooltip()
closePopup()
projectId = loc.path[5] as Ref<Project>
})
)
const projectQuery = createQuery()
$: if (projectId !== undefined) {
projectQuery.query(tracker.class.Project, { _id: projectId }, (result) => {
project = result.shift()
})
} else {
projectQuery.unsubscribe()
project = undefined
}
</script>
{#if project}
<EditProject
{project}
on:project={(evt) => {
const loc = getCurrentLocation()
loc.path[5] = evt.detail
navigate(loc)
}}
/>
{:else}
<ProjectBrowser {label} {query} {search} {mode} />
{/if}
@@ -1,245 +0,0 @@
<!--
// 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 '@hcengineering/contact'
import { Class, Doc, FindOptions, getObjectValue, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Issue, Project } from '@hcengineering/tracker'
import { CheckBox, Spinner, tooltip } from '@hcengineering/ui'
import { BuildModelKey } from '@hcengineering/view'
import { buildModel, LoadingProps } from '@hcengineering/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
}
}
$: options = { ...baseOptions } as FindOptions<Project>
$: selectedObjectIdsSet = new Set<Ref<Doc>>(selectedObjectIds.map((it) => it._id))
$: objectRefs.length = projects?.length ?? 0
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">
<div
class="eListGridCheckBox"
use:tooltip={{ direction: 'bottom', label: tracker.string.SelectIssue }}
>
<CheckBox
checked={selectedObjectIdsSet.has(docObject._id)}
on:value={(event) => {
onObjectChecked([docObject], event.detail)
}}
/>
</div>
<div class="iconPresenter">
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
{...attributeModel.props}
/>
</div>
</div>
{:else if attributeModelIndex === 1}
<div class="projectPresenter flex-grow">
<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) ?? ''}
parentId={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(--caption-color);
border-bottom: 1px solid var(--divider-color);
&.mListGridChecked {
background-color: var(--highlight-select);
border-bottom-color: var(--highlight-select-border);
.eListGridCheckBox {
opacity: 1;
}
}
&.mListGridSelected {
background-color: var(--highlight-hover);
}
&.mListGridChecked.mListGridSelected {
background-color: var(--highlight-select-hover);
}
.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>
@@ -1,94 +0,0 @@
<!--
// 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 '@hcengineering/core'
import { BuildModelKey } from '@hcengineering/view'
import {
ActionContext,
focusStore,
ListSelectionProvider,
SelectDirection,
selectionStore,
LoadingProps
} from '@hcengineering/view-resources'
import { Project } from '@hcengineering/tracker'
import { onMount } from 'svelte'
import ProjectsList from './ProjectsList.svelte'
import ProjectsTimeline from './ProjectsTimeline.svelte'
export let _class: Ref<Class<Doc>>
export let itemsConfig: (BuildModelKey | string)[]
export let loadingProps: LoadingProps | undefined = undefined
export let projects: Project[] = []
export let viewMode: 'list' | 'timeline' = 'list'
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
if (dir === 'vertical') {
if (viewMode === 'list') projectsList.onElementSelected(offset, of)
else projectsTimeline.onElementSelected(offset, of)
}
})
let projectsList: ProjectsList
let projectsTimeline: ProjectsTimeline
$: if (projectsList !== undefined) {
listProvider.update(projects)
}
onMount(() => {
;(document.activeElement as HTMLElement)?.blur()
})
</script>
<ActionContext
context={{
mode: 'browser'
}}
/>
{#if viewMode === 'list'}
<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)
}}
/>
{:else}
<ProjectsTimeline
bind:this={projectsTimeline}
{_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)
}}
/>
{/if}
@@ -1,510 +0,0 @@
<!--
// 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 '@hcengineering/contact'
import { Class, Doc, FindOptions, getObjectValue, Ref, Timestamp } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Issue, Project } from '@hcengineering/tracker'
import { CheckBox, Spinner, Timeline, TimelineRow } from '@hcengineering/ui'
import { AttributeModel, BuildModelKey } from '@hcengineering/view'
import { buildModel, LoadingProps } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import tracker from '../../plugin'
import ProjectPresenter from './ProjectPresenter.svelte'
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 baseOptions: FindOptions<Issue> = {
lookup: {
assignee: contact.class.Employee,
status: tracker.class.IssueStatus
}
}
$: options = { ...baseOptions } as FindOptions<Project>
$: selectedObjectIdsSet = new Set<Ref<Doc>>(selectedObjectIds.map((it) => it._id))
let selectedRows: number[] = []
$: if (selectedObjectIdsSet.size > 0 && projects !== undefined) {
const tRows: number[] = []
selectedObjectIdsSet.forEach((it) => {
const index = projects?.findIndex((f) => f._id === it)
if (index !== undefined) tRows.push(index)
})
selectedRows = tRows
} else selectedRows = []
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
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
}
let itemModels: AttributeModel[] | undefined = undefined
$: buildModel({ client, _class, keys: itemsConfig, lookup: options.lookup }).then((res) => (itemModels = res))
let lines: TimelineRow[] | undefined
$: lines = projects?.map((proj) => {
const tR: TimelineRow = { items: [] }
tR.items = [
{
icon: proj.icon,
presenter: ProjectPresenter,
props: { value: proj },
startDate: proj.startDate as Timestamp,
targetDate: proj.targetDate as Timestamp
}
]
return tR
})
</script>
{#if projects && itemModels && lines}
<Timeline
{lines}
{selectedRows}
selectedRow={selectedRowIndex}
on:row-focus={(ev) => {
if (ev.detail !== undefined && projects !== undefined) handleRowFocused(projects[ev.detail])
}}
on:check={(ev) => {
if (ev.detail !== undefined && projects !== undefined) onObjectChecked([projects[ev.detail.row]], ev.detail.value)
}}
>
<svelte:fragment let:row>
{#each itemModels as attributeModel, attributeModelIndex}
{#if attributeModelIndex === 0}
<div class="gridElement">
<div class="iconPresenter">
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, projects[row]) ?? ''}
{...attributeModel.props}
/>
</div>
</div>
{:else if attributeModelIndex === 1}
<div class="projectPresenter flex-grow">
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, projects[row]) ?? ''}
{...attributeModel.props}
/>
</div>
<div class="filler" />
{:else}
<div class="gridElement">
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, projects[row]) ?? ''}
parentId={projects[row]._id}
{...attributeModel.props}
/>
</div>
{/if}
{/each}
</svelte:fragment>
</Timeline>
{: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}
<style lang="scss">
// .timeline-container {
// overflow: hidden;
// position: relative;
// display: flex;
// flex-direction: column;
// width: 100%;
// height: 100%;
// min-width: 0;
// min-height: 0;
// & > * {
// overscroll-behavior-x: contain;
// }
// }
.timeline-header {
display: flex;
align-items: center;
min-height: 4rem;
border-bottom: 1px solid var(--divider-color);
}
.timeline-header__title {
display: flex;
align-items: center;
flex-shrink: 0;
padding: 0 2.25rem;
height: 100%;
background-color: var(--body-accent);
box-shadow: var(--accent-shadow);
// z-index: 2;
}
.timeline-header__time {
// overflow: hidden;
position: relative;
flex-grow: 1;
height: 100%;
background-color: var(--body-color);
mask-image: linear-gradient(
90deg,
rgba(0, 0, 0, 0) 0,
rgba(0, 0, 0, 1) 2rem,
rgba(0, 0, 0, 1) calc(100% - 2rem),
rgba(0, 0, 0, 0) 100%
);
&-content {
width: 100%;
height: 100%;
will-change: transform;
.day,
.month {
position: absolute;
pointer-events: none;
}
.month {
width: max-content;
top: 0.25rem;
font-size: 1rem;
color: var(--accent-color);
&:first-letter {
text-transform: uppercase;
}
}
.day {
bottom: 0.5rem;
font-size: 1rem;
color: var(--content-color);
transform: translateX(-50%);
}
.cursor {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
padding-bottom: 1px;
width: 1.75rem;
height: 1.75rem;
bottom: 0.375rem;
font-size: 1rem;
font-weight: 600;
color: #fff;
background-color: var(--primary-bg-color);
border-radius: 50%;
transform: translateX(-50%);
pointer-events: none;
}
}
}
.todayMarker,
.monthMarker {
position: absolute;
top: 0;
bottom: 0;
width: 0;
height: 100%;
pointer-events: none;
}
.monthMarker {
border-left: 1px dashed var(--highlight-select);
}
.todayMarker {
border-left: 1px solid var(--primary-bg-color);
}
.timeline-background__headers,
.timeline-background__viewbox,
.timeline-foreground__viewbox {
overflow: hidden;
position: absolute;
top: 4rem;
bottom: 0;
height: 100%;
z-index: -1;
}
.timeline-background__headers {
left: 0;
background-color: var(--body-accent);
}
.timeline-background__viewbox,
.timeline-foreground__viewbox {
right: 0;
mask-image: linear-gradient(
90deg,
rgba(0, 0, 0, 0) 0,
rgba(0, 0, 0, 1) 2rem,
rgba(0, 0, 0, 1) calc(100% - 2rem),
rgba(0, 0, 0, 0) 100%
);
}
.timeline-foreground__viewbox {
z-index: 1;
pointer-events: none;
}
.timeline-splitter,
.timeline-splitter::before {
position: absolute;
top: 0;
bottom: 0;
height: 100%;
transform: translateX(-50%);
}
.timeline-splitter {
width: 1px;
background-color: var(--divider-color);
cursor: col-resize;
z-index: 3;
transition-property: width, background-color;
transition-timing-function: var(--timing-main);
transition-duration: 0.1s;
transition-delay: 0s;
&:hover {
width: 3px;
background-color: var(--button-border-hover);
transition-duration: 0.15s;
transition-delay: 0.3s;
}
&::before {
content: '';
width: 10px;
left: 50%;
}
&.moving {
width: 2px;
background-color: var(--primary-edit-border-color);
transition-duration: 0.1s;
transition-delay: 0s;
}
}
.headerWrapper {
display: flex;
align-items: center;
height: 100%;
min-width: 0;
padding-left: 0.75rem;
padding-right: 1.15rem;
// border-bottom: 1px solid var(--accent-bg-color);
}
.contentWrapper {
overflow: hidden;
position: relative;
display: flex;
align-items: center;
flex-grow: 1;
height: 100%;
min-width: 0;
min-height: 0;
mask-image: linear-gradient(
90deg,
rgba(0, 0, 0, 0) 0,
rgba(0, 0, 0, 1) 2rem,
rgba(0, 0, 0, 1) calc(100% - 2rem),
rgba(0, 0, 0, 0) 100%
);
&.nullRow {
cursor: pointer;
}
}
.timeline-wrapped_content {
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
will-change: transform;
}
.timeline-action__button,
.project-item {
position: absolute;
display: flex;
align-items: center;
padding: 0.5rem;
box-shadow: var(--button-shadow);
}
.project-item {
top: 0.25rem;
bottom: 0.25rem;
background-color: var(--button-bg-color);
border: 1px solid var(--button-border-color);
border-radius: 0.75rem;
&:hover {
background-color: var(--button-bg-hover);
border-color: var(--button-border-hover);
}
&.noTarget {
mask-image: linear-gradient(to left, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 1) 2rem);
border-right-color: transparent;
}
.project-presenter {
display: flex;
align-items: center;
.space {
flex-shrink: 0;
width: 0.25rem;
min-width: 0.25rem;
max-width: 0.25rem;
}
}
}
.timeline-action__button {
top: 0.625rem;
bottom: 0.625rem;
width: 2rem;
color: var(--content-color);
background-color: var(--button-bg-color);
border: 1px solid var(--button-border-color);
border-radius: 0.5rem;
&:hover {
color: var(--accent-color);
background-color: var(--button-bg-hover);
border-color: var(--button-border-hover);
}
&.left {
left: 1rem;
}
&.right {
right: 1rem;
}
&.add {
transform: translateX(-50%);
pointer-events: none;
}
}
.listGrid {
display: flex;
justify-content: stretch;
align-items: center;
flex-shrink: 0;
width: 100%;
height: 3.25rem;
min-height: 0;
color: var(--caption-color);
z-index: 2;
&.mListGridChecked {
.headerWrapper {
background-color: var(--highlight-select);
}
.contentWrapper {
background-color: var(--trans-content-05);
}
.eListGridCheckBox {
opacity: 1;
}
}
&.mListGridSelected {
.headerWrapper {
background-color: var(--highlight-select-hover);
}
.contentWrapper {
background-color: var(--trans-content-10);
}
}
.eListGridCheckBox {
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
}
&:hover .eListGridCheckBox {
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;
}
}
.projectPresenter {
display: flex;
align-items: center;
flex-shrink: 0;
width: 5.5rem;
margin-left: 0.5rem;
}
</style>
@@ -1,20 +0,0 @@
<!--
// 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 Projects from './Projects.svelte'
import tracker from '../../plugin'
</script>
<Projects label={tracker.string.Roadmap} />
@@ -1,31 +0,0 @@
<!--
// 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 '@hcengineering/tracker'
import { getClient } from '@hcengineering/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} />
@@ -1,23 +0,0 @@
<!--
// 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 '@hcengineering/core'
import { Team } from '@hcengineering/tracker'
import Projects from './Projects.svelte'
export let currentSpace: Ref<Team>
</script>
<Projects query={{ space: currentSpace }} />