@object-ui/plugin-view 3.1.0 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -419,7 +419,7 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
419
419
  }
420
420
  if (navigationConfig.mode === 'new_window' || navigationConfig.openNewTab) {
421
421
  const recordId = record._id || record.id;
422
- const url = `/${schema.objectName}/${recordId}`;
422
+ const url = `/${schema.objectName}/${encodeURIComponent(String(recordId))}`;
423
423
  window.open(url, '_blank');
424
424
  return;
425
425
  }
@@ -546,7 +546,7 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
546
546
 
547
547
  const filterableFieldDefs = fieldEntries.map(([key, f]: [string, any]) => {
548
548
  const fieldType = f.type || 'text';
549
- let filterType: 'text' | 'number' | 'select' | 'date' | 'boolean' = 'text';
549
+ let filterType: 'text' | 'number' | 'select' | 'multi-select' | 'date' | 'boolean' = 'text';
550
550
  let options: Array<{ label: string; value: any }> | undefined;
551
551
 
552
552
  if (fieldType === 'number' || fieldType === 'currency' || fieldType === 'percent') {
@@ -555,11 +555,18 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
555
555
  filterType = 'boolean';
556
556
  } else if (fieldType === 'date' || fieldType === 'datetime') {
557
557
  filterType = 'date';
558
- } else if (fieldType === 'select' || f.options) {
558
+ } else if (fieldType === 'select' || fieldType === 'status' || f.options) {
559
559
  filterType = 'select';
560
560
  options = (f.options || []).map((o: any) =>
561
561
  typeof o === 'string' ? { label: o, value: o } : { label: o.label, value: o.value },
562
562
  );
563
+ } else if (fieldType === 'lookup' || fieldType === 'master_detail' || fieldType === 'user' || fieldType === 'owner') {
564
+ if (f.options && f.options.length > 0) {
565
+ filterType = 'multi-select';
566
+ options = (f.options || []).map((o: any) =>
567
+ typeof o === 'string' ? { label: o, value: o } : { label: o.label, value: o.value },
568
+ );
569
+ }
563
570
  }
564
571
  return {
565
572
  field: key,
@@ -875,6 +882,7 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
875
882
  addRecord: activeView?.addRecord ?? (schema as any).addRecord,
876
883
  conditionalFormatting: activeView?.conditionalFormatting ?? (schema as any).conditionalFormatting,
877
884
  quickFilters: activeView?.quickFilters ?? (schema as any).quickFilters,
885
+ userFilters: activeView?.userFilters ?? (schema as any).userFilters,
878
886
  showRecordCount: activeView?.showRecordCount ?? (schema as any).showRecordCount,
879
887
  allowPrinting: activeView?.allowPrinting ?? (schema as any).allowPrinting,
880
888
  virtualScroll: activeView?.virtualScroll ?? (schema as any).virtualScroll,
@@ -541,4 +541,101 @@ describe('FilterUI', () => {
541
541
  window.removeEventListener('filter:changed', spy);
542
542
  });
543
543
  });
544
+
545
+ // -------------------------------------------------------------------------
546
+ // 9. Multi-select filter type
547
+ // -------------------------------------------------------------------------
548
+ describe('multi-select filter type', () => {
549
+ const multiSelectFilters: FilterUISchema['filters'] = [
550
+ {
551
+ field: 'tags',
552
+ label: 'Tags',
553
+ type: 'multi-select',
554
+ options: [
555
+ { label: 'Frontend', value: 'frontend' },
556
+ { label: 'Backend', value: 'backend' },
557
+ { label: 'DevOps', value: 'devops' },
558
+ ],
559
+ },
560
+ ];
561
+
562
+ it('renders checkboxes for multi-select type', () => {
563
+ render(
564
+ <FilterUI
565
+ schema={makeSchema({ filters: multiSelectFilters })}
566
+ />,
567
+ );
568
+
569
+ expect(screen.getByText('Tags')).toBeInTheDocument();
570
+ const checkboxes = screen.getAllByTestId('checkbox');
571
+ expect(checkboxes.length).toBe(3);
572
+ expect(screen.getByText('Frontend')).toBeInTheDocument();
573
+ expect(screen.getByText('Backend')).toBeInTheDocument();
574
+ expect(screen.getByText('DevOps')).toBeInTheDocument();
575
+ });
576
+
577
+ it('calls onChange with array when multi-select checkbox is toggled', () => {
578
+ const onChange = vi.fn();
579
+ render(
580
+ <FilterUI
581
+ schema={makeSchema({ filters: multiSelectFilters })}
582
+ onChange={onChange}
583
+ />,
584
+ );
585
+
586
+ const checkboxes = screen.getAllByTestId('checkbox');
587
+ fireEvent.click(checkboxes[0]); // Frontend
588
+ expect(onChange).toHaveBeenCalledWith({ tags: ['frontend'] });
589
+ });
590
+
591
+ it('adds to selection when another checkbox is checked', () => {
592
+ const onChange = vi.fn();
593
+ render(
594
+ <FilterUI
595
+ schema={makeSchema({
596
+ filters: multiSelectFilters,
597
+ values: { tags: ['frontend'] },
598
+ })}
599
+ onChange={onChange}
600
+ />,
601
+ );
602
+
603
+ const checkboxes = screen.getAllByTestId('checkbox');
604
+ fireEvent.click(checkboxes[1]); // Backend
605
+ expect(onChange).toHaveBeenCalledWith({ tags: ['frontend', 'backend'] });
606
+ });
607
+
608
+ it('removes from selection when checkbox is unchecked', () => {
609
+ const onChange = vi.fn();
610
+ render(
611
+ <FilterUI
612
+ schema={makeSchema({
613
+ filters: multiSelectFilters,
614
+ values: { tags: ['frontend', 'backend'] },
615
+ })}
616
+ onChange={onChange}
617
+ />,
618
+ );
619
+
620
+ // Uncheck Frontend (first checkbox)
621
+ const checkboxes = screen.getAllByTestId('checkbox');
622
+ fireEvent.click(checkboxes[0]); // Frontend
623
+ expect(onChange).toHaveBeenCalledWith({ tags: ['backend'] });
624
+ });
625
+
626
+ it('shows selected count in active badge for popover layout', () => {
627
+ render(
628
+ <FilterUI
629
+ schema={makeSchema({
630
+ layout: 'popover',
631
+ filters: multiSelectFilters,
632
+ values: { tags: ['frontend', 'backend'] },
633
+ })}
634
+ />,
635
+ );
636
+
637
+ // Active count should be 1 (tags field has a value)
638
+ expect(screen.getByText('1')).toBeInTheDocument();
639
+ });
640
+ });
544
641
  });
@@ -661,5 +661,45 @@ describe('ObjectView', () => {
661
661
  expect(callSchema?.showFilters).toBe(false);
662
662
  expect(callSchema?.showSort).toBe(false);
663
663
  });
664
+
665
+ it('should propagate userFilters from activeView in renderListView', async () => {
666
+ const schema: ObjectViewSchema = {
667
+ type: 'object-view',
668
+ objectName: 'contacts',
669
+ };
670
+
671
+ const renderListViewSpy = vi.fn(({ schema: listSchema }: any) => (
672
+ <div data-testid="custom-list">Custom ListView</div>
673
+ ));
674
+
675
+ const views = [
676
+ {
677
+ id: 'v1',
678
+ label: 'View 1',
679
+ type: 'grid' as const,
680
+ userFilters: {
681
+ element: 'dropdown' as const,
682
+ fields: [{ field: 'status' }],
683
+ },
684
+ },
685
+ ];
686
+
687
+ render(
688
+ <ObjectView
689
+ schema={schema}
690
+ dataSource={mockDataSource}
691
+ views={views}
692
+ activeViewId="v1"
693
+ renderListView={renderListViewSpy}
694
+ />,
695
+ );
696
+
697
+ expect(renderListViewSpy).toHaveBeenCalled();
698
+ const callSchema = renderListViewSpy.mock.calls[0]?.[0]?.schema;
699
+ expect(callSchema?.userFilters).toEqual({
700
+ element: 'dropdown',
701
+ fields: [{ field: 'status' }],
702
+ });
703
+ });
664
704
  });
665
705
  });
@@ -1,32 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import { describe, it, expect } from 'vitest';
10
- import { ObjectView, ViewSwitcher, FilterUI, SortUI } from '../index';
11
-
12
- describe('Plugin View Registration', () => {
13
- it('exports ObjectView component', () => {
14
- expect(ObjectView).toBeDefined();
15
- expect(typeof ObjectView).toBe('function');
16
- });
17
-
18
- it('exports ViewSwitcher component', () => {
19
- expect(ViewSwitcher).toBeDefined();
20
- expect(typeof ViewSwitcher).toBe('function');
21
- });
22
-
23
- it('exports FilterUI component', () => {
24
- expect(FilterUI).toBeDefined();
25
- expect(typeof FilterUI).toBe('function');
26
- });
27
-
28
- it('exports SortUI component', () => {
29
- expect(SortUI).toBeDefined();
30
- expect(typeof SortUI).toBe('function');
31
- });
32
- });