mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-28 20:45:00 +02:00
UBER-1053 (#3884)
This commit is contained in:
@@ -14,11 +14,11 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { PersonAccount } from '@hcengineering/contact'
|
||||
import { Class, Doc, DocumentQuery, getCurrentAccount, Ref } from '@hcengineering/core'
|
||||
import { Class, Doc, DocumentQuery, getCurrentAccount, Ref, Status } from '@hcengineering/core'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import tags, { selectedTagElements, TagCategory, TagElement } from '@hcengineering/tags'
|
||||
import { DoneState, Task } from '@hcengineering/task'
|
||||
import { Task } from '@hcengineering/task'
|
||||
import {
|
||||
Component,
|
||||
IModeSelector,
|
||||
@@ -32,6 +32,7 @@
|
||||
import {
|
||||
FilterBar,
|
||||
FilterButton,
|
||||
statusStore,
|
||||
TableBrowser,
|
||||
ViewletSelector,
|
||||
ViewletSettingButton
|
||||
@@ -70,17 +71,16 @@
|
||||
let preference: ViewletPreference | undefined
|
||||
|
||||
let documentIds: Ref<Task>[] = []
|
||||
function updateResultQuery (search: string, documentIds: Ref<Task>[], doneStates: DoneState[]): void {
|
||||
resultQuery.doneState = { $nin: doneStates.map((it) => it._id) }
|
||||
function updateResultQuery (search: string, documentIds: Ref<Task>[], doneStates: Status[]): void {
|
||||
resultQuery.status = { $nin: doneStates.map((it) => it._id) }
|
||||
if (documentIds.length > 0) {
|
||||
resultQuery._id = { $in: documentIds }
|
||||
}
|
||||
}
|
||||
|
||||
let doneStates: DoneState[] = []
|
||||
|
||||
const doneStateQuery = createQuery()
|
||||
doneStateQuery.query(task.class.DoneState, {}, (res) => (doneStates = res))
|
||||
$: doneStates = $statusStore.array.filter(
|
||||
(it) => it.category === task.statusCategory.Lost || it.category === task.statusCategory.Won
|
||||
)
|
||||
|
||||
// Find all tags for object class with matched elements
|
||||
const query = createQuery()
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact, { Employee, getName } from '@hcengineering/contact'
|
||||
import contact, { Employee, Person, getName } from '@hcengineering/contact'
|
||||
import { Class, Doc, Ref, Space } from '@hcengineering/core'
|
||||
import { Task } from '@hcengineering/task'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
@@ -24,7 +24,7 @@
|
||||
import { IntlString, getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import task from '../plugin'
|
||||
|
||||
export let value: Ref<Employee> | Employee | null | undefined
|
||||
export let value: Ref<Person> | Person | null | undefined
|
||||
export let issueId: Ref<Task>
|
||||
export let defaultClass: Ref<Class<Doc>> | undefined = undefined
|
||||
export let currentSpace: Ref<Space> | undefined = undefined
|
||||
@@ -33,7 +33,7 @@
|
||||
export let defaultName: IntlString | undefined = undefined
|
||||
export let placeholderLabel: IntlString | undefined = undefined
|
||||
|
||||
$: employeeValue = typeof value === 'string' ? $employeeByIdStore.get(value) : value
|
||||
$: employeeValue = typeof value === 'string' ? $employeeByIdStore.get(value as Ref<Employee>) : value
|
||||
|
||||
const client = getClient()
|
||||
|
||||
|
||||
@@ -13,65 +13,74 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Attribute, Class, Data, Ref, Status } from '@hcengineering/core'
|
||||
import presentation, { Card, createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { DoneState, SpaceWithStates, State, createState } from '@hcengineering/task'
|
||||
import { EditBox, Label } from '@hcengineering/ui'
|
||||
import { Attribute, Class, Ref, Status, StatusCategory } from '@hcengineering/core'
|
||||
import presentation, { Card, getClient } from '@hcengineering/presentation'
|
||||
import { ProjectType, createState } from '@hcengineering/task'
|
||||
import { EditBox } from '@hcengineering/ui'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import task from '../plugin'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
export let status: State | undefined = undefined
|
||||
export let _class: Ref<Class<State | DoneState>> | undefined = status?._class
|
||||
export let status: Status | undefined = undefined
|
||||
export let _class: Ref<Class<Status>> | undefined = status?._class
|
||||
export let type: ProjectType
|
||||
export let ofAttribute: Ref<Attribute<Status>>
|
||||
export let category: Ref<StatusCategory> | undefined = status?.category
|
||||
export let value = status?.name ?? ''
|
||||
export let space: Ref<SpaceWithStates>
|
||||
|
||||
let _space: SpaceWithStates | undefined = undefined
|
||||
const query = createQuery()
|
||||
$: query.query(task.class.SpaceWithStates, { _id: space }, (res) => {
|
||||
_space = res[0]
|
||||
})
|
||||
|
||||
const canSave = true
|
||||
async function save () {
|
||||
if (_space === undefined || _class === undefined) return
|
||||
if (type === undefined || _class === undefined) return
|
||||
if (status === undefined) {
|
||||
if (!hierarchy.isDerived(_class, task.class.DoneState)) {
|
||||
const newDoc: Data<State> = {
|
||||
ofAttribute,
|
||||
name: value.trim(),
|
||||
const _id = await createState(client, _class, {
|
||||
ofAttribute,
|
||||
name: value.trim(),
|
||||
category,
|
||||
color: 9
|
||||
})
|
||||
|
||||
const states = type.statuses.map((p) => $statusStore.byId.get(p._id)).filter((p) => p !== undefined) as Status[]
|
||||
const lastIndex = states.findLastIndex((p) => p.category === category)
|
||||
const statuses = [
|
||||
...type.statuses.slice(0, lastIndex + 1),
|
||||
{
|
||||
_id,
|
||||
color: 9
|
||||
}
|
||||
const id = await createState(client, _class, newDoc)
|
||||
await client.update(_space, { $push: { states: id } })
|
||||
} else {
|
||||
const newDoc: Data<DoneState> = {
|
||||
ofAttribute,
|
||||
name: value.trim()
|
||||
}
|
||||
const id = await createState(client, _class, newDoc)
|
||||
await client.update(_space, { $push: { states: id } })
|
||||
}
|
||||
} else {
|
||||
const id = await createState(client, _class, { ...status, name: value.trim() })
|
||||
if (!hierarchy.isDerived(_class, task.class.DoneState)) {
|
||||
const states = _space.states
|
||||
const index = states.findIndex((x) => x === status?._id)
|
||||
if (index !== -1) {
|
||||
states[index] = id
|
||||
await client.update(_space, { states })
|
||||
}
|
||||
} else {
|
||||
const states = _space.doneStates ?? []
|
||||
const index = states.findIndex((x) => x === status?._id)
|
||||
if (index !== -1) {
|
||||
states[index] = id
|
||||
await client.update(_space, { doneStates: states })
|
||||
}
|
||||
}
|
||||
},
|
||||
...type.statuses.slice(lastIndex + 1)
|
||||
]
|
||||
await client.update(type, {
|
||||
statuses
|
||||
})
|
||||
} else if (status.name.trim() !== value.trim()) {
|
||||
const _id = await createState(client, _class, {
|
||||
ofAttribute,
|
||||
name: value.trim(),
|
||||
category,
|
||||
color: 9
|
||||
})
|
||||
const index = type.statuses.findIndex((p) => p._id === status?._id)
|
||||
const oldStatus = type.statuses[index]
|
||||
const statuses = [
|
||||
...type.statuses.slice(0, index),
|
||||
{
|
||||
_id,
|
||||
color: oldStatus.color
|
||||
},
|
||||
...type.statuses.slice(index + 1)
|
||||
]
|
||||
const projects = await client.findAll(task.class.Project, { type: type._id })
|
||||
const docs = await client.findAll(task.class.Task, {
|
||||
status: status._id,
|
||||
space: { $in: projects.map((p) => p._id) }
|
||||
})
|
||||
const op = client.apply(_id)
|
||||
await op.update(type, {
|
||||
statuses
|
||||
})
|
||||
docs.map((p) => op.update(p, { status: _id }))
|
||||
await op.commit()
|
||||
}
|
||||
dispatch('close')
|
||||
}
|
||||
@@ -86,9 +95,4 @@
|
||||
onCancel={() => dispatch('close')}
|
||||
>
|
||||
<EditBox focusIndex={1} bind:value placeholder={task.string.StatusName} kind={'large-style'} autoFocus fullSize />
|
||||
<svelte:fragment slot="error">
|
||||
{#if !canSave}
|
||||
<Label label={task.string.NameAlreadyExists} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
|
||||
@@ -1,85 +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 { Attribute, Class, Data, Ref, SortingOrder, Status } from '@hcengineering/core'
|
||||
import presentation, { Card, getClient } from '@hcengineering/presentation'
|
||||
import { DoneStateTemplate, KanbanTemplate, KanbanTemplateSpace, StateTemplate, calcRank } from '@hcengineering/task'
|
||||
import { EditBox, Label } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import task from '../plugin'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
export let status: StateTemplate | undefined = undefined
|
||||
export let _class: Ref<Class<StateTemplate | DoneStateTemplate>> | undefined = status?._class
|
||||
export let template: KanbanTemplate
|
||||
export let ofAttribute: Ref<Attribute<Status>>
|
||||
export let space: KanbanTemplateSpace
|
||||
export let value = status?.name ?? ''
|
||||
|
||||
let canSave = true
|
||||
async function save () {
|
||||
if (space === undefined && template === undefined && status?.space === undefined) return
|
||||
const attachedTo = { attachedTo: template._id }
|
||||
if (_class !== undefined && status === undefined) {
|
||||
const lastOne = await client.findOne(_class, attachedTo, { sort: { rank: SortingOrder.Descending } })
|
||||
let newDoc: Data<StateTemplate> = {
|
||||
ofAttribute,
|
||||
name: value.trim(),
|
||||
rank: calcRank(lastOne, undefined),
|
||||
...attachedTo
|
||||
}
|
||||
if (!hierarchy.isDerived(_class, task.class.DoneState)) {
|
||||
newDoc = {
|
||||
ofAttribute,
|
||||
name: value.trim(),
|
||||
color: 9,
|
||||
rank: calcRank(lastOne, undefined),
|
||||
...attachedTo
|
||||
}
|
||||
}
|
||||
const ops = client.apply(template.space).notMatch(_class, {
|
||||
space: template.space,
|
||||
name: value.trim(),
|
||||
...attachedTo
|
||||
})
|
||||
await ops.createDoc(_class, template.space, newDoc)
|
||||
canSave = await ops.commit()
|
||||
}
|
||||
if (status !== undefined && _class !== undefined) {
|
||||
const ops = client.apply(status._id).notMatch(_class, { space: status.space, name: value.trim(), ...attachedTo })
|
||||
await ops.update(status, { name: value.trim() })
|
||||
canSave = await ops.commit()
|
||||
}
|
||||
if (canSave) dispatch('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card
|
||||
label={task.string.StatusPopupTitle}
|
||||
okAction={save}
|
||||
canSave
|
||||
okLabel={presentation.string.Save}
|
||||
on:changeContent
|
||||
onCancel={() => dispatch('close')}
|
||||
>
|
||||
<EditBox focusIndex={1} bind:value placeholder={task.string.StatusName} kind={'large-style'} autoFocus fullSize />
|
||||
<svelte:fragment slot="error">
|
||||
{#if !canSave}
|
||||
<Label label={task.string.NameAlreadyExists} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
@@ -13,46 +13,29 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import core, { Class, DocumentQuery, Ref, SortingOrder, Timestamp } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import type { DoneState, SpaceWithStates, State, Task } from '@hcengineering/task'
|
||||
import task from '@hcengineering/task'
|
||||
import core, { Class, DocumentQuery, IdMap, Ref, Status, Timestamp } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import type { Project, Task } from '@hcengineering/task'
|
||||
import task, { getStates } from '@hcengineering/task'
|
||||
import { BarDashboard, DashboardItem } from '@hcengineering/ui'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
import CreateFilter from './CreateFilter.svelte'
|
||||
import { typeStore } from '..'
|
||||
|
||||
export let _class: Ref<Class<Task>>
|
||||
export let space: Ref<SpaceWithStates>
|
||||
export let space: Ref<Project>
|
||||
export let query: DocumentQuery<Task>
|
||||
|
||||
const client = getClient()
|
||||
const hieararchy = client.getHierarchy()
|
||||
|
||||
let states: State[] = []
|
||||
let project: Project | undefined = undefined
|
||||
let states: Status[] = []
|
||||
const statesQuery = createQuery()
|
||||
$: updateStates(space)
|
||||
|
||||
function updateStates (space: Ref<SpaceWithStates>): void {
|
||||
statesQuery.query(
|
||||
task.class.State,
|
||||
{ space },
|
||||
(result) => {
|
||||
states = result
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
rank: SortingOrder.Ascending
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
$: states = getStates(project, $typeStore, $statusStore.byId)
|
||||
|
||||
let wonStates: Set<Ref<DoneState>> = new Set<Ref<DoneState>>()
|
||||
const doneStatesQuery = createQuery()
|
||||
$: updateDoneStates(space)
|
||||
|
||||
function updateDoneStates (space: Ref<SpaceWithStates>): void {
|
||||
doneStatesQuery.query(task.class.DoneState, { space }, (result) => {
|
||||
wonStates = new Set(result.filter((p) => hieararchy.isDerived(p._class, task.class.WonState)).map((p) => p._id))
|
||||
function updateStates (space: Ref<Project>): void {
|
||||
statesQuery.query(task.class.Project, { _id: space }, (result) => {
|
||||
project = result[0]
|
||||
})
|
||||
}
|
||||
|
||||
@@ -60,7 +43,7 @@
|
||||
let ids: Ref<Task>[] = []
|
||||
const txQuery = createQuery()
|
||||
|
||||
function updateTxes (_class: Ref<Class<Task>>, space: Ref<SpaceWithStates>, modified: Timestamp | undefined): void {
|
||||
function updateTxes (_class: Ref<Class<Task>>, space: Ref<Project>, modified: Timestamp | undefined): void {
|
||||
if (modified === undefined) {
|
||||
ids = []
|
||||
return
|
||||
@@ -80,6 +63,7 @@
|
||||
}
|
||||
|
||||
let items: DashboardItem[] = []
|
||||
let tasks: Task[] = []
|
||||
|
||||
$: updateTxes(_class, space, modified)
|
||||
|
||||
@@ -92,7 +76,41 @@
|
||||
}
|
||||
: query
|
||||
|
||||
function updateDocs (_class: Ref<Class<Task>>, states: State[], query: DocumentQuery<Task>): void {
|
||||
$: group(tasks, $statusStore.byId)
|
||||
|
||||
function group (tasks: Task[], statuses: IdMap<Status>): void {
|
||||
const template: Map<Ref<Status>, DashboardItem> = new Map(
|
||||
states.map((p) => {
|
||||
return [
|
||||
p._id,
|
||||
{
|
||||
_id: p._id,
|
||||
label: p.name,
|
||||
values: [
|
||||
{ color: 10, value: 0 },
|
||||
{ color: 0, value: 0 },
|
||||
{ color: 11, value: 0 }
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
)
|
||||
for (const value of tasks) {
|
||||
const group = template.get(value.status)
|
||||
if (group === undefined) continue
|
||||
const status = statuses.get(value.status)
|
||||
if (status === undefined) continue
|
||||
if (status.category === task.statusCategory.Won) {
|
||||
group.values[1].value++
|
||||
} else if (status.category === task.statusCategory.Lost) {
|
||||
group.values[2].value++
|
||||
} else group.values[0].value++
|
||||
template.set(value.status, group)
|
||||
}
|
||||
items = Array.from(template.values())
|
||||
}
|
||||
|
||||
function updateDocs (_class: Ref<Class<Task>>, states: Status[], query: DocumentQuery<Task>): void {
|
||||
if (states.length === 0) {
|
||||
return
|
||||
}
|
||||
@@ -100,41 +118,11 @@
|
||||
_class,
|
||||
query,
|
||||
(result) => {
|
||||
const template: Map<Ref<State>, DashboardItem> = new Map(
|
||||
states.map((p) => {
|
||||
return [
|
||||
p._id,
|
||||
{
|
||||
_id: p._id,
|
||||
label: p.name,
|
||||
values: [
|
||||
{ color: 10, value: 0 },
|
||||
{ color: 0, value: 0 },
|
||||
{ color: 11, value: 0 }
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
)
|
||||
for (const value of result) {
|
||||
const group = template.get(value.status)
|
||||
if (group === undefined) continue
|
||||
if (value.doneState === null) {
|
||||
group.values[0].value++
|
||||
} else {
|
||||
const won = wonStates.has(value.doneState)
|
||||
if (won === undefined) continue
|
||||
const index = won ? 1 : 2
|
||||
group.values[index].value++
|
||||
}
|
||||
template.set(value.status, group)
|
||||
}
|
||||
items = Array.from(template.values())
|
||||
tasks = result
|
||||
},
|
||||
{
|
||||
projection: {
|
||||
status: 1,
|
||||
doneState: 1
|
||||
status: 1
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { DueDatePresenter, ButtonSize, ButtonKind } from '@hcengineering/ui'
|
||||
import { WithLookup } from '@hcengineering/core'
|
||||
import { Task } from '@hcengineering/task'
|
||||
import task, { Task } from '@hcengineering/task'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
|
||||
export let object: WithLookup<Task>
|
||||
export let width: string | undefined = undefined
|
||||
@@ -11,7 +12,9 @@
|
||||
export let editable: boolean = true
|
||||
|
||||
const client = getClient()
|
||||
$: shouldIgnoreOverdue = object.doneState !== null
|
||||
$: status = $statusStore.byId.get(object.status)
|
||||
|
||||
$: shouldIgnoreOverdue = status?.category === task.statusCategory.Lost || status?.category === task.statusCategory.Won
|
||||
|
||||
const handleDueDateChanged = async (newDueDate: number | undefined | null) => {
|
||||
if (newDueDate === undefined || object.dueDate === newDueDate) {
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import type { KanbanTemplate } from '@hcengineering/task'
|
||||
import type { ProjectType } from '@hcengineering/task'
|
||||
|
||||
export let value: KanbanTemplate
|
||||
export let value: ProjectType
|
||||
|
||||
const client = getClient()
|
||||
const shortLabel = client.getHierarchy().getClass(value._class).shortLabel
|
||||
@@ -24,5 +24,5 @@
|
||||
|
||||
<span class="label nowrap">
|
||||
{#if shortLabel}
|
||||
{shortLabel}-{/if}{value.title}</span
|
||||
{shortLabel}-{/if}{value.name}</span
|
||||
>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<script lang="ts">
|
||||
import core, { Attribute, Class, DocumentQuery, FindOptions, Ref, SortingOrder, Status } from '@hcengineering/core'
|
||||
import { ObjectPopup, createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Label, resizeObserver } from '@hcengineering/ui'
|
||||
import { ObjectPresenter } from '@hcengineering/view-resources'
|
||||
import view from '@hcengineering/view-resources/src/plugin'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import task from '../plugin'
|
||||
import { SpaceWithStates, Task } from '@hcengineering/task'
|
||||
import { Attribute, Class, IdMap, Ref, Status } from '@hcengineering/core'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { DocPopup, createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Project, ProjectType, Task, getStates } from '@hcengineering/task'
|
||||
import { ObjectPresenter, statusStore } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { typeStore } from '..'
|
||||
import task from '../plugin'
|
||||
|
||||
export let value: Task | Task[]
|
||||
export let width: 'medium' | 'large' | 'full' = 'medium'
|
||||
@@ -16,16 +15,8 @@
|
||||
export let _class: Ref<Class<Status>>
|
||||
export let embedded: boolean = false
|
||||
|
||||
const queryOptions: FindOptions<Status> = {
|
||||
lookup: {
|
||||
category: core.class.StatusCategory
|
||||
},
|
||||
sort: { category: SortingOrder.Ascending, name: SortingOrder.Ascending }
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
const h = client.getHierarchy()
|
||||
const changeStatus = async (newStatus: any) => {
|
||||
if (newStatus === undefined) {
|
||||
dispatch('close', undefined)
|
||||
@@ -34,10 +25,9 @@
|
||||
const docs = Array.isArray(value) ? value : [value]
|
||||
|
||||
const changed = (d: Task) => d.status !== newStatus
|
||||
const field = h.isDerived(_class, task.class.DoneState) ? 'doneState' : 'status'
|
||||
await Promise.all(
|
||||
docs.filter(changed).map((it) => {
|
||||
return client.update(it, { [field]: newStatus })
|
||||
return client.update(it, { status: newStatus })
|
||||
})
|
||||
)
|
||||
|
||||
@@ -50,70 +40,60 @@
|
||||
: undefined
|
||||
: value.status
|
||||
|
||||
let finalQuery: DocumentQuery<Status> = {}
|
||||
|
||||
let docMatch = true
|
||||
|
||||
$: _space = Array.isArray(value)
|
||||
? value.every((v) => v.space === (value as Array<Task>)[0].space)
|
||||
? (value as Array<Task>)[0].space
|
||||
: undefined
|
||||
: value.space
|
||||
|
||||
let project: SpaceWithStates | undefined
|
||||
let project: Project | undefined
|
||||
|
||||
const query = createQuery()
|
||||
$: _space
|
||||
? query.query(task.class.SpaceWithStates, { _id: _space as Ref<SpaceWithStates> }, (res) => (project = res[0]))
|
||||
? query.query(task.class.Project, { _id: _space as Ref<Project> }, (res) => (project = res[0]))
|
||||
: (project = undefined)
|
||||
|
||||
function updateQuery (space: SpaceWithStates | undefined): void {
|
||||
function updateStatuses (
|
||||
space: Project | undefined,
|
||||
types: IdMap<ProjectType>,
|
||||
store: IdMap<Status>,
|
||||
allStatuses: Status[]
|
||||
): void {
|
||||
if (space === undefined) {
|
||||
finalQuery = { ofAttribute }
|
||||
statuses = allStatuses.filter((p) => p.ofAttribute === ofAttribute)
|
||||
} else {
|
||||
finalQuery = {
|
||||
ofAttribute,
|
||||
_id: { $in: !h.isDerived(_class, task.class.DoneState) ? space.states : space?.doneStates }
|
||||
}
|
||||
statuses = getStates(space, types, store)
|
||||
}
|
||||
docMatch = true
|
||||
}
|
||||
|
||||
$: updateQuery(project)
|
||||
$: updateStatuses(project, $typeStore, $statusStore.byId, $statusStore.array)
|
||||
|
||||
let statuses: Status[] = []
|
||||
</script>
|
||||
|
||||
{#if docMatch}
|
||||
<ObjectPopup
|
||||
{_class}
|
||||
docQuery={finalQuery}
|
||||
options={queryOptions}
|
||||
allowDeselect={true}
|
||||
selected={current}
|
||||
on:close={(evt) => {
|
||||
changeStatus(evt.detail === null ? null : evt.detail?._id)
|
||||
}}
|
||||
{placeholder}
|
||||
{width}
|
||||
{embedded}
|
||||
on:changeContent
|
||||
>
|
||||
<svelte:fragment slot="item" let:item>
|
||||
<div class="flex-row-center flex-grow overflow-label">
|
||||
<ObjectPresenter
|
||||
objectId={item._id}
|
||||
_class={item._class}
|
||||
value={item}
|
||||
inline={false}
|
||||
noUnderline
|
||||
props={{ disabled: true, inline: false, size: 'small', avatarSize: 'smaller' }}
|
||||
/>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</ObjectPopup>
|
||||
{:else}
|
||||
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')}>
|
||||
<div class="flex-center w-60 h-18">
|
||||
<Label label={view.string.DontMatchCriteria} />
|
||||
<DocPopup
|
||||
{_class}
|
||||
objects={statuses}
|
||||
allowDeselect={true}
|
||||
selected={current}
|
||||
on:close={(evt) => {
|
||||
changeStatus(evt.detail === null ? null : evt.detail?._id)
|
||||
}}
|
||||
{placeholder}
|
||||
{width}
|
||||
{embedded}
|
||||
on:changeContent
|
||||
>
|
||||
<svelte:fragment slot="item" let:item>
|
||||
<div class="flex-row-center flex-grow overflow-label">
|
||||
<ObjectPresenter
|
||||
objectId={item._id}
|
||||
_class={item._class}
|
||||
value={item}
|
||||
inline={false}
|
||||
noUnderline
|
||||
props={{ disabled: true, inline: false, size: 'small', avatarSize: 'smaller' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</DocPopup>
|
||||
|
||||
@@ -14,42 +14,45 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Class, DocumentQuery, FindOptions, IdMap, Ref, Status } from '@hcengineering/core'
|
||||
import { Class, DocumentQuery, FindOptions, Ref, Status } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { DoneState, SpaceWithStates, State, Task } from '@hcengineering/task'
|
||||
import { Project, Task, getStates } from '@hcengineering/task'
|
||||
import type { TabItem } from '@hcengineering/ui'
|
||||
import { TabList, ScrollerBar } from '@hcengineering/ui'
|
||||
import { ScrollerBar, TabList } from '@hcengineering/ui'
|
||||
import { TableBrowser, statusStore } from '@hcengineering/view-resources'
|
||||
import { typeStore } from '..'
|
||||
import task from '../plugin'
|
||||
import StatesBar from './state/StatesBar.svelte'
|
||||
import Lost from './icons/Lost.svelte'
|
||||
import Won from './icons/Won.svelte'
|
||||
import StatesBar from './state/StatesBar.svelte'
|
||||
|
||||
export let _class: Ref<Class<Task>>
|
||||
export let space: Ref<SpaceWithStates>
|
||||
export let space: Ref<Project>
|
||||
export let query: DocumentQuery<Task>
|
||||
export let options: FindOptions<Task> | undefined
|
||||
export let config: string[]
|
||||
|
||||
let doneStatusesView: boolean = false
|
||||
let state: Ref<State> | undefined = undefined
|
||||
let _space: SpaceWithStates | undefined = undefined
|
||||
const selectedDoneStates: Set<Ref<DoneState>> = new Set<Ref<DoneState>>()
|
||||
let state: Ref<Status> | undefined = undefined
|
||||
let _space: Project | undefined = undefined
|
||||
const selectedDoneStates: Set<Ref<Status>> = new Set<Ref<Status>>()
|
||||
$: resConfig = updateConfig(config)
|
||||
$: doneStates = getDoneStates(_space, $statusStore)
|
||||
$: doneStates = getStates(_space, $typeStore, $statusStore.byId).filter(
|
||||
(p) => p.category && [task.statusCategory.Won, task.statusCategory.Lost].includes(p.category)
|
||||
)
|
||||
$: itemsDS = getItems(doneStates)
|
||||
let selectedDS: string[] = []
|
||||
let withoutDone: boolean = false
|
||||
let resultQuery: DocumentQuery<Task>
|
||||
let divScroll: HTMLElement
|
||||
|
||||
function getItems (doneStates: DoneState[]): TabItem[] {
|
||||
function getItems (doneStates: Status[]): TabItem[] {
|
||||
const itemsDS: TabItem[] = doneStates.map((s) => {
|
||||
return {
|
||||
id: s._id,
|
||||
label: s.name,
|
||||
icon: s._class === task.class.WonState ? Won : Lost,
|
||||
color: s._class === task.class.WonState ? 'var(--theme-won-color)' : 'var(--theme-lost-color)'
|
||||
icon: s.category === task.statusCategory.Won ? Won : Lost,
|
||||
color: s.category === task.statusCategory.Won ? 'var(--theme-won-color)' : 'var(--theme-lost-color)'
|
||||
}
|
||||
})
|
||||
itemsDS.unshift({ id: 'NoDoneState', labelIntl: task.string.NoDoneState })
|
||||
@@ -69,7 +72,7 @@
|
||||
const spaceQuery = createQuery()
|
||||
|
||||
$: spaceQuery.query(
|
||||
task.class.SpaceWithStates,
|
||||
task.class.Project,
|
||||
{
|
||||
_id: space
|
||||
},
|
||||
@@ -78,35 +81,28 @@
|
||||
}
|
||||
)
|
||||
|
||||
function getDoneStates (space: SpaceWithStates | undefined, statusStore: IdMap<Status>): DoneState[] {
|
||||
if (space === undefined) {
|
||||
return []
|
||||
}
|
||||
const doneStates = space.doneStates
|
||||
? space.doneStates.map((x) => statusStore.get(x) as DoneState).filter((p) => p !== undefined)
|
||||
: []
|
||||
return doneStates
|
||||
}
|
||||
|
||||
const client = getClient()
|
||||
|
||||
async function updateQuery (query: DocumentQuery<Task>, selectedDoneStates: Set<Ref<DoneState>>): Promise<void> {
|
||||
async function updateQuery (query: DocumentQuery<Task>, selectedDoneStates: Set<Ref<Status>>): Promise<void> {
|
||||
resConfig = updateConfig(config)
|
||||
const result = client.getHierarchy().clone(query)
|
||||
if (state) {
|
||||
result.status = state
|
||||
}
|
||||
if (selectedDoneStates.size > 0) {
|
||||
result.doneState = {
|
||||
$in: Array.from(selectedDoneStates)
|
||||
} else {
|
||||
if (selectedDoneStates.size > 0) {
|
||||
result.status = {
|
||||
$in: Array.from(selectedDoneStates)
|
||||
}
|
||||
} else if (withoutDone) {
|
||||
result.status = {
|
||||
$nin: doneStates.map((p) => p._id)
|
||||
}
|
||||
}
|
||||
} else if (withoutDone) {
|
||||
result.doneState = null
|
||||
}
|
||||
resultQuery = result
|
||||
}
|
||||
|
||||
function doneStateClick (id: Ref<DoneState>): void {
|
||||
function doneStateClick (id: Ref<Status>): void {
|
||||
withoutDone = false
|
||||
if (selectedDoneStates.has(id)) selectedDoneStates.delete(id)
|
||||
else selectedDoneStates.add(id)
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
return contact.mixin.Employee
|
||||
}
|
||||
|
||||
const taskKeys = ['status', 'assignee', 'doneState']
|
||||
const taskKeys = ['status', 'assignee']
|
||||
|
||||
$: filtredKeys = keys.filter((p) => !taskKeys.includes(p.key)) // todo
|
||||
</script>
|
||||
@@ -76,7 +76,7 @@
|
||||
<AttributesBar {object} _class={object._class} keys={filtredKeys} />
|
||||
</div>
|
||||
</div>
|
||||
<AttributesBar {object} _class={object._class} keys={['doneState', 'status']} showHeader={false} />
|
||||
<AttributesBar {object} _class={object._class} keys={['status']} showHeader={false} />
|
||||
</div>
|
||||
{:else}
|
||||
<DocAttributeBar {object} {ignoreKeys} {mixins} {allowedCollections} on:update />
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
<!--
|
||||
// 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 { Class, Doc, DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
|
||||
import { IntlString, getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Project, ProjectType, ProjectTypeCategory } from '@hcengineering/task'
|
||||
import {
|
||||
AnyComponent,
|
||||
Button,
|
||||
Component,
|
||||
IModeSelector,
|
||||
IconAdd,
|
||||
Label,
|
||||
Loading,
|
||||
ModeSelector,
|
||||
SearchEdit,
|
||||
resolvedLocationStore,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { ViewOptions, Viewlet, ViewletDescriptor, ViewletPreference } from '@hcengineering/view'
|
||||
import { FilterBar, FilterButton, ViewletSelector, ViewletSettingButton } from '@hcengineering/view-resources'
|
||||
import task from '../plugin'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let space: Ref<Space> | undefined = undefined
|
||||
export let label: IntlString
|
||||
export let createLabel: IntlString | undefined
|
||||
export let createComponent: AnyComponent | undefined
|
||||
export let createComponentProps: Record<string, any> = {}
|
||||
export let isCreationDisabled = false
|
||||
export let descriptors: Ref<ViewletDescriptor>[] | undefined = undefined
|
||||
export let baseQuery: DocumentQuery<Doc> | undefined = undefined
|
||||
export let category: Ref<ProjectTypeCategory>
|
||||
|
||||
let search = ''
|
||||
let viewlet: WithLookup<Viewlet> | undefined
|
||||
|
||||
let preference: ViewletPreference | undefined
|
||||
let viewlets: WithLookup<Viewlet>[] = []
|
||||
let viewOptions: ViewOptions | undefined
|
||||
let types: ProjectType[] = []
|
||||
|
||||
const typeQ = createQuery()
|
||||
$: typeQ.query(task.class.ProjectType, { category, archived: false }, (result) => {
|
||||
types = result
|
||||
})
|
||||
|
||||
const spacesQ = createQuery()
|
||||
let spaces: Project[] = []
|
||||
$: spacesQ.query(task.class.Project, { type: mode as Ref<ProjectType> }, (result) => {
|
||||
spaces = result
|
||||
})
|
||||
|
||||
$: query = { ...(baseQuery ?? {}), ...(viewlet?.baseQuery ?? {}), space: { $in: spaces.map((it) => it._id) } }
|
||||
$: searchQuery = search === '' ? query : { ...query, $search: search }
|
||||
$: resultQuery = searchQuery
|
||||
|
||||
function showCreateDialog () {
|
||||
if (createComponent === undefined) return
|
||||
showPopup(createComponent, createComponentProps, 'top')
|
||||
}
|
||||
|
||||
$: mode = $resolvedLocationStore.query?.mode ?? undefined
|
||||
|
||||
let config: Array<[string, IntlString, object]> = []
|
||||
$: config = types.map((p) => {
|
||||
return [p._id, getEmbeddedLabel(p.name), {}]
|
||||
})
|
||||
let modeSelectorProps: IModeSelector | undefined = undefined
|
||||
$: if (mode === undefined && config.length > 0) {
|
||||
;[[mode]] = config
|
||||
}
|
||||
$: if (mode !== undefined) {
|
||||
modeSelectorProps = {
|
||||
mode,
|
||||
config,
|
||||
onChange: (_mode: string) => {
|
||||
mode = _mode
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="ac-header full divide caption-height">
|
||||
<div class="ac-header__wrap-title mr-3">
|
||||
<span class="ac-header__title"><Label {label} /></span>
|
||||
{#if modeSelectorProps !== undefined && modeSelectorProps.config.length > 1}
|
||||
<ModeSelector props={modeSelectorProps} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="ac-header-full medium-gap mb-1">
|
||||
<ViewletSelector
|
||||
bind:viewlet
|
||||
bind:preference
|
||||
bind:viewlets
|
||||
viewletQuery={{
|
||||
attachTo: _class,
|
||||
variant: { $exists: false },
|
||||
...(descriptors !== undefined ? { descriptor: { $in: descriptors } } : {})
|
||||
}}
|
||||
/>
|
||||
{#if createLabel && createComponent}
|
||||
<Button
|
||||
icon={IconAdd}
|
||||
label={createLabel}
|
||||
kind={'primary'}
|
||||
disabled={isCreationDisabled}
|
||||
on:click={() => showCreateDialog()}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ac-header full divide search-start">
|
||||
<div class="ac-header-full small-gap">
|
||||
<SearchEdit bind:value={search} />
|
||||
<!-- <ActionIcon icon={IconMoreH} size={'small'} /> -->
|
||||
<div class="buttons-divider" />
|
||||
<FilterButton {_class} />
|
||||
</div>
|
||||
<div class="ac-header-full medium-gap">
|
||||
<ViewletSettingButton bind:viewOptions bind:viewlet />
|
||||
<!-- <ActionIcon icon={IconMoreH} size={'small'} /> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !viewlet?.$lookup?.descriptor?.component || viewlet?.attachTo !== _class || (preference !== undefined && viewlet?._id !== preference.attachedTo)}
|
||||
<Loading />
|
||||
{:else if viewOptions && viewlet}
|
||||
<FilterBar
|
||||
{_class}
|
||||
{space}
|
||||
query={searchQuery}
|
||||
{viewOptions}
|
||||
on:change={(e) => {
|
||||
resultQuery = { ...query, ...e.detail }
|
||||
}}
|
||||
/>
|
||||
<Component
|
||||
is={viewlet.$lookup.descriptor.component}
|
||||
props={{
|
||||
_class,
|
||||
space,
|
||||
options: viewlet.options,
|
||||
config: preference?.config ?? viewlet.config,
|
||||
viewlet,
|
||||
viewOptions,
|
||||
viewOptionsConfig: viewlet.viewOptions?.other,
|
||||
createItemDialog: createComponent,
|
||||
createItemLabel: createLabel,
|
||||
query: resultQuery,
|
||||
totalQuery: query
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
@@ -13,39 +13,34 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { IdMap, Ref, Status } from '@hcengineering/core'
|
||||
import { Ref, Status } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import type { SpaceWithStates } from '@hcengineering/task'
|
||||
import task, { DoneState, LostState, WonState } from '@hcengineering/task'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Won from '../icons/Won.svelte'
|
||||
import Lost from '../icons/Lost.svelte'
|
||||
import type { Project } from '@hcengineering/task'
|
||||
import task, { getStates } from '@hcengineering/task'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { typeStore } from '../..'
|
||||
import Lost from '../icons/Lost.svelte'
|
||||
import Won from '../icons/Won.svelte'
|
||||
|
||||
export let space: Ref<SpaceWithStates>
|
||||
let wonStates: WonState[] = []
|
||||
let lostStates: LostState[] = []
|
||||
let _space: SpaceWithStates | undefined = undefined
|
||||
export let space: Ref<Project>
|
||||
let wonStates: Status[] = []
|
||||
let lostStates: Status[] = []
|
||||
let _space: Project | undefined = undefined
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const query = createQuery()
|
||||
query.query(task.class.SpaceWithStates, { _id: space }, (result) => {
|
||||
query.query(task.class.Project, { _id: space }, (result) => {
|
||||
_space = result[0]
|
||||
})
|
||||
|
||||
function getStates (space: SpaceWithStates | undefined, statusStore: IdMap<Status>): void {
|
||||
if (space === undefined) return
|
||||
const result: Status[] =
|
||||
(space.doneStates?.map((p) => statusStore.get(p))?.filter((p) => p !== undefined) as Status[]) ?? []
|
||||
wonStates = result.filter((x) => x._class === task.class.WonState)
|
||||
lostStates = result.filter((x) => x._class === task.class.LostState)
|
||||
}
|
||||
$: states = getStates(_space, $typeStore, $statusStore.byId)
|
||||
$: wonStates = states.filter((x) => x.category === task.statusCategory.Won)
|
||||
$: lostStates = states.filter((x) => x.category === task.statusCategory.Lost)
|
||||
|
||||
$: getStates(_space, $statusStore)
|
||||
let hoveredDoneState: Ref<Status> | undefined
|
||||
|
||||
let hoveredDoneState: Ref<DoneState> | undefined
|
||||
|
||||
const onDone = (state: DoneState) => async () => {
|
||||
const onDone = (state: Status) => async () => {
|
||||
hoveredDoneState = undefined
|
||||
dispatch('done', state)
|
||||
}
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 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 core, { Ref, SortingOrder, Space } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import type {
|
||||
DoneState,
|
||||
DoneStateTemplate,
|
||||
KanbanTemplate,
|
||||
KanbanTemplateSpace,
|
||||
State,
|
||||
StateTemplate
|
||||
} from '@hcengineering/task'
|
||||
import task, { calcRank } from '@hcengineering/task'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import StatesTemplateEditor from '../state/StatesTemplateEditor.svelte'
|
||||
|
||||
export let kanban: KanbanTemplate
|
||||
export let folder: KanbanTemplateSpace
|
||||
|
||||
let states: StateTemplate[] = []
|
||||
let doneStates: DoneStateTemplate[] = []
|
||||
let wonStates: DoneStateTemplate[] = []
|
||||
let lostStates: DoneStateTemplate[] = []
|
||||
$: wonStates = doneStates.filter((x) => x._class === task.class.WonStateTemplate)
|
||||
$: lostStates = doneStates.filter((x) => x._class === task.class.LostStateTemplate)
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
|
||||
const statesQ = createQuery()
|
||||
$: statesQ.query(
|
||||
task.class.StateTemplate,
|
||||
{ attachedTo: kanban._id },
|
||||
(result) => {
|
||||
states = result
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
rank: SortingOrder.Ascending
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const doneStatesQ = createQuery()
|
||||
$: doneStatesQ.query(
|
||||
task.class.DoneStateTemplate,
|
||||
{ attachedTo: kanban._id },
|
||||
(result) => {
|
||||
doneStates = result
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
rank: SortingOrder.Ascending
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
let space: Space | undefined
|
||||
const spaceQ = createQuery()
|
||||
$: spaceQ.query(core.class.Space, { _id: kanban.space }, (result) => {
|
||||
space = result[0]
|
||||
})
|
||||
|
||||
async function onMove ({
|
||||
detail: { stateID, position }
|
||||
}: {
|
||||
detail: { stateID: Ref<StateTemplate>; position: number }
|
||||
}) {
|
||||
const [prev, next] = [states[position - 1], states[position + 1]]
|
||||
const state = states.find((x) => x._id === stateID)
|
||||
|
||||
if (state === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
await client.updateDoc(state._class, state.space, state._id, {
|
||||
rank: calcRank(prev, next)
|
||||
})
|
||||
}
|
||||
|
||||
function onDelete ({ detail: { state } }: { detail: { state: State | DoneState } }) {
|
||||
if (space === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
dispatch('delete', { state })
|
||||
}
|
||||
</script>
|
||||
|
||||
<StatesTemplateEditor
|
||||
template={kanban}
|
||||
space={folder}
|
||||
{states}
|
||||
{wonStates}
|
||||
{lostStates}
|
||||
on:delete={onDelete}
|
||||
on:move={onMove}
|
||||
/>
|
||||
@@ -14,7 +14,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import {
|
||||
import core, {
|
||||
CategoryType,
|
||||
Class,
|
||||
Doc,
|
||||
@@ -27,7 +27,7 @@
|
||||
import { Item, Kanban as KanbanUI } from '@hcengineering/kanban'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { createQuery, getClient, ActionContext } from '@hcengineering/presentation'
|
||||
import { SpaceWithStates, Task, TaskGrouping, TaskOrdering } from '@hcengineering/task'
|
||||
import { Project, Task, TaskGrouping, TaskOrdering } from '@hcengineering/task'
|
||||
import {
|
||||
ColorDefinition,
|
||||
defaultBackground,
|
||||
@@ -64,7 +64,7 @@
|
||||
import KanbanDragDone from './KanbanDragDone.svelte'
|
||||
|
||||
export let _class: Ref<Class<Task>>
|
||||
export let space: Ref<SpaceWithStates> | undefined = undefined
|
||||
export let space: Ref<Project> | undefined = undefined
|
||||
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
|
||||
export let query: DocumentQuery<Task> = {}
|
||||
export let viewOptionsConfig: ViewOptionModel[] | undefined
|
||||
@@ -137,9 +137,8 @@
|
||||
...options,
|
||||
lookup: {
|
||||
...options?.lookup,
|
||||
space: task.class.SpaceWithStates,
|
||||
status: task.class.State,
|
||||
doneState: task.class.DoneState
|
||||
space: task.class.Project,
|
||||
status: core.class.Status
|
||||
},
|
||||
sort: {
|
||||
...options?.sort,
|
||||
@@ -161,7 +160,7 @@
|
||||
|
||||
async function updateCategories (
|
||||
_class: Ref<Class<Doc>>,
|
||||
space: Ref<SpaceWithStates> | undefined,
|
||||
space: Ref<Project> | undefined,
|
||||
docs: Doc[],
|
||||
groupByKey: string,
|
||||
viewOptions: ViewOptions,
|
||||
@@ -226,7 +225,7 @@
|
||||
$: presenterMixin = client.getHierarchy().as(clazz, task.mixin.KanbanCard)
|
||||
$: cardPresenter = getResource(presenterMixin.card)
|
||||
|
||||
const getDoneUpdate = (e: any) => ({ doneState: e.detail._id } as DocumentUpdate<Doc>)
|
||||
const getDoneUpdate = (e: any) => ({ status: e.detail._id } as DocumentUpdate<Doc>)
|
||||
</script>
|
||||
|
||||
{#await cardPresenter then presenter}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021, 2022, 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 { Ref, Status } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { ProjectType, ProjectTypeCategory } from '@hcengineering/task'
|
||||
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
import StatesProjectEditor from '../state/StatesProjectEditor.svelte'
|
||||
|
||||
export let type: ProjectType
|
||||
export let category: ProjectTypeCategory
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let states: Status[] = []
|
||||
$: states = type.statuses.map((p) => $statusStore.byId.get(p._id)).filter((p) => p !== undefined) as Status[]
|
||||
|
||||
async function onMove ({ detail: { stateID, position } }: { detail: { stateID: Ref<Status>; position: number } }) {
|
||||
const index = type.statuses.findIndex((p) => p._id === stateID)
|
||||
const state = type.statuses.splice(index, 1)[0]
|
||||
|
||||
const statuses = [...type.statuses.slice(0, position), state, ...type.statuses.slice(position)]
|
||||
await client.update(type, {
|
||||
statuses
|
||||
})
|
||||
}
|
||||
|
||||
async function onDelete ({ detail: { state } }: { detail: { state: Status } }) {
|
||||
const index = type.statuses.findIndex((p) => p._id === state._id)
|
||||
type.statuses.splice(index, 1)
|
||||
await client.update(type, {
|
||||
statuses: type.statuses
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<StatesProjectEditor {type} {category} {states} on:delete={onDelete} on:move={onMove} />
|
||||
+17
-18
@@ -13,40 +13,38 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Doc, Ref, Space } from '@hcengineering/core'
|
||||
import type { Ref } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import type { KanbanTemplate, KanbanTemplateSpace } from '@hcengineering/task'
|
||||
import type { ProjectType, ProjectTypeCategory } from '@hcengineering/task'
|
||||
import task from '@hcengineering/task'
|
||||
import type { DropdownTextItem } from '@hcengineering/ui'
|
||||
import { DropdownLabels, ButtonKind, ButtonSize } from '@hcengineering/ui'
|
||||
import { ButtonKind, ButtonSize, DropdownLabels } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import plugin from '../../plugin'
|
||||
|
||||
export let folders: Ref<KanbanTemplateSpace>[]
|
||||
export let template: Ref<KanbanTemplate> | undefined = undefined
|
||||
export let categories: Ref<ProjectTypeCategory>[]
|
||||
export let type: Ref<ProjectType> | undefined = undefined
|
||||
export let kind: ButtonKind = 'no-border'
|
||||
export let size: ButtonSize = 'small'
|
||||
export let focusIndex = -1
|
||||
export let disabled: boolean = false
|
||||
|
||||
let templates: KanbanTemplate[] = []
|
||||
const templatesQ = createQuery()
|
||||
$: templatesQ.query(
|
||||
task.class.KanbanTemplate,
|
||||
{ space: { $in: folders as Ref<Doc>[] as Ref<Space>[] } },
|
||||
(result) => {
|
||||
templates = result
|
||||
}
|
||||
)
|
||||
let types: ProjectType[] = []
|
||||
const typesQ = createQuery()
|
||||
const query = disabled ? { category: { $in: categories } } : { category: { $in: categories }, archived: false }
|
||||
$: typesQ.query(task.class.ProjectType, query, (result) => {
|
||||
types = result
|
||||
})
|
||||
|
||||
let items: DropdownTextItem[] = []
|
||||
$: items = templates.map((x) => ({ id: x._id, label: x.title }))
|
||||
$: items = types.map((x) => ({ id: x._id, label: x.name }))
|
||||
|
||||
let selectedItem: string | undefined
|
||||
let selectedItem: string | undefined = type
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
$: {
|
||||
template = selectedItem === undefined ? undefined : (selectedItem as Ref<KanbanTemplate>)
|
||||
dispatch('change', template)
|
||||
type = selectedItem === undefined ? undefined : (selectedItem as Ref<ProjectType>)
|
||||
dispatch('change', type)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -55,6 +53,7 @@
|
||||
{items}
|
||||
{kind}
|
||||
{size}
|
||||
{disabled}
|
||||
icon={task.icon.ManageTemplates}
|
||||
bind:selected={selectedItem}
|
||||
label={plugin.string.States}
|
||||
@@ -1,88 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 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 { createQuery } from '@hcengineering/presentation'
|
||||
import { DoneState, SpaceWithStates } from '@hcengineering/task'
|
||||
import { Label, showPopup, Button } from '@hcengineering/ui'
|
||||
import type { ButtonKind, ButtonSize } from '@hcengineering/ui'
|
||||
import DoneStatePresenter from './DoneStatePresenter.svelte'
|
||||
import DoneStatesPopup from './DoneStatesPopup.svelte'
|
||||
import task from '../../plugin'
|
||||
import Unknown from '../icons/Unknown.svelte'
|
||||
|
||||
export let value: Ref<DoneState> | null | undefined
|
||||
export let onChange: (value: any) => void
|
||||
export let space: Ref<SpaceWithStates> | undefined
|
||||
export let kind: ButtonKind = 'no-border'
|
||||
export let size: ButtonSize = 'small'
|
||||
export let justify: 'left' | 'center' = 'center'
|
||||
export let width: string = 'min-content'
|
||||
|
||||
let state: DoneState | undefined
|
||||
let container: HTMLButtonElement
|
||||
let opened: boolean = false
|
||||
|
||||
const query = createQuery()
|
||||
$: if (value != null) {
|
||||
query.query(
|
||||
task.class.DoneState,
|
||||
{ _id: value },
|
||||
(res) => {
|
||||
state = res[0]
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
} else {
|
||||
query.unsubscribe()
|
||||
state = undefined
|
||||
}
|
||||
</script>
|
||||
|
||||
<Button
|
||||
{kind}
|
||||
{size}
|
||||
{justify}
|
||||
{width}
|
||||
bind:input={container}
|
||||
on:click={() => {
|
||||
if (!opened) {
|
||||
opened = true
|
||||
showPopup(DoneStatesPopup, { space }, container, (result) => {
|
||||
if (result && result._id !== value) {
|
||||
value = result._id
|
||||
onChange(value)
|
||||
} else if (result === null && value !== null) {
|
||||
value = null
|
||||
onChange(value)
|
||||
}
|
||||
|
||||
opened = false
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="content">
|
||||
{#if state}
|
||||
<div class="pointer-events-none"><DoneStatePresenter value={state} showTitle /></div>
|
||||
{:else}
|
||||
<div class="flex-row-center pointer-events-none">
|
||||
<div class="content-dark-color mr-2"><Unknown size={'small'} /></div>
|
||||
<span class="overflow-label"><Label label={task.string.NoDoneState} /></span>
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
@@ -1,41 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 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 { DoneState } from '@hcengineering/task'
|
||||
import task from '@hcengineering/task'
|
||||
import { PaletteColorIndexes, getPlatformColorDef, themeStore } from '@hcengineering/ui'
|
||||
import Lost from '../icons/Lost.svelte'
|
||||
import Won from '../icons/Won.svelte'
|
||||
|
||||
export let value: DoneState | null | undefined
|
||||
export let showTitle: boolean = true
|
||||
|
||||
$: color =
|
||||
value?._class === task.class.WonState
|
||||
? getPlatformColorDef(PaletteColorIndexes.Crocodile, $themeStore.dark)
|
||||
: getPlatformColorDef(PaletteColorIndexes.Firework, $themeStore.dark)
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<div class="flex-center">
|
||||
<div class:mr-2={showTitle} style="color: {color.color};">
|
||||
<svelte:component this={value._class === task.class.WonState ? Won : Lost} size={'small'} />
|
||||
</div>
|
||||
{#if showTitle}
|
||||
{value.name}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1,45 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 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, Status, StatusValue } from '@hcengineering/core'
|
||||
import { DoneState, SpaceWithStates } from '@hcengineering/task'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
import task from '../../plugin'
|
||||
import Unknown from '../icons/Unknown.svelte'
|
||||
import DoneStateEditor from './DoneStateEditor.svelte'
|
||||
import DoneStatePresenter from './DoneStatePresenter.svelte'
|
||||
|
||||
export let value: Ref<DoneState> | StatusValue
|
||||
export let space: Ref<SpaceWithStates> | undefined
|
||||
export let showTitle: boolean = true
|
||||
export let onChange: ((value: Ref<DoneState>) => void) | undefined = undefined
|
||||
|
||||
$: state = $statusStore.get(typeof value === 'string' ? value : (value?.values?.[0]?._id as Ref<Status>))
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
{#if onChange !== undefined && state !== undefined}
|
||||
<DoneStateEditor value={state._id} {space} {onChange} kind="link" size="medium" />
|
||||
{:else}
|
||||
<DoneStatePresenter value={state} {showTitle} />
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex-row-center pointer-events-none">
|
||||
<div class="content-dark-color mr-2"><Unknown size={'small'} /></div>
|
||||
<span class="overflow-label"><Label label={task.string.NoDoneState} /></span>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1,85 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Class, Doc, IdMap, Ref, Status } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { DoneState, SpaceWithStates } from '@hcengineering/task'
|
||||
import { Label, PaletteColorIndexes, getPlatformColor, resizeObserver, themeStore } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import task from '../../plugin'
|
||||
import Lost from '../icons/Lost.svelte'
|
||||
import Unknown from '../icons/Unknown.svelte'
|
||||
import Won from '../icons/Won.svelte'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
|
||||
export let space: Ref<SpaceWithStates>
|
||||
let states: DoneState[] = []
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let _space: SpaceWithStates
|
||||
|
||||
function getStates (space: SpaceWithStates | undefined, statesStore: IdMap<Status>): void {
|
||||
if (space === undefined) return
|
||||
const res: Status[] =
|
||||
(space.doneStates?.map((p) => statesStore.get(p))?.filter((p) => p !== undefined) as Status[]) ?? []
|
||||
res.sort((a, b) => a._class.localeCompare(b._class))
|
||||
states = res
|
||||
}
|
||||
|
||||
$: getStates(_space, $statusStore)
|
||||
|
||||
const spaceQuery = createQuery()
|
||||
spaceQuery.query(task.class.SpaceWithStates, { _id: space }, (res) => {
|
||||
_space = res[0]
|
||||
})
|
||||
|
||||
function getColor (_class: Ref<Class<Doc>>): string {
|
||||
return _class === task.class.WonState
|
||||
? getPlatformColor(PaletteColorIndexes.Crocodile, $themeStore.dark)
|
||||
: getPlatformColor(PaletteColorIndexes.Firework, $themeStore.dark)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')}>
|
||||
<div class="menu-space" />
|
||||
<div class="scroll">
|
||||
<div class="box">
|
||||
{#each states as state}
|
||||
<button
|
||||
class="menu-item"
|
||||
on:click={() => {
|
||||
dispatch('close', state)
|
||||
}}
|
||||
>
|
||||
<div class="mr-2" style="color: {getColor(state._class)}">
|
||||
<svelte:component this={state._class === task.class.WonState ? Won : Lost} size={'small'} />
|
||||
</div>
|
||||
<span class="label">{state.name}</span>
|
||||
</button>
|
||||
{/each}
|
||||
<button
|
||||
class="menu-item"
|
||||
on:click={() => {
|
||||
dispatch('close', null)
|
||||
}}
|
||||
>
|
||||
<div class="content-dark-color mr-2"><Unknown size={'small'} /></div>
|
||||
<span class="overflow-label"><Label label={task.string.NoDoneState} /></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-space" />
|
||||
</div>
|
||||
@@ -1,169 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 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 { Attribute, Doc, DocumentQuery, IdMap, Ref, Status } from '@hcengineering/core'
|
||||
import { MessageBox, createQuery, getClient } from '@hcengineering/presentation'
|
||||
import type { DoneState, LostState, SpaceWithStates, State, WonState } from '@hcengineering/task'
|
||||
import { Icon, Label, Panel, Scroller, showPopup } from '@hcengineering/ui'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import task from '../../plugin'
|
||||
import StatesEditor from './StatesEditor.svelte'
|
||||
|
||||
export let _id: Ref<SpaceWithStates>
|
||||
export let ofAttribute: Ref<Attribute<Status>>
|
||||
export let doneOfAttribute: Ref<Attribute<Status>>
|
||||
|
||||
let spaceInstance: SpaceWithStates | undefined
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const spaceI = createQuery()
|
||||
$: spaceI.query<SpaceWithStates>(task.class.SpaceWithStates, { _id }, (result) => {
|
||||
spaceInstance = result[0]
|
||||
})
|
||||
|
||||
$: spaceClass = spaceInstance ? hierarchy.getClass(spaceInstance._class) : undefined
|
||||
|
||||
$: [states, doneStates] = getStates(spaceInstance, $statusStore)
|
||||
|
||||
$: wonStates = doneStates.filter((x) => x._class === task.class.WonState) as WonState[]
|
||||
$: lostStates = doneStates.filter((x) => x._class === task.class.LostState) as LostState[]
|
||||
|
||||
function getStates (space: SpaceWithStates | undefined, statusStore: IdMap<Status>): [Status[], DoneState[]] {
|
||||
if (space === undefined) {
|
||||
return [[], []]
|
||||
}
|
||||
|
||||
const states = space.states.map((x) => statusStore.get(x) as Status).filter((p) => p !== undefined)
|
||||
const doneStates = space.doneStates
|
||||
? space.doneStates.map((x) => statusStore.get(x) as DoneState).filter((p) => p !== undefined)
|
||||
: []
|
||||
|
||||
return [states, doneStates]
|
||||
}
|
||||
|
||||
async function deleteState ({ state }: { state: State | DoneState }) {
|
||||
if (spaceInstance === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
let query: DocumentQuery<Doc>
|
||||
if (hierarchy.isDerived(state._class, task.class.DoneState)) {
|
||||
query = { doneState: state._id, space: _id }
|
||||
} else {
|
||||
query = { status: state._id, space: _id }
|
||||
}
|
||||
|
||||
const objectsInThisState = await client.findAll(task.class.Task, query)
|
||||
|
||||
if (objectsInThisState.length > 0) {
|
||||
showPopup(MessageBox, {
|
||||
label: task.string.CantStatusDelete,
|
||||
message: task.string.CantStatusDeleteError,
|
||||
canSubmit: false
|
||||
})
|
||||
} else {
|
||||
showPopup(
|
||||
MessageBox,
|
||||
{
|
||||
label: task.string.StatusDelete,
|
||||
message: task.string.StatusDeleteConfirm
|
||||
},
|
||||
undefined,
|
||||
async (result) => {
|
||||
if (result !== undefined) {
|
||||
if (hierarchy.isDerived(state._class, task.class.DoneState)) {
|
||||
const index = doneStates.findIndex((x) => x._id === state._id)
|
||||
if (index === -1) {
|
||||
return
|
||||
}
|
||||
states.splice(index, 1)
|
||||
if (spaceInstance) {
|
||||
await client.update(spaceInstance, { doneStates: states.map((x) => x._id) })
|
||||
}
|
||||
} else {
|
||||
const index = states.findIndex((x) => x._id === state._id)
|
||||
if (index === -1) {
|
||||
return
|
||||
}
|
||||
states.splice(index, 1)
|
||||
if (spaceInstance) {
|
||||
await client.update(spaceInstance, { states: states.map((x) => x._id) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function onMove (stateID: Ref<State>, position: number) {
|
||||
const index = states.findIndex((x) => x._id === stateID)
|
||||
if (index === -1) {
|
||||
return
|
||||
}
|
||||
const elem = states.splice(index, 1)
|
||||
states = [...states.slice(0, position), elem[0], ...states.slice(position)]
|
||||
if (spaceInstance) {
|
||||
await client.update(spaceInstance, { states: states.map((x) => x._id) })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Panel
|
||||
isHeader={false}
|
||||
isAside={false}
|
||||
isFullSize
|
||||
on:fullsize
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="title">
|
||||
<div class="antiTitle icon-wrapper">
|
||||
<div class="wrapped-icon">
|
||||
<Icon icon={task.icon.ManageTemplates} size={'small'} />
|
||||
</div>
|
||||
<div class="title-wrapper">
|
||||
<span class="wrapped-title">
|
||||
<Label label={task.string.ManageStatusesWithin} />
|
||||
{#if spaceClass}<Label label={spaceClass?.label} />{:else}...{/if}
|
||||
</span>
|
||||
{#if spaceInstance?.name}<span class="wrapped-subtitle">{spaceInstance?.name}</span>{/if}
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
<Scroller>
|
||||
<div class="popupPanel-body__main-content py-10 clear-mins">
|
||||
{#if spaceInstance}
|
||||
<StatesEditor
|
||||
space={_id}
|
||||
{ofAttribute}
|
||||
{doneOfAttribute}
|
||||
{states}
|
||||
{wonStates}
|
||||
{lostStates}
|
||||
on:delete={(e) => deleteState(e.detail)}
|
||||
on:move={(e) => onMove(e.detail.stateID, e.detail.position)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</Scroller>
|
||||
</Panel>
|
||||
@@ -14,16 +14,16 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref, Space } from '@hcengineering/core'
|
||||
import { State } from '@hcengineering/task'
|
||||
import { Ref, Status } from '@hcengineering/core'
|
||||
import { Project } from '@hcengineering/task'
|
||||
import type { ButtonKind, ButtonSize } from '@hcengineering/ui'
|
||||
import { Button, eventToHTMLElement, showPopup } from '@hcengineering/ui'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
import StatePresenter from './StatePresenter.svelte'
|
||||
import StatesPopup from './StatesPopup.svelte'
|
||||
|
||||
export let value: Ref<State>
|
||||
export let space: Ref<Space>
|
||||
export let value: Ref<Status>
|
||||
export let space: Ref<Project>
|
||||
export let onChange: (value: any) => void
|
||||
export let kind: ButtonKind = 'no-border'
|
||||
export let size: ButtonSize = 'small'
|
||||
@@ -33,7 +33,7 @@
|
||||
export let shrink: number = 0
|
||||
export let disabled: boolean = false
|
||||
|
||||
$: state = $statusStore.get(value)
|
||||
$: state = $statusStore.byId.get(value)
|
||||
let opened: boolean = false
|
||||
|
||||
const handleClick = (ev: MouseEvent) => {
|
||||
@@ -51,13 +51,13 @@
|
||||
</script>
|
||||
|
||||
{#if kind === 'list' || kind === 'list-header'}
|
||||
<StatePresenter value={state} {shouldShowName} {disabled} shouldShowTooltip on:click={handleClick} />
|
||||
<StatePresenter value={state} {space} {shouldShowName} {disabled} shouldShowTooltip on:click={handleClick} />
|
||||
{:else}
|
||||
<Button {kind} {size} {width} {justify} {shrink} on:click={handleClick}>
|
||||
<svelte:fragment slot="content">
|
||||
{#if state}
|
||||
<div class="pointer-events-none clear-mins">
|
||||
<StatePresenter value={state} {shouldShowName} {disabled} />
|
||||
<StatePresenter value={state} {space} {shouldShowName} {disabled} />
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { State } from '@hcengineering/task'
|
||||
import core, { IdMap, Ref, Status, StatusCategory } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import task, { Project, ProjectType } from '@hcengineering/task'
|
||||
import {
|
||||
ColorDefinition,
|
||||
defaultBackground,
|
||||
@@ -23,8 +25,9 @@
|
||||
themeStore
|
||||
} from '@hcengineering/ui'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import { typeStore } from '../..'
|
||||
|
||||
export let value: State | undefined
|
||||
export let value: Status | undefined
|
||||
export let shouldShowAvatar = true
|
||||
export let inline: boolean = false
|
||||
export let disabled: boolean = false
|
||||
@@ -33,17 +36,58 @@
|
||||
export let shouldShowTooltip: boolean = false
|
||||
export let noUnderline: boolean = false
|
||||
export let shrink: number = 0
|
||||
export let space: Ref<Project>
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: color = value ? getPlatformColorDef(value.color ?? getColorNumberByText(value.name), $themeStore.dark) : undefined
|
||||
const dispatchAccentColor = (color?: ColorDefinition) => dispatch('accent-color', color)
|
||||
$: color = viewState
|
||||
? getPlatformColorDef(viewState.color ?? category?.color ?? getColorNumberByText(viewState.name), $themeStore.dark)
|
||||
: undefined
|
||||
const dispatchAccentColor = (color?: ColorDefinition) => {
|
||||
dispatch('accent-color', color)
|
||||
}
|
||||
|
||||
$: dispatchAccentColor(color)
|
||||
|
||||
onMount(() => {
|
||||
dispatchAccentColor(color)
|
||||
})
|
||||
|
||||
function getViewState (type: ProjectType | undefined, state: Status | undefined): Status | undefined {
|
||||
if (state === undefined) return
|
||||
if (type === undefined) return state
|
||||
const targetColor = type.statuses.find((p) => p._id === state._id)?.color
|
||||
if (targetColor === undefined) return state
|
||||
return {
|
||||
...state,
|
||||
color: targetColor
|
||||
}
|
||||
}
|
||||
|
||||
let category: StatusCategory | undefined
|
||||
$: updateCategory(value)
|
||||
|
||||
async function updateCategory (value: Status | undefined) {
|
||||
if (value === undefined) return
|
||||
category = await client.findOne(core.class.StatusCategory, { _id: value.category })
|
||||
}
|
||||
|
||||
$: viewState = getViewState(type, value)
|
||||
|
||||
let type: ProjectType | undefined = undefined
|
||||
|
||||
$: getType(space, $typeStore)
|
||||
|
||||
const client = getClient()
|
||||
|
||||
async function getType (space: Ref<Project>, types: IdMap<ProjectType>): Promise<ProjectType | undefined> {
|
||||
const _space = await client.findOne(task.class.Project, { _id: space })
|
||||
if (_space === undefined) {
|
||||
type = undefined
|
||||
return
|
||||
}
|
||||
type = types.get(_space.type)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
|
||||
@@ -14,29 +14,29 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref, Space, Status, StatusValue } from '@hcengineering/core'
|
||||
import { State } from '@hcengineering/task'
|
||||
import { Ref, Status } from '@hcengineering/core'
|
||||
import { Project } from '@hcengineering/task'
|
||||
import type { ButtonKind, ButtonSize } from '@hcengineering/ui'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
import StateEditor from './StateEditor.svelte'
|
||||
import StatePresenter from './StatePresenter.svelte'
|
||||
|
||||
export let value: Ref<State> | StatusValue
|
||||
export let space: Ref<Space>
|
||||
export let onChange: ((value: Ref<State>) => void) | undefined = undefined
|
||||
export let value: Ref<Status>
|
||||
export let space: Ref<Project>
|
||||
export let onChange: ((value: Ref<Status>) => void) | undefined = undefined
|
||||
export let kind: ButtonKind = 'link'
|
||||
export let size: ButtonSize = 'medium'
|
||||
export let shouldShowName: boolean = true
|
||||
export let shrink: number = 0
|
||||
export let disabled: boolean = false
|
||||
|
||||
$: state = $statusStore.get(typeof value === 'string' ? value : (value?.values?.[0]?._id as Ref<Status>))
|
||||
$: state = $statusStore.byId.get(value)
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
{#if onChange !== undefined && state !== undefined}
|
||||
<StateEditor value={state._id} {space} {onChange} {kind} {size} {shouldShowName} {shrink} {disabled} />
|
||||
{:else}
|
||||
<StatePresenter value={state} {shouldShowName} {disabled} {shrink} on:accent-color />
|
||||
<StatePresenter value={state} {space} {shouldShowName} {disabled} {shrink} on:accent-color />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -14,23 +14,23 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import core, { IdMap, Ref, Status, StatusCategory, toIdMap } from '@hcengineering/core'
|
||||
import { BreadcrumbsElement, createQuery } from '@hcengineering/presentation'
|
||||
import task, { SpaceWithStates, State, getStates } from '@hcengineering/task'
|
||||
import task, { Project, ProjectType, getStates } from '@hcengineering/task'
|
||||
import { ScrollerBar, getColorNumberByText, getPlatformColor, themeStore } from '@hcengineering/ui'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import type { StatesBarPosition } from '../..'
|
||||
import { typeStore, type StatesBarPosition } from '../..'
|
||||
|
||||
export let space: Ref<SpaceWithStates>
|
||||
export let state: Ref<State> | undefined = undefined
|
||||
export let space: Ref<Project>
|
||||
export let state: Ref<Status> | undefined = undefined
|
||||
export let gap: 'none' | 'small' | 'big' = 'small'
|
||||
|
||||
let _space: SpaceWithStates | undefined = undefined
|
||||
let _space: Project | undefined = undefined
|
||||
|
||||
const spaceQuery = createQuery()
|
||||
spaceQuery.query(
|
||||
task.class.SpaceWithStates,
|
||||
task.class.Project,
|
||||
{
|
||||
_id: space
|
||||
},
|
||||
@@ -39,12 +39,14 @@
|
||||
}
|
||||
)
|
||||
|
||||
$: states = getStates(_space, $statusStore)
|
||||
$: states = getStates(_space, $typeStore, $statusStore.byId).filter(
|
||||
(p) => p.category !== task.statusCategory.Lost && p.category !== task.statusCategory.Won
|
||||
)
|
||||
let divScroll: HTMLElement
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const selectItem = (ev: Event, item: State): void => {
|
||||
const selectItem = (ev: Event, item: Status): void => {
|
||||
const el: HTMLElement = ev.currentTarget as HTMLElement
|
||||
const rect = el.getBoundingClientRect()
|
||||
const rectScroll = divScroll.getBoundingClientRect()
|
||||
@@ -66,6 +68,25 @@
|
||||
else if (n === states.length - 1) return 'end'
|
||||
else return 'middle'
|
||||
}
|
||||
|
||||
let categories: IdMap<StatusCategory> = new Map()
|
||||
|
||||
const q = createQuery()
|
||||
q.query(core.class.StatusCategory, {}, (res) => {
|
||||
categories = toIdMap(res)
|
||||
})
|
||||
|
||||
function getColor (
|
||||
state: Status,
|
||||
_space: Project | undefined,
|
||||
typeStore: IdMap<ProjectType>,
|
||||
categories: IdMap<StatusCategory>
|
||||
): string {
|
||||
const type = _space ? typeStore.get(_space.type) : undefined
|
||||
const category = state.category ? categories.get(state.category) : undefined
|
||||
const targetColor = type?.statuses?.find((p) => p._id === state._id)?.color ?? state.color ?? category?.color
|
||||
return getPlatformColor(targetColor ?? getColorNumberByText(state.name), $themeStore.dark)
|
||||
}
|
||||
</script>
|
||||
|
||||
<ScrollerBar {gap} bind:scroller={divScroll}>
|
||||
@@ -74,7 +95,7 @@
|
||||
label={item.name}
|
||||
position={getPosition(i)}
|
||||
selected={item._id === state}
|
||||
color={getPlatformColor(item.color ?? getColorNumberByText(item.name), $themeStore.dark)}
|
||||
color={getColor(item, _space, $typeStore, categories)}
|
||||
on:click={(ev) => {
|
||||
ev.stopPropagation()
|
||||
if (item._id !== state) selectItem(ev, item)
|
||||
|
||||
@@ -1,329 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 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 { Attribute, Ref, Status } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import type { DoneState, SpaceWithStates, State } from '@hcengineering/task'
|
||||
import {
|
||||
CircleButton,
|
||||
IconAdd,
|
||||
IconCircles,
|
||||
IconMoreH,
|
||||
Label,
|
||||
PaletteColorIndexes,
|
||||
defaultBackground,
|
||||
eventToHTMLElement,
|
||||
getColorNumberByText,
|
||||
getPlatformColorDef,
|
||||
showPopup,
|
||||
themeStore
|
||||
} from '@hcengineering/ui'
|
||||
import { ColorsPopup, StringPresenter } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import task from '../../plugin'
|
||||
import Lost from '../icons/Lost.svelte'
|
||||
import Won from '../icons/Won.svelte'
|
||||
import StatusesPopup from './StatusesPopup.svelte'
|
||||
|
||||
export let space: Ref<SpaceWithStates>
|
||||
export let ofAttribute: Ref<Attribute<Status>> = task.attribute.State
|
||||
export let doneOfAttribute: Ref<Attribute<Status>> = task.attribute.DoneState
|
||||
export let states: State[] = []
|
||||
export let wonStates: DoneState[] = []
|
||||
export let lostStates: DoneState[] = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
|
||||
const elements: HTMLElement[] = []
|
||||
let selected: number | undefined
|
||||
let dragState: Ref<State>
|
||||
|
||||
function dragswap (ev: MouseEvent, i: number): boolean {
|
||||
const s = selected as number
|
||||
if (i < s) {
|
||||
return ev.offsetY < elements[i].offsetHeight / 2
|
||||
} else if (i > s) {
|
||||
return ev.offsetY > elements[i].offsetHeight / 2
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function dragover (ev: MouseEvent, i: number) {
|
||||
const s = selected as number
|
||||
if (dragswap(ev, i)) {
|
||||
;[states[i], states[s]] = [states[s], states[i]]
|
||||
selected = i
|
||||
}
|
||||
}
|
||||
|
||||
async function onMove (to: number) {
|
||||
dispatch('move', {
|
||||
stateID: dragState,
|
||||
position: to
|
||||
})
|
||||
}
|
||||
|
||||
const onColorChange =
|
||||
(state: State) =>
|
||||
async (color: number | undefined): Promise<void> => {
|
||||
if (color == null) {
|
||||
return
|
||||
}
|
||||
|
||||
await client.updateDoc(state._class, state.space, state._id, { color })
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex-no-shrink flex-between trans-title uppercase">
|
||||
<Label label={task.string.ActiveStates} />
|
||||
<CircleButton
|
||||
icon={IconAdd}
|
||||
size={'medium'}
|
||||
on:click={() => {
|
||||
showPopup(
|
||||
task.component.CreateStatePopup,
|
||||
{
|
||||
space,
|
||||
ofAttribute,
|
||||
_class: task.class.State
|
||||
},
|
||||
undefined
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-col flex-no-shrink mt-3">
|
||||
{#each states as state, i}
|
||||
{@const color = getPlatformColorDef(state.color ?? getColorNumberByText(state.name), $themeStore.dark)}
|
||||
{#if state}
|
||||
<div
|
||||
bind:this={elements[i]}
|
||||
class="flex-between states"
|
||||
style:background={color.background ?? defaultBackground($themeStore.dark)}
|
||||
draggable={true}
|
||||
on:dragover|preventDefault={(ev) => {
|
||||
dragover(ev, i)
|
||||
}}
|
||||
on:drop|preventDefault={() => {
|
||||
onMove(i)
|
||||
}}
|
||||
on:dragstart={() => {
|
||||
selected = i
|
||||
dragState = states[i]._id
|
||||
}}
|
||||
on:dragend={() => {
|
||||
selected = undefined
|
||||
}}
|
||||
>
|
||||
<div class="bar"><IconCircles size={'small'} /></div>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="color"
|
||||
style:background-color={color.color}
|
||||
on:click={() => {
|
||||
showPopup(ColorsPopup, { selected: color.name }, elements[i], onColorChange(state))
|
||||
}}
|
||||
/>
|
||||
<div class="flex-grow caption-color">
|
||||
<StringPresenter value={state.name} oneLine />
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="tool hover-trans"
|
||||
on:click={(ev) => {
|
||||
showPopup(
|
||||
StatusesPopup,
|
||||
{
|
||||
onDelete: () => dispatch('delete', { state }),
|
||||
showDelete: states.length > 1,
|
||||
onUpdate: () => {
|
||||
showPopup(task.component.CreateStatePopup, { status: state, space, ofAttribute }, undefined)
|
||||
}
|
||||
},
|
||||
eventToHTMLElement(ev),
|
||||
() => {}
|
||||
)
|
||||
}}
|
||||
>
|
||||
<IconMoreH size={'medium'} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex-col flex-no-shrink mt-9">
|
||||
<div class="flex-no-shrink flex-between trans-title uppercase">
|
||||
<Label label={task.string.DoneStatesWon} />
|
||||
<CircleButton
|
||||
icon={IconAdd}
|
||||
size={'medium'}
|
||||
on:click={() => {
|
||||
showPopup(
|
||||
task.component.CreateStatePopup,
|
||||
{
|
||||
space,
|
||||
ofAttribute: doneOfAttribute,
|
||||
_class: task.class.WonState
|
||||
},
|
||||
undefined
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-col mt-4">
|
||||
{#each wonStates as state}
|
||||
{@const color = getPlatformColorDef(PaletteColorIndexes.Crocodile, $themeStore.dark)}
|
||||
{#if state}
|
||||
<div
|
||||
class="states flex-row-center"
|
||||
style:color={color.title}
|
||||
style:background={color.background ?? defaultBackground($themeStore.dark)}
|
||||
>
|
||||
<div class="bar" />
|
||||
<div class="mr-2">
|
||||
<Won size={'medium'} />
|
||||
</div>
|
||||
<div class="flex-grow caption-color">
|
||||
<StringPresenter value={state.name} oneLine />
|
||||
<!-- <AttributeEditor maxWidth={'13rem'} _class={state._class} object={state} key="name" />-->
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="tool hover-trans"
|
||||
on:click={(ev) => {
|
||||
showPopup(
|
||||
StatusesPopup,
|
||||
{
|
||||
onDelete: () => dispatch('delete', { state }),
|
||||
showDelete: wonStates.length > 1,
|
||||
onUpdate: () => {
|
||||
showPopup(
|
||||
task.component.CreateStatePopup,
|
||||
{ status: state, space, ofAttribute: doneOfAttribute },
|
||||
undefined
|
||||
)
|
||||
}
|
||||
},
|
||||
eventToHTMLElement(ev),
|
||||
() => {}
|
||||
)
|
||||
}}
|
||||
>
|
||||
<IconMoreH size={'medium'} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-9 flex-no-shrink">
|
||||
<div class="flex-no-shrink flex-between trans-title uppercase">
|
||||
<Label label={task.string.DoneStatesLost} />
|
||||
<CircleButton
|
||||
icon={IconAdd}
|
||||
size={'medium'}
|
||||
on:click={() => {
|
||||
showPopup(
|
||||
task.component.CreateStatePopup,
|
||||
{
|
||||
space,
|
||||
ofAttribute: doneOfAttribute,
|
||||
_class: task.class.LostState
|
||||
},
|
||||
undefined
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 mb-10">
|
||||
{#each lostStates as state}
|
||||
{@const color = getPlatformColorDef(PaletteColorIndexes.Firework, $themeStore.dark)}
|
||||
{#if state}
|
||||
<div
|
||||
class="states flex-row-center"
|
||||
style:color={color.title}
|
||||
style:background={color.background ?? defaultBackground($themeStore.dark)}
|
||||
>
|
||||
<div class="bar" />
|
||||
<div class="mr-2">
|
||||
<Lost size={'medium'} />
|
||||
</div>
|
||||
<div class="flex-grow caption-color">
|
||||
<StringPresenter value={state.name} oneLine />
|
||||
<!-- <AttributeEditor maxWidth={'13rem'} _class={state._class} object={state} key="name" />-->
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="tool hover-trans"
|
||||
on:click={(ev) => {
|
||||
showPopup(
|
||||
StatusesPopup,
|
||||
{
|
||||
onDelete: () => dispatch('delete', { state }),
|
||||
showDelete: lostStates.length > 1,
|
||||
onUpdate: () => {
|
||||
showPopup(
|
||||
task.component.CreateStatePopup,
|
||||
{ status: state, space, ofAttribute: doneOfAttribute },
|
||||
undefined
|
||||
)
|
||||
}
|
||||
},
|
||||
eventToHTMLElement(ev),
|
||||
() => {}
|
||||
)
|
||||
}}
|
||||
>
|
||||
<IconMoreH size={'medium'} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.states {
|
||||
padding: 0.5rem 1rem 0.5rem 0.25rem;
|
||||
color: var(--theme-caption-color);
|
||||
background-color: var(--theme-button-default);
|
||||
border: 1px solid var(--theme-button-border);
|
||||
border-radius: 0.5rem;
|
||||
user-select: none;
|
||||
|
||||
.bar {
|
||||
margin-right: 0.25rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
opacity: 0.4;
|
||||
cursor: grabbing;
|
||||
}
|
||||
.color {
|
||||
margin-right: 0.75rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tool {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
.states + .states {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -14,23 +14,56 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import task, { SpaceWithStates, getStates } from '@hcengineering/task'
|
||||
import { getColorNumberByText, getPlatformColorDef, resizeObserver, themeStore } from '@hcengineering/ui'
|
||||
import core, { IdMap, Ref, Status, StatusCategory, toIdMap } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import task, { Project, ProjectType } from '@hcengineering/task'
|
||||
import {
|
||||
ColorDefinition,
|
||||
getColorNumberByText,
|
||||
getPlatformColorDef,
|
||||
resizeObserver,
|
||||
themeStore
|
||||
} from '@hcengineering/ui'
|
||||
import { statusStore } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { typeStore } from '../..'
|
||||
|
||||
export let space: Ref<SpaceWithStates>
|
||||
$: states = getStates(_space, $statusStore)
|
||||
export let space: Ref<Project>
|
||||
|
||||
$: states = type
|
||||
? ($typeStore
|
||||
.get(type._id)
|
||||
?.statuses?.map((p) => $statusStore.byId.get(p._id))
|
||||
.filter((p) => p !== undefined) as Status[]) ?? []
|
||||
: []
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let _space: SpaceWithStates | undefined = undefined
|
||||
function getColor (state: Status, type: ProjectType | undefined, categories: IdMap<StatusCategory>): ColorDefinition {
|
||||
const category = state.category ? categories.get(state.category) : undefined
|
||||
const targetColor = type?.statuses?.find((p) => p._id === state._id)?.color ?? state.color ?? category?.color
|
||||
return getPlatformColorDef(targetColor ?? getColorNumberByText(state.name), $themeStore.dark)
|
||||
}
|
||||
|
||||
const query = createQuery()
|
||||
$: query.query(task.class.SpaceWithStates, { _id: space }, (res) => {
|
||||
_space = res[0]
|
||||
let categories: IdMap<StatusCategory> = new Map()
|
||||
|
||||
const q = createQuery()
|
||||
q.query(core.class.StatusCategory, {}, (res) => {
|
||||
categories = toIdMap(res)
|
||||
})
|
||||
|
||||
$: getType(space, $typeStore)
|
||||
|
||||
const client = getClient()
|
||||
|
||||
async function getType (space: Ref<Project>, types: IdMap<ProjectType>): Promise<ProjectType | undefined> {
|
||||
const _space = await client.findOne(task.class.Project, { _id: space })
|
||||
if (_space === undefined) {
|
||||
type = undefined
|
||||
return
|
||||
}
|
||||
type = types.get(_space.type)
|
||||
}
|
||||
let type: ProjectType | undefined = undefined
|
||||
</script>
|
||||
|
||||
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')}>
|
||||
@@ -38,7 +71,7 @@
|
||||
<div class="scroll">
|
||||
<div class="box">
|
||||
{#each states as state}
|
||||
{@const color = getPlatformColorDef(state.color ?? getColorNumberByText(state.name), $themeStore.dark)}
|
||||
{@const color = getColor(state, type, categories)}
|
||||
<button
|
||||
class="menu-item"
|
||||
on:click={() => {
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 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 core, { Attribute, IdMap, Ref, Status, StatusCategory, toIdMap } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { ProjectType, ProjectTypeCategory } from '@hcengineering/task'
|
||||
import {
|
||||
CircleButton,
|
||||
ColorDefinition,
|
||||
Component,
|
||||
IconAdd,
|
||||
IconCircles,
|
||||
IconMoreH,
|
||||
Label,
|
||||
defaultBackground,
|
||||
eventToHTMLElement,
|
||||
getColorNumberByText,
|
||||
getPlatformColorDef,
|
||||
showPopup,
|
||||
themeStore
|
||||
} from '@hcengineering/ui'
|
||||
import { ColorsPopup, StringPresenter } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import task from '../../plugin'
|
||||
import StatusesPopup from './StatusesPopup.svelte'
|
||||
|
||||
export let type: ProjectType
|
||||
export let category: ProjectTypeCategory
|
||||
export let states: Status[] = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
|
||||
const elements: HTMLElement[] = []
|
||||
let selected: number | undefined
|
||||
let dragState: Ref<Status>
|
||||
|
||||
function dragswap (ev: MouseEvent, i: number): boolean {
|
||||
const s = selected as number
|
||||
if (i < s) {
|
||||
return ev.offsetY < elements[i].offsetHeight / 2
|
||||
} else if (i > s) {
|
||||
return ev.offsetY > elements[i].offsetHeight / 2
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function dragover (ev: MouseEvent, i: number) {
|
||||
const s = selected as number
|
||||
if (dragswap(ev, i)) {
|
||||
;[states[i], states[s]] = [states[s], states[i]]
|
||||
selected = i
|
||||
}
|
||||
}
|
||||
|
||||
async function onMove (to: number) {
|
||||
dispatch('move', {
|
||||
stateID: dragState,
|
||||
position: to
|
||||
})
|
||||
}
|
||||
|
||||
function onColor (state: Status, color: ColorDefinition, el: HTMLElement): void {
|
||||
showPopup(ColorsPopup, { selected: color.name }, el, async (res) => {
|
||||
if (res == null) {
|
||||
return
|
||||
}
|
||||
const targetColor = type.statuses.find((p) => p._id === state._id)
|
||||
if (targetColor !== undefined) {
|
||||
targetColor.color = res
|
||||
await client.update(type, { statuses: type.statuses })
|
||||
}
|
||||
})
|
||||
}
|
||||
const categoryEditor = category.editor
|
||||
|
||||
function add (ofAttribute: Ref<Attribute<Status>>, cat: Ref<StatusCategory>) {
|
||||
showPopup(task.component.CreateStatePopup, {
|
||||
ofAttribute,
|
||||
_class: category.statusClass,
|
||||
category: cat,
|
||||
type
|
||||
})
|
||||
}
|
||||
|
||||
function edit (status: Status) {
|
||||
showPopup(task.component.CreateStatePopup, { status, type, ofAttribute: status.ofAttribute })
|
||||
}
|
||||
|
||||
let categories: StatusCategory[] = []
|
||||
let categoriesMap: IdMap<StatusCategory> = new Map()
|
||||
const query = createQuery()
|
||||
$: query.query(core.class.StatusCategory, { _id: { $in: category.statusCategories } }, (res) => {
|
||||
categories = res
|
||||
categoriesMap = toIdMap(res)
|
||||
})
|
||||
|
||||
function click (ev: MouseEvent, state: Status) {
|
||||
showPopup(
|
||||
StatusesPopup,
|
||||
{
|
||||
onDelete: () => dispatch('delete', { state }),
|
||||
showDelete: states.length > 1,
|
||||
onUpdate: () => edit(state)
|
||||
},
|
||||
eventToHTMLElement(ev),
|
||||
() => {}
|
||||
)
|
||||
}
|
||||
|
||||
function getColor (state: Status, categoriesMap: IdMap<StatusCategory>): ColorDefinition {
|
||||
const category = state.category ? categoriesMap.get(state.category) : undefined
|
||||
const targetColor = type.statuses.find((p) => p._id === state._id)?.color ?? state.color ?? category?.color
|
||||
return getPlatformColorDef(targetColor ?? getColorNumberByText(state.name), $themeStore.dark)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if categoryEditor}
|
||||
<Component is={categoryEditor} props={{ type }} />
|
||||
{/if}
|
||||
{#each categories as cat, i}
|
||||
<div class="flex-no-shrink flex-between trans-title uppercase" class:mt-4={i > 0}>
|
||||
<Label label={cat.label} />
|
||||
<CircleButton icon={IconAdd} size={'medium'} on:click={() => add(cat.ofAttribute, cat._id)} />
|
||||
</div>
|
||||
<div class="flex-col flex-no-shrink mt-3">
|
||||
{#each states.filter((p) => p.category === cat._id) as state, i}
|
||||
{@const color = getColor(state, categoriesMap)}
|
||||
<div
|
||||
bind:this={elements[i]}
|
||||
class="flex-between states"
|
||||
style:background={color.background ?? defaultBackground($themeStore.dark)}
|
||||
draggable={true}
|
||||
on:dragover|preventDefault={(ev) => {
|
||||
dragover(ev, i)
|
||||
}}
|
||||
on:drop|preventDefault={() => {
|
||||
onMove(i)
|
||||
}}
|
||||
on:dragstart={() => {
|
||||
selected = i
|
||||
dragState = states[i]._id
|
||||
}}
|
||||
on:dragend={() => {
|
||||
selected = undefined
|
||||
}}
|
||||
>
|
||||
<div class="bar"><IconCircles size={'small'} /></div>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="color"
|
||||
style:background-color={color.color}
|
||||
on:click={() => {
|
||||
onColor(state, color, elements[i])
|
||||
}}
|
||||
/>
|
||||
<div class="flex-grow caption-color">
|
||||
<StringPresenter value={state.name} oneLine />
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="tool hover-trans" on:click={(e) => click(e, state)}>
|
||||
<IconMoreH size={'medium'} />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<style lang="scss">
|
||||
.states {
|
||||
padding: 0.5rem 1rem 0.5rem 0.25rem;
|
||||
color: var(--theme-caption-color);
|
||||
background-color: var(--theme-button-default);
|
||||
border: 1px solid var(--theme-button-border);
|
||||
border-radius: 0.5rem;
|
||||
user-select: none;
|
||||
|
||||
.bar {
|
||||
margin-right: 0.25rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
opacity: 0.4;
|
||||
cursor: grabbing;
|
||||
}
|
||||
.color {
|
||||
margin-right: 0.75rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tool {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
.states + .states {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,321 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Class, Ref } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import type {
|
||||
DoneStateTemplate,
|
||||
KanbanTemplate,
|
||||
KanbanTemplateSpace,
|
||||
State,
|
||||
StateTemplate
|
||||
} from '@hcengineering/task'
|
||||
import {
|
||||
CircleButton,
|
||||
Component,
|
||||
IconAdd,
|
||||
IconCircles,
|
||||
IconMoreH,
|
||||
Label,
|
||||
PaletteColorIndexes,
|
||||
defaultBackground,
|
||||
eventToHTMLElement,
|
||||
getColorNumberByText,
|
||||
getPlatformColorDef,
|
||||
showPopup,
|
||||
themeStore
|
||||
} from '@hcengineering/ui'
|
||||
import { ColorsPopup, StringPresenter } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import task from '../../plugin'
|
||||
import Lost from '../icons/Lost.svelte'
|
||||
import Won from '../icons/Won.svelte'
|
||||
import StatusesPopup from './StatusesPopup.svelte'
|
||||
|
||||
export let template: KanbanTemplate
|
||||
export let space: KanbanTemplateSpace
|
||||
export let states: StateTemplate[] = []
|
||||
export let wonStates: DoneStateTemplate[] = []
|
||||
export let lostStates: DoneStateTemplate[] = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
const elements: HTMLElement[] = []
|
||||
let selected: number | undefined
|
||||
let dragState: Ref<State>
|
||||
|
||||
function dragswap (ev: MouseEvent, i: number): boolean {
|
||||
const s = selected as number
|
||||
if (i < s) {
|
||||
return ev.offsetY < elements[i].offsetHeight / 2
|
||||
} else if (i > s) {
|
||||
return ev.offsetY > elements[i].offsetHeight / 2
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function dragover (ev: MouseEvent, i: number) {
|
||||
const s = selected as number
|
||||
if (dragswap(ev, i)) {
|
||||
;[states[i], states[s]] = [states[s], states[i]]
|
||||
selected = i
|
||||
}
|
||||
}
|
||||
|
||||
async function onMove (to: number) {
|
||||
dispatch('move', {
|
||||
stateID: dragState,
|
||||
position: to
|
||||
})
|
||||
}
|
||||
|
||||
const onColorChange =
|
||||
(state: State) =>
|
||||
async (color: number | undefined): Promise<void> => {
|
||||
if (color == null) {
|
||||
return
|
||||
}
|
||||
|
||||
await client.updateDoc(state._class, state.space, state._id, { color })
|
||||
}
|
||||
const spaceEditor = space.editor
|
||||
|
||||
function add (_class: Ref<Class<StateTemplate | DoneStateTemplate>>) {
|
||||
const ofAttribute = hierarchy.isDerived(_class, task.class.DoneStateTemplate)
|
||||
? space.doneAttribute
|
||||
: space.ofAttribute
|
||||
showPopup(task.component.CreateStateTemplatePopup, {
|
||||
ofAttribute,
|
||||
space,
|
||||
template,
|
||||
_class
|
||||
})
|
||||
}
|
||||
|
||||
function edit (status: StateTemplate) {
|
||||
const ofAttribute = hierarchy.isDerived(status._class, task.class.DoneStateTemplate)
|
||||
? space.doneAttribute
|
||||
: space.ofAttribute
|
||||
showPopup(task.component.CreateStateTemplatePopup, { status, template, space, ofAttribute })
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if spaceEditor}
|
||||
<Component is={spaceEditor} props={{ template }} />
|
||||
{/if}
|
||||
<div class="flex-no-shrink flex-between trans-title uppercase">
|
||||
<Label label={task.string.ActiveStates} />
|
||||
<CircleButton
|
||||
icon={IconAdd}
|
||||
size={'medium'}
|
||||
on:click={() => {
|
||||
add(task.class.StateTemplate)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-col flex-no-shrink mt-3">
|
||||
{#each states as state, i}
|
||||
{@const color = getPlatformColorDef(state.color ?? getColorNumberByText(state.name), $themeStore.dark)}
|
||||
{#if state}
|
||||
<div
|
||||
bind:this={elements[i]}
|
||||
class="flex-between states"
|
||||
style:background={color.background ?? defaultBackground($themeStore.dark)}
|
||||
draggable={true}
|
||||
on:dragover|preventDefault={(ev) => {
|
||||
dragover(ev, i)
|
||||
}}
|
||||
on:drop|preventDefault={() => {
|
||||
onMove(i)
|
||||
}}
|
||||
on:dragstart={() => {
|
||||
selected = i
|
||||
dragState = states[i]._id
|
||||
}}
|
||||
on:dragend={() => {
|
||||
selected = undefined
|
||||
}}
|
||||
>
|
||||
<div class="bar"><IconCircles size={'small'} /></div>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="color"
|
||||
style:background-color={color.color}
|
||||
on:click={() => {
|
||||
showPopup(ColorsPopup, { selected: color.name }, elements[i], onColorChange(state))
|
||||
}}
|
||||
/>
|
||||
<div class="flex-grow caption-color">
|
||||
<StringPresenter value={state.name} oneLine />
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="tool hover-trans"
|
||||
on:click={(ev) => {
|
||||
showPopup(
|
||||
StatusesPopup,
|
||||
{
|
||||
onDelete: () => dispatch('delete', { state }),
|
||||
showDelete: states.length > 1,
|
||||
onUpdate: () => edit(state)
|
||||
},
|
||||
eventToHTMLElement(ev),
|
||||
() => {}
|
||||
)
|
||||
}}
|
||||
>
|
||||
<IconMoreH size={'medium'} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex-col flex-no-shrink mt-9">
|
||||
<div class="flex-no-shrink flex-between trans-title uppercase">
|
||||
<Label label={task.string.DoneStatesWon} />
|
||||
<CircleButton
|
||||
icon={IconAdd}
|
||||
size={'medium'}
|
||||
on:click={() => {
|
||||
add(task.class.WonStateTemplate)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-col mt-4">
|
||||
{#each wonStates as state}
|
||||
{@const color = getPlatformColorDef(PaletteColorIndexes.Crocodile, $themeStore.dark)}
|
||||
{#if state}
|
||||
<div
|
||||
class="states flex-row-center"
|
||||
style:color={color.title}
|
||||
style:background={color.background ?? defaultBackground($themeStore.dark)}
|
||||
>
|
||||
<div class="bar" />
|
||||
<div class="mr-2">
|
||||
<Won size={'medium'} />
|
||||
</div>
|
||||
<div class="flex-grow caption-color">
|
||||
<StringPresenter value={state.name} oneLine />
|
||||
<!-- <AttributeEditor maxWidth={'13rem'} _class={state._class} object={state} key="name" />-->
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="tool hover-trans"
|
||||
on:click={(ev) => {
|
||||
showPopup(
|
||||
StatusesPopup,
|
||||
{
|
||||
onDelete: () => dispatch('delete', { state }),
|
||||
showDelete: wonStates.length > 1,
|
||||
onUpdate: () => edit(state)
|
||||
},
|
||||
eventToHTMLElement(ev),
|
||||
() => {}
|
||||
)
|
||||
}}
|
||||
>
|
||||
<IconMoreH size={'medium'} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-9 flex-no-shrink">
|
||||
<div class="flex-no-shrink flex-between trans-title uppercase">
|
||||
<Label label={task.string.DoneStatesLost} />
|
||||
<CircleButton
|
||||
icon={IconAdd}
|
||||
size={'medium'}
|
||||
on:click={() => {
|
||||
add(task.class.LostStateTemplate)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 mb-10">
|
||||
{#each lostStates as state}
|
||||
{@const color = getPlatformColorDef(PaletteColorIndexes.Firework, $themeStore.dark)}
|
||||
{#if state}
|
||||
<div
|
||||
class="states flex-row-center"
|
||||
style:color={color.title}
|
||||
style:background={color.background ?? defaultBackground($themeStore.dark)}
|
||||
>
|
||||
<div class="bar" />
|
||||
<div class="mr-2">
|
||||
<Lost size={'medium'} />
|
||||
</div>
|
||||
<div class="flex-grow caption-color">
|
||||
<StringPresenter value={state.name} oneLine />
|
||||
<!-- <AttributeEditor maxWidth={'13rem'} _class={state._class} object={state} key="name" />-->
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="tool hover-trans"
|
||||
on:click={(ev) => {
|
||||
showPopup(
|
||||
StatusesPopup,
|
||||
{
|
||||
onDelete: () => dispatch('delete', { state }),
|
||||
showDelete: lostStates.length > 1,
|
||||
onUpdate: () => edit(state)
|
||||
},
|
||||
eventToHTMLElement(ev),
|
||||
() => {}
|
||||
)
|
||||
}}
|
||||
>
|
||||
<IconMoreH size={'medium'} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.states {
|
||||
padding: 0.5rem 1rem 0.5rem 0.25rem;
|
||||
color: var(--theme-caption-color);
|
||||
background-color: var(--theme-button-default);
|
||||
border: 1px solid var(--theme-button-border);
|
||||
border-radius: 0.5rem;
|
||||
user-select: none;
|
||||
|
||||
.bar {
|
||||
margin-right: 0.25rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
opacity: 0.4;
|
||||
cursor: grabbing;
|
||||
}
|
||||
.color {
|
||||
margin-right: 0.75rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tool {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
.states + .states {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -13,10 +13,10 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import view from '@hcengineering/view'
|
||||
import { Label, IconDelete as Delete, IconEdit } from '@hcengineering/ui'
|
||||
import task from '../../plugin'
|
||||
import { IconDelete, IconEdit, Label } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let onDelete: () => void
|
||||
export let showDelete = true
|
||||
@@ -27,21 +27,6 @@
|
||||
|
||||
<div class="antiPopup">
|
||||
<div class="ap-space x2" />
|
||||
{#if showDelete}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="ap-menuItem hoverable flex-row-center redlight"
|
||||
on:click={() => {
|
||||
dispatch('close')
|
||||
onDelete()
|
||||
}}
|
||||
>
|
||||
<div class="mr-2">
|
||||
<Delete size={'small'} />
|
||||
</div>
|
||||
<Label label={task.string.Delete} />
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
class="ap-menuItem hoverable flex-row-center"
|
||||
on:click={() => {
|
||||
@@ -54,5 +39,20 @@
|
||||
</div>
|
||||
<Label label={view.string.Rename} />
|
||||
</div>
|
||||
{#if showDelete}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="ap-menuItem hoverable flex-row-center redlight"
|
||||
on:click={() => {
|
||||
dispatch('close')
|
||||
onDelete()
|
||||
}}
|
||||
>
|
||||
<div class="mr-2">
|
||||
<IconDelete size={'small'} />
|
||||
</div>
|
||||
<Label label={task.string.Delete} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="ap-space x2" />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user