Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions plugins/view-assets/lang/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@
"Grouping": "Seskupení",
"Ordering": "Řazení",
"Manual": "Manuální",
"Ascending": "Vzestupně",
"Descending": "Sestupně",

"FilteredViews": "Filtrovaná zobrazení",
"NewFilteredView": "Nové filtrované zobrazení",
Expand Down
2 changes: 2 additions & 0 deletions plugins/view-assets/lang/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@
"Grouping": "Gruppierung",
"Ordering": "Sortierung",
"Manual": "Manuell",
"Ascending": "Aufsteigend",
"Descending": "Absteigend",

"FilteredViews": "Gefilterte Ansichten",
"NewFilteredView": "Neue gefilterte Ansicht",
Expand Down
2 changes: 2 additions & 0 deletions plugins/view-assets/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@
"Grouping": "Grouping",
"Ordering": "Ordering",
"Manual": "Manual",
"Ascending": "Ascending",
"Descending": "Descending",

"FilteredViews": "Filtered views",
"NewFilteredView": "New filtered view",
Expand Down
2 changes: 2 additions & 0 deletions plugins/view-assets/lang/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@
"Grouping": "Agrupación",
"Ordering": "Orden",
"Manual": "Manual",
"Ascending": "Ascendente",
"Descending": "Descendente",
"FilteredViews": "Vistas filtradas",
"NewFilteredView": "Nueva vista filtrada",
"FilteredViewName": "Nombre de la vista filtrada",
Expand Down
2 changes: 2 additions & 0 deletions plugins/view-assets/lang/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@
"Grouping": "Regroupement",
"Ordering": "Ordonnancement",
"Manual": "Manuel",
"Ascending": "Croissant",
"Descending": "Décroissant",
"FilteredViews": "Vues filtrées",
"NewFilteredView": "Nouvelle vue filtrée",
"FilteredViewName": "Nom de la vue filtrée",
Expand Down
2 changes: 2 additions & 0 deletions plugins/view-assets/lang/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@
"Grouping": "Raggruppamento",
"Ordering": "Ordinamento",
"Manual": "Manuale",
"Ascending": "Crescente",
"Descending": "Decrescente",
"FilteredViews": "Viste filtrate",
"NewFilteredView": "Nuova vista filtrata",
"FilteredViewName": "Nome vista filtrata",
Expand Down
2 changes: 2 additions & 0 deletions plugins/view-assets/lang/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@
"Grouping": "グループ化",
"Ordering": "順序",
"Manual": "手動",
"Ascending": "昇順",
"Descending": "降順",
"FilteredViews": "フィルターされたビュー",
"NewFilteredView": "新しいフィルターされたビュー",
"FilteredViewName": "フィルターされたビュー名",
Expand Down
2 changes: 2 additions & 0 deletions plugins/view-assets/lang/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@
"Grouping": "그룹화",
"Ordering": "정렬",
"Manual": "수동",
"Ascending": "오름차순",
"Descending": "내림차순",
"FilteredViews": "필터링된 뷰",
"NewFilteredView": "새 필터링된 뷰",
"FilteredViewName": "필터링된 뷰 이름",
Expand Down
2 changes: 2 additions & 0 deletions plugins/view-assets/lang/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@
"Grouping": "Grupowanie po",
"Ordering": "Sortowanie według",
"Manual": "Ręczne",
"Ascending": "Rosnąco",
"Descending": "Malejąco",

"FilteredViews": "Filtrowane widoki",
"NewFilteredView": "Nowy widok filtrowany",
Expand Down
2 changes: 2 additions & 0 deletions plugins/view-assets/lang/pt-br.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@
"Grouping": "Agrupamento",
"Ordering": "Ordenação",
"Manual": "Manual",
"Ascending": "Crescente",
"Descending": "Decrescente",
"FilteredViews": "Visualizações filtradas",
"NewFilteredView": "Nova visualização com filtros",
"FilteredViewName": "Nome da visualização com filtros",
Expand Down
2 changes: 2 additions & 0 deletions plugins/view-assets/lang/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@
"Grouping": "Agrupamento",
"Ordering": "Ordenação",
"Manual": "Manual",
"Ascending": "Crescente",
"Descending": "Decrescente",
"FilteredViews": "Vistas filtradas",
"NewFilteredView": "Nova vista filtrada",
"FilteredViewName": "Nome da vista filtrada",
Expand Down
2 changes: 2 additions & 0 deletions plugins/view-assets/lang/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@
"Grouping": "Группировка",
"Ordering": "Сортировка",
"Manual": "Пользовательский",
"Ascending": "По возрастанию",
"Descending": "По убыванию",
"FilteredViews": "Фильтрованные отображения",
"NewFilteredView": "Новое фильтрованное отображение",
"FilteredViewName": "Имя фильтрованного отображения",
Expand Down
2 changes: 2 additions & 0 deletions plugins/view-assets/lang/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@
"Grouping": "Gruplama",
"Ordering": "Sıralama",
"Manual": "Manuel",
"Ascending": "Artan",
"Descending": "Azalan",
"FilteredViews": "Filtreli görünümler",
"NewFilteredView": "Yeni filtreli görünüm",
"FilteredViewName": "Filtreli görünüm adı",
Expand Down
2 changes: 2 additions & 0 deletions plugins/view-assets/lang/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@
"Grouping": "分组",
"Ordering": "排序",
"Manual": "手动",
"Ascending": "升序",
"Descending": "降序",

"FilteredViews": "筛选视图",
"NewFilteredView": "新筛选视图",
Expand Down
58 changes: 58 additions & 0 deletions plugins/view-resources/src/__tests__/orderDirection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright © 2026 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.
import { SortingOrder } from '@hcengineering/core'
import { type OrderOption } from '@hcengineering/view'

import { hasOrderDirection, reverseOrder, selectOrder } from '../orderDirection'

const options: OrderOption[] = [
['modifiedOn', SortingOrder.Descending],
['title', SortingOrder.Ascending],
['rank', SortingOrder.Ascending]
]

describe('orderDirection', () => {
test('manual ordering has no direction', () => {
expect(hasOrderDirection('rank')).toBe(false)
expect(hasOrderDirection(undefined)).toBe(false)
expect(hasOrderDirection('modifiedOn')).toBe(true)
expect(hasOrderDirection(['title', SortingOrder.Ascending])).toBe(false)
})

test('reverseOrder flips the direction without mutating the input', () => {
const order: OrderOption = ['title', SortingOrder.Ascending]
expect(reverseOrder(order)).toEqual(['title', SortingOrder.Descending])
expect(reverseOrder(['title', SortingOrder.Descending])).toEqual(['title', SortingOrder.Ascending])
expect(order).toEqual(['title', SortingOrder.Ascending])
})

test('selecting another key uses its default direction', () => {
const result = selectOrder(['title', SortingOrder.Descending], 'modifiedOn', options)
expect(result).toEqual(['modifiedOn', SortingOrder.Descending])
expect(result).not.toBe(options[0])
})

test('selecting the active key again reverses the direction', () => {
expect(selectOrder(['title', SortingOrder.Ascending], 'title', options)).toEqual(['title', SortingOrder.Descending])
expect(selectOrder(['title', SortingOrder.Descending], 'title', options)).toEqual(['title', SortingOrder.Ascending])
})

test('selecting manual ordering again keeps it ascending', () => {
expect(selectOrder(['rank', SortingOrder.Descending], 'rank', options)).toEqual(['rank', SortingOrder.Ascending])
})

test('unknown keys and missing current ordering', () => {
expect(selectOrder(['title', SortingOrder.Ascending], 'unknown', options)).toBeUndefined()
expect(selectOrder(undefined, 'title', options)).toEqual(['title', SortingOrder.Ascending])
})
})
66 changes: 42 additions & 24 deletions plugins/view-resources/src/components/ViewOptions.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
<script lang="ts">
import { getClient } from '@hcengineering/presentation'
import { DropdownIntlItem, DropdownLabelsIntl, Label, Toggle } from '@hcengineering/ui'
import { Viewlet, ViewOptions, ViewOptionsModel, ViewOptionModel } from '@hcengineering/view'
import { ButtonIcon, DropdownIntlItem, DropdownLabelsIntl, Label, Toggle } from '@hcengineering/ui'
import { OrderOption, Viewlet, ViewOptions, ViewOptionsModel, ViewOptionModel } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import view from '../plugin'
import { buildConfigLookup, canResolveAttribute, getKeyLabel } from '../utils'
import { isDropdownType, isToggleType, noCategory } from '../viewOptions'
import { SortingOrder } from '@hcengineering/core'
import { hasOrderDirection, reverseOrder, selectOrder } from '../orderDirection'
import IconUpDown from './icons/UpDown.svelte'

export let viewlet: Viewlet
export let config: ViewOptionsModel
Expand Down Expand Up @@ -77,6 +79,14 @@
return groupBy.filter((p) => !notAllowed.includes(p.id as string))
}

function setOrder (value: OrderOption | undefined): void {
if (value === undefined) return
viewOptions.orderBy = value
dispatch('update', { key: 'orderBy', value })
}

$: descending = viewOptions.orderBy?.[1] === SortingOrder.Descending

const changeToggle = (model: ViewOptionModel) => {
const value = !(viewOptions[model.key] ?? model.defaultValue)
viewOptions[model.key] = value
Expand Down Expand Up @@ -130,28 +140,36 @@
{#if !hideGroupingAndOrdering && hasMultipleSelections(config.orderBy)}
<div class="antiCard-menu__item ordering">
<span class="overflow-label"><Label label={view.string.Ordering} /></span>
<DropdownLabelsIntl
label={view.string.Ordering}
kind={'regular'}
size={'medium'}
items={orderBy}
selected={viewOptions.orderBy?.[0]}
width="10rem"
justify="left"
on:selected={(e) => {
const key = e.detail
const value = config.orderBy.find((p) => p[0] === key)
if (value !== undefined) {
if (viewOptions.orderBy[0] === key) {
viewOptions.orderBy[1] =
viewOptions.orderBy[1] === SortingOrder.Ascending ? SortingOrder.Descending : SortingOrder.Ascending
} else {
viewOptions.orderBy = value
}
dispatch('update', { key: 'orderBy', value: viewOptions.orderBy })
}
}}
/>
<div class="flex-row-center gap-1">
<DropdownLabelsIntl
label={view.string.Ordering}
kind={'regular'}
size={'medium'}
items={orderBy}
selected={viewOptions.orderBy?.[0]}
width="8.25rem"
justify="left"
on:selected={(e) => {
setOrder(selectOrder(viewOptions.orderBy, e.detail, config.orderBy))
}}
/>
<!-- Keep the button in the layout for manual ordering, so the dropdown does not shift.
Dropdown + gap + button together keep the 10rem width of the other rows. -->
<div style:visibility={hasOrderDirection(viewOptions.orderBy?.[0]) ? 'visible' : 'hidden'}>
<ButtonIcon
kind={'tertiary'}
size={'small'}
icon={IconUpDown}
iconProps={{ descending }}
dataId={'orderDirection'}
tooltip={{ label: descending ? view.string.Descending : view.string.Ascending }}
disabled={!hasOrderDirection(viewOptions.orderBy?.[0])}
on:click={() => {
setOrder(reverseOrder(viewOptions.orderBy))
}}
/>
</div>
</div>
</div>
{/if}
{#if visibleOthers.length > 0 && !hideGroupingAndOrdering && (hasMultipleSelections(config.groupBy) || hasMultipleSelections(config.orderBy))}
Expand Down
46 changes: 46 additions & 0 deletions plugins/view-resources/src/orderDirection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// Copyright © 2026 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.
//

import { SortingOrder } from '@hcengineering/core'
import { type OrderOption } from '@hcengineering/view'

/**
* Manual (rank) ordering follows the user's drag-and-drop order, so a reversed
* direction has no meaning there and must not be offered. A key that is not a
* string means the editor got no single order option, so nothing is offered either.
*/
export function hasOrderDirection (key: unknown): boolean {
return typeof key === 'string' && key !== 'rank'
}

export function reverseOrder (order: OrderOption): OrderOption {
const direction = order[1] === SortingOrder.Ascending ? SortingOrder.Descending : SortingOrder.Ascending
return [order[0], direction]
}

/**
* Picks the new ordering when `key` is chosen in the Ordering dropdown: choosing
* the active key again reverses the direction, any other key uses its default.
*/
export function selectOrder (
current: OrderOption | undefined,
key: string,
options: OrderOption[]
): OrderOption | undefined {
const option = options.find((p) => p[0] === key)
if (option === undefined) return undefined
if (current?.[0] === key && hasOrderDirection(key)) return reverseOrder(current)
return [...option]
}
2 changes: 2 additions & 0 deletions plugins/view-resources/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ export default mergeIds(viewId, typedView, {
Grouping: '' as IntlString,
Ordering: '' as IntlString,
Manual: '' as IntlString,
Ascending: '' as IntlString,
Descending: '' as IntlString,
ShowPreviewOnClick: '' as IntlString,
Shown: '' as IntlString,
ShowEmptyGroups: '' as IntlString,
Expand Down