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
1 change: 1 addition & 0 deletions etc/blocks/ui-examples/model/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ export const platforma = BlockModel.create('Heavy')
{ type: 'link', href: '/pl-autocomplete', label: 'PlAutocomplete' },
{ type: 'link', href: '/pl-autocomplete-multi', label: 'PlAutocompleteMulti' },
{ type: 'link', href: '/radio', label: 'PlRadio' },
{ type: 'link', href: '/advanced-filter', label: 'PlAdvancedFilter' },
...(dynamicSections.length
? [
{ type: 'delimiter' },
Expand Down
2 changes: 2 additions & 0 deletions etc/blocks/ui-examples/ui/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { platforma } from '@milaboratories/milaboratories.ui-examples.model';
import { animate, defineApp, makeEaseOut } from '@platforma-sdk/ui-vue';
import { computed, reactive, ref } from 'vue';
import AddSectionPage from './pages/AddSectionPage.vue';
import AdvancedFilterPage from './pages/PlAdvancedFilterPage.vue';
import { AgGridVuePage, AgGridVuePageWithBuilder } from './pages/AgGridVuePage';
import ButtonsPage from './pages/ButtonsPage.vue';
import DownloadsPage from './pages/DownloadsPage.vue';
Expand Down Expand Up @@ -120,6 +121,7 @@ export const sdkPlugin = defineApp(platforma, (app) => {
'/add-section': () => AddSectionPage,
'/section': () => SectionPage,
'/radio': () => RadioPage,
'/advanced-filter': () => AdvancedFilterPage,
},
};
}, {
Expand Down
238 changes: 238 additions & 0 deletions etc/blocks/ui-examples/ui/src/pages/PlAdvancedFilterPage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
<script setup lang="ts">
import type { FilterSpec, ListOptionBase, SUniversalPColumnId } from '@platforma-sdk/model';
import { PlAdvancedFilter, PlBlockPage, PlCheckbox, PlDropdown } from '@platforma-sdk/ui-vue';
import { computed, ref, watch } from 'vue';

const uniqueValuesByColumnOrAxisId: Record<string, ListOptionBase<string>[]> = {
1: [{ value: '1', label: 'Value 1' }, { value: '2', label: 'Value 2' }],
2: [{ value: '3', label: 'Value 3' }, { value: '4', label: 'Value 4' }],
3: [{ value: '5', label: 'Value 5' }, { value: '6', label: 'Value 6' }],
};
const uniqueValuesByAxisIdx: Record<string, Record<number, ListOptionBase<string>[]>> = {
1: { 0: [{ value: 'axisValue1', label: 'Axis Value 1' }, { value: 'axisValue2', label: 'Axis Value 2' }] },
};

const options = [
{
id: '1' as SUniversalPColumnId,
info: {
label: 'Column 1',
error: false,
axesToBeFixed: [{
idx: 0,
label: 'Axis 1 label',
info: {
label: 'Axis 1 label',
spec: { type: 'String' as const, name: 'nameAxis1' },
uniqueValues: [{ value: 'axisValue1', label: 'Axis Value 1' }, { value: 'axisValue2', label: 'Axis Value 2' }],
},
}],
spec: { kind: 'PColumn' as const, valueType: 'Int' as const, name: 'c1', axesSpec: [{ type: 'String' as const, name: 'nameAxis1' }] },
},
},
{
id: '2' as SUniversalPColumnId,
info: {
label: 'Column 2',
error: false,
spec: { kind: 'PColumn' as const, valueType: 'String' as const, name: 'c2', axesSpec: [] },
},
},
{
id: '3' as SUniversalPColumnId,
info: {
label: 'Column 3',
error: false,
spec: { kind: 'PColumn' as const, valueType: 'Double' as const, name: 'c3', axesSpec: [] },
},
},
];
const dndMode = ref(false);
const draggedId = ref<string | undefined>();

async function searchOptions(id: string, str: string, axisIdx?: number) {
if (axisIdx !== undefined) {
return uniqueValuesByAxisIdx[id]?.[axisIdx] || [];
}
return uniqueValuesByColumnOrAxisId[id] || [];
}
async function searchModel(id: string, modelStr: string, axisIdx?: number) {
if (axisIdx !== undefined) {
const axisValues = uniqueValuesByAxisIdx[id]?.[axisIdx];
return axisValues.find((v) => v.value === modelStr) || { value: modelStr, label: `Label of ${modelStr}` };
}
const columnValues = uniqueValuesByColumnOrAxisId[id];
return columnValues.find((v) => v.value === modelStr) || { value: modelStr, label: `Label of ${modelStr}` };
}

const errorState = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you highlight where exactly mistake in this state?

type: 'and' as const,
filters: [
{
type: 'and' as const,
filters: [
{
type: 'patternEquals' as const,
column: 'someColumn' as SUniversalPColumnId, // error - column id is not from available columns
value: 'A',
},
{
type: 'or' as const,
filters: [
{
type: 'patternEquals' as const,
column: 'someColumn' as SUniversalPColumnId, // error - column id is not from available columns
value: 'A',
},
],
},
],
}, {
type: 'and' as const,
filters: [
{
type: 'isNA' as const,
column: 'someColumn' as SUniversalPColumnId, // error - column id is not from available columns
},
{
type: 'isNotNA' as const,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's create objet with types for reusing it

const FilterUiTypes = {
  IsNa: 'isNa'
  ...
} as const

column: 'someColumn' as SUniversalPColumnId, // error - column id is not from available columns
},
],
}, {
type: 'and' as const,
filters: [
{
type: 'patternContainSubsequence' as const,
column: 'someColumn' as SUniversalPColumnId, // error - column id is not from available columns
value: 'someString',
},
],
}],
};

const normalState: FilterSpec = {
type: 'and' as const,
filters: [
{
type: 'or' as const,
filters: [{
type: 'isNA' as const,
column: '1' as SUniversalPColumnId,
}, {
type: 'equal' as const,
column: '2' as SUniversalPColumnId,
x: 10,
}],
},
{
type: 'and' as const,
filters: [
{
type: 'isNotNA' as const,
column: '3' as SUniversalPColumnId,
}, {
type: 'patternFuzzyContainSubsequence' as const,
column: '3' as SUniversalPColumnId,
value: 'abc',
},
],
},
],
};

const filterStates: Record<string, FilterSpec> = {
normalState: normalState,
errorState: errorState,
emptyState: {
type: 'and',
filters: [],
},
};

const filtersModel = computed<FilterSpec>({
get: () => {
return filterStates[selectedSavedFilters.value];
},
set: (v) => {
console.log('updated filters state: ', v);
},
});

const selectedSavedFilters = ref<keyof typeof filterStates>('normalState');
const filterStatesOptions = [
{ value: 'normalState', label: 'Normal state' },
{ value: 'errorState', label: 'State with errors' },
{ value: 'emptyState', label: 'Empty state' },
];

watch(() => filtersModel.value, (m) => {
console.log('Model changed: ', m);
});
</script>

<template>
<PlBlockPage>
<div :class="$style.controls">
<PlCheckbox v-model="dndMode" >Drag-n-Drop mode</PlCheckbox>
<PlDropdown v-model="selectedSavedFilters" :options="filterStatesOptions" label="Examples" :style="{width: '300px'}" />
</div>
<div :class="$style.block">
<div v-if="dndMode" :class="$style.leftColumn">
<div
v-for="option in options"
:key="option.id"
:draggable="dndMode ? 'true' : undefined"
:class="$style.columnChip"
@dragstart="() => draggedId = option.id"
@dragend="() => draggedId = undefined"
>
{{ option.info.label }}
</div>
</div>
<div :key="selectedSavedFilters" :class="$style.rightColumn" >
<PlAdvancedFilter
v-model="filtersModel"
:items="options"
:dnd-mode="dndMode"
:dragged-id="draggedId"
:search-options="searchOptions"
:search-model="searchModel"
/>
</div>
</div>
</PlBlockPage>
</template>
<style module>
.controls {
display: flex;
gap: 16px;
}
.block {
display: flex;
height: 100%;
overflow: hidden;
}
.leftColumn {
display: flex;
flex-direction: column;
gap: 16px;
width: 280px;
padding: 10px;
margin: 10px;
border: 1px solid grey;
}
.rightColumn {
display: flex;
flex-direction: column;
width: 280px;
height: 100%;
overflow: auto;
}
.columnChip {
background: #fff;
border: 1px solid black;
border-radius: 6px;
padding: 6px;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ const props = withDefaults(
* Formatter for the selected value if its label is absent
*/
formatValue?: (value: M) => string;
/**
* Makes some of corners not rounded
* */
position?: 'top' | 'bottom' | 'left' | 'right' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'middle';
}>(),
{
modelSearch: undefined,
Expand All @@ -107,6 +111,7 @@ const props = withDefaults(
arrowIconLarge: undefined,
optionSize: 'small',
formatValue: (v: M) => String(v),
position: undefined,
},
);

Expand Down Expand Up @@ -397,7 +402,7 @@ watch(() => optionsRequest.loading || modelOptionRequest.loading, (loading) => {
/>
<div v-if="!renderedOptionsRef.length" class="nothing-found">Nothing found</div>
</DropdownOverlay>
<DoubleContour class="pl-autocomplete__contour" />
<DoubleContour class="pl-autocomplete__contour" :position="position" />
</div>
</div>
<div v-if="computedError" class="pl-autocomplete__error">{{ computedError }}</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ const props = withDefaults(
* The text to display when no options are found.
*/
emptyOptionsText?: string;
/**
* Makes some of corners not rounded
* */
position?: 'top' | 'bottom' | 'left' | 'right' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'middle';
}>(),
{
modelValue: () => [],
Expand All @@ -131,6 +135,7 @@ const props = withDefaults(
debounce: 300,
emptyOptionsText: 'Nothing found',
sourceId: undefined,
position: undefined,
},
);

Expand Down Expand Up @@ -406,7 +411,7 @@ const computedError = computed(() => {
/>
<div v-if="!filteredOptionsRef.length && !isOptionsLoading" class="nothing-found">{{ emptyOptionsText }}</div>
</DropdownOverlay>
<DoubleContour class="pl-autocomplete-multi__contour" />
<DoubleContour class="pl-autocomplete-multi__contour" :position="position" />
</div>
</div>
<div v-if="computedError" class="pl-autocomplete-multi__error">{{ computedError }}</div>
Expand Down
14 changes: 12 additions & 2 deletions lib/ui/uikit/src/components/PlDropdown/PlDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ const props = withDefaults(
* Error message displayed below the dropdown (optional)
*/
error?: unknown;
/**
* Shows red border even without an error message
*/
errorStatus?: boolean;
/**
* Placeholder text shown when no value is selected.
*/
Expand Down Expand Up @@ -88,12 +92,17 @@ const props = withDefaults(
* Option list item size
*/
optionSize?: 'small' | 'medium';
/**
* Makes some of corners not rounded
* */
position?: 'top' | 'bottom' | 'left' | 'right' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'middle';
}>(),
{
label: '',
helper: undefined,
loadingOptionsHelper: undefined,
error: undefined,
showErrorMessage: true,
placeholder: '...',
clearable: false,
required: false,
Expand All @@ -102,6 +111,7 @@ const props = withDefaults(
arrowIconLarge: undefined,
optionSize: 'small',
options: undefined,
position: undefined,
},
);

Expand Down Expand Up @@ -316,7 +326,7 @@ watchPostEffect(() => {
ref="rootRef"
:tabindex="tabindex"
class="pl-dropdown"
:class="{ open: data.open, error, disabled: isDisabled }"
:class="{ open: data.open, error: error || errorStatus, disabled: isDisabled }"
@keydown="handleKeydown"
@focusout="onFocusOut"
>
Expand Down Expand Up @@ -367,7 +377,7 @@ watchPostEffect(() => {
:option-size="optionSize"
:select-option="selectOptionWrapper"
/>
<DoubleContour class="pl-dropdown__contour" />
<DoubleContour class="pl-dropdown__contour" :position="position" />
</div>
</div>
<div v-if="computedError" class="pl-dropdown__error">{{ computedError }}</div>
Expand Down
Loading