This commit is contained in:
Denis Bykhov
2023-10-27 12:22:43 +07:00
committed by GitHub
parent 2378461df3
commit 156741cf48
189 changed files with 2926 additions and 4879 deletions
@@ -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} />
@@ -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>