@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 +679 -61
- package/dist/legacy/index.d.mts +6 -10
- package/dist/legacy/index.d.ts +6 -10
- package/dist/legacy/index.js +542 -435
- package/dist/legacy/index.mjs +570 -451
- package/dist/modern/index.d.mts +6 -10
- package/dist/modern/index.d.ts +6 -10
- package/dist/modern/index.js +540 -434
- package/dist/modern/index.mjs +568 -450
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,90 +1,708 @@
|
|
|
1
|
-
# @procore/saved-views
|
|
1
|
+
# @procore/saved-views - Consumer Integration Guide
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
## Table of Contents
|
|
8
6
|
|
|
9
|
-
|
|
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
|
-
|
|
16
|
+
## Quick Start
|
|
12
17
|
|
|
13
|
-
|
|
18
|
+
### Installation
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
```shell script
|
|
20
|
+
```bash
|
|
18
21
|
yarn add @procore/saved-views
|
|
19
22
|
```
|
|
20
23
|
|
|
21
|
-
**
|
|
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
|
+

|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
**
|
|
140
|
+
**Key Points**:
|
|
36
141
|
|
|
37
|
-
-
|
|
38
|
-
- `
|
|
39
|
-
- `
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+

|
|
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
|
-
|
|
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
|
+

|
|
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
|
-
|
|
703
|
+
## Additional Resources
|
|
79
704
|
|
|
80
|
-
-
|
|
81
|
-
-
|
|
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
|
-
|
|
90
|
-
[yarn]: https://classic.yarnpkg.com/
|
|
708
|
+
---
|