diff --git a/apps/jetstream/src/app/components/core/AppStateResetOnOrgChange.tsx b/apps/jetstream/src/app/components/core/AppStateResetOnOrgChange.tsx index f2e80ec05..246484309 100644 --- a/apps/jetstream/src/app/components/core/AppStateResetOnOrgChange.tsx +++ b/apps/jetstream/src/app/components/core/AppStateResetOnOrgChange.tsx @@ -1,5 +1,5 @@ import { SalesforceOrgUi } from '@jetstream/types'; -import { Fragment, FunctionComponent, useEffect, useState } from 'react'; +import { FunctionComponent, useEffect, useState } from 'react'; import { Resetter, useRecoilValue, useResetRecoilState } from 'recoil'; import * as fromAppState from '../../app-state'; import * as fromAutomationControlState from '../automation-control/automation-control.state'; @@ -49,6 +49,7 @@ export const AppStateResetOnOrgChange: FunctionComponent; + return null; }; export default AppStateResetOnOrgChange; diff --git a/apps/jetstream/src/app/components/manage-permissions/ManagePermissions.tsx b/apps/jetstream/src/app/components/manage-permissions/ManagePermissions.tsx index 105e2e22d..e2ed0164d 100644 --- a/apps/jetstream/src/app/components/manage-permissions/ManagePermissions.tsx +++ b/apps/jetstream/src/app/components/manage-permissions/ManagePermissions.tsx @@ -25,6 +25,7 @@ export const ManagePermissions: FunctionComponent = () = const resetFieldsByKey = useResetRecoilState(fromPermissionsState.fieldsByKey); const resetObjectPermissionMap = useResetRecoilState(fromPermissionsState.objectPermissionMap); const resetFieldPermissionMap = useResetRecoilState(fromPermissionsState.fieldPermissionMap); + const resetTabVisibilityPermissionMap = useResetRecoilState(fromPermissionsState.tabVisibilityPermissionMap); const [priorSelectedOrg, setPriorSelectedOrg] = useState(null); const hasSelectionsMade = useRecoilValue(fromPermissionsState.hasSelectionsMade); @@ -45,6 +46,7 @@ export const ManagePermissions: FunctionComponent = () = resetFieldsByKey(); resetObjectPermissionMap(); resetFieldPermissionMap(); + resetTabVisibilityPermissionMap(); } else if (!selectedOrg) { resetProfilesState(); resetSelectedProfilesPermSetState(); @@ -56,6 +58,7 @@ export const ManagePermissions: FunctionComponent = () = resetFieldsByKey(); resetObjectPermissionMap(); resetFieldPermissionMap(); + resetTabVisibilityPermissionMap(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedOrg, priorSelectedOrg]); @@ -75,6 +78,7 @@ export const ManagePermissions: FunctionComponent = () = ['fieldsByKey', fromPermissionsState.fieldsByKey], ['objectPermissionMap', fromPermissionsState.objectPermissionMap], ['fieldPermissionMap', fromPermissionsState.fieldPermissionMap], + ['tabVisibilityPermissionMap', fromPermissionsState.tabVisibilityPermissionMap], ]} /> {location.pathname.endsWith('/editor') && !hasSelectionsMade ? : } diff --git a/apps/jetstream/src/app/components/manage-permissions/ManagePermissionsEditor.tsx b/apps/jetstream/src/app/components/manage-permissions/ManagePermissionsEditor.tsx index 60bfbcc5b..d3cac9b9a 100644 --- a/apps/jetstream/src/app/components/manage-permissions/ManagePermissionsEditor.tsx +++ b/apps/jetstream/src/app/components/manage-permissions/ManagePermissionsEditor.tsx @@ -25,6 +25,7 @@ import { RequireMetadataApiBanner } from '../core/RequireMetadataApiBanner'; import * as fromJetstreamEvents from '../core/jetstream-events'; import ManagePermissionsEditorFieldTable from './ManagePermissionsEditorFieldTable'; import ManagePermissionsEditorObjectTable from './ManagePermissionsEditorObjectTable'; +import ManagePermissionsEditorTabVisibilityTable from './ManagePermissionsEditorTabVisibilityTable'; import * as fromPermissionsState from './manage-permissions.state'; import { usePermissionRecords } from './usePermissionRecords'; import { generateExcelWorkbookFromTable } from './utils/permission-manager-export-utils'; @@ -32,12 +33,16 @@ import { getConfirmationModalContent, getDirtyFieldPermissions, getDirtyObjectPermissions, + getDirtyTabVisibilityPermissions, getFieldColumns, getFieldRows, getObjectColumns, getObjectRows, + getTabVisibilityColumns, + getTabVisibilityRows, updateFieldRowsAfterSave, updateObjectRowsAfterSave, + updateTabVisibilityRowsAfterSave, } from './utils/permission-manager-table-utils'; import { DirtyRow, @@ -49,20 +54,27 @@ import { PermissionFieldSaveData, PermissionObjectSaveData, PermissionSaveResults, + PermissionTabVisibilitySaveData, PermissionTableFieldCell, PermissionTableFieldCellPermission, PermissionTableObjectCell, PermissionTableObjectCellPermission, PermissionTableSummaryRow, + PermissionTableTabVisibilityCell, + PermissionTableTabVisibilityCellPermission, + TabVisibilityPermissionDefinitionMap, + TabVisibilityPermissionRecordForSave, } from './utils/permission-manager-types'; import { clearPermissionErrorMessage, collectProfileAndPermissionIds, getUpdatedFieldPermissions, getUpdatedObjectPermissions, + getUpdatedTabVisibilityPermissions, permissionsHaveError, prepareFieldPermissionSaveData, prepareObjectPermissionSaveData, + prepareTabVisibilityPermissionSaveData, savePermissionRecords, updatePermissionSetRecords, } from './utils/permission-manager-utils'; @@ -113,10 +125,13 @@ export const ManagePermissionsEditor: FunctionComponent>>({}); const [objectFilter, setObjectFilter] = useState(''); + const [tabVisibilityColumns, setTabVisibilityColumns] = useState< + ColumnWithFilter[] + >([]); + const [tabVisibilityRows, setTabVisibilityRows] = useState(null); + const [visibleTabVisibilityRows, setVisibleTabVisibilityRows] = useState(null); + const [dirtyTabVisibilityRows, setDirtyTabVisibilityRows] = useState>>({}); + const [tabVisibilityFilter, setTabVisibilityFilter] = useState(''); + const [fieldColumns, setFieldColumns] = useState[]>([]); const [fieldRows, setFieldRows] = useState(null); const [visibleFieldRows, setVisibleFieldRows] = useState(null); @@ -134,9 +157,11 @@ export const ManagePermissionsEditor: FunctionComponent(0); const [dirtyFieldCount, setDirtyFieldCount] = useState(0); + const [dirtyTabVisibilityCount, setDirtyTabVisibilityCount] = useState(0); const [objectsHaveErrors, setObjectsHaveErrors] = useState(false); const [fieldsHaveErrors, setFieldsHaveErrors] = useState(false); + const [tabVisibilityHaveErrors, setTabVisibilityHaveErrors] = useState(false); useEffect(() => { isMounted.current = true; @@ -150,6 +175,7 @@ export const ManagePermissionsEditor: FunctionComponent { - if (objectPermissionMap && fieldPermissionMap) { + if (objectPermissionMap && fieldPermissionMap && tabVisibilityPermissionMap) { setObjectsHaveErrors(permissionsHaveError(objectPermissionMap)); setFieldsHaveErrors(permissionsHaveError(fieldPermissionMap)); + setTabVisibilityHaveErrors(permissionsHaveError(tabVisibilityPermissionMap)); } - }, [objectPermissionMap, fieldPermissionMap]); + }, [objectPermissionMap, fieldPermissionMap, tabVisibilityPermissionMap]); useEffect(() => { setDirtyFieldCount(Object.values(dirtyFieldRows).reduce((output, { dirtyCount }) => output + dirtyCount, 0)); @@ -184,6 +211,10 @@ export const ManagePermissionsEditor: FunctionComponent output + dirtyCount, 0)); }, [dirtyObjectRows]); + useEffect(() => { + setDirtyTabVisibilityCount(Object.values(dirtyTabVisibilityRows).reduce((output, { dirtyCount }) => output + dirtyCount, 0)); + }, [dirtyTabVisibilityRows]); + useEffect(() => { if (fieldRows && fieldFilter) { setVisibleFieldRows(fieldRows.filter(multiWordObjectFilter(['label', 'apiName'], fieldFilter))); @@ -200,6 +231,14 @@ export const ManagePermissionsEditor: FunctionComponent { + if (tabVisibilityRows && tabVisibilityFilter) { + setVisibleTabVisibilityRows(tabVisibilityRows.filter(multiWordObjectFilter(['label', 'apiName'], tabVisibilityFilter))); + } else { + setVisibleTabVisibilityRows(tabVisibilityRows); + } + }, [tabVisibilityFilter, tabVisibilityRows]); + const handleObjectBulkRowUpdate = useCallback((rows: PermissionTableObjectCell[], indexes?: number[]) => { const rowsByKey = getMapOf(rows, 'key'); setObjectRows((prevRows) => (prevRows ? prevRows?.map((row) => rowsByKey[row.key] || row) : rows)); @@ -211,12 +250,7 @@ export const ManagePermissionsEditor: FunctionComponent { - output += createIsDirty ? 1 : 0; - output += readIsDirty ? 1 : 0; - output += editIsDirty ? 1 : 0; - output += deleteIsDirty ? 1 : 0; - output += viewAllIsDirty ? 1 : 0; - output += modifyAllIsDirty ? 1 : 0; + output += createIsDirty || readIsDirty || editIsDirty || deleteIsDirty || viewAllIsDirty || modifyAllIsDirty ? 1 : 0; return output; }, 0 @@ -243,8 +277,7 @@ export const ManagePermissionsEditor: FunctionComponent { - output += readIsDirty ? 1 : 0; - output += editIsDirty ? 1 : 0; + output += readIsDirty || editIsDirty ? 1 : 0; return output; }, 0); newValues[rowKey] = { rowKey, dirtyCount, row }; @@ -259,14 +292,41 @@ export const ManagePermissionsEditor: FunctionComponent { + const rowsByKey = getMapOf(rows, 'key'); + setTabVisibilityRows((prevRows) => (prevRows ? prevRows.map((row) => rowsByKey[row.key] || row) : rows)); + indexes = indexes || rows.map((row, index) => index); + setDirtyTabVisibilityRows((priorValue) => { + const newValues = { ...priorValue }; + indexes?.forEach((rowIndex) => { + const row = rows[rowIndex]; + const rowKey = row.key; // e.x. Obj__c.Field__c + const dirtyCount = Object.values(row.permissions).reduce((output, { availableIsDirty, visibleIsDirty }) => { + output += availableIsDirty || visibleIsDirty ? 1 : 0; + return output; + }, 0); + newValues[rowKey] = { rowKey, dirtyCount, row }; + }); + // remove items with a dirtyCount of 0 to reduce future processing required + return Object.keys(newValues).reduce((output: MapOf>, key) => { + if (newValues[key].dirtyCount) { + output[key] = newValues[key]; + } + return output; + }, {}); + }); + }, []); + function initTableData( includeColumns = true, objectPermissionMapOverride?: MapOf, - fieldPermissionMapOverride?: MapOf + fieldPermissionMapOverride?: MapOf, + tabVisibilityPermissionMapOverride?: MapOf ) { if (includeColumns) { setObjectColumns(getObjectColumns(selectedProfiles, selectedPermissionSets, profilesById, permissionSetsById)); setFieldColumns(getFieldColumns(selectedProfiles, selectedPermissionSets, profilesById, permissionSetsById)); + setTabVisibilityColumns(getTabVisibilityColumns(selectedProfiles, selectedPermissionSets, profilesById, permissionSetsById)); } const tempObjectRows = getObjectRows(selectedSObjects, objectPermissionMapOverride || objectPermissionMap || {}); setObjectRows(tempObjectRows); @@ -277,9 +337,16 @@ export const ManagePermissionsEditor: FunctionComponent 0) { + tabVisibilityPermissionData = prepareTabVisibilityPermissionSaveData(dirtyPermissions); + const ids = collectProfileAndPermissionIds(dirtyPermissions, profilesById, permissionSetsById); + profileIds = [...profileIds, ...ids.profileIds]; + permissionSetIds = [...permissionSetIds, ...ids.permissionSetIds]; + } + } let objectSaveResults: PermissionSaveResults[] | undefined = undefined; let fieldSaveResults: PermissionSaveResults[] | undefined = undefined; + let tabVisibilitySaveResults: + | PermissionSaveResults[] + | undefined = undefined; if (objectPermissionData) { objectSaveResults = await savePermissionRecords( @@ -353,6 +437,14 @@ export const ManagePermissionsEditor: FunctionComponent(selectedOrg, 'PermissionSetTabSetting', tabVisibilityPermissionData); + logger.log({ tabVisibilitySaveResults }); + } + // Update records so that SFDX is aware of the changes try { await updatePermissionSetRecords(selectedOrg, { @@ -378,6 +470,13 @@ export const ManagePermissionsEditor: FunctionComponent Reset Changes @@ -444,7 +554,7 @@ export const ManagePermissionsEditor: FunctionComponent Save @@ -495,6 +605,38 @@ export const ManagePermissionsEditor: FunctionComponent ), }, + + { + id: 'tab-visibility-permissions', + title: ( + + + + + Tab Visibility {dirtyTabVisibilityCount ? `(${dirtyTabVisibilityCount})` : ''} + + + ), + titleText: 'Tab Visibility', + disabled: true, + content: ( + + ), + }, + { id: 'field-permissions', title: ( diff --git a/apps/jetstream/src/app/components/manage-permissions/ManagePermissionsEditorObjectTable.tsx b/apps/jetstream/src/app/components/manage-permissions/ManagePermissionsEditorObjectTable.tsx index a34a80712..1ac19e0a4 100644 --- a/apps/jetstream/src/app/components/manage-permissions/ManagePermissionsEditorObjectTable.tsx +++ b/apps/jetstream/src/app/components/manage-permissions/ManagePermissionsEditorObjectTable.tsx @@ -31,9 +31,7 @@ export interface ManagePermissionsEditorObjectTableProps { export const ManagePermissionsEditorObjectTable = forwardRef( ({ columns, rows, totalCount, onFilter, onBulkUpdate, onDirtyRows }, ref) => { const [dirtyRows, setDirtyRows] = useState>>({}); - // const [expandedGroupIds, setExpandedGroupIds] = useState(() => new Set(rows.map((row) => row.sobject))); - // FIXME: figure out what we do and do not need here useImperativeHandle(ref, () => ({ resetChanges() { resetGridChanges({ rows, type: 'object' }); diff --git a/apps/jetstream/src/app/components/manage-permissions/ManagePermissionsEditorTabVisibilityTable.tsx b/apps/jetstream/src/app/components/manage-permissions/ManagePermissionsEditorTabVisibilityTable.tsx new file mode 100644 index 000000000..0a094ebe4 --- /dev/null +++ b/apps/jetstream/src/app/components/manage-permissions/ManagePermissionsEditorTabVisibilityTable.tsx @@ -0,0 +1,85 @@ +import { useNonInitialEffect } from '@jetstream/shared/ui-utils'; +import { MapOf } from '@jetstream/types'; +import { AutoFullHeightContainer, ColumnWithFilter, DataTable } from '@jetstream/ui'; +import { forwardRef, useCallback, useImperativeHandle, useState } from 'react'; +import { resetGridChanges, updateRowsFromColumnAction } from './utils/permission-manager-table-utils'; +import { + DirtyRow, + FieldPermissionTypes, + ManagePermissionsEditorTableRef, + PermissionManagerTableContext, + PermissionTableSummaryRow, + PermissionTableTabVisibilityCell, +} from './utils/permission-manager-types'; + +function getRowKey(row: PermissionTableTabVisibilityCell) { + return row.key; +} + +// summary row is just a placeholder for rendered content +const SUMMARY_ROWS: PermissionTableSummaryRow[] = [{ type: 'HEADING' }, { type: 'ACTION' }]; + +export interface ManagePermissionsEditorTabVisibilityTableProps { + columns: ColumnWithFilter[]; + rows: PermissionTableTabVisibilityCell[]; + totalCount: number; + onFilter: (value: string) => void; + onBulkUpdate: (rows: PermissionTableTabVisibilityCell[], indexes?: number[]) => void; + onDirtyRows?: (values: MapOf>) => void; +} + +export const ManagePermissionsEditorTabVisibilityTable = forwardRef( + ({ columns, rows, totalCount, onFilter, onBulkUpdate, onDirtyRows }, ref) => { + const [dirtyRows, setDirtyRows] = useState>>({}); + + useImperativeHandle(ref, () => ({ + resetChanges() { + resetGridChanges({ rows, type: 'tabVisibility' }); + setDirtyRows({}); + }, + })); + + useNonInitialEffect(() => { + dirtyRows && onDirtyRows && onDirtyRows(dirtyRows); + }, [dirtyRows, onDirtyRows]); + + function handleColumnAction(action: 'selectAll' | 'unselectAll' | 'reset', columnKey: string) { + const [id, typeLabel] = columnKey.split('-'); + onBulkUpdate(updateRowsFromColumnAction('tabVisibility', action, typeLabel as FieldPermissionTypes, id, rows)); + } + + const handleRowsChange = useCallback( + (rows: PermissionTableTabVisibilityCell[], { indexes }) => { + onBulkUpdate(rows, indexes); + }, + [onBulkUpdate] + ); + + return ( +
+ + + +
+ ); + } +); + +export default ManagePermissionsEditorTabVisibilityTable; diff --git a/apps/jetstream/src/app/components/manage-permissions/ManagePermissionsSelection.tsx b/apps/jetstream/src/app/components/manage-permissions/ManagePermissionsSelection.tsx index 6f02a8dee..3bbb516be 100644 --- a/apps/jetstream/src/app/components/manage-permissions/ManagePermissionsSelection.tsx +++ b/apps/jetstream/src/app/components/manage-permissions/ManagePermissionsSelection.tsx @@ -43,6 +43,7 @@ export const ManagePermissionsSelection: FunctionComponent({ key: 'permission-manager.sObjectsState', @@ -51,24 +55,6 @@ export const fieldsByKey = atom | null>({ default: null, }); -// // key = either Sobject name or field name with object prefix -// export const permissionsByObjectAndField = atom>({ -// key: 'permission-manager.permissionsByObjectAndField', -// default: null, -// }); - -// //KEY = {Id-SObjectName} ex: `${record.ParentId}-${record.Field}` -// export const objectPermissionsByKey = atom>({ -// key: 'permission-manager.objectPermissionsByKey', -// default: null, -// }); - -// //KEY = {Id-FieldName} ex: `${record.ParentId}-${record.Field}` -// export const fieldPermissionsByKey = atom>({ -// key: 'permission-manager.fieldPermissionsByKey', -// default: null, -// }); - export const objectPermissionMap = atom | null>({ key: 'permission-manager.objectPermissionMap', default: null, @@ -79,6 +65,11 @@ export const fieldPermissionMap = atom | nul default: null, }); +export const tabVisibilityPermissionMap = atom | null>({ + key: 'permission-manager.tabVisibilityPermissionMap', + default: null, +}); + /** * Returns true if all selections have been made */ diff --git a/apps/jetstream/src/app/components/manage-permissions/usePermissionRecords.tsx b/apps/jetstream/src/app/components/manage-permissions/usePermissionRecords.tsx index 6a299799b..74a3ca9e2 100644 --- a/apps/jetstream/src/app/components/manage-permissions/usePermissionRecords.tsx +++ b/apps/jetstream/src/app/components/manage-permissions/usePermissionRecords.tsx @@ -1,14 +1,29 @@ import { logger } from '@jetstream/shared/client-logger'; import { queryAll, queryAllUsingOffset } from '@jetstream/shared/data'; import { useRollbar } from '@jetstream/shared/ui-utils'; -import { EntityParticlePermissionsRecord, FieldPermissionRecord, MapOf, ObjectPermissionRecord, SalesforceOrgUi } from '@jetstream/types'; +import { getMapOf } from '@jetstream/shared/utils'; +import { + EntityParticlePermissionsRecord, + FieldPermissionRecord, + MapOf, + ObjectPermissionRecord, + SalesforceOrgUi, + TabDefinitionRecord, + TabVisibilityPermissionRecord, +} from '@jetstream/types'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { FieldPermissionDefinitionMap, ObjectPermissionDefinitionMap } from './utils/permission-manager-types'; +import { + FieldPermissionDefinitionMap, + ObjectPermissionDefinitionMap, + TabVisibilityPermissionDefinitionMap, +} from './utils/permission-manager-types'; import { getFieldDefinitionKey, getQueryForAllPermissionableFields, getQueryForFieldPermissions, getQueryObjectPermissions, + getQueryTabDefinition, + getQueryTabVisibilityPermissions, } from './utils/permission-manager-utils'; export function usePermissionRecords(selectedOrg: SalesforceOrgUi, sobjects: string[], profilePermSetIds: string[], permSetIds: string[]) { @@ -22,6 +37,7 @@ export function usePermissionRecords(selectedOrg: SalesforceOrgUi, sobjects: str const [objectPermissionMap, setObjectPermissionMap] = useState | null>(null); const [fieldPermissionMap, setFieldPermissionMap] = useState | null>(null); + const [tabVisibilityPermissionMap, setTabVisibilityPermissionMap] = useState | null>(null); useEffect(() => { isMounted.current = true; @@ -50,12 +66,26 @@ export function usePermissionRecords(selectedOrg: SalesforceOrgUi, sobjects: str queryAndCombineResults(selectedOrg, getQueryForAllPermissionableFields(sobjects), true, true), queryAndCombineResults(selectedOrg, getQueryObjectPermissions(sobjects, permSetIds, profilePermSetIds)), queryAndCombineResults(selectedOrg, getQueryForFieldPermissions(sobjects, permSetIds, profilePermSetIds)), - ]).then(([fieldDefinition, objectPermissions, fieldPermissions]) => { + queryAndCombineResults( + selectedOrg, + getQueryTabVisibilityPermissions(sobjects, permSetIds, profilePermSetIds) + ).then((record) => record.map((item) => ({ ...item, Name: item.Name.replace('standard-', '') }))), + queryAndCombineResults(selectedOrg, getQueryTabDefinition(sobjects), false, true).then((tabs) => + getMapOf(tabs, 'SobjectName') + ), + ]).then(([fieldDefinition, objectPermissions, fieldPermissions, tabVisibilityPermissions, tabDefinitions]) => { return { fieldsByObject: getAllFieldsByObject(fieldDefinition), fieldsByKey: groupFields(fieldDefinition), objectPermissionMap: getObjectPermissionMap(sobjects, profilePermSetIds, permSetIds, objectPermissions), fieldPermissionMap: getFieldPermissionMap(fieldDefinition, profilePermSetIds, permSetIds, fieldPermissions), + tabVisibilityPermissionMap: getTabVisibilityPermissionMap( + sobjects, + profilePermSetIds, + permSetIds, + tabVisibilityPermissions, + tabDefinitions + ), }; }); if (isMounted.current) { @@ -63,6 +93,7 @@ export function usePermissionRecords(selectedOrg: SalesforceOrgUi, sobjects: str setFieldsByKey(output.fieldsByKey); setObjectPermissionMap(output.objectPermissionMap); setFieldPermissionMap(output.fieldPermissionMap); + setTabVisibilityPermissionMap(output.tabVisibilityPermissionMap); } } catch (ex) { logger.warn('[useProfilesAndPermSets][ERROR]', ex.message); @@ -85,6 +116,7 @@ export function usePermissionRecords(selectedOrg: SalesforceOrgUi, sobjects: str /** permissionsByObjectAndField, objectPermissionsByKey, fieldPermissionsByKey, */ objectPermissionMap, fieldPermissionMap, + tabVisibilityPermissionMap, hasError, }; } @@ -226,3 +258,54 @@ function getFieldPermissionMap( return output; }, {}); } + +function getTabVisibilityPermissionMap( + sobjects: string[], + selectedProfiles: string[], + selectedPermissionSets: string[], + permissions: TabVisibilityPermissionRecord[], + tabDefinitions: MapOf +): MapOf { + const objectPermissionsByFieldByParentId = permissions.reduce((output: MapOf>, item) => { + output[item.Name] = output[item.Name] || {}; + output[item.Name][item.ParentId] = item; + return output; + }, {}); + + return sobjects.reduce((output: MapOf, item) => { + const currItem: TabVisibilityPermissionDefinitionMap = { + apiName: item, + label: item, + metadata: item, + permissions: {}, + permissionKeys: [], + canSetPermission: !!tabDefinitions[item], + }; + + function processProfileAndPermSet(id: string) { + const permissionRecord = objectPermissionsByFieldByParentId[item]?.[id]; + currItem.permissionKeys.push(id); + if (permissionRecord) { + currItem.permissions[id] = { + available: true, + visible: permissionRecord.Visibility === 'DefaultOn' ? true : false, + record: permissionRecord, + canSetPermission: true, + }; + } else { + currItem.permissions[id] = { + available: false, + visible: false, + record: null, + canSetPermission: !!tabDefinitions[item], + }; + } + } + + selectedProfiles.forEach(processProfileAndPermSet); + selectedPermissionSets.forEach(processProfileAndPermSet); + + output[item] = currItem; + return output; + }, {}); +} diff --git a/apps/jetstream/src/app/components/manage-permissions/utils/permission-manager-export-utils.ts b/apps/jetstream/src/app/components/manage-permissions/utils/permission-manager-export-utils.ts index 3cdca26db..c78a03119 100644 --- a/apps/jetstream/src/app/components/manage-permissions/utils/permission-manager-export-utils.ts +++ b/apps/jetstream/src/app/components/manage-permissions/utils/permission-manager-export-utils.ts @@ -1,29 +1,38 @@ import { excelWorkbookToArrayBuffer, getMaxWidthFromColumnContent, initXlsx } from '@jetstream/shared/ui-utils'; import { ColumnWithFilter } from '@jetstream/ui'; import * as XLSX from 'xlsx'; -import { PermissionTableFieldCell, PermissionTableObjectCell, PermissionTableSummaryRow } from './permission-manager-types'; +import { + PermissionTableFieldCell, + PermissionTableObjectCell, + PermissionTableSummaryRow, + PermissionTableTabVisibilityCell, +} from './permission-manager-types'; initXlsx(XLSX); -type ObjectOrFieldColumn = +type ObjectOrFieldOrTabVisibilityColumn = | ColumnWithFilter - | ColumnWithFilter; + | ColumnWithFilter + | ColumnWithFilter; export function generateExcelWorkbookFromTable( - objectData: { columns: ObjectOrFieldColumn[]; rows: PermissionTableObjectCell[] }, - fieldData: { columns: ObjectOrFieldColumn[]; rows: PermissionTableFieldCell[] } + objectData: { columns: ObjectOrFieldOrTabVisibilityColumn[]; rows: PermissionTableObjectCell[] }, + tabVisibilityData: { columns: ObjectOrFieldOrTabVisibilityColumn[]; rows: PermissionTableTabVisibilityCell[] }, + fieldData: { columns: ObjectOrFieldOrTabVisibilityColumn[]; rows: PermissionTableFieldCell[] } ) { const workbook = XLSX.utils.book_new(); const objectWorksheet = generateObjectWorksheet(objectData.columns, objectData.rows); + const tabVisibilityWorksheet = generateTabVisibilityWorksheet(tabVisibilityData.columns, tabVisibilityData.rows); const fieldWorksheet = generateFieldWorksheet(fieldData.columns, fieldData.rows); XLSX.utils.book_append_sheet(workbook, objectWorksheet, 'Object Permissions'); + XLSX.utils.book_append_sheet(workbook, tabVisibilityWorksheet, 'Tab Visibility'); XLSX.utils.book_append_sheet(workbook, fieldWorksheet, 'Field Permissions'); return excelWorkbookToArrayBuffer(workbook); } -function generateObjectWorksheet(columns: ObjectOrFieldColumn[], rows: PermissionTableObjectCell[]) { +function generateObjectWorksheet(columns: ObjectOrFieldOrTabVisibilityColumn[], rows: PermissionTableObjectCell[]) { const merges: XLSX.Range[] = []; const header1: string[] = ['']; const header2: string[] = ['Object']; @@ -77,7 +86,7 @@ function generateObjectWorksheet(columns: ObjectOrFieldColumn[], rows: Permissio return worksheet; } -function generateFieldWorksheet(columns: ObjectOrFieldColumn[], rows: PermissionTableFieldCell[]) { +function generateFieldWorksheet(columns: ObjectOrFieldOrTabVisibilityColumn[], rows: PermissionTableFieldCell[]) { const merges: XLSX.Range[] = []; const header1: string[] = ['', '', '']; const header2: string[] = ['Object', 'Field Api Name', 'Field Label']; @@ -121,3 +130,46 @@ function generateFieldWorksheet(columns: ObjectOrFieldColumn[], rows: Permission worksheet['!merges'] = merges; return worksheet; } + +function generateTabVisibilityWorksheet(columns: ObjectOrFieldOrTabVisibilityColumn[], rows: PermissionTableTabVisibilityCell[]) { + const merges: XLSX.Range[] = []; + const header1: string[] = ['']; + const header2: string[] = ['Object']; + const excelRows = [header1, header2]; + + const permissionKeys: string[] = []; + + columns + .filter((col) => col.key?.endsWith('-available')) + .forEach((col) => { + // header 1 + header1.push(col.name as string); + header1.push(''); + // header1.push(''); + // merge the added cells + merges.push({ + s: { r: 0, c: header1.length - 2 }, + e: { r: 0, c: header1.length - 1 }, + }); + // header 2 + header2.push('Available'); + header2.push('Visible'); + // keep track of group order to ensure same across all rows + permissionKeys.push(col.key.split('-')[0]); + }); + + rows.forEach((row, i) => { + const currRow = [row.sobject]; + permissionKeys.forEach((key) => { + const permission = row.permissions[key]; + currRow.push(permission.available ? 'TRUE' : 'FALSE'); + currRow.push(permission.visible ? 'TRUE' : 'FALSE'); + }); + excelRows.push(currRow); + }); + + const worksheet = XLSX.utils.aoa_to_sheet(excelRows); + worksheet['!cols'] = getMaxWidthFromColumnContent(excelRows, new Set([0])); + worksheet['!merges'] = merges; + return worksheet; +} diff --git a/apps/jetstream/src/app/components/manage-permissions/utils/permission-manager-table-utils.tsx b/apps/jetstream/src/app/components/manage-permissions/utils/permission-manager-table-utils.tsx index 94dc27cf9..961554283 100644 --- a/apps/jetstream/src/app/components/manage-permissions/utils/permission-manager-table-utils.tsx +++ b/apps/jetstream/src/app/components/manage-permissions/utils/permission-manager-table-utils.tsx @@ -17,7 +17,7 @@ import { setColumnFromType, } from '@jetstream/ui'; import startCase from 'lodash/startCase'; -import { Fragment, FunctionComponent, useContext, useRef, useState } from 'react'; +import { Fragment, FunctionComponent, useContext, useMemo, useRef, useState } from 'react'; import { RenderCellProps, RenderSummaryCellProps } from 'react-data-grid'; import { BulkActionCheckbox, @@ -29,14 +29,45 @@ import { ObjectPermissionItem, ObjectPermissionTypes, PermissionManagerTableContext, + PermissionTableCellExtended, PermissionTableFieldCell, PermissionTableFieldCellPermission, PermissionTableObjectCell, PermissionTableObjectCellPermission, PermissionTableSummaryRow, + PermissionTableTabVisibilityCell, + PermissionTableTabVisibilityCellPermission, PermissionType, + PermissionTypes, + TabVisibilityPermissionDefinitionMap, + TabVisibilityPermissionItem, + TabVisibilityPermissionTypes, } from './permission-manager-types'; +type PermissionTypeColumn = T extends 'object' + ? ColumnWithFilter + : T extends 'field' + ? ColumnWithFilter + : T extends 'tabVisibility' + ? ColumnWithFilter + : never; + +type PermissionActionType = T extends 'object' + ? 'Create' | 'Read' | 'Edit' | 'Delete' | 'ViewAll' | 'ModifyAll' + : T extends 'field' + ? 'Read' | 'Edit' + : T extends 'tabVisibility' + ? 'Available' | 'Visible' + : never; + +type PermissionActionAction = T extends 'object' + ? ObjectPermissionTypes + : T extends 'field' + ? FieldPermissionTypes + : T extends 'tabVisibility' + ? TabVisibilityPermissionTypes + : never; + function setObjectValue(which: ObjectPermissionTypes, row: PermissionTableObjectCell, permissionId: string, value: boolean) { const newRow = { ...row, permissions: { ...row.permissions, [permissionId]: { ...row.permissions[permissionId] } } }; const permission = newRow.permissions[permissionId]; @@ -75,6 +106,24 @@ function setFieldValue(which: FieldPermissionTypes, row: PermissionTableFieldCel return newRow; } +function setTabVisibilityValue( + which: TabVisibilityPermissionTypes, + row: PermissionTableTabVisibilityCell, + permissionId: string, + value: boolean +) { + const newRow = { ...row, permissions: { ...row.permissions, [permissionId]: { ...row.permissions[permissionId] } } }; + const permission = newRow.permissions[permissionId]; + if (which === 'available') { + permission.available = value; + setTabVisibilityDependencies(permission, value, [], ['visible']); + } else if (which === 'visible') { + permission.visible = value; + setTabVisibilityDependencies(permission, value, ['available'], []); + } + return newRow; +} + /** * Set dependent fields based on what selections are made */ @@ -115,11 +164,33 @@ function setFieldDependencies( permission.editIsDirty = permission.edit !== permission.record.edit; } -export function resetGridChanges(options: { rows: PermissionTableFieldCell[] | PermissionTableObjectCell[]; type: PermissionType }); +function setTabVisibilityDependencies( + permission: PermissionTableTabVisibilityCellPermission, + value: boolean, + setIfTrue: TabVisibilityPermissionTypes[], + setIfFalse: TabVisibilityPermissionTypes[] +) { + if (value) { + setIfTrue.forEach((prop) => (permission[prop] = value)); + } else { + setIfFalse.forEach((prop) => (permission[prop] = value)); + } + permission.availableIsDirty = permission.available !== permission.record.available; + permission.visibleIsDirty = permission.visible !== permission.record.visible; +} + +export function resetGridChanges(options: { + rows: PermissionTableFieldCell[] | PermissionTableObjectCell[] | PermissionTableTabVisibilityCell[]; + type: PermissionType; +}); +// eslint-disable-next-line no-redeclare export function resetGridChanges({ rows, type, -}: { rows: PermissionTableObjectCell[]; type: 'object' } | { rows: PermissionTableFieldCell[]; type: 'field' }) { +}: + | { rows: PermissionTableObjectCell[]; type: 'object' } + | { rows: PermissionTableFieldCell[]; type: 'field' } + | { rows: PermissionTableTabVisibilityCell[]; type: 'tabVisibility' }) { if (type === 'object') { return rows.map((row) => { row = { ...row }; @@ -149,7 +220,7 @@ export function resetGridChanges({ }); return row; }); - } else { + } else if (type === 'field') { return rows.map((row) => { Object.keys(row.permissions).forEach((permissionKey) => { let permission = row.permissions[permissionKey]; @@ -164,6 +235,21 @@ export function resetGridChanges({ }); return row; }); + } else if (type === 'tabVisibility') { + return rows.map((row) => { + Object.keys(row.permissions).forEach((permissionKey) => { + let permission = row.permissions[permissionKey]; + if (permission.visibleIsDirty) { + permission = { ...permission }; + row.permissions[permissionKey] = permission; + permission.available = permission.availableIsDirty ? !permission.available : permission.available; + permission.visible = permission.visibleIsDirty ? !permission.visible : permission.visible; + permission.availableIsDirty = false; + permission.visibleIsDirty = false; + } + }); + return row; + }); } } @@ -187,6 +273,12 @@ export function getDirtyFieldPermissions(dirtyRows: MapOf>) { + return Object.values(dirtyRows).flatMap(({ row }) => + Object.values(row.permissions).filter((permission) => permission.availableIsDirty || permission.visibleIsDirty) + ); +} + export function getObjectColumns( selectedProfiles: string[], selectedPermissionSets: string[], @@ -278,9 +370,7 @@ export function getObjectRows(selectedSObjects: string[], objectPermissionMap: M apiName: objectPermission.apiName, label: objectPermission.label, tableLabel: `${objectPermission.label} (${objectPermission.apiName})`, - // FIXME: are there circumstances where it should be read-only? - // // formula fields and auto-number fields do not allow editing - allowEditPermission: true, // objectPermission.metadata.IsCompound || objectPermission.metadata.IsCreatable, + allowEditPermission: true, permissions: {}, }; @@ -442,20 +532,6 @@ export function getFieldColumns( return newColumns; } -type PermissionTypeColumn = T extends 'object' - ? ColumnWithFilter - : T extends 'field' - ? ColumnWithFilter - : never; - -type PermissionActionType = T extends 'object' - ? 'Create' | 'Read' | 'Edit' | 'Delete' | 'ViewAll' | 'ModifyAll' - : T extends 'field' - ? 'Read' | 'Edit' - : never; - -type PermissionActionAction = T extends 'object' ? ObjectPermissionTypes : T extends 'field' ? FieldPermissionTypes : never; - function getColumnForProfileOrPermSet({ permissionType, isFirstItem, @@ -475,45 +551,78 @@ function getColumnForProfileOrPermSet({ }): PermissionTypeColumn { const numItems = permissionType === 'object' ? 6 : 2; const colWidth = Math.max(116, (`${label} (${type})`.length * 7.5) / numItems); - const column: ColumnWithFilter = { + const column: ColumnWithFilter = { name: `${label} (${type})`, key: `${id}-${actionKey}`, width: colWidth, filters: ['BOOLEAN_SET'], cellClass: (row) => { - const permission = row.permissions[id]; if (permissionType === 'object') { const permission = row.permissions[id] as PermissionTableObjectCellPermission; if ( (actionKey === 'create' && permission.createIsDirty) || + (actionKey === 'read' && permission.readIsDirty) || + (actionKey === 'edit' && permission.editIsDirty) || (actionKey === 'delete' && permission.deleteIsDirty) || (actionKey === 'viewAll' && permission.viewAllIsDirty) || (actionKey === 'modifyAll' && permission.modifyAllIsDirty) ) { return 'active-item-yellow-bg'; } - } - if ((actionKey === 'read' && permission.readIsDirty) || (actionKey === 'edit' && permission.editIsDirty)) { - return 'active-item-yellow-bg'; + } else if (permissionType === 'field') { + const permission = row.permissions[id] as PermissionTableFieldCellPermission; + if ((actionKey === 'read' && permission.readIsDirty) || (actionKey === 'edit' && permission.editIsDirty)) { + return 'active-item-yellow-bg'; + } + } else if (permissionType === 'tabVisibility') { + if ('canSetPermission' in row && !row.canSetPermission) { + return 'is-disabled'; + } + const permission = row.permissions[id] as PermissionTableTabVisibilityCellPermission; + if ((actionKey === 'available' && permission.availableIsDirty) || (actionKey === 'visible' && permission.visibleIsDirty)) { + return 'active-item-yellow-bg'; + } } return ''; }, - colSpan: (args) => (args.type === 'HEADER' && isFirstItem ? numItems : undefined), + colSpan: (args) => { + if (args.type === 'HEADER' && isFirstItem) { + return numItems; + } + // If the row is not editable, then we don't want to show the checkbox + if (args.type === 'ROW' && permissionType === 'tabVisibility' && 'canSetPermission' in args.row && !args.row.canSetPermission) { + return numItems; + } + return undefined; + }, renderCell: ({ row, onRowChange }) => { + // If the row is not editable, then we don't want to show the checkbox + if (permissionType === 'tabVisibility' && 'canSetPermission' in row && !row.canSetPermission) { + return null; + } + const errorMessage = row.permissions[id].errorMessage; const value = row.permissions[id][actionKey as any]; function handleChange(value: boolean) { if (permissionType === 'object') { - const newRow = setObjectValue(actionKey, row as PermissionTableObjectCell, id, value); + const newRow = setObjectValue(actionKey as PermissionActionAction<'object'>, row as PermissionTableObjectCell, id, value); onRowChange(newRow); - } else { + } else if (permissionType === 'field') { const newRow = setFieldValue(actionKey as PermissionActionAction<'field'>, row as PermissionTableFieldCell, id, value); onRowChange(newRow); + } else if (permissionType === 'tabVisibility') { + const newRow = setTabVisibilityValue( + actionKey as PermissionActionAction<'tabVisibility'>, + row as PermissionTableTabVisibilityCell, + id, + value + ); + onRowChange(newRow); } } - const disabled = actionKey === 'edit' && !row.allowEditPermission; + const disabled = actionKey === 'edit' && 'allowEditPermission' in row && !row.allowEditPermission; return (
!disabled && handleChange(!value)}> @@ -527,15 +636,6 @@ function getColumnForProfileOrPermSet({ }} disabled={disabled} > - {/* Rendering this custom checkbox was really slow, lot's of DOM elements */} - {/* */} {errorMessage && (
, + permissionSetsById: MapOf +) { + const newColumns: ColumnWithFilter[] = [ + { + ...setColumnFromType('tableLabel', 'text'), + name: 'Object', + key: 'tableLabel', + frozen: true, + width: 300, + getValue: ({ column, row }) => { + const data: PermissionTableFieldCell = row[column.key]; + return data && `${data.label} (${data.apiName})`; + }, + summaryCellClass: 'bg-color-gray-dark no-outline', + renderSummaryCell: ({ row }) => { + if (row.type === 'HEADING') { + return ; + } else if (row.type === 'ACTION') { + return ; + } + return undefined; + }, + cellClass: (row) => { + if ('canSetPermission' in row && !row.canSetPermission) { + return 'slds-text-color_weak'; + } + }, + }, + { + name: '', + key: '_ROW_ACTION', + width: 100, + resizable: false, + frozen: true, + renderCell: (props) => { + if (!props.row.canSetPermission) { + return ( +
+ + This object does not have a Tab. +
+ } + > + + +
+ ); + } + return ; + }, + summaryCellClass: ({ type }) => (type === 'HEADING' ? 'bg-color-gray' : null), + renderSummaryCell: ({ row }) => { + if (row.type === 'ACTION') { + return ; + } + return undefined; + }, + }, + ]; + // Create column groups for profiles + selectedProfiles.forEach((profileId) => { + const profile = profilesById[profileId]; + (['available', 'visible'] as const).forEach((actionKey, i) => { + newColumns.push( + getColumnForProfileOrPermSet({ + isFirstItem: i === 0, + permissionType: 'tabVisibility', + id: profileId, + type: 'Profile', + label: profile.Profile.Name, + actionType: startCase(actionKey) as 'Available' | 'Visible', + actionKey, + }) + ); + }); + }); + // Create column groups for permission sets + selectedPermissionSets.forEach((permissionSetId) => { + const permissionSet = permissionSetsById[permissionSetId]; + (['available', 'visible'] as const).forEach((actionKey, i) => { + newColumns.push( + getColumnForProfileOrPermSet({ + isFirstItem: i === 0, + permissionType: 'tabVisibility', + id: permissionSetId, + type: 'Permission Set', + label: permissionSet?.Name || '', + actionType: startCase(actionKey) as 'Available' | 'Visible', + actionKey, + }) + ); + }); + }); + return newColumns; +} + +export function getTabVisibilityRows(selectedSObjects: string[], tabVisibilityPermissionMap: MapOf) { + const rows: PermissionTableTabVisibilityCell[] = []; + orderStringsBy(selectedSObjects).forEach((sobject) => { + const fieldPermission = tabVisibilityPermissionMap[sobject]; + + const currRow: PermissionTableTabVisibilityCell = { + key: sobject, + sobject: sobject, + apiName: fieldPermission.apiName, + label: fieldPermission.label, + tableLabel: `${fieldPermission.label} (${fieldPermission.apiName})`, + canSetPermission: fieldPermission.canSetPermission, + permissions: {}, + }; + + fieldPermission.permissionKeys.forEach((key) => { + const item = fieldPermission.permissions[key]; + currRow.permissions[key] = getRowTabVisibilityPermissionFromFieldPermissionItem(key, sobject, item); + }); + + rows.push(currRow); + }); + return rows; +} + +export function updateTabVisibilityRowsAfterSave( + rows: PermissionTableTabVisibilityCell[], + tabVisibilityPermissionsMap: MapOf +): PermissionTableTabVisibilityCell[] { + return rows.map((oldRow) => { + const row = { ...oldRow }; + tabVisibilityPermissionsMap[row.key].permissionKeys.forEach((key) => { + row.permissions = { ...row.permissions }; + const objectPermission = tabVisibilityPermissionsMap[row.key].permissions[key]; + if (objectPermission.errorMessage) { + row.permissions[key] = { ...row.permissions[key], errorMessage: objectPermission.errorMessage }; + } else { + row.permissions[key] = getRowTabVisibilityPermissionFromFieldPermissionItem(key, row.sobject, objectPermission); + } + }); + return row; + }); +} + +function getRowTabVisibilityPermissionFromFieldPermissionItem( + key: string, + sobject: string, + item: TabVisibilityPermissionItem +): PermissionTableTabVisibilityCellPermission { + return { + rowKey: sobject, + parentId: key, + sobject, + visible: item.visible, + available: item.available, + visibleIsDirty: false, + availableIsDirty: false, + record: item, + errorMessage: item.errorMessage, + }; +} + /** * * JSX Components * */ -export function getConfirmationModalContent(dirtyObjectCount: number, dirtyFieldCount: number) { - let output; - const dirtyObj = ( - - - {dirtyObjectCount} Object {pluralizeFromNumber('Permission', dirtyObjectCount)} - - - ); - const dirtyField = ( - - - {dirtyFieldCount} Field {pluralizeFromNumber('Permission', dirtyFieldCount)} - - - ); - if (dirtyObjectCount && dirtyFieldCount) { - output = ( - - {dirtyObj} and {dirtyField} - - ); - } else if (dirtyObjectCount) { - output = dirtyObj; - } else { - output = dirtyField; - } +export function getConfirmationModalContent(dirtyObjectCount: number, dirtyFieldCount: number, dirtyTabVisibilityCount: number) { return (
-

You have made changes to {output}.

+

You have made changes to:

+
    + {[ + { + dirty: !!dirtyObjectCount, + jsx: ( + + {dirtyObjectCount} Object {pluralizeFromNumber('Permission', dirtyObjectCount)} + + ), + }, + { + dirty: !!dirtyFieldCount, + jsx: ( + + {dirtyFieldCount} Field {pluralizeFromNumber('Permission', dirtyFieldCount)} + + ), + }, + { + dirty: !!dirtyTabVisibilityCount, + jsx: ( + + {dirtyTabVisibilityCount} Tab Visibility {pluralizeFromNumber('Permission', dirtyTabVisibilityCount)} + + ), + }, + ] + .filter(({ dirty }) => dirty) + .map(({ jsx }, i) => ( +
  • {jsx}
  • + ))} +
); } @@ -691,10 +961,10 @@ export function getConfirmationModalContent(dirtyObjectCount: number, dirtyField /** * Performs bulk action against a column */ -export function updateRowsFromColumnAction( +export function updateRowsFromColumnAction( type: PermissionType, action: 'selectAll' | 'unselectAll' | 'reset', - which: ObjectPermissionTypes | FieldPermissionTypes, + which: PermissionTypes, id: string, rows: TRows[] ): TRows[] { @@ -731,23 +1001,34 @@ export function updateRowsFromColumnAction( +export function updateRowsFromRowAction( type: PermissionType, checkboxesById: MapOf, rows: TRows[] @@ -762,7 +1043,7 @@ export function updateRowsFromRowAction(type: PermissionType, rows: TRows[]): TRows[] { +export function resetRow(type: PermissionType, rows: TRows[]): TRows[] { const newRows = [...rows]; return newRows.map((row) => { row = { ...row }; @@ -823,7 +1111,7 @@ export function resetRow, value: boolean @@ -962,7 +1268,7 @@ export function updateCheckboxDependencies( checkboxesById['viewAll'].value = true; } } - } else { + } else if (type === 'field') { if (which === 'read') { checkboxesById['read'] = { ...checkboxesById['read'], value: value }; if (!checkboxesById['read'].value) { @@ -974,55 +1280,37 @@ export function updateCheckboxDependencies( checkboxesById['read'].value = true; } } + } else if (type === 'tabVisibility') { + if (which === 'available') { + checkboxesById['available'] = { ...checkboxesById['available'], value: value }; + if (!checkboxesById['available'].value) { + checkboxesById['visible'].value = false; + } + } else if (which === 'visible') { + checkboxesById['visible'] = { ...checkboxesById['visible'], value: value }; + if (checkboxesById['visible'].value) { + checkboxesById['available'].value = true; + } + } } } -function getDirtyCount({ row, type }: { row: PermissionTableObjectCell | PermissionTableFieldCell; type: PermissionType }); -function getDirtyCount({ - row, - type, -}: { row: PermissionTableObjectCell; type: 'object' } | { row: PermissionTableFieldCell; type: 'field' }): number { - let dirtyCount = 0; - if (type === 'object') { - // const data: PermissionTableObjectCell = rowNode.data; - dirtyCount = Object.values(row.permissions).reduce((output, permission) => { - output += permission.createIsDirty ? 1 : 0; - output += permission.readIsDirty ? 1 : 0; - output += permission.editIsDirty ? 1 : 0; - output += permission.deleteIsDirty ? 1 : 0; - output += permission.viewAllIsDirty ? 1 : 0; - output += permission.modifyAllIsDirty ? 1 : 0; - return output; - }, 0); - } else { - dirtyCount = Object.values(row.permissions).reduce((output, permission) => { - output += permission.readIsDirty ? 1 : 0; - output += permission.editIsDirty ? 1 : 0; - return output; - }, 0); - } - return dirtyCount; -} - /** * Row action renderer * * This component provides a popover that the user can open to make changes that apply to an entire row */ -export const RowActionRenderer: FunctionComponent> = ({ - column, - onRowChange, - row, -}) => { +export const RowActionRenderer: FunctionComponent> = ({ column, onRowChange, row }) => { const { type } = useContext(DataTableGenericContext) as PermissionManagerTableContext; const popoverRef = useRef(null); - const [dirtyItemCount, setDirtyItemCount] = useState(0); - const [checkboxes, setCheckboxes] = useState(defaultRowActionCheckboxes(type, row?.allowEditPermission)); + const [checkboxes, setCheckboxes] = useState(() => { + return defaultRowActionCheckboxes(type, 'allowEditPermission' in row ? row?.allowEditPermission : true); + }); /** * Set all dependencies when fields change */ - function handleChange(which: ObjectPermissionTypes, value: boolean) { + function handleChange(which: PermissionTypes, value: boolean) { const checkboxesById = getMapOf(checkboxes, 'id'); updateCheckboxDependencies(which, type, checkboxesById, value); if (type === 'object') { @@ -1034,8 +1322,10 @@ export const RowActionRenderer: FunctionComponent { const [isOpen, setIsOpen] = useState(false); const [checkboxes, setCheckboxes] = useState(defaultRowActionCheckboxes(type, true)); + const rowCount = useMemo(() => rows.filter((row) => !('canSetPermission' in row) || row.canSetPermission).length, [rows]); + /** * Set all dependencies when fields change */ - function handleChange(which: ObjectPermissionTypes, value: boolean) { + function handleChange(which: PermissionTypes, value: boolean) { const checkboxesById = getMapOf(checkboxes, 'id'); updateCheckboxDependencies(which, type, checkboxesById, value); if (type === 'object') { @@ -1163,8 +1451,10 @@ export const BulkActionRenderer = () => { checkboxesById['viewAll'], checkboxesById['modifyAll'], ]); - } else { + } else if (type === 'field') { setCheckboxes([checkboxesById['read'], checkboxesById['edit']]); + } else if (type === 'tabVisibility') { + setCheckboxes([checkboxesById['available'], checkboxesById['visible']]); } } @@ -1194,7 +1484,7 @@ export const BulkActionRenderer = () => { - @@ -1207,7 +1497,7 @@ export const BulkActionRenderer = () => {

This change will apply to{' '} - {formatNumber(rows.length)} {pluralizeFromNumber(type, rows.length)} + {formatNumber(rowCount)} {pluralizeFromNumber(type, rowCount)} {' '} and all selected profiles and permission sets

diff --git a/apps/jetstream/src/app/components/manage-permissions/utils/permission-manager-types.ts b/apps/jetstream/src/app/components/manage-permissions/utils/permission-manager-types.ts index c55013364..3d45385dd 100644 --- a/apps/jetstream/src/app/components/manage-permissions/utils/permission-manager-types.ts +++ b/apps/jetstream/src/app/components/manage-permissions/utils/permission-manager-types.ts @@ -6,22 +6,28 @@ import { ObjectPermissionRecord, RecordAttributes, RecordResult, + TabVisibilityPermissionRecord, } from '@jetstream/types'; -export type PermissionType = 'object' | 'field'; +export type PermissionType = 'object' | 'field' | 'tabVisibility'; export type ObjectPermissionTypes = 'create' | 'read' | 'edit' | 'delete' | 'viewAll' | 'modifyAll'; export type FieldPermissionTypes = 'read' | 'edit'; +export type TabVisibilityPermissionTypes = 'available' | 'visible'; + +export type PermissionTypes = ObjectPermissionTypes | FieldPermissionTypes | TabVisibilityPermissionTypes; export type BulkActionCheckbox = { - id: ObjectPermissionTypes; + id: PermissionTypes; label: string; value: boolean; disabled: boolean; }; +export type PermissionDefinitionMap = ObjectPermissionDefinitionMap | FieldPermissionDefinitionMap | TabVisibilityPermissionDefinitionMap; + export interface ObjectPermissionDefinitionMap { apiName: string; - label: string; // TODO: ;( + label: string; metadata: string; // FIXME: this should probably be Describe metadata // used to retain order of permissions permissionKeys: string[]; // this is permission set ids, which could apply to profile or perm set @@ -55,6 +61,31 @@ export interface FieldPermissionItem { errorMessage?: Maybe; } +export interface TabVisibilityPermissionDefinitionMap { + apiName: string; + label: string; + metadata: string; + /** + * False if a tab does not exist for this object + */ + canSetPermission: boolean; + // used to retain order of permissions + permissionKeys: string[]; // this is permission set ids, which could apply to profile or perm set + permissions: MapOf; +} + +export interface TabVisibilityPermissionItem { + available: boolean; + visible: boolean; + record?: Maybe; + /** + * False if a tab does not exist for this object + * In which case, permissions cannot be set + */ + canSetPermission: boolean; + errorMessage?: string; +} + export interface ObjectPermissionRecordForSave extends Omit { attributes: Partial; Id?: Maybe; @@ -65,11 +96,18 @@ export interface FieldPermissionRecordForSave extends Omit { + attributes: Partial; + Id?: string; + Name?: string; + ParentId?: string; +} + export interface PermissionSaveResults { dirtyPermission: DirtyPermType; dirtyPermissionIdx: number; - operation: 'insert' | 'update'; - record: RecordType; + operation: 'insert' | 'update' | 'delete'; + record?: RecordType; recordIdx: number; response?: Maybe; } @@ -83,6 +121,8 @@ export interface PermissionTableCell; } +export type PermissionTableCellExtended = PermissionTableObjectCell | PermissionTableFieldCell | PermissionTableTabVisibilityCell; + export interface PermissionTableObjectCell extends PermissionTableCell { allowEditPermission: boolean; // TODO: what other permissions may be restricted here?? } @@ -92,11 +132,15 @@ export interface PermissionTableFieldCell extends PermissionTableCell { + canSetPermission: boolean; +} + export interface PermissionTableSummaryRow { type: 'HEADING' | 'ACTION'; } -export interface PermissionTableObjectCellPermissionBase { +export interface PermissionTableObjectCellPermissionBase { rowKey: string; parentId: string; // permissions set (placeholder profile or permission set Id) sobject: string; @@ -126,6 +170,18 @@ export interface PermissionTableFieldCellPermission extends PermissionTableObjec editIsDirty: boolean; } +export interface PermissionTableTabVisibilityCellPermission extends PermissionTableObjectCellPermissionBase { + available: boolean; + availableIsDirty: boolean; + visible: boolean; + visibleIsDirty: boolean; +} + +export type PermissionTableCellPermission = + | PermissionTableObjectCellPermission + | PermissionTableFieldCellPermission + | PermissionTableTabVisibilityCellPermission; + export interface ManagePermissionsEditorTableProps { fieldsByObject: MapOf; } @@ -144,20 +200,29 @@ export interface PermissionObjectSaveData { permissionSaveResults: PermissionSaveResults[]; recordsToInsert: ObjectPermissionRecordForSave[]; recordsToUpdate: ObjectPermissionRecordForSave[]; + recordsToDelete: string[]; } export interface PermissionFieldSaveData { permissionSaveResults: PermissionSaveResults[]; recordsToInsert: FieldPermissionRecordForSave[]; recordsToUpdate: FieldPermissionRecordForSave[]; + recordsToDelete: string[]; +} + +export interface PermissionTabVisibilitySaveData { + permissionSaveResults: PermissionSaveResults[]; + recordsToInsert: TabVisibilityPermissionRecordForSave[]; + recordsToUpdate: TabVisibilityPermissionRecordForSave[]; + recordsToDelete: string[]; } export interface PermissionManagerTableContext { type: PermissionType; - rows: (PermissionTableObjectCell | PermissionTableFieldCell)[]; + rows: PermissionTableCellExtended[]; totalCount: number; onFilterRows: (value: string) => void; onRowAction: (action: 'selectAll' | 'unselectAll' | 'reset', columnKey: string) => void; onColumnAction: (action: 'selectAll' | 'unselectAll' | 'reset', columnKey: string) => void; - onBulkAction: (rows: (PermissionTableObjectCell | PermissionTableFieldCell)[]) => void; + onBulkAction: (rows: PermissionTableCellExtended[]) => void; } diff --git a/apps/jetstream/src/app/components/manage-permissions/utils/permission-manager-utils.ts b/apps/jetstream/src/app/components/manage-permissions/utils/permission-manager-utils.ts index 2eea1926b..33cab3cd2 100644 --- a/apps/jetstream/src/app/components/manage-permissions/utils/permission-manager-utils.ts +++ b/apps/jetstream/src/app/components/manage-permissions/utils/permission-manager-utils.ts @@ -11,19 +11,26 @@ import { PermissionSetWithProfileRecord, RecordResult, SalesforceOrgUi, + TabVisibilityPermissionRecord, } from '@jetstream/types'; import type { DescribeGlobalSObjectResult } from 'jsforce'; -import { composeQuery, getField, Query, WhereClause } from 'soql-parser-js'; +import { Query, WhereClause, composeQuery, getField } from 'soql-parser-js'; import { FieldPermissionDefinitionMap, FieldPermissionRecordForSave, ObjectPermissionDefinitionMap, ObjectPermissionRecordForSave, + PermissionDefinitionMap, PermissionFieldSaveData, PermissionObjectSaveData, PermissionSaveResults, + PermissionTabVisibilitySaveData, + PermissionTableCellPermission, PermissionTableFieldCellPermission, PermissionTableObjectCellPermission, + PermissionTableTabVisibilityCellPermission, + TabVisibilityPermissionDefinitionMap, + TabVisibilityPermissionRecordForSave, } from './permission-manager-types'; const MAX_OBJ_IN_QUERY = 100; @@ -58,6 +65,7 @@ export function prepareObjectPermissionSaveData(dirtyPermissions: PermissionTabl permissionSaveResults: PermissionSaveResults[]; recordsToInsert: ObjectPermissionRecordForSave[]; recordsToUpdate: ObjectPermissionRecordForSave[]; + recordsToDelete: string[]; }, perm, i @@ -93,7 +101,7 @@ export function prepareObjectPermissionSaveData(dirtyPermissions: PermissionTabl return output; }, - { permissionSaveResults: [], recordsToInsert: [], recordsToUpdate: [] } + { permissionSaveResults: [], recordsToInsert: [], recordsToUpdate: [], recordsToDelete: [] } ); } @@ -105,6 +113,7 @@ export function prepareFieldPermissionSaveData(dirtyPermissions: PermissionTable permissionSaveResults: PermissionSaveResults[]; recordsToInsert: FieldPermissionRecordForSave[]; recordsToUpdate: FieldPermissionRecordForSave[]; + recordsToDelete: string[]; }, perm, i @@ -137,22 +146,80 @@ export function prepareFieldPermissionSaveData(dirtyPermissions: PermissionTable return output; }, - { permissionSaveResults: [], recordsToInsert: [], recordsToUpdate: [] } + { permissionSaveResults: [], recordsToInsert: [], recordsToUpdate: [], recordsToDelete: [] } + ); +} + +export function prepareTabVisibilityPermissionSaveData( + dirtyPermissions: PermissionTableTabVisibilityCellPermission[] +): PermissionTabVisibilitySaveData { + return dirtyPermissions.reduce( + ( + output: { + // used to easily keep track of the input data with the actual results + permissionSaveResults: PermissionSaveResults[]; + recordsToInsert: TabVisibilityPermissionRecordForSave[]; + recordsToUpdate: TabVisibilityPermissionRecordForSave[]; + recordsToDelete: string[]; + }, + perm, + i + ) => { + let newRecord: TabVisibilityPermissionRecordForSave | undefined = undefined; + let recordIdx: number; + let operation: 'insert' | 'update' | 'delete' = 'insert'; + if (perm.record.record && !perm.available) { + output.recordsToDelete.push(perm.record.record.Id); + recordIdx = output.recordsToDelete.length - 1; + operation = 'delete'; + } else if (perm.record.record) { + newRecord = { + attributes: { type: 'PermissionSetTabSetting' }, + Id: perm.record.record.Id, + Visibility: perm.visible ? 'DefaultOn' : 'DefaultOff', + }; + output.recordsToUpdate.push(newRecord); + recordIdx = output.recordsToUpdate.length - 1; + operation = 'update'; + } else { + newRecord = { + attributes: { type: 'PermissionSetTabSetting' }, + Name: perm.sobject.endsWith('__c') ? perm.sobject : `standard-${perm.sobject}`, + Visibility: perm.visible ? 'DefaultOn' : 'DefaultOff', + ParentId: perm.parentId, + }; + output.recordsToInsert.push(newRecord); + recordIdx = output.recordsToInsert.length - 1; + } + + output.permissionSaveResults.push({ + dirtyPermission: perm, + dirtyPermissionIdx: i, + operation, + record: newRecord, + recordIdx, + }); + + return output; + }, + { permissionSaveResults: [], recordsToInsert: [], recordsToUpdate: [], recordsToDelete: [] } ); } export async function savePermissionRecords( org: SalesforceOrgUi, - type: 'ObjectPermissions' | 'FieldPermissions', + type: 'ObjectPermissions' | 'FieldPermissions' | 'PermissionSetTabSetting', preparedData: { permissionSaveResults: PermissionSaveResults[]; recordsToInsert: RecordType[]; recordsToUpdate: RecordType[]; + recordsToDelete: string[]; } ): Promise[]> { - const { permissionSaveResults, recordsToInsert, recordsToUpdate } = preparedData; + const { permissionSaveResults, recordsToInsert, recordsToUpdate, recordsToDelete } = preparedData; let recordInsertResults: RecordResult[] = []; let recordUpdateResults: RecordResult[] = []; + let recordDeleteResults: RecordResult[] = []; if (recordsToInsert.length) { recordInsertResults = ( await Promise.all( @@ -167,12 +234,21 @@ export async function savePermissionRecords( ) ).flat(); } + if (recordsToDelete.length) { + recordDeleteResults = ( + await Promise.all( + splitArrayToMaxSize(recordsToDelete, 200).map((ids) => sobjectOperation(org, type, 'delete', { ids }, { allOrNone: false })) + ) + ).flat(); + } permissionSaveResults.forEach((saveResults) => { if (saveResults.operation === 'insert') { saveResults.response = recordInsertResults[saveResults.recordIdx]; - } else { + } else if (saveResults.operation === 'update') { saveResults.response = recordUpdateResults[saveResults.recordIdx]; + } else if (saveResults.operation === 'delete') { + saveResults.response = recordDeleteResults[saveResults.recordIdx]; } }); @@ -209,7 +285,7 @@ export async function updatePermissionSetRecords( } export function collectProfileAndPermissionIds( - dirtyPermissions: (PermissionTableObjectCellPermission | PermissionTableFieldCellPermission)[], + dirtyPermissions: PermissionTableCellPermission[], profilesById: MapOf, permissionSetsById: MapOf ) { @@ -408,9 +484,88 @@ export function getUpdatedFieldPermissions( return output; } -export function clearPermissionErrorMessage( - permissionMap: MapOf -): MapOf { +export function getUpdatedTabVisibilityPermissions( + objectPermissionMap: MapOf, + permissionSaveResults: PermissionSaveResults[] +) { + const output: MapOf = { ...objectPermissionMap }; + // remove all error messages across all objects + Object.keys(output).forEach((key) => { + output[key] = { ...output[key] }; + output[key].permissionKeys.forEach((permKey) => { + output[key].permissions = { + ...output[key].permissions, + [permKey]: { + ...output[key].permissions[permKey], + errorMessage: undefined, + }, + }; + }); + }); + + permissionSaveResults.forEach(({ dirtyPermission, operation, response }) => { + const fieldKey = dirtyPermission.sobject; + if (!isErrorResponse(response)) { + const fieldPermission: Partial = { + Id: response?.id, + ParentId: dirtyPermission.parentId, + Visibility: dirtyPermission.visible ? 'DefaultOn' : 'DefaultOff', + Name: dirtyPermission.sobject, + // missing Parent related lookup, as we do not have data for it + }; + if (operation === 'insert') { + output[fieldKey] = { + ...output[fieldKey], + permissions: { + ...output[fieldKey].permissions, + [dirtyPermission.parentId]: { + ...output[fieldKey].permissions[dirtyPermission.parentId], + available: dirtyPermission.available, + visible: dirtyPermission.visible, + record: fieldPermission as TabVisibilityPermissionRecord, + }, + }, + }; + } else { + const isDelete = !dirtyPermission.available; + output[fieldKey] = { + ...output[fieldKey], + permissions: { + ...output[fieldKey].permissions, + [dirtyPermission.parentId]: { + ...output[fieldKey].permissions[dirtyPermission.parentId], + available: dirtyPermission.available, + visible: dirtyPermission.visible, + record: isDelete ? null : (fieldPermission as TabVisibilityPermissionRecord), + }, + }, + }; + } + } else { + logger.warn('[SAVE ERROR]', { dirtyPermission, response }); + output[fieldKey] = { + ...output[fieldKey], + permissions: { + ...output[fieldKey].permissions, + [dirtyPermission.parentId]: { + ...output[fieldKey].permissions[dirtyPermission.parentId], + errorMessage: response.errors + .map((err) => + // (field not detectable in advance): Field Name: bad value for restricted picklist field: X.X + err.statusCode === 'INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST' + ? 'Salesforce does not allow modification of permissions for this field.' + : err.message + ) + .join('\n'), + }, + }, + }; + } + }); + return output; +} + +export function clearPermissionErrorMessage(permissionMap: MapOf): MapOf { return Object.keys(permissionMap).reduce((output: MapOf, key) => { output[key] = { ...permissionMap[key] }; output[key].permissions = { ...output[key].permissions }; @@ -421,9 +576,7 @@ export function clearPermissionErrorMessage( - permissionMap: MapOf -): boolean { +export function permissionsHaveError(permissionMap: MapOf): boolean { return Object.values(permissionMap).some((item) => Object.values(item.permissions).some((permission) => permission.errorMessage)); } @@ -567,6 +720,70 @@ export function getQueryForFieldPermissions(allSobjects: string[], profilePermSe return queries; } +/** + * Gets query object permissions + * @param allSobjects + * @param permSetIds + * @param profilePermSetIds + * @returns query object permissions + */ +export function getQueryTabVisibilityPermissions(allSobjects: string[], permSetIds: string[], profilePermSetIds: string[]): string[] { + const queries = splitArrayToMaxSize(allSobjects, MAX_OBJ_IN_QUERY).map((sobjects) => { + const query: Query = { + fields: [ + getField('Id'), + getField('Name'), + getField('Visibility'), + getField('ParentId'), + getField('Parent.Id'), + getField('Parent.Name'), + getField('Parent.IsOwnedByProfile'), + getField('Parent.ProfileId'), + ], + sObject: 'PermissionSetTabSetting', + where: getWhereClauseForPermissionQuery( + sobjects.map((sobject) => (sobject.endsWith('__c') ? sobject : `standard-${sobject}`)), + permSetIds, + profilePermSetIds, + 'Name' + ), + orderBy: { + field: 'Name', + order: 'ASC', + }, + }; + + return composeQuery(query); + }); + logger.log('getQueryTabVisibilityPermissions()', queries); + return queries; +} + +export function getQueryTabDefinition(allSobjects: string[]): string[] { + const queries = splitArrayToMaxSize(allSobjects, MAX_OBJ_IN_QUERY).map((sobjects) => { + const query: Query = { + fields: [getField('Id'), getField('Name'), getField('Label'), getField('SobjectName')], + sObject: 'TabDefinition', + where: { + left: { + field: 'SobjectName', + operator: 'IN', + value: sobjects, + literalType: 'STRING', + }, + }, + orderBy: { + field: 'SobjectName', + order: 'ASC', + }, + }; + + return composeQuery(query); + }); + logger.log('getQueryTabDefinition()', queries); + return queries; +} + /** * Build WHERE clause for object/field permissions * Assumptions: @@ -577,13 +794,18 @@ export function getQueryForFieldPermissions(allSobjects: string[], profilePermSe * @param permSetIds * @param profilePermSetIds */ -function getWhereClauseForPermissionQuery(sobjects: string[], profilePermSetIds: string[], permSetIds: string[]): WhereClause | undefined { +function getWhereClauseForPermissionQuery( + sobjects: string[], + profilePermSetIds: string[], + permSetIds: string[], + sobjectNameField: 'SobjectType' | 'Name' = 'SobjectType' +): WhereClause | undefined { if (!sobjects.length || (!permSetIds.length && !profilePermSetIds.length)) { return undefined; } return { left: { - field: 'SobjectType', + field: sobjectNameField, operator: 'IN', value: sobjects, literalType: 'STRING', diff --git a/libs/icon-factory/src/lib/icon-factory.tsx b/libs/icon-factory/src/lib/icon-factory.tsx index a5e73a29c..67ab77ab7 100644 --- a/libs/icon-factory/src/lib/icon-factory.tsx +++ b/libs/icon-factory/src/lib/icon-factory.tsx @@ -31,6 +31,7 @@ import StandardIcon_MultiPicklist from './icons/standard/MultiPicklist'; import StandardIcon_Opportunity from './icons/standard/Opportunity'; import StandardIcon_Outcome from './icons/standard/Outcome'; import StandardIcon_Portal from './icons/standard/Portal'; +import StandardIcon_PortalRolesAndSubordinates from './icons/standard/PortalRolesAndSubordinates'; import StandardIcon_ProductConsumed from './icons/standard/ProductConsumed'; import StandardIcon_Record from './icons/standard/Record'; import StandardIcon_RecordCreate from './icons/standard/RecordCreate'; @@ -171,6 +172,7 @@ const standardIcons = { opportunity: StandardIcon_Opportunity, outcome: StandardIcon_Outcome, portal: StandardIcon_Portal, + portal_roles_and_subordinates: StandardIcon_PortalRolesAndSubordinates, product_consumed: StandardIcon_ProductConsumed, record_create: StandardIcon_RecordCreate, record_lookup: StandardIcon_RecordLookup, diff --git a/libs/types/src/lib/salesforce/types.ts b/libs/types/src/lib/salesforce/types.ts index 88344c57f..583c432d1 100644 --- a/libs/types/src/lib/salesforce/types.ts +++ b/libs/types/src/lib/salesforce/types.ts @@ -289,6 +289,25 @@ export type TabPermissionRecordInsert = { Visibility: 'DefaultOn' | 'DefaultOff'; }; +export interface TabVisibilityPermissionRecord { + Id: string; + Name: string; + Visibility: 'DefaultOff' | 'DefaultOn'; + ParentId: string; + Parent: PermissionPermissionSetRecord; +} + +export interface TabDefinitionRecord { + Id: string; + Name: string; + Label: string; + SobjectName: string; +} + +export type TabVisibilityPermissionRecordInsert = Omit & { + attributes?: { type: 'ObjectPermissions' }; +}; + export type BulkJobWithBatches = BulkJob & { batches: BulkJobBatchInfo[] }; export interface BulkJob {