mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-07 16:07:24 +00:00
Fix Tracker projects layout (#2039)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
bb187fce40
commit
dd186c0478
@ -401,12 +401,6 @@ export function createModel (builder: Builder): void {
|
|||||||
label: tracker.string.Views,
|
label: tracker.string.Views,
|
||||||
icon: tracker.icon.Views,
|
icon: tracker.icon.Views,
|
||||||
component: tracker.component.Views
|
component: tracker.component.Views
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'project',
|
|
||||||
label: tracker.string.Project,
|
|
||||||
component: tracker.component.EditProject,
|
|
||||||
visibleIf: tracker.function.ProjectVisible
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
spaces: [
|
spaces: [
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
$: updateSpace(currentSpace)
|
$: updateSpace(currentSpace)
|
||||||
|
|
||||||
async function updateSpace (spaceId: Ref<Space> | undefined): Promise<void> {
|
async function updateSpace (spaceId: Ref<Space> | undefined): Promise<void> {
|
||||||
if (space) {
|
if (spaceId !== undefined) {
|
||||||
space = spaceId
|
space = spaceId
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -56,4 +56,5 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<Button icon={IconOptions} kind={'link'} on:click={handleOptionsEditorOpened} />
|
<Button icon={IconOptions} kind={'link'} on:click={handleOptionsEditorOpened} />
|
||||||
|
<slot name="extra" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,19 +1,22 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import core, { Ref, Space, WithLookup } from '@anticrm/core'
|
import core, { Ref, Space, WithLookup } from '@anticrm/core'
|
||||||
|
import { IntlString, translate } from '@anticrm/platform'
|
||||||
import { getClient } from '@anticrm/presentation'
|
import { getClient } from '@anticrm/presentation'
|
||||||
|
import { IssuesDateModificationPeriod, IssuesGrouping, IssuesOrdering, Team } from '@anticrm/tracker'
|
||||||
|
import { Button, IconDetails } from '@anticrm/ui'
|
||||||
import view, { Filter, Viewlet } from '@anticrm/view'
|
import view, { Filter, Viewlet } from '@anticrm/view'
|
||||||
|
import { FilterBar } from '@anticrm/view-resources'
|
||||||
|
import tracker from '../../plugin'
|
||||||
import IssuesContent from './IssuesContent.svelte'
|
import IssuesContent from './IssuesContent.svelte'
|
||||||
import IssuesHeader from './IssuesHeader.svelte'
|
import IssuesHeader from './IssuesHeader.svelte'
|
||||||
import { IssuesDateModificationPeriod, IssuesGrouping, IssuesOrdering, Team } from '@anticrm/tracker'
|
|
||||||
import tracker from '../../plugin'
|
|
||||||
import { IntlString, translate } from '@anticrm/platform'
|
|
||||||
import { FilterBar } from '@anticrm/view-resources'
|
|
||||||
|
|
||||||
export let currentSpace: Ref<Team> | undefined
|
export let currentSpace: Ref<Team> | undefined
|
||||||
export let query = {}
|
export let query = {}
|
||||||
export let title: IntlString | undefined = undefined
|
export let title: IntlString | undefined = undefined
|
||||||
export let label: string = ''
|
export let label: string = ''
|
||||||
|
|
||||||
|
export let panelWidth: number = 0
|
||||||
|
|
||||||
let viewlet: WithLookup<Viewlet> | undefined = undefined
|
let viewlet: WithLookup<Viewlet> | undefined = undefined
|
||||||
let filters: Filter[]
|
let filters: Filter[]
|
||||||
let viewOptions = {
|
let viewOptions = {
|
||||||
@ -50,19 +53,53 @@
|
|||||||
label = res
|
label = res
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let asideFloat: boolean = false
|
||||||
|
let asideShown: boolean = true
|
||||||
|
$: if (panelWidth < 900 && !asideFloat) asideFloat = true
|
||||||
|
$: if (panelWidth >= 900 && asideFloat) {
|
||||||
|
asideFloat = false
|
||||||
|
asideShown = false
|
||||||
|
}
|
||||||
|
let docWidth: number
|
||||||
|
let docSize: boolean = false
|
||||||
|
$: if (docWidth <= 900 && !docSize) docSize = true
|
||||||
|
$: if (docWidth > 900 && docSize) docSize = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if currentSpace}
|
{#if currentSpace}
|
||||||
<IssuesHeader {currentSpace} {viewlets} {label} bind:viewlet bind:viewOptions bind:filters />
|
<div class="header">
|
||||||
|
<IssuesHeader {currentSpace} {viewlets} {label} bind:viewlet bind:viewOptions bind:filters>
|
||||||
|
<svelte:fragment slot="extra">
|
||||||
|
{#if asideFloat && $$slots.aside}
|
||||||
|
<Button
|
||||||
|
icon={IconDetails}
|
||||||
|
kind={'transparent'}
|
||||||
|
size={'medium'}
|
||||||
|
selected={asideShown}
|
||||||
|
on:click={() => {
|
||||||
|
asideShown = !asideShown
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</svelte:fragment>
|
||||||
|
</IssuesHeader>
|
||||||
<FilterBar _class={tracker.class.Issue} {query} bind:filters on:change={(e) => (resultQuery = e.detail)} />
|
<FilterBar _class={tracker.class.Issue} {query} bind:filters on:change={(e) => (resultQuery = e.detail)} />
|
||||||
|
</div>
|
||||||
<div class="flex h-full">
|
<div class="flex h-full">
|
||||||
<div class="antiPanel-component">
|
<div class="antiPanel-component">
|
||||||
<IssuesContent {currentSpace} {viewlet} query={resultQuery} {viewOptions} />
|
<IssuesContent {currentSpace} {viewlet} query={resultQuery} {viewOptions} />
|
||||||
</div>
|
</div>
|
||||||
{#if $$slots.aside !== undefined}
|
{#if $$slots.aside !== undefined && asideShown}
|
||||||
<div class="antiPanel-component aside border-left">
|
<div class="popupPanel-body__aside" class:float={asideFloat} class:shown={asideShown}>
|
||||||
<slot name="aside" />
|
<slot name="aside" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.header {
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -1,48 +1,37 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Ref } from '@anticrm/core'
|
import { getClient } from '@anticrm/presentation'
|
||||||
import { createQuery, getClient } from '@anticrm/presentation'
|
|
||||||
import { StyledTextBox } from '@anticrm/text-editor'
|
import { StyledTextBox } from '@anticrm/text-editor'
|
||||||
import { Project } from '@anticrm/tracker'
|
import { Project } from '@anticrm/tracker'
|
||||||
import { EditBox, getCurrentLocation } from '@anticrm/ui'
|
import { EditBox } from '@anticrm/ui'
|
||||||
import { DocAttributeBar } from '@anticrm/view-resources'
|
import { DocAttributeBar } from '@anticrm/view-resources'
|
||||||
|
import tracker from '../../plugin'
|
||||||
import IssuesView from '../issues/IssuesView.svelte'
|
import IssuesView from '../issues/IssuesView.svelte'
|
||||||
|
|
||||||
import tracker from '../../plugin'
|
export let project: Project
|
||||||
|
|
||||||
let object: Project | undefined
|
|
||||||
$: project = getCurrentLocation().path[3] as Ref<Project>
|
|
||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
const projectQuery = createQuery()
|
|
||||||
$: projectQuery.query(tracker.class.Project, { _id: project }, (result) => {
|
|
||||||
;[object] = result
|
|
||||||
})
|
|
||||||
|
|
||||||
async function change (field: string, value: any) {
|
async function change (field: string, value: any) {
|
||||||
if (object) {
|
await client.update(project, { [field]: value })
|
||||||
await client.update(object, { [field]: value })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if object}
|
<IssuesView currentSpace={project.space} query={{ project: project._id }} label={project.label}>
|
||||||
<IssuesView currentSpace={object.space} query={{ project }} label={object.label}>
|
|
||||||
<svelte:fragment slot="aside">
|
<svelte:fragment slot="aside">
|
||||||
<div class="flex-row p-4">
|
<div class="flex-row p-4">
|
||||||
<div class="fs-title text-xl">
|
<div class="fs-title text-xl">
|
||||||
<EditBox bind:value={object.label} maxWidth="39rem" on:change={() => change('label', object?.label)} />
|
<EditBox bind:value={project.label} maxWidth="39rem" on:change={() => change('label', project.label)} />
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<StyledTextBox
|
<StyledTextBox
|
||||||
alwaysEdit={true}
|
alwaysEdit={true}
|
||||||
showButtons={false}
|
showButtons={false}
|
||||||
placeholder={tracker.string.Description}
|
placeholder={tracker.string.Description}
|
||||||
content={object.description ?? ''}
|
content={project.description ?? ''}
|
||||||
on:value={(evt) => change('description', evt.detail)}
|
on:value={(evt) => change('description', evt.detail)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<DocAttributeBar {object} mixins={[]} ignoreKeys={['icon', 'label', 'description']} />
|
<DocAttributeBar object={project} mixins={[]} ignoreKeys={['icon', 'label', 'description']} />
|
||||||
</div>
|
</div>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</IssuesView>
|
</IssuesView>
|
||||||
{/if}
|
|
||||||
|
@ -0,0 +1,204 @@
|
|||||||
|
<!--
|
||||||
|
// 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 { DocumentQuery, FindOptions, Ref, SortingOrder } from '@anticrm/core'
|
||||||
|
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 { getIncludedProjectStatuses, projectsTitleMap, ProjectsViewMode } from '../../utils'
|
||||||
|
import NewProject from './NewProject.svelte'
|
||||||
|
import ProjectsListBrowser from './ProjectsListBrowser.svelte'
|
||||||
|
|
||||||
|
export let currentSpace: Ref<Team>
|
||||||
|
export let query: DocumentQuery<Project> = {}
|
||||||
|
export let search: string = ''
|
||||||
|
export let mode: ProjectsViewMode = 'all'
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
space: currentSpace,
|
||||||
|
...includedProjectsQuery,
|
||||||
|
...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)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleViewModeChanged = (newMode: ProjectsViewMode) => {
|
||||||
|
if (newMode === undefined || newMode === mode) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mode = newMode
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="fs-title flex-between header">
|
||||||
|
<div class="flex-center">
|
||||||
|
<Label label={tracker.string.Projects} />
|
||||||
|
<div class="projectTitle">
|
||||||
|
› <Label label={title} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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
|
||||||
|
size="small"
|
||||||
|
shape="rectangle-right"
|
||||||
|
selected={mode === 'all'}
|
||||||
|
label={tracker.string.AllProjects}
|
||||||
|
on:click={() => handleViewModeChanged('all')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="buttonWrapper">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
shape="rectangle"
|
||||||
|
selected={mode === 'backlog'}
|
||||||
|
label={tracker.string.BacklogProjects}
|
||||||
|
on:click={() => handleViewModeChanged('backlog')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="buttonWrapper">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
shape="rectangle"
|
||||||
|
selected={mode === 'active'}
|
||||||
|
label={tracker.string.ActiveProjects}
|
||||||
|
on:click={() => handleViewModeChanged('active')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="buttonWrapper">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
shape="rectangle-left"
|
||||||
|
selected={mode === 'closed'}
|
||||||
|
label={tracker.string.ClosedProjects}
|
||||||
|
on:click={() => handleViewModeChanged('closed')}
|
||||||
|
/>
|
||||||
|
</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} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ProjectsListBrowser
|
||||||
|
_class={tracker.class.Project}
|
||||||
|
itemsConfig={[
|
||||||
|
{ key: '', presenter: tracker.component.IconPresenter },
|
||||||
|
{ key: '', presenter: tracker.component.ProjectPresenter },
|
||||||
|
{
|
||||||
|
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 }
|
||||||
|
]}
|
||||||
|
projects={resultProjects}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.header {
|
||||||
|
min-height: 3.5rem;
|
||||||
|
padding-left: 2.25rem;
|
||||||
|
padding-right: 1.35rem;
|
||||||
|
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;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonWrapper {
|
||||||
|
margin-right: 1px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterButton {
|
||||||
|
color: var(--caption-color);
|
||||||
|
}
|
||||||
|
</style>
|
@ -20,9 +20,8 @@
|
|||||||
export let value: WithLookup<Project>
|
export let value: WithLookup<Project>
|
||||||
function navigateToProject () {
|
function navigateToProject () {
|
||||||
const loc = getCurrentLocation()
|
const loc = getCurrentLocation()
|
||||||
loc.path[2] = 'project'
|
loc.path[4] = value._id
|
||||||
loc.path[3] = value._id
|
loc.path.length = 5
|
||||||
loc.path.length = 4
|
|
||||||
navigate(loc)
|
navigate(loc)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -13,192 +13,45 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import contact from '@anticrm/contact'
|
import { DocumentQuery, Ref } from '@anticrm/core'
|
||||||
import { DocumentQuery, FindOptions, Ref, SortingOrder } from '@anticrm/core'
|
|
||||||
import { createQuery } from '@anticrm/presentation'
|
import { createQuery } from '@anticrm/presentation'
|
||||||
import { Project, Team } from '@anticrm/tracker'
|
import { Project, Team } from '@anticrm/tracker'
|
||||||
import { Button, IconAdd, IconOptions, Label, showPopup } from '@anticrm/ui'
|
import { closePopup, closeTooltip, location } from '@anticrm/ui'
|
||||||
import NewProject from './NewProject.svelte'
|
import { onDestroy } from 'svelte'
|
||||||
import ProjectsListBrowser from './ProjectsListBrowser.svelte'
|
|
||||||
import tracker from '../../plugin'
|
import tracker from '../../plugin'
|
||||||
import { getIncludedProjectStatuses, ProjectsViewMode, projectsTitleMap } from '../../utils'
|
import { ProjectsViewMode } from '../../utils'
|
||||||
|
import EditProject from './EditProject.svelte'
|
||||||
|
import ProjectBrowser from './ProjectBrowser.svelte'
|
||||||
|
|
||||||
export let currentSpace: Ref<Team>
|
export let currentSpace: Ref<Team>
|
||||||
export let query: DocumentQuery<Project> = {}
|
export let query: DocumentQuery<Project> = {}
|
||||||
export let search: string = ''
|
export let search: string = ''
|
||||||
export let mode: ProjectsViewMode = 'all'
|
export let mode: ProjectsViewMode = 'all'
|
||||||
|
|
||||||
const ENTRIES_LIMIT = 200
|
let projectId: Ref<Project> | undefined
|
||||||
const resultProjectsQuery = createQuery()
|
let project: Project | undefined
|
||||||
|
|
||||||
const projectOptions: FindOptions<Project> = {
|
onDestroy(
|
||||||
sort: { modifiedOn: SortingOrder.Descending },
|
location.subscribe(async (loc) => {
|
||||||
limit: ENTRIES_LIMIT,
|
closeTooltip()
|
||||||
lookup: { lead: contact.class.Employee, members: contact.class.Employee }
|
closePopup()
|
||||||
}
|
|
||||||
|
|
||||||
let resultProjects: Project[] = []
|
projectId = loc.path[4] as Ref<Project>
|
||||||
|
})
|
||||||
$: includedProjectStatuses = getIncludedProjectStatuses(mode)
|
|
||||||
$: title = projectsTitleMap[mode]
|
|
||||||
$: includedProjectsQuery = { status: { $in: includedProjectStatuses } }
|
|
||||||
|
|
||||||
$: baseQuery = {
|
|
||||||
space: currentSpace,
|
|
||||||
...includedProjectsQuery,
|
|
||||||
...query
|
|
||||||
}
|
|
||||||
|
|
||||||
$: resultQuery = search === '' ? baseQuery : { $search: search, ...baseQuery }
|
|
||||||
|
|
||||||
$: resultProjectsQuery.query<Project>(
|
|
||||||
tracker.class.Project,
|
|
||||||
{ ...resultQuery },
|
|
||||||
(result) => {
|
|
||||||
resultProjects = result
|
|
||||||
},
|
|
||||||
projectOptions
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const showCreateDialog = async () => {
|
const projectQuery = createQuery()
|
||||||
showPopup(NewProject, { space: currentSpace, targetElement: null }, null)
|
$: if (projectId !== undefined) {
|
||||||
}
|
projectQuery.query(tracker.class.Project, { _id: projectId }, (result) => {
|
||||||
|
;[project] = result
|
||||||
const handleViewModeChanged = (newMode: ProjectsViewMode) => {
|
})
|
||||||
if (newMode === undefined || newMode === mode) {
|
} else {
|
||||||
return
|
project = undefined
|
||||||
}
|
|
||||||
|
|
||||||
mode = newMode
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
{#if project}
|
||||||
<div class="fs-title flex-between header">
|
<EditProject {project} />
|
||||||
<div class="flex-center">
|
{:else}
|
||||||
<Label label={tracker.string.Projects} />
|
<ProjectBrowser {currentSpace} {query} {search} {mode} />
|
||||||
<div class="projectTitle">
|
{/if}
|
||||||
› <Label label={title} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<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
|
|
||||||
size="small"
|
|
||||||
shape="rectangle-right"
|
|
||||||
selected={mode === 'all'}
|
|
||||||
label={tracker.string.AllProjects}
|
|
||||||
on:click={() => handleViewModeChanged('all')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="buttonWrapper">
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
shape="rectangle"
|
|
||||||
selected={mode === 'backlog'}
|
|
||||||
label={tracker.string.BacklogProjects}
|
|
||||||
on:click={() => handleViewModeChanged('backlog')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="buttonWrapper">
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
shape="rectangle"
|
|
||||||
selected={mode === 'active'}
|
|
||||||
label={tracker.string.ActiveProjects}
|
|
||||||
on:click={() => handleViewModeChanged('active')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="buttonWrapper">
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
shape="rectangle-left"
|
|
||||||
selected={mode === 'closed'}
|
|
||||||
label={tracker.string.ClosedProjects}
|
|
||||||
on:click={() => handleViewModeChanged('closed')}
|
|
||||||
/>
|
|
||||||
</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} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ProjectsListBrowser
|
|
||||||
_class={tracker.class.Project}
|
|
||||||
itemsConfig={[
|
|
||||||
{ key: '', presenter: tracker.component.IconPresenter },
|
|
||||||
{ key: '', presenter: tracker.component.ProjectPresenter },
|
|
||||||
{
|
|
||||||
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 }
|
|
||||||
]}
|
|
||||||
projects={resultProjects}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.header {
|
|
||||||
min-height: 3.5rem;
|
|
||||||
padding-left: 2.25rem;
|
|
||||||
padding-right: 1.35rem;
|
|
||||||
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;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonWrapper {
|
|
||||||
margin-right: 1px;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.filterButton {
|
|
||||||
color: var(--caption-color);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@ -103,6 +103,19 @@
|
|||||||
|
|
||||||
async function handleKeys (evt: KeyboardEvent): Promise<void> {
|
async function handleKeys (evt: KeyboardEvent): Promise<void> {
|
||||||
const targetTagName = (evt.target as any)?.tagName?.toLowerCase()
|
const targetTagName = (evt.target as any)?.tagName?.toLowerCase()
|
||||||
|
|
||||||
|
let elm = evt.target as HTMLElement
|
||||||
|
while (true) {
|
||||||
|
if (elm.contentEditable === 'true') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const prt = elm.parentElement
|
||||||
|
if (prt === null) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
elm = prt
|
||||||
|
}
|
||||||
|
|
||||||
let currentActions = actions
|
let currentActions = actions
|
||||||
|
|
||||||
// For none we ignore all actions.
|
// For none we ignore all actions.
|
||||||
|
@ -211,8 +211,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectSpace (spaceId?: Ref<Space>, spaceSpecial?: string): void {
|
function selectSpace (spaceId?: Ref<Space>, spaceSpecial?: string): void {
|
||||||
if (currentSpace === spaceId && (spaceSpecial === currentSpecial || spaceSpecial === asideId)) return
|
|
||||||
|
|
||||||
doNavigate([], undefined, {
|
doNavigate([], undefined, {
|
||||||
mode: 'space',
|
mode: 'space',
|
||||||
space: spaceId,
|
space: spaceId,
|
||||||
|
37
tests/sanity/tests/tracker.projects.spec.ts
Normal file
37
tests/sanity/tests/tracker.projects.spec.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { expect, test } from '@playwright/test'
|
||||||
|
import { generateId, PlatformSetting, PlatformURI } from './utils'
|
||||||
|
|
||||||
|
test.use({
|
||||||
|
storageState: PlatformSetting
|
||||||
|
})
|
||||||
|
|
||||||
|
test.describe('contact tests', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
// Create user and workspace
|
||||||
|
await page.goto(`${PlatformURI}/workbench%3Acomponent%3AWorkbenchApp`)
|
||||||
|
})
|
||||||
|
test('create-project-issue', async ({ page }) => {
|
||||||
|
await page.click('[id="app-tracker\\:string\\:TrackerApplication"]')
|
||||||
|
|
||||||
|
// Click text=Projects
|
||||||
|
await page.click('text=Projects')
|
||||||
|
await expect(page).toHaveURL(
|
||||||
|
`${PlatformURI}/workbench%3Acomponent%3AWorkbenchApp/tracker%3Aapp%3ATracker/tracker%3Ateam%3ADefaultTeam/projects`
|
||||||
|
)
|
||||||
|
await page.click('button:has-text("Project")')
|
||||||
|
await page.click('[placeholder="Project\\ name"]')
|
||||||
|
const prjId = 'project-' + generateId()
|
||||||
|
await page.fill('[placeholder="Project\\ name"]', prjId)
|
||||||
|
|
||||||
|
await page.click('button:has-text("Create project")')
|
||||||
|
|
||||||
|
await page.click(`text=${prjId}`)
|
||||||
|
await page.click('button:has-text("New issue")')
|
||||||
|
await page.fill('[placeholder="Issue\\ title"]', 'issue')
|
||||||
|
await page.click('button:has-text("Project")')
|
||||||
|
await page.click(`button:has-text("${prjId}")`)
|
||||||
|
await page.click('button:has-text("Save issue")')
|
||||||
|
await page.click(`button:has-text("${prjId}")`)
|
||||||
|
await page.click('button:has-text("No project")')
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user