mirror of
https://github.com/hcengineering/platform.git
synced 2025-01-26 21:34:27 +00:00
Tracker: View options - Completed issues period, empty groups display (#1490)
Signed-off-by: Artyom Grigorovich <grigorovichartyom@gmail.com>
This commit is contained in:
parent
2efc19044e
commit
04ed6e0e25
@ -12,21 +12,29 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { IntlString } from '@anticrm/platform'
|
import type { IntlString } from '@anticrm/platform'
|
||||||
import Label from './Label.svelte'
|
import Label from './Label.svelte'
|
||||||
|
|
||||||
export let label: IntlString
|
export let label: IntlString | undefined = undefined
|
||||||
export let on: boolean = false
|
export let on: boolean = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex-row-center">
|
<div class="flex-row-center">
|
||||||
<label class="mini-toggle">
|
<label class="mini-toggle">
|
||||||
<input class="chBox" type="checkbox" bind:checked={on} on:change>
|
<input class="chBox" type="checkbox" bind:checked={on} on:change />
|
||||||
<span class="toggle-switch"></span>
|
<span class="toggle-switch" />
|
||||||
</label>
|
</label>
|
||||||
<span class="mini-toggle-label" on:click={() => { on = !on }}><Label {label} /></span>
|
{#if label}
|
||||||
|
<span
|
||||||
|
class="mini-toggle-label"
|
||||||
|
on:click={() => {
|
||||||
|
on = !on
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Label {label} />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@ -48,19 +56,25 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
clip: rect(0 0 0 0);
|
clip: rect(0 0 0 0);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&:checked + .toggle-switch {
|
&:checked + .toggle-switch {
|
||||||
background-color: var(--toggle-on-bg-color);
|
background-color: var(--toggle-on-bg-color);
|
||||||
&:hover { background-color: var(--toggle-on-bg-hover); }
|
&:hover {
|
||||||
|
background-color: var(--toggle-on-bg-hover);
|
||||||
|
}
|
||||||
&:before {
|
&:before {
|
||||||
left: 9px;
|
left: 9px;
|
||||||
background: var(--toggle-on-sw-color);
|
background: var(--toggle-on-sw-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:not(:disabled) + .toggle-switch { cursor: pointer; }
|
&:not(:disabled) + .toggle-switch {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
&:disabled + .toggle-switch {
|
&:disabled + .toggle-switch {
|
||||||
filter: grayscale(70%);
|
filter: grayscale(70%);
|
||||||
&:before { background: #eee; }
|
&:before {
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// &:focus-within + .toggle-switch { box-shadow: 0 0 0 2px var(--primary-button-outline); }
|
// &:focus-within + .toggle-switch { box-shadow: 0 0 0 2px var(--primary-button-outline); }
|
||||||
}
|
}
|
||||||
@ -72,7 +86,7 @@
|
|||||||
height: 14px;
|
height: 14px;
|
||||||
border-radius: 4.5rem;
|
border-radius: 4.5rem;
|
||||||
background-color: var(--toggle-bg-color);
|
background-color: var(--toggle-bg-color);
|
||||||
transition: left .2s, background-color .2s;
|
transition: left 0.2s, background-color 0.2s;
|
||||||
&:before {
|
&:before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -84,14 +98,16 @@
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--toggle-sw-color);
|
background: var(--toggle-sw-color);
|
||||||
// box-shadow: 1px 2px 7px rgba(119, 129, 142, 0.1);
|
// box-shadow: 1px 2px 7px rgba(119, 129, 142, 0.1);
|
||||||
transition: all .1s ease-out;
|
transition: all 0.1s ease-out;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--toggle-bg-hover);
|
||||||
}
|
}
|
||||||
&:hover { background-color: var(--toggle-bg-hover); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&-label {
|
&-label {
|
||||||
margin-left: .375rem;
|
margin-left: 0.375rem;
|
||||||
font-size: .75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--content-color);
|
color: var(--content-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
@ -12,8 +12,10 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
export const DAYS_IN_WEEK = 7
|
export const DAYS_IN_WEEK = 7
|
||||||
export const MILLISECONDS_IN_DAY = 86400000
|
|
||||||
export const MILLISECONDS_IN_MINUTE = 60000
|
export const MILLISECONDS_IN_MINUTE = 60000
|
||||||
|
export const MILLISECONDS_IN_DAY = 86400000
|
||||||
|
export const MILLISECONDS_IN_WEEK = DAYS_IN_WEEK * MILLISECONDS_IN_DAY
|
||||||
|
|
||||||
export function firstDay (date: Date, mondayStart: boolean): Date {
|
export function firstDay (date: Date, mondayStart: boolean): Date {
|
||||||
const firstDayOfMonth = new Date(date)
|
const firstDayOfMonth = new Date(date)
|
||||||
@ -108,3 +110,7 @@ export const getDaysDifference = (from: Date, to: Date): number => {
|
|||||||
|
|
||||||
return Math.round(Math.abs(secondDateMs - firstDateMs) / MILLISECONDS_IN_DAY)
|
return Math.round(Math.abs(secondDateMs - firstDateMs) / MILLISECONDS_IN_DAY)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getMillisecondsInMonth = (date: Date): number => {
|
||||||
|
return daysInMonth(date) * MILLISECONDS_IN_DAY
|
||||||
|
}
|
||||||
|
@ -68,10 +68,15 @@
|
|||||||
"DueDatePopupOverdueDescription": "{value, plural, =1 {1 day overdue} other {# days overdue}}",
|
"DueDatePopupOverdueDescription": "{value, plural, =1 {1 day overdue} other {# days overdue}}",
|
||||||
"Grouping": "Grouping",
|
"Grouping": "Grouping",
|
||||||
"Ordering": "Ordering",
|
"Ordering": "Ordering",
|
||||||
|
"CompletedIssues": "Completed issues",
|
||||||
|
"ShowEmptyGroups": "Show empty groups",
|
||||||
"NoGrouping": "No grouping",
|
"NoGrouping": "No grouping",
|
||||||
"NoAssignee": "No assignee",
|
"NoAssignee": "No assignee",
|
||||||
"LastUpdated": "Last updated",
|
"LastUpdated": "Last updated",
|
||||||
"DueDate": "Due date"
|
"DueDate": "Due date",
|
||||||
|
"All": "All",
|
||||||
|
"PastWeek": "Past week",
|
||||||
|
"PastMonth": "Past month"
|
||||||
},
|
},
|
||||||
"status": {}
|
"status": {}
|
||||||
}
|
}
|
@ -1,14 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Ref } from '@anticrm/core'
|
import { Ref } from '@anticrm/core'
|
||||||
import { IssueStatus, Team } from '@anticrm/tracker'
|
import { IssueStatus, Team, IssuesDateModificationPeriod } from '@anticrm/tracker'
|
||||||
import Issues from './Issues.svelte'
|
import Issues from './Issues.svelte'
|
||||||
import tracker from '../../plugin'
|
import tracker from '../../plugin'
|
||||||
|
|
||||||
export let currentSpace: Ref<Team>
|
export let currentSpace: Ref<Team>
|
||||||
|
|
||||||
|
const completedIssuesPeriod: IssuesDateModificationPeriod | null = null
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Issues
|
<Issues
|
||||||
{currentSpace}
|
{currentSpace}
|
||||||
|
{completedIssuesPeriod}
|
||||||
includedGroups={{ status: [IssueStatus.InProgress, IssueStatus.Todo] }}
|
includedGroups={{ status: [IssueStatus.InProgress, IssueStatus.Todo] }}
|
||||||
title={tracker.string.ActiveIssues}
|
title={tracker.string.ActiveIssues}
|
||||||
/>
|
/>
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Ref } from '@anticrm/core'
|
import { Ref } from '@anticrm/core'
|
||||||
import { IssueStatus, Team } from '@anticrm/tracker'
|
import { IssueStatus, Team, IssuesDateModificationPeriod } from '@anticrm/tracker'
|
||||||
import Issues from './Issues.svelte'
|
import Issues from './Issues.svelte'
|
||||||
import tracker from '../../plugin'
|
import tracker from '../../plugin'
|
||||||
|
|
||||||
export let currentSpace: Ref<Team>
|
export let currentSpace: Ref<Team>
|
||||||
|
|
||||||
|
const completedIssuesPeriod: IssuesDateModificationPeriod | null = null
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Issues title={tracker.string.BacklogIssues} {currentSpace} includedGroups={{ status: [IssueStatus.Backlog] }} />
|
<Issues
|
||||||
|
title={tracker.string.BacklogIssues}
|
||||||
|
{currentSpace}
|
||||||
|
{completedIssuesPeriod}
|
||||||
|
includedGroups={{ status: [IssueStatus.Backlog] }}
|
||||||
|
/>
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="category" class:visible={issuesAmount > 0}>
|
<div class="category">
|
||||||
{#if headerComponent}
|
{#if headerComponent}
|
||||||
<div class="header categoryHeader flex-between label">
|
<div class="header categoryHeader flex-between label">
|
||||||
<div class="flex-row-center gap-2">
|
<div class="flex-row-center gap-2">
|
||||||
@ -81,13 +81,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.category {
|
|
||||||
display: none;
|
|
||||||
&.visible {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.categoryHeader {
|
.categoryHeader {
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
background-color: var(--theme-table-bg-hover);
|
background-color: var(--theme-table-bg-hover);
|
||||||
|
@ -16,13 +16,26 @@
|
|||||||
import contact from '@anticrm/contact'
|
import contact from '@anticrm/contact'
|
||||||
import type { DocumentQuery, Ref } from '@anticrm/core'
|
import type { DocumentQuery, Ref } from '@anticrm/core'
|
||||||
import { createQuery } from '@anticrm/presentation'
|
import { createQuery } from '@anticrm/presentation'
|
||||||
import { Issue, Team, IssuesGrouping, IssuesOrdering } from '@anticrm/tracker'
|
import {
|
||||||
|
Issue,
|
||||||
|
Team,
|
||||||
|
IssuesGrouping,
|
||||||
|
IssuesOrdering,
|
||||||
|
IssuesDateModificationPeriod,
|
||||||
|
IssueStatus
|
||||||
|
} from '@anticrm/tracker'
|
||||||
import { Button, Label, ScrollBox, IconOptions, showPopup, eventToHTMLElement } from '@anticrm/ui'
|
import { Button, Label, ScrollBox, IconOptions, showPopup, eventToHTMLElement } from '@anticrm/ui'
|
||||||
import CategoryPresenter from './CategoryPresenter.svelte'
|
import CategoryPresenter from './CategoryPresenter.svelte'
|
||||||
import tracker from '../../plugin'
|
import tracker from '../../plugin'
|
||||||
import { IntlString } from '@anticrm/platform'
|
import { IntlString } from '@anticrm/platform'
|
||||||
import ViewOptionsPopup from './ViewOptionsPopup.svelte'
|
import ViewOptionsPopup from './ViewOptionsPopup.svelte'
|
||||||
import { IssuesGroupByKeys, issuesGroupKeyMap, issuesOrderKeyMap } from '../../utils'
|
import {
|
||||||
|
IssuesGroupByKeys,
|
||||||
|
issuesGroupKeyMap,
|
||||||
|
issuesOrderKeyMap,
|
||||||
|
defaultIssueCategories,
|
||||||
|
getIssuesModificationDatePeriodTime
|
||||||
|
} from '../../utils'
|
||||||
|
|
||||||
export let currentSpace: Ref<Team>
|
export let currentSpace: Ref<Team>
|
||||||
export let title: IntlString = tracker.string.AllIssues
|
export let title: IntlString = tracker.string.AllIssues
|
||||||
@ -30,6 +43,8 @@
|
|||||||
export let search: string = ''
|
export let search: string = ''
|
||||||
export let groupingKey: IssuesGrouping = IssuesGrouping.Status
|
export let groupingKey: IssuesGrouping = IssuesGrouping.Status
|
||||||
export let orderingKey: IssuesOrdering = IssuesOrdering.LastUpdated
|
export let orderingKey: IssuesOrdering = IssuesOrdering.LastUpdated
|
||||||
|
export let completedIssuesPeriod: IssuesDateModificationPeriod | null = IssuesDateModificationPeriod.All
|
||||||
|
export let shouldShowEmptyGroups: boolean | undefined = false
|
||||||
export let includedGroups: Partial<Record<IssuesGroupByKeys, Array<any>>> = {}
|
export let includedGroups: Partial<Record<IssuesGroupByKeys, Array<any>>> = {}
|
||||||
|
|
||||||
const ENTRIES_LIMIT = 200
|
const ENTRIES_LIMIT = 200
|
||||||
@ -41,25 +56,30 @@
|
|||||||
|
|
||||||
$: totalIssues = getTotalIssues(issuesMap)
|
$: totalIssues = getTotalIssues(issuesMap)
|
||||||
|
|
||||||
$: resultQuery =
|
$: baseQuery = {
|
||||||
search === ''
|
space: currentSpace,
|
||||||
? { space: currentSpace, ...includedIssuesQuery, ...query }
|
...includedIssuesQuery,
|
||||||
: { $search: search, space: currentSpace, ...includedIssuesQuery, ...query }
|
...filteredIssuesQuery,
|
||||||
|
...query
|
||||||
|
}
|
||||||
|
|
||||||
|
$: resultQuery = search === '' ? baseQuery : { $search: search, ...baseQuery }
|
||||||
|
|
||||||
$: spaceQuery.query(tracker.class.Team, { _id: currentSpace }, (res) => {
|
$: spaceQuery.query(tracker.class.Team, { _id: currentSpace }, (res) => {
|
||||||
currentTeam = res.shift()
|
currentTeam = res.shift()
|
||||||
})
|
})
|
||||||
|
|
||||||
$: groupByKey = issuesGroupKeyMap[groupingKey]
|
$: groupByKey = issuesGroupKeyMap[groupingKey]
|
||||||
$: categories = getCategories(groupByKey, issues)
|
$: categories = getCategories(groupByKey, issues, !!shouldShowEmptyGroups)
|
||||||
$: displayedCategories = (categories as any[]).filter((x: ReturnType<typeof getCategories>) => {
|
$: displayedCategories = (categories as any[]).filter((x: ReturnType<typeof getCategories>) => {
|
||||||
return (
|
return (
|
||||||
groupByKey === undefined || includedGroups[groupByKey] === undefined || includedGroups[groupByKey]?.includes(x)
|
groupByKey === undefined || includedGroups[groupByKey] === undefined || includedGroups[groupByKey]?.includes(x)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
$: includedIssuesQuery = getIncludedIssues(includedGroups)
|
$: includedIssuesQuery = getIncludedIssuesQuery(includedGroups)
|
||||||
|
$: filteredIssuesQuery = getModifiedOnIssuesFilterQuery(issues, completedIssuesPeriod)
|
||||||
|
|
||||||
const getIncludedIssues = (groups: Partial<Record<IssuesGroupByKeys, Array<any>>>) => {
|
const getIncludedIssuesQuery = (groups: Partial<Record<IssuesGroupByKeys, Array<any>>>) => {
|
||||||
const resultMap: { [p: string]: { $in: any[] } } = {}
|
const resultMap: { [p: string]: { $in: any[] } } = {}
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(groups)) {
|
for (const [key, value] of Object.entries(groups)) {
|
||||||
@ -69,6 +89,24 @@
|
|||||||
return resultMap
|
return resultMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getModifiedOnIssuesFilterQuery = (currentIssues: Issue[], period: IssuesDateModificationPeriod | null) => {
|
||||||
|
const filter: { _id: { $in: Array<Ref<Issue>> } } = { _id: { $in: [] } }
|
||||||
|
|
||||||
|
if (!period || period === IssuesDateModificationPeriod.All) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const issue of currentIssues) {
|
||||||
|
if (issue.status === IssueStatus.Done && issue.modifiedOn < getIssuesModificationDatePeriodTime(period)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
filter._id.$in.push(issue._id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter
|
||||||
|
}
|
||||||
|
|
||||||
$: issuesQuery.query<Issue>(
|
$: issuesQuery.query<Issue>(
|
||||||
tracker.class.Issue,
|
tracker.class.Issue,
|
||||||
{ ...includedIssuesQuery },
|
{ ...includedIssuesQuery },
|
||||||
@ -78,18 +116,20 @@
|
|||||||
{ limit: ENTRIES_LIMIT, lookup: { assignee: contact.class.Employee } }
|
{ limit: ENTRIES_LIMIT, lookup: { assignee: contact.class.Employee } }
|
||||||
)
|
)
|
||||||
|
|
||||||
const getCategories = (key: IssuesGroupByKeys | undefined, elements: Issue[]) => {
|
const getCategories = (key: IssuesGroupByKeys | undefined, elements: Issue[], shouldShowAll: boolean) => {
|
||||||
if (!key) {
|
if (!key) {
|
||||||
return [undefined]
|
return [undefined] // No grouping
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(
|
const existingCategories = Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
elements.map((x) => {
|
elements.map((x) => {
|
||||||
return x[key]
|
return x[key]
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return shouldShowAll ? defaultIssueCategories[key] ?? existingCategories : existingCategories
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTotalIssues = (map: { [status: string]: number }) => {
|
const getTotalIssues = (map: { [status: string]: number }) => {
|
||||||
@ -102,7 +142,16 @@
|
|||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOptionsUpdated = (result: { orderBy: IssuesOrdering; groupBy: IssuesGrouping } | undefined) => {
|
const handleOptionsUpdated = (
|
||||||
|
result:
|
||||||
|
| {
|
||||||
|
orderBy: IssuesOrdering
|
||||||
|
groupBy: IssuesGrouping
|
||||||
|
completedIssuesPeriod: IssuesDateModificationPeriod
|
||||||
|
shouldShowEmptyGroups: boolean
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
) => {
|
||||||
if (result === undefined) {
|
if (result === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -113,6 +162,12 @@
|
|||||||
|
|
||||||
groupingKey = result.groupBy
|
groupingKey = result.groupBy
|
||||||
orderingKey = result.orderBy
|
orderingKey = result.orderBy
|
||||||
|
completedIssuesPeriod = result.completedIssuesPeriod
|
||||||
|
shouldShowEmptyGroups = result.shouldShowEmptyGroups
|
||||||
|
|
||||||
|
if (result.groupBy === IssuesGrouping.Assignee || result.groupBy === IssuesGrouping.NoGrouping) {
|
||||||
|
shouldShowEmptyGroups = undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOptionsEditorOpened = (event: MouseEvent) => {
|
const handleOptionsEditorOpened = (event: MouseEvent) => {
|
||||||
@ -122,7 +177,7 @@
|
|||||||
|
|
||||||
showPopup(
|
showPopup(
|
||||||
ViewOptionsPopup,
|
ViewOptionsPopup,
|
||||||
{ groupBy: groupingKey, orderBy: orderingKey },
|
{ groupBy: groupingKey, orderBy: orderingKey, completedIssuesPeriod, shouldShowEmptyGroups },
|
||||||
eventToHTMLElement(event),
|
eventToHTMLElement(event),
|
||||||
undefined,
|
undefined,
|
||||||
handleOptionsUpdated
|
handleOptionsUpdated
|
||||||
|
@ -13,10 +13,10 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { IssuesGrouping, IssuesOrdering } from '@anticrm/tracker'
|
import { IssuesGrouping, IssuesOrdering, IssuesDateModificationPeriod } from '@anticrm/tracker'
|
||||||
import { Label } from '@anticrm/ui'
|
import { Label, MiniToggle } from '@anticrm/ui'
|
||||||
import tracker from '../../plugin'
|
import tracker from '../../plugin'
|
||||||
import { issuesGroupByOptions, issuesOrderByOptions } from '../../utils'
|
import { issuesGroupByOptions, issuesOrderByOptions, issuesDateModificationPeriodOptions } from '../../utils'
|
||||||
import DropdownNative from '../DropdownNative.svelte'
|
import DropdownNative from '../DropdownNative.svelte'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
|
||||||
@ -24,20 +24,23 @@
|
|||||||
|
|
||||||
export let groupBy: IssuesGrouping | undefined = undefined
|
export let groupBy: IssuesGrouping | undefined = undefined
|
||||||
export let orderBy: IssuesOrdering | undefined = undefined
|
export let orderBy: IssuesOrdering | undefined = undefined
|
||||||
|
export let completedIssuesPeriod: IssuesDateModificationPeriod | null = null
|
||||||
|
export let shouldShowEmptyGroups: boolean | undefined = false
|
||||||
|
|
||||||
const groupByItems = issuesGroupByOptions
|
const groupByItems = issuesGroupByOptions
|
||||||
const orderByItems = issuesOrderByOptions
|
const orderByItems = issuesOrderByOptions
|
||||||
|
const dateModificationPeriodItems = issuesDateModificationPeriodOptions
|
||||||
|
|
||||||
$: dispatch('update', { groupBy, orderBy })
|
$: dispatch('update', { groupBy, orderBy, completedIssuesPeriod, shouldShowEmptyGroups })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root">
|
<div class="root">
|
||||||
<div class="sortingContainer">
|
<div class="groupContainer">
|
||||||
<div class="viewOption">
|
<div class="viewOption">
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<Label label={tracker.string.Grouping} />
|
<Label label={tracker.string.Grouping} />
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdownContainer">
|
<div class="optionContainer">
|
||||||
<DropdownNative items={groupByItems} bind:selected={groupBy} />
|
<DropdownNative items={groupByItems} bind:selected={groupBy} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -45,11 +48,33 @@
|
|||||||
<div class="label">
|
<div class="label">
|
||||||
<Label label={tracker.string.Ordering} />
|
<Label label={tracker.string.Ordering} />
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdownContainer">
|
<div class="optionContainer">
|
||||||
<DropdownNative items={orderByItems} bind:selected={orderBy} />
|
<DropdownNative items={orderByItems} bind:selected={orderBy} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="groupContainer">
|
||||||
|
{#if completedIssuesPeriod}
|
||||||
|
<div class="viewOption">
|
||||||
|
<div class="label">
|
||||||
|
<Label label={tracker.string.CompletedIssues} />
|
||||||
|
</div>
|
||||||
|
<div class="optionContainer">
|
||||||
|
<DropdownNative items={dateModificationPeriodItems} bind:selected={completedIssuesPeriod} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if groupBy === IssuesGrouping.Status || groupBy === IssuesGrouping.Priority}
|
||||||
|
<div class="viewOption">
|
||||||
|
<div class="label">
|
||||||
|
<Label label={tracker.string.ShowEmptyGroups} />
|
||||||
|
</div>
|
||||||
|
<div class="optionContainer">
|
||||||
|
<MiniToggle bind:on={shouldShowEmptyGroups} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@ -60,7 +85,7 @@
|
|||||||
background-color: var(--board-card-bg-color);
|
background-color: var(--board-card-bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sortingContainer {
|
.groupContainer {
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
border-bottom: 1px solid var(--popup-divider);
|
border-bottom: 1px solid var(--popup-divider);
|
||||||
}
|
}
|
||||||
@ -77,7 +102,7 @@
|
|||||||
color: var(--theme-content-dark-color);
|
color: var(--theme-content-dark-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdownContainer {
|
.optionContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
@ -83,10 +83,15 @@ export default mergeIds(trackerId, tracker, {
|
|||||||
DueDatePopupOverdueDescription: '' as IntlString,
|
DueDatePopupOverdueDescription: '' as IntlString,
|
||||||
Grouping: '' as IntlString,
|
Grouping: '' as IntlString,
|
||||||
Ordering: '' as IntlString,
|
Ordering: '' as IntlString,
|
||||||
|
CompletedIssues: '' as IntlString,
|
||||||
|
ShowEmptyGroups: '' as IntlString,
|
||||||
NoGrouping: '' as IntlString,
|
NoGrouping: '' as IntlString,
|
||||||
NoAssignee: '' as IntlString,
|
NoAssignee: '' as IntlString,
|
||||||
LastUpdated: '' as IntlString,
|
LastUpdated: '' as IntlString,
|
||||||
DueDate: '' as IntlString,
|
DueDate: '' as IntlString,
|
||||||
|
All: '' as IntlString,
|
||||||
|
PastWeek: '' as IntlString,
|
||||||
|
PastMonth: '' as IntlString,
|
||||||
|
|
||||||
IssueTitlePlaceholder: '' as IntlString,
|
IssueTitlePlaceholder: '' as IntlString,
|
||||||
IssueDescriptionPlaceholder: '' as IntlString,
|
IssueDescriptionPlaceholder: '' as IntlString,
|
||||||
|
@ -16,8 +16,8 @@
|
|||||||
|
|
||||||
import { Ref, SortingOrder } from '@anticrm/core'
|
import { Ref, SortingOrder } from '@anticrm/core'
|
||||||
import type { Asset, IntlString } from '@anticrm/platform'
|
import type { Asset, IntlString } from '@anticrm/platform'
|
||||||
import { IssuePriority, IssueStatus, Team, IssuesGrouping, IssuesOrdering, Issue } from '@anticrm/tracker'
|
import { IssuePriority, IssueStatus, Team, IssuesGrouping, IssuesOrdering, Issue, IssuesDateModificationPeriod } from '@anticrm/tracker'
|
||||||
import { AnyComponent } from '@anticrm/ui'
|
import { AnyComponent, getMillisecondsInMonth, MILLISECONDS_IN_WEEK } from '@anticrm/ui'
|
||||||
import { LexoDecimal, LexoNumeralSystem36, LexoRank } from 'lexorank'
|
import { LexoDecimal, LexoNumeralSystem36, LexoRank } from 'lexorank'
|
||||||
import LexoRankBucket from 'lexorank/lib/lexoRank/lexoRankBucket'
|
import LexoRankBucket from 'lexorank/lib/lexoRank/lexoRankBucket'
|
||||||
import tracker from './plugin'
|
import tracker from './plugin'
|
||||||
@ -93,6 +93,12 @@ export const issuesOrderByOptions: Record<IssuesOrdering, IntlString> = {
|
|||||||
[IssuesOrdering.DueDate]: tracker.string.DueDate
|
[IssuesOrdering.DueDate]: tracker.string.DueDate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const issuesDateModificationPeriodOptions: Record<IssuesDateModificationPeriod, IntlString> = {
|
||||||
|
[IssuesDateModificationPeriod.All]: tracker.string.All,
|
||||||
|
[IssuesDateModificationPeriod.PastWeek]: tracker.string.PastWeek,
|
||||||
|
[IssuesDateModificationPeriod.PastMonth]: tracker.string.PastMonth
|
||||||
|
}
|
||||||
|
|
||||||
export type IssuesGroupByKeys = keyof Pick<Issue, 'status' | 'priority' | 'assignee' >
|
export type IssuesGroupByKeys = keyof Pick<Issue, 'status' | 'priority' | 'assignee' >
|
||||||
export type IssuesOrderByKeys = keyof Pick<Issue, 'status' | 'priority' | 'modifiedOn' | 'dueDate'>
|
export type IssuesOrderByKeys = keyof Pick<Issue, 'status' | 'priority' | 'modifiedOn' | 'dueDate'>
|
||||||
|
|
||||||
@ -122,3 +128,26 @@ export const issuesGroupPresenterMap: Record<IssuesGroupByKeys, AnyComponent | u
|
|||||||
priority: tracker.component.PriorityPresenter,
|
priority: tracker.component.PriorityPresenter,
|
||||||
assignee: tracker.component.AssigneePresenter
|
assignee: tracker.component.AssigneePresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const defaultIssueCategories: Partial<Record<IssuesGroupByKeys, Array<Issue[IssuesGroupByKeys]> | undefined>> = {
|
||||||
|
status: [IssueStatus.InProgress, IssueStatus.Todo, IssueStatus.Backlog, IssueStatus.Done, IssueStatus.Canceled],
|
||||||
|
priority: [IssuePriority.NoPriority, IssuePriority.Urgent, IssuePriority.High, IssuePriority.Medium, IssuePriority.Low]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getIssuesModificationDatePeriodTime = (
|
||||||
|
period: IssuesDateModificationPeriod | null
|
||||||
|
): number => {
|
||||||
|
const today = new Date(Date.now())
|
||||||
|
|
||||||
|
switch (period) {
|
||||||
|
case IssuesDateModificationPeriod.PastWeek: {
|
||||||
|
return today.getTime() - MILLISECONDS_IN_WEEK
|
||||||
|
}
|
||||||
|
case IssuesDateModificationPeriod.PastMonth: {
|
||||||
|
return today.getTime() - getMillisecondsInMonth(today)
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -69,6 +69,15 @@ export enum IssuesOrdering {
|
|||||||
DueDate = 'dueDate'
|
DueDate = 'dueDate'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export enum IssuesDateModificationPeriod {
|
||||||
|
All = 'all',
|
||||||
|
PastWeek = 'pastWeek',
|
||||||
|
PastMonth = 'pastMonth',
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
|
Loading…
Reference in New Issue
Block a user