@procore/saved-views 5.1.0-alpha → 5.1.0-alpha.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.
package/README.md CHANGED
@@ -1,90 +1,708 @@
1
- # @procore/saved-views
1
+ # @procore/saved-views - Consumer Integration Guide
2
2
 
3
- > [!WARNING]
4
- > It's an initial version of this package (0.0.0) intended to demo the Saved Views capabilities. The design and API integration aren't finished yet, so please don't use it on production.
5
- > The final design and API integration are going to be delivered with next version (0.0.1) soon.
3
+ Enable users to save, share, and manage custom table configurations with URL synchronization and cross-user sharing capabilities.
6
4
 
7
- Saved Views Component for integration with @procore/data-table and @procore/smart-grid-core
5
+ ## Table of Contents
8
6
 
9
- **✨ Latest Updates:**
7
+ - [Quick Start](#quick-start)
8
+ - [Core Concepts](#core-concepts)
9
+ - [What Gets Stored in a View?](#what-gets-stored-in-a-view)
10
+ - [DataTable Integration](#datatable-integration)
11
+ - [SmartGrid Integration](#smartgrid-integration)
12
+ - [Backend Changes Required](#backend-changes-required)
13
+ - [Complete End-to-End Example](#complete-end-to-end-example)
14
+ - [Additional Resources](#additional-resources)
10
15
 
11
- - Upgraded to @tanstack/react-query v5.83.0 (latest) for better performance and modern API and made it a direct dependecy to not force our consumers
16
+ ## Quick Start
12
17
 
13
- ## Installation
18
+ ### Installation
14
19
 
15
- ### In a web application, micro frontend or hydra client
16
-
17
- ```shell script
20
+ ```bash
18
21
  yarn add @procore/saved-views
19
22
  ```
20
23
 
21
- **NOTE:** Ensure that the following peer dependencies for `@procore/saved-views` are also installed:
24
+ **Peer Dependencies Required:**
22
25
 
23
26
  - `@procore/core-react`: `>=11.4.0 <13`
24
27
  - `@procore/toast-alert`: `>=5.1.0`
25
28
  - `react`: `>=16.8`
26
29
  - `styled-components`: `^5.3.0`
30
+ - `@tanstack/react-query`: `^5.0.0`
31
+
32
+ ### What is SavedViews?
33
+
34
+ SavedViews allows users to save, manage, and share custom table/grid configurations:
35
+
36
+ - **Save configurations**: Column arrangements, widths, sorting, and filtering
37
+ - **Multiple views**: Create different views with different view levels
38
+ - **Share with team**: Project & Company level views visible to all users in a project/company.
39
+ - **Personal views**: Private views only visible to you
40
+ - **Shareable URLs**: Bookmark and share specific views
41
+
42
+ ## Core Concepts
43
+
44
+ ### View Levels
45
+
46
+ | Level | Visibility |
47
+ | ------------ | ----------------- |
48
+ | **Personal** | Only you |
49
+ | **Project** | All project users |
50
+ | **Default** | All company users |
51
+
52
+ ### Key Terms
53
+
54
+ - **Domain**: Maps to the permission domain for your tool (e.g., "rfi", "submittal_log")
55
+ - **Table Name**: Unique table identifier (e.g., "list", "calendar")
56
+ - **Sticky Views Key**: LocalStorage key to remember last selected view
57
+ - **Table API/Grid API**: Interface to control table programmatically
58
+ - **Ref**: Enables bidirectional communication (DataTable only)
59
+
60
+ ## What Gets Stored in a View?
61
+
62
+ When a user saves a view, the following configuration is persisted:
63
+
64
+ ### Column Configuration & Filtering
65
+
66
+ - **Column Widths**: Custom width for each column
67
+ - **Column Order**: The sequence of columns from left to right
68
+ - **Column Visibility**: Which columns are shown or hidden
69
+ - **Column Pinning**: Columns pinned to left or right side
70
+ - **Filter State**: All active filters on columns
71
+
72
+ ### Metadata
73
+
74
+ - **View Name**: User-provided name (e.g., "My Open RFIs")
75
+ - **Description**: Optional description of the view
76
+ - **View Level**: Personal, Project, or Company visibility
77
+ - **Creator**: User who created the view
78
+
79
+ ## DataTable Integration
80
+
81
+ DataTable integration uses a ref-based pattern for tight coupling between the table and saved views components. This allows saved views to update the table configuration and vice versa.
82
+
83
+ ![DataTable Ref Communication Flow](./assets/refFlow.png)
84
+
85
+ ### Props Reference
86
+
87
+ #### Required Props
88
+
89
+ | Prop | Type | Description |
90
+ | --------------------- | ----------------------------------- | ------------------------------------------------ |
91
+ | `ref` | `IDataTableSavedViewsRef` | Ref for parent-to-child communication |
92
+ | `tableApi` | `TableApi` | DataTable API instance for controlling the table |
93
+ | `tableName` | `string` | Unique identifier for the table (e.g., "list") |
94
+ | `domain` | `string` | Permission domain (e.g., "rfi", "submittal_log") |
95
+ | `stickyViewsKey` | `string` | LocalStorage key to persist selected view |
96
+ | `enableSavedViews` | `boolean` | Feature flag to enable/disable saved views |
97
+ | `userId` | `number` | Current user ID |
98
+ | `projectId` | `number` | Current project ID |
99
+ | `companyId` | `number` | Current company ID |
100
+ | `columnDefinitions` | `ColumnDef[]` | Array of column definitions |
101
+ | `onTableConfigChange` | `(config: DataTableConfig) => void` | Callback when table config changes |
102
+
103
+ #### Optional Props
27
104
 
28
- ### In a npm package or library
105
+ | Prop | Type | Default Behavior if Not Passed |
106
+ | ------------------- | ----------------- | ----------------------------------------------------------------- |
107
+ | `defaultViewName` | `string` | Displays as "Default View" |
108
+ | `defaultViewConfig` | `DataTableConfig` | Built automatically from passed `columnDefinitions` from our side |
29
109
 
110
+ ### Step-by-Step Integration
111
+
112
+ #### Step 1: Set Up Dependencies and Imports
113
+
114
+ ```tsx
115
+ import React, { useRef, useState, useEffect } from 'react';
116
+ import { ServerSideDataTable, TableApi } from '@procore/data-table';
117
+ import {
118
+ DataTableSavedViews,
119
+ useSavedViewsPanel,
120
+ IDataTableSavedViewsRef,
121
+ } from '@procore/saved-views';
122
+ import { useSearchParams } from 'react-router-dom';
30
123
  ```
31
- yarn add -P @procore/saved-views^1.0.0 # adds the package as a peer dependency
32
- yarn add -D @procore/saved-views # adds as a dev dependency, for tests, storybook, etc.
124
+
125
+ #### Step 2: Create Required State and Refs
126
+
127
+ ```tsx
128
+ const MyDataTableView = () => {
129
+ // Essential state for table API communication
130
+ const [tableApi, setTableApi] = useState<TableApi | null>(null);
131
+ const [isTableReady, setIsTableReady] = useState(false);
132
+
133
+ // Critical: Ref for bidirectional communication
134
+ const savedViewsWrapperRef = useRef<IDataTableSavedViewsRef>(null);
135
+
136
+ // Panel visibility control
137
+ const { SavedViewsButton, isOpen: isPanelOpen } = useSavedViewsPanel('your_domain', 'your_table');
33
138
  ```
34
139
 
35
- **NOTE:** Ensure that the following peer dependencies for `@procore/saved-views` are also installed _as peer dependencies_ and as dev dependencies (for tests, storybook, etc.):
140
+ **Key Points**:
36
141
 
37
- - `@procore/core-react`: `^12`
38
- - `react`: `>=16.8`
39
- - `styled-components`: `^5.3.0`
142
+ - `savedViewsWrapperRef` is essential for the table to notify saved views of configuration changes
143
+ - `useSavedViewsPanel` manages the visibility state of the saved views panel
144
+ - `tableApi` state tracks when the table is ready for interaction
145
+
146
+ #### Step 3: Configure Table Configuration Handlers
147
+
148
+ ```tsx
149
+ // Configuration persistence handler
150
+ const handleTableConfigChange = useCallback((config: DataTableConfig) => {
151
+ // Save to localStorage, update URL, etc.
152
+ persistTableConfiguration(config);
153
+
154
+ // Critical: Notify saved views of configuration changes
155
+ savedViewsWrapperRef.current?.setTableConfig(config);
156
+ }, []);
157
+
158
+ // Prepare default configuration for saved views [This is an OPTIONAL step if you want to send your own tool's default view]
159
+ const defaultViewConfig = useMemo(
160
+ () => ({
161
+ columnState: columnDefinitions.map((col) => ({
162
+ colId: col.field,
163
+ width: col.width,
164
+ hide: col.hide,
165
+ pinned: col.pinned,
166
+ })),
167
+ filterState: {},
168
+ sortState: [],
169
+ }),
170
+ [columnDefinitions]
171
+ );
172
+ ```
173
+
174
+ **Key Points**:
175
+
176
+ - The `handleTableConfigChange` callback must call `savedViewsWrapperRef.current?.setTableConfig(config)`
177
+ - This keeps saved views in sync with table state changes
178
+ - `defaultViewConfig` represents the baseline table configuration
179
+
180
+ #### Step 4: Implement Table Component with Saved Views Integration
181
+
182
+ ```tsx
183
+ return (
184
+ <div className="table-with-saved-views">
185
+ <ServerSideDataTable
186
+ columnDefinitions={columnDefinitions}
187
+ initialTableConfig={defaultConfig}
188
+ onTableConfigChange={handleTableConfigChange} // Critical integration point
189
+ onServerSideDataRequest={handleDataRequest}
190
+ onTableReady={(api) => {
191
+ setTableApi(api); // Store API reference
192
+ setIsTableReady(true);
193
+ }}
194
+ >
195
+ <div style={{ display: 'flex', alignItems: 'stretch' }}>
196
+
197
+ {/* Left Panel: Saved Views */}
198
+ <div style={{ flex: '0 1 0px', paddingRight: '12px' }}>
199
+ {isPanelOpen && (
200
+ <DataTableSavedViews
201
+ ref={savedViewsWrapperRef} // Critical: Enable ref communication
202
+ tableApi={tableApi}
203
+ onTableConfigChange={handleTableConfigChange}
204
+
205
+ // Required identifiers
206
+ tableName="list"
207
+ domain="your_domain"
208
+ stickyViewsKey="currentSavedView"
209
+
210
+ // Configuration
211
+ enableSavedViews={true}
212
+ defaultViewConfig={defaultViewConfig}
213
+ defaultViewName="All Items"
214
+ columnDefinitions={columnDefinitions}
215
+
216
+ // User context
217
+ userId={userId}
218
+ projectId={projectId}
219
+ companyId={companyId}
220
+ />
221
+ )}
222
+ <ServerSideDataTable.FiltersPanel />
223
+ </div>
40
224
 
41
- ## Usage
225
+ {/* Main Content Area */}
226
+ <div style={{ flex: '1', display: 'flex', flexDirection: 'column' }}>
227
+ {/* Top Controls */}
228
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '12px' }}>
229
+ <div style={{ display: 'flex', gap: '8px' }}>
230
+ <SavedViewsButton /> {/* Toggle saved views panel */}
231
+ <ServerSideDataTable.Search />
232
+ <ServerSideDataTable.FiltersPanelButton />
233
+ </div>
234
+ <ServerSideDataTable.ConfigPanelButton />
235
+ </div>
42
236
 
43
- ```js
44
- import { useSavedViews } from '@procore/saved-views';
237
+ <ServerSideDataTable.QuickFilters />
238
+ <ServerSideDataTable.BulkActions />
239
+
240
+ {/* Main Table */}
241
+ <ServerSideDataTable.Table
242
+ pagination
243
+ paginationPageSize={pageSize}
244
+ loading={!isTableReady}
245
+ // ... other table props
246
+ />
247
+ </div>
248
+
249
+ {/* Right Panel: Context */}
250
+ <div style={{ flex: '0 1 0px', paddingLeft: '12px' }}>
251
+ <ServerSideDataTable.ContextPanel />
252
+ </div>
253
+ </div>
254
+ </ServerSideDataTable>
255
+ </div>
256
+ );
257
+ };
45
258
  ```
46
259
 
47
- ```jsx
48
- const { SavedViews } = useSavedViews(
49
- onSelect,
50
- onEdit,
51
- onDelete,
52
- onCreate,
53
- onUpdate,
54
- AllItemsOptionFlag,
55
- savedViewsGroups
56
- :
57
- savedGroups,
58
- isAdmin
59
- :
60
- true,
61
- currentFilters,
62
- currentColumnState,
63
- onTableConfigChange,
64
- tableConfig,
65
- tableApi,
66
- currentRowHeight,
67
- stickyViewsKey
68
- :
69
- 'currentSavedView'
70
- )
71
- ;
72
-
73
- <SavedViews />
260
+ **Key Integration Points**:
261
+
262
+ 1. **Ref Connection**: `ref={savedViewsWrapperRef}` enables bidirectional communication
263
+ 2. **API Sharing**: `tableApi={tableApi}` gives saved views control over table state
264
+ 3. **Config Sync**: `onTableConfigChange={handleTableConfigChange}` keeps everything in sync
265
+ 4. **Layout**: Saved views typically go in a left sidebar with fixed width
266
+
267
+ ---
268
+
269
+ ## SmartGrid Integration
270
+
271
+ SmartGrid integration uses direct `gridApi` access and the built-in tool panel system for a simpler integration. SavedViews appears as a dedicated tool panel alongside columns and filters. **No ref needed!**
272
+
273
+ ![DataTable Ref Communication Flow](./assets/SGFlow.png)
274
+
275
+ ### Props Reference
276
+
277
+ #### Required Props
278
+
279
+ | Prop | Type | Description |
280
+ | -------------------- | ------------ | ------------------------------------------------------ | --------------------------------------------------- |
281
+ | `gridApi` | `GridApi` | Grid API instance (auto-passed by tool panel system) |
282
+ | `tableName` | `string` | Unique identifier for the table (e.g., "list") |
283
+ | `domain` | `string` | Permission domain (e.g., "submittal_log") |
284
+ | `stickyViewsKey` | `string` | LocalStorage key to persist selected view |
285
+ | `enableSavedViews` | `boolean` | Feature flag to enable/disable saved views |
286
+ | `userId` | `number` | Current user ID |
287
+ | `projectId` | `number` | Current project ID |
288
+ | `companyId` | `number` | Current company ID |
289
+ | `defaultViewFilters` | `FilterModel | null` | Default filters applied when selecting default view |
290
+ | `defaultRowHeight` | `number` | Default row height applied when selecting default view |
291
+
292
+ #### Optional Props
293
+
294
+ | Prop | Type | Default Behavior if Not Passed |
295
+ | ----------------- | -------- | ------------------------------ |
296
+ | `defaultViewName` | `string` | Displays as "Default View" |
297
+
298
+ **Note:** Unlike DataTable, SmartGrid doesn't need `defaultViewConfig`, `columnDefinitions`, or `onTableConfigChange` - the grid handles configuration automatically through the gridApi!
299
+
300
+ ### Step-by-Step Integration
301
+
302
+ #### Step 1: Create the SavedViews Tool Panel Renderer
303
+
304
+ First, create a component that wraps `SmartGridSavedViews`. The tool panel system will automatically pass the `api` prop.
305
+
306
+ ```tsx
307
+ import React from 'react';
308
+ import { SmartGridSavedViews } from '@procore/saved-views';
309
+ import { rowHeights } from '@procore/smart-grid-core';
310
+ import { useProcoreEnvironment } from '@procore/environment';
311
+
312
+ const SavedViewsToolPanelRenderer = ({ api }) => {
313
+ const { companyId, projectId, userId } = useProcoreEnvironment();
314
+
315
+ return (
316
+ <SmartGridSavedViews
317
+ gridApi={api} // Passed automatically by tool panel system
318
+ tableName="list" // Your table identifier
319
+ domain="your_domain" // Your permission domain
320
+ stickyViewsKey="currentSavedView" // LocalStorage key
321
+ defaultViewName="All Items" // Default view display name
322
+ enableSavedViews={true} // Feature flag
323
+ userId={userId}
324
+ projectId={projectId}
325
+ companyId={companyId}
326
+ defaultViewFilters={{}} // Default filters (optional)
327
+ defaultRowHeight={rowHeights.md} // Grid-specific config
328
+ />
329
+ );
330
+ };
74
331
  ```
75
332
 
76
- ## Development
333
+ **Key Points:**
334
+
335
+ - `api` prop is **automatically provided** by SmartGrid's tool panel system
336
+ - No ref creation needed
337
+ - No manual state synchronization required
338
+
339
+ #### Step 2: Create the SavedViews Configuration Object
340
+
341
+ Create a configuration object (or hook) that defines your saved views setup:
342
+
343
+ ```tsx
344
+ import { SavedViews } from '@procore/smart-grid-core';
345
+ import { YourDataType } from '@/types';
346
+
347
+ // Option A: Simple configuration object
348
+ const savedViewsConfig: SavedViews<YourDataType> = {
349
+ renderer: SavedViewsToolPanelRenderer,
350
+ enabled: true,
351
+ };
352
+
353
+ // Option B: Custom hook (recommended for multiple views)
354
+ const useSavedViews = (view: 'list' | 'calendar') => {
355
+ const savedViews = {
356
+ list: {
357
+ renderer: SavedViewsToolPanelRenderer,
358
+ enabled: true,
359
+ },
360
+ // Add more views as needed
361
+ };
362
+
363
+ return savedViews[view];
364
+ };
365
+ ```
366
+
367
+ #### Step 3: Add SavedViews to SmartGridServer
368
+
369
+ Pass the configuration to `SmartGridServer` and add `'savedViews'` to the tool panels:
370
+
371
+ ```tsx
372
+ import { SmartGridServer } from '@procore/smart-grid-core';
373
+
374
+ const MySmartGridView = () => {
375
+ const [gridApi, setGridApi] = useState(null);
376
+ const savedViews = useSavedViews('list'); // Or use savedViewsConfig directly
377
+
378
+ return (
379
+ <SmartGridServer
380
+ // Column and data configuration
381
+ columnDefs={columnDefinitions}
382
+ handleServerSideDatasource={handleDataSource}
383
+ // SavedViews integration - just two props!
384
+ savedViews={savedViews}
385
+ sideBar={{
386
+ toolPanels: [
387
+ 'savedViews', // Add saved views to tool panel list
388
+ 'filters', // Built-in filters panel
389
+ 'columns', // Built-in columns panel
390
+ ],
391
+ }}
392
+ // Grid lifecycle
393
+ onGridReady={({ api }) => {
394
+ setGridApi(api);
395
+ }}
396
+
397
+ // Other grid props...
398
+ />
399
+ );
400
+ };
401
+ ```
402
+
403
+ **That's it!** SmartGrid handles everything else automatically.
404
+
405
+ ---
406
+
407
+ ### Key Differences: DataTable vs SmartGrid Integration
408
+
409
+ | Aspect | DataTable | SmartGrid |
410
+ | ----------------------- | ----------------------------- | -------------------------- |
411
+ | **Integration Pattern** | Ref-based with external panel | Tool panel integrated |
412
+ | **API Access** | `tableApi` prop + ref methods | Direct `gridApi` prop |
413
+ | **State Management** | Manual sync required | Automatic grid integration |
414
+
415
+ ![DataTable Ref Communication Flow](./assets/SG_and_DB_comparison.png)
416
+
417
+ ---
418
+
419
+ ## Backend Changes Required
420
+
421
+ ### Overview
422
+
423
+ SavedViews functionality is provided by the `saved_views` component in the Ruby monolith. No database migrations are required - the infrastructure already exists. You only need to configure permissions for your domain.
424
+
425
+ ### API Endpoints
426
+
427
+ All API endpoints are already available under the `saved_views` component:
428
+
429
+ **Base Path:** `/rest/v2.0/companies/:company_id/projects/:project_id/saved_views`
430
+
431
+ | Method | Endpoint | Description | Query Params |
432
+ | ---------- | -------------------------- | ---------------------------------- | ---------------------------------- |
433
+ | **GET** | `/saved_views` | List all saved views for a table | `permissions_domain`, `table_name` |
434
+ | **POST** | `/saved_views` | Create a new saved view | - |
435
+ | **PUT** | `/saved_views/:id` | Update an existing saved view | - |
436
+ | **DELETE** | `/saved_views/:id` | Delete a saved view | - |
437
+ | **GET** | `/saved_views/permissions` | Get user's saved views permissions | `permissions_domain` |
438
+
439
+ **Example Request:**
440
+
441
+ ```javascript
442
+ GET /rest/v2.0/companies/123/projects/456/saved_views?permissions_domain=rfi&table_name=list
443
+ ```
444
+
445
+ **Example Response:**
446
+
447
+ ```json
448
+ {
449
+ "data": [
450
+ {
451
+ "id": 789,
452
+ "name": "My Open RFIs",
453
+ "description": "Shows all open RFIs assigned to me",
454
+ "table_config": {
455
+ /* column state, filters, etc. */
456
+ },
457
+ "table_name": "list",
458
+ "domain": "rfi",
459
+ "view_level": "personal",
460
+ "created_by_id": 101,
461
+ "project_id": 456,
462
+ "company_id": 123,
463
+ "created_at": "2024-01-15T10:30:00Z",
464
+ "updated_at": "2024-01-15T10:30:00Z"
465
+ }
466
+ ]
467
+ }
468
+ ```
469
+
470
+ ### Database Schema
471
+
472
+ The `saved_views` table has the following structure:
473
+
474
+ | Column | Type | Description |
475
+ | --------------- | -------- | ----------------------------------- |
476
+ | `id` | bigint | Primary key |
477
+ | `name` | text | View name (max 150 chars) |
478
+ | `description` | text | Optional description |
479
+ | `table_config` | jsonb | Serialized table configuration |
480
+ | `table_name` | text | Table identifier (e.g., "list") |
481
+ | `domain` | text | Permissions domain (e.g., "rfi") |
482
+ | `view_level` | enum | `personal`, `project`, or `company` |
483
+ | `created_by_id` | bigint | User who created the view |
484
+ | `project_id` | bigint | Associated project |
485
+ | `company_id` | bigint | Associated company |
486
+ | `created_at` | datetime | Creation timestamp |
487
+ | `updated_at` | datetime | Last update timestamp |
488
+
489
+ **Unique Constraints:**
490
+
491
+ - Personal views: Unique per `(name, created_by_id, project_id, domain, table_name)`
492
+ - Project views: Unique per `(name, project_id, domain, table_name)`
493
+ - Company views: Unique per `(name, company_id, domain, table_name)`
494
+
495
+ ### Permission Setup
496
+
497
+ SavedViews uses the **Dynamic Saved Views Policy** system, which automatically generates policies based on your domain's UAL (User Access Level) definitions.
498
+
499
+ #### Required Permissions
500
+
501
+ You need to define **two permissions** in your domain's permission file:
502
+
503
+ 1. **`manage_personal_saved_views`** - Allows users to create/manage their own views
504
+ 2. **`manage_project_saved_views`** - Allows users to create/manage project-wide views
505
+
506
+ ---
507
+
508
+ ### Method 1: Standard Domain Permissions
509
+
510
+ Add these two lines to your domain's permissions file:
511
+
512
+ **File:** `app/lib/permissions/domain/your_domain.rb`
513
+
514
+ #### Example 1: RFIs Domain
515
+
516
+ ```ruby
517
+ # frozen_string_literal: true
518
+ # domain: RFIs
519
+
520
+ module Permissions
521
+ class Domain
522
+ Rfis = define :rfi do
523
+ permissions do
524
+ # ... existing permissions ...
525
+ can_view ual: :read
526
+ can_update ual: :update
527
+ can_delete ual: :admin
528
+
529
+ # SavedViews permissions (add these two lines)
530
+ manage_personal_saved_views ual: :read
531
+ manage_project_saved_views ual: :admin, granular: :feature_flagged
532
+
533
+ # ... other permissions ...
534
+ end
535
+
536
+ legacy_domain :project, :rfi
537
+ end
538
+ end
539
+ end
540
+ ```
541
+
542
+ **Path:** `app/lib/permissions/domain/rfis.rb` ([View File](https://github.com/procore/procore/blob/master/app/lib/permissions/domain/rfis.rb))
543
+
544
+ #### Example 2: Submittals Domain
545
+
546
+ ```ruby
547
+ # frozen_string_literal: true
548
+ # domain: Submittals
549
+
550
+ module Permissions
551
+ class Domain
552
+ SubmittalLogs = define :submittal_log do
553
+ permissions do
554
+ # ... existing permissions ...
555
+ can_view ual: :read
556
+ can_update ual: :update
557
+ can_delete ual: :admin
558
+
559
+ # SavedViews permissions (add these two lines)
560
+ manage_personal_saved_views ual: :read
561
+ manage_project_saved_views ual: :admin, granular: :feature_flagged
562
+
563
+ # ... other permissions ...
564
+ end
565
+
566
+ legacy_domain :project, :submittal_log
567
+ end
568
+ end
569
+ end
570
+ ```
571
+
572
+ **Path:** `app/lib/permissions/domain/submittal_logs.rb` ([View File](https://github.com/procore/procore/blob/master/app/lib/permissions/domain/submittal_logs.rb))
573
+
574
+ **How it works:**
575
+
576
+ - The `DynamicSavedViewsPolicy` automatically looks up your domain (e.g., `"rfi"`, `"submittal_log"`)
577
+ - It creates a policy class on-the-fly that checks these two permissions
578
+ - The frontend `@procore/saved-views` package automatically checks these permissions
579
+
580
+ ---
581
+
582
+ ### Method 2: Custom Policy Class
583
+
584
+ **Best for:** Use this if you need some special permission logic
585
+
586
+ Create a custom policy class that inherits from `ApplicationPolicy` or an aggregate policy.
587
+
588
+ ```ruby
589
+ # frozen_string_literal: true
590
+ # domain: Generic Tools
591
+
592
+ class CustomSavedViewsPolicy < CommunicationTypesAggregatePolicy
593
+ def self.resource_class
594
+ nil
595
+ end
596
+
597
+ def index?
598
+ can_manage_personal_saved_views?
599
+ end
600
+
601
+ .......
602
+ .......
603
+ .......
604
+ .......
605
+ .......
606
+
607
+ private
608
+
609
+ def can_manage_project_saved_views?
610
+ has_admin?
611
+ end
612
+
613
+ .......
614
+ .......
615
+ .......
616
+ .......
617
+ .......
618
+
619
+ end
620
+ ```
621
+
622
+ **Register in DynamicSavedViewsPolicy ([Here](https://github.com/procore/procore/blob/master/components/saved_views/app/policies/dynamic_saved_views_policy.rb)):**
623
+
624
+ The `DynamicSavedViewsPolicy` checks for special cases:
625
+
626
+ ```ruby
627
+ def for_permissions_domain(permissions_domain)
628
+ return CustomSavedViewsPolicy if permissions_domain == 'your-tool-domian'
629
+
630
+ # ... falls back to dynamic generation
631
+ end
632
+ ```
633
+
634
+ #### Example: Correspondence Types (Generic Tools)
635
+
636
+ **Path:** `components/generic_tools/app/policies/correspondence_types_saved_views_policy.rb` ([View File](https://github.com/procore/procore/blob/master/components/generic_tools/app/policies/correspondence_types_saved_views_policy.rb))
637
+
638
+ ---
639
+
640
+ ### Understanding DynamicSavedViewsPolicy
641
+
642
+ The `DynamicSavedViewsPolicy` is a singleton that dynamically generates policy classes for any domain.
643
+
644
+ **File:** `components/saved_views/app/policies/dynamic_saved_views_policy.rb`
645
+
646
+ **How it works:**
647
+
648
+ 1. **Controller requests policy** for a domain (e.g., `"rfi"`):
649
+
650
+ ```ruby
651
+ def policy_class
652
+ DynamicSavedViewsPolicy.instance.for_permissions_domain(permissions_domain)
653
+ end
654
+ ```
655
+
656
+ 2. **Policy looks up domain** in `Permissions.domains`:
657
+
658
+ ```ruby
659
+ return nil if ::Permissions.domains.find(permissions_domain).nil?
660
+ ```
661
+
662
+ 3. **Generates policy class on-the-fly**:
663
+
664
+ ```ruby
665
+ @policies[permissions_domain] ||= Class.new(ApplicationPolicy) do
666
+ self.permissions_domain = permissions_domain
667
+
668
+ def index?
669
+ check(:manage_personal_saved_views)
670
+ end
671
+
672
+ def create?
673
+ can_edit?
674
+ end
675
+
676
+ # ... etc
677
+ end
678
+ ```
679
+
680
+ 4. **Checks UAL permissions** defined in your domain:
681
+ - `manage_personal_saved_views` - Required for all view operations
682
+ - `manage_project_saved_views` - Required for project-level views
683
+
684
+ ---
685
+
686
+ ### Questions?
687
+
688
+ - **Slack:** #saved-views-updates
689
+ - **Backend Component:** `components/saved_views/`
690
+ - **Permissions Guide:** [Permissions Documentation](https://github.com/procore/procore/wiki/Permissions)
691
+
692
+ ---
693
+
694
+ ## Complete End-to-End Example
695
+
696
+ Refer to:
697
+
698
+ - **RFIs implementation**: `rfis-ui-service/src/pages/ModernizedList/components/Table/`
699
+ - **Submittals implementation**: `submittals-ui-service/src/components/List/`
700
+
701
+ ---
77
702
 
78
- This project uses [`yarn`][yarn], and supports the following commands:
703
+ ## Additional Resources
79
704
 
80
- - `build`: builds and bundles the project.
81
- - `format`: runs [`prettier`][prettier] on the project.
82
- - `format:check`: validates that the source code conforms to the [`prettier`][prettier] configuration.
83
- - `lint`: lints the source code.
84
- - `storybook`: starts up the storybook development server on port 6006.
85
- - `build-storybook`: generates a static version of the storybook.
86
- - `test`: runs the unit test suite.
87
- - `test:dev`: run the unit test suite in watch mode.
705
+ - **Support:** #saved-views-updates Slack channel
706
+ - **Saved Views Deep Dive:** [Confluence](https://procoretech.atlassian.net/wiki/spaces/PMQS/pages/4503371946/Saved+Views+Deep+Dive)
88
707
 
89
- [prettier]: https://prettier.io/
90
- [yarn]: https://classic.yarnpkg.com/
708
+ ---