Fix Tracker projects layout (#2039)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-06-08 22:49:50 +07:00 committed by GitHub
parent bb187fce40
commit dd186c0478
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 352 additions and 227 deletions

View File

@ -401,12 +401,6 @@ export function createModel (builder: Builder): void {
label: tracker.string.Views,
icon: tracker.icon.Views,
component: tracker.component.Views
},
{
id: 'project',
label: tracker.string.Project,
component: tracker.component.EditProject,
visibleIf: tracker.function.ProjectVisible
}
],
spaces: [

View File

@ -27,7 +27,7 @@
$: updateSpace(currentSpace)
async function updateSpace (spaceId: Ref<Space> | undefined): Promise<void> {
if (space) {
if (spaceId !== undefined) {
space = spaceId
return
}

View File

@ -56,4 +56,5 @@
</div>
{/if}
<Button icon={IconOptions} kind={'link'} on:click={handleOptionsEditorOpened} />
<slot name="extra" />
</div>

View File

@ -1,19 +1,22 @@
<script lang="ts">
import core, { Ref, Space, WithLookup } from '@anticrm/core'
import { IntlString, translate } from '@anticrm/platform'
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 { FilterBar } from '@anticrm/view-resources'
import tracker from '../../plugin'
import IssuesContent from './IssuesContent.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 query = {}
export let title: IntlString | undefined = undefined
export let label: string = ''
export let panelWidth: number = 0
let viewlet: WithLookup<Viewlet> | undefined = undefined
let filters: Filter[]
let viewOptions = {
@ -50,19 +53,53 @@
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>
{#if currentSpace}
<IssuesHeader {currentSpace} {viewlets} {label} bind:viewlet bind:viewOptions bind:filters />
<FilterBar _class={tracker.class.Issue} {query} bind:filters on:change={(e) => (resultQuery = e.detail)} />
<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)} />
</div>
<div class="flex h-full">
<div class="antiPanel-component">
<IssuesContent {currentSpace} {viewlet} query={resultQuery} {viewOptions} />
</div>
{#if $$slots.aside !== undefined}
<div class="antiPanel-component aside border-left">
{#if $$slots.aside !== undefined && asideShown}
<div class="popupPanel-body__aside" class:float={asideFloat} class:shown={asideShown}>
<slot name="aside" />
</div>
{/if}
</div>
{/if}
<style lang="scss">
.header {
border-bottom: 1px solid var(--divider-color);
}
</style>

View File

@ -1,48 +1,37 @@
<script lang="ts">
import { Ref } from '@anticrm/core'
import { createQuery, getClient } from '@anticrm/presentation'
import { getClient } from '@anticrm/presentation'
import { StyledTextBox } from '@anticrm/text-editor'
import { Project } from '@anticrm/tracker'
import { EditBox, getCurrentLocation } from '@anticrm/ui'
import { EditBox } from '@anticrm/ui'
import { DocAttributeBar } from '@anticrm/view-resources'
import tracker from '../../plugin'
import IssuesView from '../issues/IssuesView.svelte'
import tracker from '../../plugin'
let object: Project | undefined
$: project = getCurrentLocation().path[3] as Ref<Project>
export let project: Project
const client = getClient()
const projectQuery = createQuery()
$: projectQuery.query(tracker.class.Project, { _id: project }, (result) => {
;[object] = result
})
async function change (field: string, value: any) {
if (object) {
await client.update(object, { [field]: value })
}
await client.update(project, { [field]: value })
}
</script>
{#if object}
<IssuesView currentSpace={object.space} query={{ project }} label={object.label}>
<svelte:fragment slot="aside">
<div class="flex-row p-4">
<div class="fs-title text-xl">
<EditBox bind:value={object.label} maxWidth="39rem" on:change={() => change('label', object?.label)} />
</div>
<div class="mt-2">
<StyledTextBox
alwaysEdit={true}
showButtons={false}
placeholder={tracker.string.Description}
content={object.description ?? ''}
on:value={(evt) => change('description', evt.detail)}
/>
</div>
<DocAttributeBar {object} mixins={[]} ignoreKeys={['icon', 'label', 'description']} />
<IssuesView currentSpace={project.space} query={{ project: project._id }} label={project.label}>
<svelte:fragment slot="aside">
<div class="flex-row p-4">
<div class="fs-title text-xl">
<EditBox bind:value={project.label} maxWidth="39rem" on:change={() => change('label', project.label)} />
</div>
</svelte:fragment>
</IssuesView>
{/if}
<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>

View File

@ -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>

View File

@ -20,9 +20,8 @@
export let value: WithLookup<Project>
function navigateToProject () {
const loc = getCurrentLocation()
loc.path[2] = 'project'
loc.path[3] = value._id
loc.path.length = 4
loc.path[4] = value._id
loc.path.length = 5
navigate(loc)
}
</script>

View File

@ -13,192 +13,45 @@
// limitations under the License.
-->
<script lang="ts">
import contact from '@anticrm/contact'
import { DocumentQuery, FindOptions, Ref, SortingOrder } from '@anticrm/core'
import { DocumentQuery, Ref } from '@anticrm/core'
import { createQuery } from '@anticrm/presentation'
import { Project, Team } from '@anticrm/tracker'
import { Button, IconAdd, IconOptions, Label, showPopup } from '@anticrm/ui'
import NewProject from './NewProject.svelte'
import ProjectsListBrowser from './ProjectsListBrowser.svelte'
import { closePopup, closeTooltip, location } from '@anticrm/ui'
import { onDestroy } from 'svelte'
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 query: DocumentQuery<Project> = {}
export let search: string = ''
export let mode: ProjectsViewMode = 'all'
const ENTRIES_LIMIT = 200
const resultProjectsQuery = createQuery()
let projectId: Ref<Project> | undefined
let project: Project | undefined
const projectOptions: FindOptions<Project> = {
sort: { modifiedOn: SortingOrder.Descending },
limit: ENTRIES_LIMIT,
lookup: { lead: contact.class.Employee, members: contact.class.Employee }
}
onDestroy(
location.subscribe(async (loc) => {
closeTooltip()
closePopup()
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
projectId = loc.path[4] as Ref<Project>
})
)
const showCreateDialog = async () => {
showPopup(NewProject, { space: currentSpace, targetElement: null }, null)
}
const handleViewModeChanged = (newMode: ProjectsViewMode) => {
if (newMode === undefined || newMode === mode) {
return
}
mode = newMode
const projectQuery = createQuery()
$: if (projectId !== undefined) {
projectQuery.query(tracker.class.Project, { _id: projectId }, (result) => {
;[project] = result
})
} else {
project = undefined
}
</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>
{#if project}
<EditProject {project} />
{:else}
<ProjectBrowser {currentSpace} {query} {search} {mode} />
{/if}

View File

@ -103,6 +103,19 @@
async function handleKeys (evt: KeyboardEvent): Promise<void> {
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
// For none we ignore all actions.

View File

@ -211,8 +211,6 @@
}
function selectSpace (spaceId?: Ref<Space>, spaceSpecial?: string): void {
if (currentSpace === spaceId && (spaceSpecial === currentSpecial || spaceSpecial === asideId)) return
doNavigate([], undefined, {
mode: 'space',
space: spaceId,

View 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")')
})
})