mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-31 15:37:19 +03:00
Update Filters layout (#1847)
Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
parent
b470ec3769
commit
02e4d1988d
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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">
|
||||
|
@ -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": {}
|
||||
}
|
||||
|
@ -75,7 +75,12 @@
|
||||
"GotoProjects": "Перейти к проекту",
|
||||
"GotoTrackerApplication": "Перейти к приложению Трекер",
|
||||
|
||||
"EditIssue": "Редактирование {title}"
|
||||
"EditIssue": "Редактирование {title}",
|
||||
|
||||
"Save": "Сохранить",
|
||||
"IncludeItemsThatMatch": "Включить элементы, которые соответствуют",
|
||||
"AnyFilter": "любому фильтру",
|
||||
"AllFilters": "всем фильтрам"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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 |
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +44,10 @@
|
||||
"FilterStatesCount": "{value, plural, =1 {1 состоянию} other {# состояний}}",
|
||||
"Before": "До",
|
||||
"After": "После",
|
||||
"Apply": "Применить"
|
||||
"Apply": "Применить",
|
||||
"Save": "Сохранить",
|
||||
"IncludeItemsThatMatch": "Включить элементы, которые соответствуют",
|
||||
"AnyFilter": "любому фильтру",
|
||||
"AllFilters": "всем фильтрам"
|
||||
}
|
||||
}
|
||||
|
@ -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`))
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
@ -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;
|
||||
}
|
||||
|
@ -176,7 +176,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
shape={'round'}
|
||||
kind={'no-border'}
|
||||
label={view.string.Apply}
|
||||
on:click={() => {
|
||||
onChange(filter)
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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>,
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user