Update Filters layout (#1847)

Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
Alexander Platov 2022-05-24 17:46:19 +03:00 committed by GitHub
parent b470ec3769
commit 02e4d1988d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 526 additions and 193 deletions

View File

@ -436,7 +436,7 @@ export function createModel (builder: Builder): void {
action: view.actionImpl.ShowPanel,
actionProps: {
component: recruit.component.EditVacancy,
element: 'right'
element: 'content'
},
input: 'focus',
category: recruit.category.Recruit,

View File

@ -96,7 +96,7 @@
.status-bar {
min-height: var(--status-bar-height);
height: var(--status-bar-height);
min-width: 1200px;
min-width: 600px;
font-size: 12px;
line-height: 150%;
background-color: var(--divider-color);
@ -120,7 +120,7 @@
.app {
height: calc(100vh - var(--status-bar-height));
min-width: 1200px;
min-width: 600px;
min-height: 480px;
.error {

View File

@ -18,12 +18,14 @@
import { createQuery, getClient } from '@anticrm/presentation'
import { ActionIcon, Button, Icon, IconAdd, Label, Loading, SearchEdit, showPopup } from '@anticrm/ui'
import view, { Viewlet, ViewletPreference } from '@anticrm/view'
import { ActionContext, TableBrowser, ViewletSetting } from '@anticrm/view-resources'
import type { Filter } from '@anticrm/view'
import { ActionContext, TableBrowser, ViewletSetting, FilterButton } from '@anticrm/view-resources'
import contact from '../plugin'
import CreateContact from './CreateContact.svelte'
let search = ''
let resultQuery: DocumentQuery<Doc> = {}
let filters: Filter[] = []
function updateResultQuery (search: string): void {
resultQuery = search === '' ? {} : { $search: search }
@ -73,6 +75,7 @@
<div class="ac-header__wrap-title">
<div class="ac-header__icon"><Icon icon={contact.icon.Person} size={'small'} /></div>
<span class="ac-header__title"><Label label={contact.string.Contacts} /></span>
<div class="ml-4"><FilterButton _class={contact.class.Contact} bind:filters /></div>
</div>
<SearchEdit
@ -108,6 +111,7 @@
config={preference?.config ?? viewlet.config}
options={viewlet.options}
query={resultQuery}
bind:filters
showNotification
/>
{/if}

View File

@ -19,7 +19,8 @@
import task from '@anticrm/task'
import { ActionIcon, Button, Icon, IconAdd, Label, Loading, SearchEdit, showPopup } from '@anticrm/ui'
import view, { Viewlet, ViewletPreference } from '@anticrm/view'
import { TableBrowser, ViewletSetting } from '@anticrm/view-resources'
import type { Filter } from '@anticrm/view'
import { TableBrowser, ViewletSetting, FilterButton } from '@anticrm/view-resources'
import recruit from '../plugin'
import CreateApplication from './CreateApplication.svelte'
@ -28,6 +29,7 @@
const baseQuery: DocumentQuery<Applicant> = {
doneState: null
}
let filters: Filter[] = []
const client = getClient()
let descr: Viewlet | undefined
@ -71,6 +73,7 @@
<div class="ac-header__wrap-title">
<div class="ac-header__icon"><Icon icon={recruit.icon.Application} size={'small'} /></div>
<span class="ac-header__title"><Label label={recruit.string.Applications} /></span>
<div class="ml-4"><FilterButton _class={recruit.class.Applicant} bind:filters /></div>
</div>
<SearchEdit
@ -101,6 +104,7 @@
config={preference?.config ?? descr.config}
options={descr.options}
query={resultQuery}
bind:filters
showNotification
/>
{/if}

View File

@ -19,12 +19,14 @@
import { createQuery, getClient } from '@anticrm/presentation'
import { ActionIcon, showPopup, Icon, Label, Loading, SearchEdit, Button, IconAdd } from '@anticrm/ui'
import view, { Viewlet, ViewletPreference } from '@anticrm/view'
import { ActionContext, TableBrowser, ViewletSetting } from '@anticrm/view-resources'
import type { Filter } from '@anticrm/view'
import { ActionContext, TableBrowser, ViewletSetting, FilterButton } from '@anticrm/view-resources'
import recruit from '../plugin'
import CreateCandidate from './CreateCandidate.svelte'
let search = ''
let resultQuery: DocumentQuery<Doc> = {}
let filters: Filter[] = []
const client = getClient()
@ -71,6 +73,7 @@
<div class="ac-header__wrap-title">
<div class="ac-header__icon"><Icon icon={contact.icon.Person} size={'small'} /></div>
<span class="ac-header__title"><Label label={recruit.string.Candidates} /></span>
<div class="ml-4"><FilterButton _class={recruit.mixin.Candidate} bind:filters /></div>
</div>
<SearchEdit
@ -106,6 +109,7 @@
config={preference?.config ?? descr.config}
options={descr.options}
query={resultQuery}
bind:filters
showNotification
/>
{/if}

View File

@ -173,7 +173,7 @@
</div>
</div>
<div class="statustableview-container">
<TableBrowser {_class} {query} config={resConfig} {options} showNotification />
<TableBrowser {_class} bind:query config={resConfig} {options} filters={[]} showNotification />
</div>
<style lang="scss">

View File

@ -128,7 +128,12 @@
"FilterIsEither": "is either of",
"FilterStatesCount": "{value, plural, =1 {1 state} other {# states}}",
"EditIssue": "Edit {title}"
"EditIssue": "Edit {title}",
"Save": "Save",
"IncludeItemsThatMatch": "Include items that match",
"AnyFilter": "any filter",
"AllFilters": "all filters"
},
"status": {}
}

View File

@ -75,7 +75,12 @@
"GotoProjects": "Перейти к проекту",
"GotoTrackerApplication": "Перейти к приложению Трекер",
"EditIssue": "Редактирование {title}"
"EditIssue": "Редактирование {title}",
"Save": "Сохранить",
"IncludeItemsThatMatch": "Включить элементы, которые соответствуют",
"AnyFilter": "любому фильтру",
"AllFilters": "всем фильтрам"
},
"status": {}
}

View File

@ -13,13 +13,14 @@
// limitations under the License.
-->
<script lang="ts">
import { Button, eventToHTMLElement, IconAdd, showPopup } from '@anticrm/ui'
import { Button, eventToHTMLElement, IconAdd, showPopup, Label } from '@anticrm/ui'
import FilterSummarySection from './FilterSummarySection.svelte'
import StatusFilterMenuSection from './issues/StatusFilterMenuSection.svelte'
import PriorityFilterMenuSection from './issues/PriorityFilterMenuSection.svelte'
import { defaultPriorities, getGroupedIssues, IssueFilter } from '../utils'
import { WithLookup } from '@anticrm/core'
import { Issue, IssueStatus } from '@anticrm/tracker'
import tracker from '../plugin'
export let filters: IssueFilter[] = []
export let issues: Issue[] = []
@ -29,6 +30,8 @@
export let onDeleteFilter: (filterIndex?: number) => void
export let onChangeMode: (index: number) => void
let allFilters: boolean = true
$: defaultStatusIds = defaultStatuses.map((x) => x._id)
$: groupedByStatus = getGroupedIssues('status', issues, defaultStatusIds)
$: groupedByPriority = getGroupedIssues('priority', issues, defaultPriorities)
@ -86,30 +89,90 @@
}
</script>
<div class="root">
{#each filters as filter, filterIndex}
{@const [key, value] = Object.entries(filter.query)[0]}
<FilterSummarySection
type={key}
mode={filter.mode}
selectedFilters={value?.[filter.mode]}
onDelete={() => onDeleteFilter(filterIndex)}
onChangeMode={() => onChangeMode(filterIndex)}
onEditFilter={(event) => handleEditFilterMenuOpened(event, key, filterIndex)}
/>
{/each}
{#if onAddFilter}
<div class="ml-2">
<Button kind={'link'} icon={IconAdd} on:click={onAddFilter} />
{#if filters}
<div class="filterbar-container">
<div class="filters">
{#each filters as filter, filterIndex}
{@const [key, value] = Object.entries(filter.query)[0]}
<FilterSummarySection
type={key}
mode={filter.mode}
selectedFilters={value?.[filter.mode]}
onDelete={() => onDeleteFilter(filterIndex)}
onChangeMode={() => onChangeMode(filterIndex)}
onEditFilter={(event) => handleEditFilterMenuOpened(event, key, filterIndex)}
/>
{/each}
{#if onAddFilter}
<div class="add-filter">
<Button kind={'transparent'} size={'small'} icon={IconAdd} on:click={onAddFilter} />
</div>
{/if}
</div>
{/if}
</div>
<div class="buttons-group small-gap">
{#if filters.length > 1}
<div class="flex-baseline">
<span class="overflow-label">
<Label label={tracker.string.IncludeItemsThatMatch} />
</span>
<button
class="filter-button"
on:click={() => {
allFilters = !allFilters
}}
>
<Label label={allFilters ? tracker.string.AllFilters : tracker.string.AnyFilter} />
</button>
</div>
<div class="buttons-divider" />
{/if}
<Button icon={tracker.icon.Views} label={tracker.string.Save} size={'small'} width={'fit-content'} />
</div>
</div>
{/if}
<style lang="scss">
.root {
.filterbar-container {
display: flex;
flex: 1 1 auto;
flex-flow: row wrap;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1.5rem 0.75rem 2.5rem;
width: 100%;
min-width: 0;
border: 1px solid var(--divider-color);
border-left: none;
border-right: none;
.filters {
display: flex;
align-items: center;
flex-wrap: wrap;
margin-bottom: -0.375rem;
min-width: 0;
}
.add-filter {
margin-bottom: 0.375rem;
}
.filter-button {
display: flex;
align-items: baseline;
flex-shrink: 0;
padding: 0 0.375rem;
height: 1.5rem;
min-width: 1.5rem;
white-space: nowrap;
line-height: 150%;
color: var(--accent-color);
background-color: transparent;
border-radius: 0.25rem;
transition-duration: background-color 0.15s ease-in-out;
&:hover {
color: var(--caption-color);
background-color: var(--noborder-bg-hover);
}
}
}
</style>

View File

@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Button, IconClose } from '@anticrm/ui'
import { IconClose, Label, Icon } from '@anticrm/ui'
import { getIssueFilterAssetsByType } from '../utils'
import tracker from '../plugin'
@ -24,49 +24,94 @@
export let onDelete: () => void
export let onChangeMode: () => void
export let onEditFilter: (event: MouseEvent) => void
$: item = getIssueFilterAssetsByType(type)
</script>
<div class="root">
<div class="buttonWrapper">
<Button shape={'rectangle-right'} {...getIssueFilterAssetsByType(type)} />
{#if item}
<div class="filter-section">
<button class="filter-button left-round">
<div class="btn-icon mr-1-5">
<Icon icon={item.icon} size={'x-small'} />
</div>
<span><Label label={item.label} /></span>
</button>
<button class="filter-button" on:click={onChangeMode}>
<span>
<Label
label={mode === '$nin'
? tracker.string.FilterIsNot
: selectedFilters.length < 2
? tracker.string.FilterIs
: tracker.string.FilterIsEither}
on:click={onChangeMode}
/>
</span>
</button>
<button class="filter-button" on:click={onEditFilter}>
<span>
<Label label={tracker.string.FilterStatesCount} params={{ value: selectedFilters.length }} />
</span>
</button>
<button class="filter-button right-round" on:click={onDelete}>
<div class="btn-icon"><Icon icon={IconClose} size={'small'} /></div>
</button>
</div>
<div class="buttonWrapper">
<Button
shape="rectangle"
label={mode === '$nin'
? tracker.string.FilterIsNot
: selectedFilters.length < 2
? tracker.string.FilterIs
: tracker.string.FilterIsEither}
on:click={onChangeMode}
/>
</div>
<div class="buttonWrapper">
<Button
shape={'rectangle'}
label={tracker.string.FilterStatesCount}
labelParams={{ value: selectedFilters.length }}
on:click={onEditFilter}
/>
</div>
<div class="buttonWrapper">
<Button shape={'rectangle-left'} icon={IconClose} on:click={onDelete} />
</div>
</div>
{/if}
<style lang="scss">
.root {
.filter-section {
display: flex;
align-items: center;
margin-bottom: 0.375rem;
&:not(:first-child) {
margin-left: 0.5rem;
&:not(:last-child) {
margin-right: 0.375rem;
}
}
.buttonWrapper {
.filter-button {
display: flex;
align-items: center;
flex-shrink: 0;
margin-right: 1px;
padding: 0 0.375rem;
font-size: 0.75rem;
height: 1.5rem;
min-width: 1.5rem;
white-space: nowrap;
color: var(--accent-color);
background-color: var(--noborder-bg-color);
border: 1px solid transparent;
transition-property: border, background-color, color, box-shadow;
transition-duration: 0.15s;
.btn-icon {
color: var(--content-color);
transition: color 0.15s;
pointer-events: none;
}
span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 10rem;
}
&:hover {
color: var(--caption-color);
background-color: var(--noborder-bg-hover);
.btn-icon {
color: var(--caption-color);
}
}
&.left-round {
border-radius: 0.25rem 0 0 0.25rem;
}
&.right-round {
border-radius: 0 0.25rem 0.25rem 0;
}
&:last-child {
margin-right: 0;
}

View File

@ -25,7 +25,17 @@
IssueStatus,
IssueStatusCategory
} from '@anticrm/tracker'
import { Button, Label, ScrollBox, IconOptions, showPopup, eventToHTMLElement, IconAdd, IconClose } from '@anticrm/ui'
import {
Button,
Label,
ScrollBox,
IconOptions,
showPopup,
eventToHTMLElement,
IconAdd,
IconClose,
Icon
} from '@anticrm/ui'
import { IntlString } from '@anticrm/platform'
import { createEventDispatcher } from 'svelte'
import ViewOptionsPopup from './ViewOptionsPopup.svelte'
@ -432,43 +442,51 @@
</script>
{#if currentTeam}
<ScrollBox vertical stretch>
<div class="fs-title flex-between header">
<div class="titleContainer">
{#if totalIssuesCount === resultIssuesCount}
<Label label={title} params={{ value: totalIssuesCount }} />
{:else}
<div class="labelsContainer">
<Label label={title} params={{ value: resultIssuesCount }} />
<div class="totalIssuesLabel">/{totalIssuesCount}</div>
</div>
{/if}
<div class="ml-3">
<Button
size="small"
icon={isFiltersEmpty ? IconAdd : IconClose}
kind={'link-bordered'}
borderStyle={'dashed'}
label={isFiltersEmpty ? tracker.string.Filter : tracker.string.ClearFilters}
on:click={isFiltersEmpty ? handleFilterMenuOpened : handleAllFiltersDeleted}
/>
<div class="fs-title flex-between header">
<div class="titleContainer">
{#if totalIssuesCount === resultIssuesCount}
<Label label={title} params={{ value: totalIssuesCount }} />
{:else}
<div class="labelsContainer">
<Label label={title} params={{ value: resultIssuesCount }} />
<div class="totalIssuesLabel">/{totalIssuesCount}</div>
</div>
{/if}
<div class="ml-4">
<Button
size="small"
kind={'link-bordered'}
borderStyle={'dashed'}
on:click={isFiltersEmpty ? handleFilterMenuOpened : handleAllFiltersDeleted}
>
<svelte:fragment slot="content">
<div class="flex-row-center">
{#if isFiltersEmpty}
<Icon icon={IconAdd} size={'x-small'} />
<span class="ml-1"><Label label={tracker.string.Filter} /></span>
{:else}
<span class="mr-1"><Label label={tracker.string.ClearFilters} /></span>
<Icon icon={IconClose} size={'x-small'} />
{/if}
</div>
</svelte:fragment>
</Button>
</div>
<Button icon={IconOptions} kind={'link'} on:click={handleOptionsEditorOpened} />
</div>
{#if filters.length > 0}
<div class="filterSummaryWrapper">
<FilterSummary
{filters}
{issues}
defaultStatuses={statuses}
onAddFilter={handleFilterMenuOpened}
onUpdateFilter={handleFiltersModified}
onDeleteFilter={handleFilterDeleted}
onChangeMode={handleFilterModeChanged}
/>
</div>
{/if}
<Button icon={IconOptions} kind={'link'} on:click={handleOptionsEditorOpened} />
</div>
{#if filters.length > 0}
<FilterSummary
{filters}
{issues}
defaultStatuses={statuses}
onAddFilter={handleFilterMenuOpened}
onUpdateFilter={handleFiltersModified}
onDeleteFilter={handleFilterDeleted}
onChangeMode={handleFilterModeChanged}
/>
{/if}
<ScrollBox vertical stretch>
<IssuesListBrowser
_class={tracker.class.Issue}
{currentSpace}
@ -521,13 +539,4 @@
.totalIssuesLabel {
color: var(--content-color);
}
.filterSummaryWrapper {
display: flex;
align-items: center;
min-height: 3.5rem;
padding-left: 2.25rem;
padding-right: 1.35rem;
border-top: 1px solid var(--theme-button-border-hovered);
}
</style>

View File

@ -145,7 +145,12 @@ export default mergeIds(trackerId, tracker, {
FilterIsEither: '' as IntlString,
FilterStatesCount: '' as IntlString,
EditIssue: '' as IntlString
EditIssue: '' as IntlString,
Save: '' as IntlString,
IncludeItemsThatMatch: '' as IntlString,
AnyFilter: '' as IntlString,
AllFilters: '' as IntlString
},
component: {
NopeComponent: '' as AnyComponent,

View File

@ -53,5 +53,8 @@
<path d="M14,5.8l-1.1-1.9c-0.6-1-0.9-1.5-1.5-1.8c-0.6-0.3-1.2-0.3-2.3-0.4L8,1.7l-1.1,0c-1.1,0-1.8,0-2.3,0.4 C4,2.4,3.7,2.9,3.1,3.9L2,5.8C1.4,6.8,1.1,7.4,1.1,8S1.4,9.2,2,10.2l1.1,1.9c0.6,1,0.9,1.5,1.5,1.8c0.6,0.3,1.2,0.3,2.3,0.4l1.1,0 l1.1,0c1.1,0,1.8,0,2.3-0.4c0.6-0.3,0.9-0.9,1.5-1.8l1.1-1.9c0.6-1,0.9-1.5,0.9-2.2C14.9,7.4,14.6,6.8,14,5.8z M13.1,9.7L12,11.6 c-0.5,0.9-0.8,1.3-1.1,1.5c-0.3,0.2-0.8,0.2-1.8,0.2l-1.1,0l-1.1,0c-1.1,0-1.5,0-1.8-0.2c-0.3-0.2-0.6-0.6-1.1-1.5L2.9,9.7 C2.3,8.8,2.1,8.4,2.1,8c0-0.4,0.2-0.8,0.7-1.7L4,4.4c0.5-0.9,0.8-1.3,1.1-1.5c0.3-0.2,0.8-0.2,1.8-0.2l1.1,0l1.1,0 c1,0,1.5,0,1.8,0.2c0.3,0.2,0.6,0.6,1.1,1.5l1.1,1.9c0.5,0.9,0.7,1.3,0.7,1.7S13.6,8.8,13.1,9.7z"/>
<path d="M8,5.5C6.6,5.5,5.5,6.6,5.5,8c0,1.4,1.1,2.5,2.5,2.5c1.4,0,2.5-1.1,2.5-2.5C10.5,6.6,9.4,5.5,8,5.5z M8,9.5 C7.2,9.5,6.5,8.8,6.5,8S7.2,6.5,8,6.5S9.5,7.2,9.5,8S8.8,9.5,8,9.5z"/>
</symbol>
<symbol id="views" viewBox="0 0 16 16">
<path d="M12.6541 10.7952L14.7544 11.6213C14.8576 11.6618 14.9394 11.7434 14.9801 11.8466C15.0511 12.0264 14.9828 12.2268 14.827 12.3284L14.755 12.3656L8.35645 14.8924C8.15935 14.9703 7.94372 14.9831 7.74052 14.9309L7.62035 14.8918L1.25259 12.3653C1.1499 12.3246 1.06864 12.2432 1.02806 12.1404C0.957068 11.9607 1.02536 11.7603 1.1812 11.6587L1.25319 11.6215L3.34307 10.7962L7.06917 12.2751C7.65895 12.5091 8.31525 12.5097 8.9054 12.2766L12.6541 10.7952ZM12.6541 6.77688L14.7544 7.60289C14.8576 7.64346 14.9394 7.72508 14.9801 7.82824C15.0511 8.00803 14.9828 8.20839 14.827 8.31004L14.755 8.3472L10.6001 9.98825L9.619 10.375L8.35645 10.8741L8.317 10.886L8.23566 10.9132C8.20301 10.9215 8.17004 10.9282 8.13688 10.9331C8.12585 10.9346 8.11547 10.936 8.10507 10.9372C8.02541 10.9468 7.94422 10.9464 7.86397 10.9363L7.74052 10.9126L7.62035 10.8735L6.391 10.385L5.38907 9.98825L1.25259 8.34697C1.1499 8.30623 1.06864 8.22483 1.02806 8.12208C0.957068 7.94229 1.02536 7.74192 1.1812 7.64029L1.25319 7.60312L3.34307 6.77788L7.06917 8.25677C7.65895 8.49078 8.31525 8.4913 8.9054 8.25824L12.6541 6.77688ZM7.62186 1.06989C7.85734 0.976906 8.11932 0.976697 8.35494 1.06931L14.7544 3.58452C14.8576 3.62509 14.9394 3.70671 14.9801 3.80987C15.0612 4.01534 14.9605 4.24769 14.755 4.32884L10.6001 5.96988L8.35565 6.856L8.27468 6.88396C8.25405 6.8901 8.23326 6.89557 8.21236 6.90036C8.09824 6.92674 7.98013 6.93258 7.86397 6.91788L7.74052 6.89419L7.62035 6.8551L1.25259 4.3286C1.1499 4.28786 1.06864 4.20646 1.02806 4.10371C0.946925 3.89823 1.04772 3.66589 1.25319 3.58475L7.62186 1.06989Z"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -46,6 +46,10 @@
"FilterStatesCount": "{value, plural, =1 {1 state} other {# states}}",
"Before": "Before",
"After": "After",
"Apply": "Apply"
"Apply": "Apply",
"Save": "Save",
"IncludeItemsThatMatch": "Include items that match",
"AnyFilter": "any filter",
"AllFilters": "all filters"
}
}

View File

@ -44,6 +44,10 @@
"FilterStatesCount": "{value, plural, =1 {1 состоянию} other {# состояний}}",
"Before": "До",
"After": "После",
"Apply": "Применить"
"Apply": "Применить",
"Save": "Сохранить",
"IncludeItemsThatMatch": "Включить элементы, которые соответствуют",
"AnyFilter": "любому фильтру",
"AllFilters": "всем фильтрам"
}
}

View File

@ -27,7 +27,8 @@ loadMetadata(view.icon, {
Statuses: `${icons}#statuses`,
Open: `${icons}#open`,
Setting: `${icons}#setting`,
ArrowRight: `${icons}#arrow-right`
ArrowRight: `${icons}#arrow-right`,
Views: `${icons}#views`
})
addStringsLoader(viewId, async (lang: string) => await import(`../lang/${lang}.json`))

View File

@ -16,6 +16,7 @@
import type { Class, Doc, DocumentQuery, FindOptions, Ref } from '@anticrm/core'
import { Scroller } from '@anticrm/ui'
import { BuildModelKey } from '@anticrm/view'
import type { Filter } from '@anticrm/view'
import { onMount } from 'svelte'
import { ActionContext } from '..'
import { focusStore, ListSelectionProvider, SelectDirection, selectionStore } from '../selection'
@ -32,6 +33,7 @@
// If defined, will show a number of dummy items before real data will appear.
export let loadingProps: LoadingProps | undefined = undefined
export let filters: Filter[] | undefined = undefined
let resultQuery = query
@ -54,14 +56,16 @@
}}
/>
<FilterBar {_class} {query} on:change={(e) => (resultQuery = e.detail)} />
{#if filters}
<FilterBar {_class} {query} bind:filters on:change={(e) => (resultQuery = e.detail)} />
{/if}
<Scroller tableFade>
<Table
bind:this={table}
{_class}
{config}
{options}
query={resultQuery}
bind:query={resultQuery}
{showNotification}
{baseMenuClass}
{loadingProps}

View File

@ -15,7 +15,7 @@
<script lang="ts">
import { Class, Doc, DocumentQuery, Ref } from '@anticrm/core'
import { getClient } from '@anticrm/presentation'
import { Button, eventToHTMLElement, IconAdd, IconClose, showPopup } from '@anticrm/ui'
import { Button, eventToHTMLElement, IconAdd, Label, showPopup } from '@anticrm/ui'
import { Filter } from '@anticrm/view'
import { createEventDispatcher } from 'svelte'
import view from '../../plugin'
@ -24,13 +24,14 @@
export let _class: Ref<Class<Doc>>
export let query: DocumentQuery<Doc>
export let filters: Filter[] = []
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
let filters: Filter[] = []
let maxIndex = 0
let maxIndex = filters ? filters.length : 0
let allFilters: boolean = true
function onChange (e: Filter | undefined) {
if (e === undefined) return
@ -118,43 +119,92 @@
$: visible = hierarchy.hasMixin(clazz, view.mixin.ClassFilters)
</script>
{#if visible}
<div class="flex-row-center pl-4 pr-4">
{#each filters as filter, i}
<FilterSection
{_class}
{filter}
on:change={() => {
makeQuery(query, filters)
}}
on:remove={() => {
remove(i)
}}
/>
{/each}
<div class="ml-2">
<Button
size="small"
icon={IconAdd}
kind={'link-bordered'}
borderStyle={'dashed'}
label={view.string.Filter}
on:click={add}
/>
</div>
{#if filters.length}
<div class="ml-2">
<Button
size="small"
icon={IconClose}
kind={'link-bordered'}
borderStyle={'dashed'}
label={view.string.ClearFilters}
on:click={() => {
filters = []
{#if visible && filters && filters.length > 0}
<div class="filterbar-container">
<div class="filters">
{#each filters as filter, i}
<FilterSection
{_class}
{filter}
on:change={() => {
makeQuery(query, filters)
}}
on:remove={() => {
remove(i)
}}
/>
{/each}
<div class="add-filter">
<Button size={'small'} icon={IconAdd} kind={'transparent'} on:click={add} />
</div>
{/if}
</div>
<div class="buttons-group small-gap ml-4">
{#if filters.length > 1}
<div class="flex-baseline">
<span class="overflow-label">
<Label label={view.string.IncludeItemsThatMatch} />
</span>
<button
class="filter-button"
on:click={() => {
allFilters = !allFilters
}}
>
<Label label={allFilters ? view.string.AllFilters : view.string.AnyFilter} />
</button>
</div>
<div class="buttons-divider" />
{/if}
<Button icon={view.icon.Views} label={view.string.Save} size={'small'} width={'fit-content'} />
</div>
</div>
{/if}
<style lang="scss">
.filterbar-container {
display: grid;
grid-template-columns: auto auto;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1.5rem 0.75rem 2.5rem;
width: 100%;
min-width: 0;
border: 1px solid var(--divider-color);
border-left: none;
border-right: none;
.filters {
display: flex;
align-items: center;
flex-wrap: wrap;
flex-grow: 1;
margin-bottom: -0.375rem;
width: 100%;
min-width: 0;
}
.add-filter {
margin-bottom: 0.375rem;
}
.filter-button {
display: flex;
align-items: baseline;
flex-shrink: 0;
padding: 0 0.375rem;
height: 1.5rem;
min-width: 1.5rem;
white-space: nowrap;
line-height: 150%;
color: var(--accent-color);
background-color: transparent;
border-radius: 0.25rem;
transition-duration: background-color 0.15s ease-in-out;
&:hover {
color: var(--caption-color);
background-color: var(--noborder-bg-hover);
}
}
}
</style>

View File

@ -0,0 +1,73 @@
<!--
// 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, Ref } from '@anticrm/core'
import { getClient } from '@anticrm/presentation'
import { Button, eventToHTMLElement, IconAdd, IconClose, Icon, showPopup, Label } from '@anticrm/ui'
import { Filter } from '@anticrm/view'
import view from '../../plugin'
import FilterTypePopup from './FilterTypePopup.svelte'
export let _class: Ref<Class<Doc>>
export let filters: Filter[]
const client = getClient()
const hierarchy = client.getHierarchy()
function onChange (e: Filter | undefined) {
if (e !== undefined) filters = [e]
}
function add (e: MouseEvent) {
const target = eventToHTMLElement(e)
showPopup(
FilterTypePopup,
{
_class,
target,
index: 1,
onChange
},
target
)
}
$: clazz = hierarchy.getClass(_class)
$: visible = hierarchy.hasMixin(clazz, view.mixin.ClassFilters)
</script>
{#if visible}
<Button
size="small"
kind={'link-bordered'}
borderStyle={'dashed'}
on:click={(ev) => {
if (filters.length === 0) add(ev)
else filters = []
}}
>
<svelte:fragment slot="content">
<div class="flex-row-center">
{#if filters.length === 0}
<Icon icon={IconAdd} size={'x-small'} />
<span class="ml-1"><Label label={view.string.Filter} /></span>
{:else}
<span class="mr-1"><Label label={view.string.ClearFilters} /></span>
<Icon icon={IconClose} size={'x-small'} />
{/if}
</div>
</svelte:fragment>
</Button>
{/if}

View File

@ -14,7 +14,7 @@
-->
<script lang="ts">
import { Class, Doc, Ref } from '@anticrm/core'
import { Button, eventToHTMLElement, IconClose, showPopup } from '@anticrm/ui'
import { eventToHTMLElement, IconClose, showPopup, Icon, Label } from '@anticrm/ui'
import { Filter } from '@anticrm/view'
import { createEventDispatcher } from 'svelte'
import view from '../../plugin'
@ -38,55 +38,99 @@
}
</script>
<div class="root">
<div class="buttonWrapper">
<Button shape={'rectangle-right'} label={filter.key.label} icon={filter.key.icon} />
</div>
<div class="buttonWrapper">
<Button shape="rectangle" label={filter.mode.label} on:click={toggle} />
</div>
<div class="buttonWrapper">
<Button
shape={'rectangle'}
label={view.string.FilterStatesCount}
labelParams={{ value: filter.value.length }}
on:click={(e) => {
showPopup(
filter.key.component,
{
_class,
filter,
onChange
},
eventToHTMLElement(e)
)
}}
/>
</div>
<div class="buttonWrapper">
<Button
shape={'rectangle-left'}
icon={IconClose}
on:click={() => {
dispatch('remove')
}}
/>
</div>
<div class="filter-section">
<button class="filter-button left-round">
{#if filter.key.icon}
<div class="btn-icon mr-1-5">
<Icon icon={filter.key.icon} size={'x-small'} />
</div>
{/if}
<span><Label label={filter.key.label} /></span>
</button>
<button class="filter-button" on:click={toggle}>
<span><Label label={filter.mode.label} /></span>
</button>
<button
class="filter-button"
on:click={(e) => {
showPopup(
filter.key.component,
{
_class,
filter,
onChange
},
eventToHTMLElement(e)
)
}}
>
<span>
<Label label={view.string.FilterStatesCount} params={{ value: filter.value.length }} />
</span>
</button>
<button
class="filter-button right-round"
on:click={() => {
dispatch('remove')
}}
>
<div class="btn-icon"><Icon icon={IconClose} size={'small'} /></div>
</button>
</div>
<style lang="scss">
.root {
.filter-section {
display: flex;
align-items: center;
margin-bottom: 0.375rem;
&:not(:first-child) {
margin-left: 0.5rem;
&:not(:last-child) {
margin-right: 0.375rem;
}
}
.buttonWrapper {
.filter-button {
display: flex;
align-items: center;
flex-shrink: 0;
margin-right: 1px;
padding: 0 0.375rem;
font-size: 0.75rem;
height: 1.5rem;
min-width: 1.5rem;
white-space: nowrap;
color: var(--accent-color);
background-color: var(--noborder-bg-color);
border: 1px solid transparent;
transition-property: border, background-color, color, box-shadow;
transition-duration: 0.15s;
.btn-icon {
color: var(--content-color);
transition: color 0.15s;
pointer-events: none;
}
span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 10rem;
}
&:hover {
color: var(--caption-color);
background-color: var(--noborder-bg-hover);
.btn-icon {
color: var(--caption-color);
}
}
&.left-round {
border-radius: 0.25rem 0 0 0.25rem;
}
&.right-round {
border-radius: 0 0.25rem 0.25rem 0;
}
&:last-child {
margin-right: 0;
}

View File

@ -176,7 +176,7 @@
</div>
</div>
<Button
shape={'round'}
kind={'no-border'}
label={view.string.Apply}
on:click={() => {
onChange(filter)

View File

@ -56,6 +56,7 @@ export { default as ActionHandler } from './components/ActionHandler.svelte'
export { default as ContextMenu } from './components/Menu.svelte'
export { default as TableBrowser } from './components/TableBrowser.svelte'
export { default as LinkPresenter } from './components/LinkPresenter.svelte'
export { default as FilterButton } from './components/filter/FilterButton.svelte'
export * from './context'
export * from './selection'
export { buildModel, getCollectionCounter, getObjectPresenter, LoadingProps } from './utils'

View File

@ -49,6 +49,10 @@ export default mergeIds(viewId, view, {
FilterStatesCount: '' as IntlString,
Before: '' as IntlString,
After: '' as IntlString,
Apply: '' as IntlString
Apply: '' as IntlString,
Save: '' as IntlString,
IncludeItemsThatMatch: '' as IntlString,
AnyFilter: '' as IntlString,
AllFilters: '' as IntlString
}
})

View File

@ -405,7 +405,8 @@ const view = plugin(viewId, {
Statuses: '' as Asset,
Setting: '' as Asset,
Open: '' as Asset,
ArrowRight: '' as Asset
ArrowRight: '' as Asset,
Views: '' as Asset
},
category: {
General: '' as Ref<ActionCategory>,

View File

@ -89,5 +89,5 @@
{:else}
<SpaceHeader spaceId={space._id} {viewlets} {createItemDialog} {createItemLabel} bind:search bind:viewlet />
{/if}
<SpaceContent space={space._id} {_class} {search} {viewlet} />
<SpaceContent space={space._id} {_class} bind:search {viewlet} />
{/if}

View File

@ -19,7 +19,7 @@ test.describe('workbench tests', () => {
`${PlatformURI}/workbench%3Acomponent%3AWorkbenchApp/recruit%3Aapp%3ARecruit/applicants`
)
// Click text=Applications Application >> span
await expect(page.locator('text=Applications Application')).toBeVisible()
await expect(page.locator('text=Applications Filter')).toBeVisible()
await expect(page.locator('text="APP-1')).toBeDefined()
// Click text=Candidates