Skip to content

Commit

Permalink
Add Tab Visibility to Permissions Editor
Browse files Browse the repository at this point in the history
Added tab visibility to permissions editor because this is often desired to be adjusted at the same time as other permissions

Some objects do not have a tab, in which case the user is unable to adjust tab permissions for those objects.

Tab visibility objects for standard objects have a prefix of 'standard-' instead of the full object name, in addition, the records need to be manually deleted to indicate no permissions.

resolves #663
  • Loading branch information
paustint committed Dec 10, 2023
1 parent e49991b commit 867fbb0
Show file tree
Hide file tree
Showing 14 changed files with 1,154 additions and 198 deletions.
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -49,6 +49,7 @@ export const AppStateResetOnOrgChange: FunctionComponent<AppStateResetOnOrgChang
useResetRecoilState(fromPermissionsState.fieldsByKey),
useResetRecoilState(fromPermissionsState.objectPermissionMap),
useResetRecoilState(fromPermissionsState.fieldPermissionMap),
useResetRecoilState(fromPermissionsState.tabVisibilityPermissionMap),
// Deploy
useResetRecoilState(fromDeployMetadataState.metadataItemsState),
useResetRecoilState(fromDeployMetadataState.metadataItemsMapState),
Expand Down Expand Up @@ -77,7 +78,7 @@ export const AppStateResetOnOrgChange: FunctionComponent<AppStateResetOnOrgChang
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedOrg, priorSelectedOrg]);

return <Fragment />;
return null;
};

export default AppStateResetOnOrgChange;
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const ManagePermissions: FunctionComponent<ManagePermissionsProps> = () =
const resetFieldsByKey = useResetRecoilState(fromPermissionsState.fieldsByKey);
const resetObjectPermissionMap = useResetRecoilState(fromPermissionsState.objectPermissionMap);
const resetFieldPermissionMap = useResetRecoilState(fromPermissionsState.fieldPermissionMap);
const resetTabVisibilityPermissionMap = useResetRecoilState(fromPermissionsState.tabVisibilityPermissionMap);
const [priorSelectedOrg, setPriorSelectedOrg] = useState<string | null>(null);

const hasSelectionsMade = useRecoilValue(fromPermissionsState.hasSelectionsMade);
Expand All @@ -45,6 +46,7 @@ export const ManagePermissions: FunctionComponent<ManagePermissionsProps> = () =
resetFieldsByKey();
resetObjectPermissionMap();
resetFieldPermissionMap();
resetTabVisibilityPermissionMap();
} else if (!selectedOrg) {
resetProfilesState();
resetSelectedProfilesPermSetState();
Expand All @@ -56,6 +58,7 @@ export const ManagePermissions: FunctionComponent<ManagePermissionsProps> = () =
resetFieldsByKey();
resetObjectPermissionMap();
resetFieldPermissionMap();
resetTabVisibilityPermissionMap();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedOrg, priorSelectedOrg]);
Expand All @@ -75,6 +78,7 @@ export const ManagePermissions: FunctionComponent<ManagePermissionsProps> = () =
['fieldsByKey', fromPermissionsState.fieldsByKey],
['objectPermissionMap', fromPermissionsState.objectPermissionMap],
['fieldPermissionMap', fromPermissionsState.fieldPermissionMap],
['tabVisibilityPermissionMap', fromPermissionsState.tabVisibilityPermissionMap],
]}
/>
{location.pathname.endsWith('/editor') && !hasSelectionsMade ? <Navigate to="." /> : <Outlet />}
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,7 @@ export interface ManagePermissionsEditorObjectTableProps {
export const ManagePermissionsEditorObjectTable = forwardRef<any, ManagePermissionsEditorObjectTableProps>(
({ columns, rows, totalCount, onFilter, onBulkUpdate, onDirtyRows }, ref) => {
const [dirtyRows, setDirtyRows] = useState<MapOf<DirtyRow<PermissionTableObjectCell>>>({});
// const [expandedGroupIds, setExpandedGroupIds] = useState(() => new Set<any>(rows.map((row) => row.sobject)));

// FIXME: figure out what we do and do not need here
useImperativeHandle<any, ManagePermissionsEditorTableRef>(ref, () => ({
resetChanges() {
resetGridChanges({ rows, type: 'object' });
Expand Down
Original file line number Diff line number Diff line change
@@ -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<PermissionTableTabVisibilityCell, PermissionTableSummaryRow>[];
rows: PermissionTableTabVisibilityCell[];
totalCount: number;
onFilter: (value: string) => void;
onBulkUpdate: (rows: PermissionTableTabVisibilityCell[], indexes?: number[]) => void;
onDirtyRows?: (values: MapOf<DirtyRow<PermissionTableTabVisibilityCell>>) => void;
}

export const ManagePermissionsEditorTabVisibilityTable = forwardRef<any, ManagePermissionsEditorTabVisibilityTableProps>(
({ columns, rows, totalCount, onFilter, onBulkUpdate, onDirtyRows }, ref) => {
const [dirtyRows, setDirtyRows] = useState<MapOf<DirtyRow<PermissionTableTabVisibilityCell>>>({});

useImperativeHandle<any, ManagePermissionsEditorTableRef>(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 (
<div>
<AutoFullHeightContainer fillHeight setHeightAttr bottomBuffer={15}>
<DataTable
columns={columns}
data={rows}
getRowKey={getRowKey}
topSummaryRows={SUMMARY_ROWS}
onRowsChange={handleRowsChange}
context={
{
type: 'tabVisibility',
totalCount,
onFilterRows: onFilter,
onColumnAction: handleColumnAction,
onBulkAction: onBulkUpdate,
} as PermissionManagerTableContext
}
rowHeight={24}
summaryRowHeight={38}
/>
</AutoFullHeightContainer>
</div>
);
}
);

export default ManagePermissionsEditorTabVisibilityTable;
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const ManagePermissionsSelection: FunctionComponent<ManagePermissionsSele
const resetFieldsByKey = useResetRecoilState(fromPermissionsState.fieldsByKey);
const resetObjectPermissionMap = useResetRecoilState(fromPermissionsState.objectPermissionMap);
const resetFieldPermissionMap = useResetRecoilState(fromPermissionsState.fieldPermissionMap);
const resetTabVisibilityPermissionMap = useResetRecoilState(fromPermissionsState.tabVisibilityPermissionMap);

const profilesAndPermSetsData = useProfilesAndPermSets(selectedOrg, profiles, permissionSets);

Expand All @@ -67,6 +68,7 @@ export const ManagePermissionsSelection: FunctionComponent<ManagePermissionsSele
resetFieldsByKey();
resetObjectPermissionMap();
resetFieldPermissionMap();
resetTabVisibilityPermissionMap();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedProfiles, selectedPermissionSets, selectedSObjects]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import {
} from '@jetstream/types';
import type { DescribeGlobalSObjectResult } from 'jsforce';
import { atom, selector } from 'recoil';
import { FieldPermissionDefinitionMap, ObjectPermissionDefinitionMap } from './utils/permission-manager-types';
import {
FieldPermissionDefinitionMap,
ObjectPermissionDefinitionMap,
TabVisibilityPermissionDefinitionMap,
} from './utils/permission-manager-types';

export const sObjectsState = atom<DescribeGlobalSObjectResult[] | null>({
key: 'permission-manager.sObjectsState',
Expand Down Expand Up @@ -51,24 +55,6 @@ export const fieldsByKey = atom<MapOf<EntityParticlePermissionsRecord> | null>({
default: null,
});

// // key = either Sobject name or field name with object prefix
// export const permissionsByObjectAndField = atom<MapOf<string[]>>({
// key: 'permission-manager.permissionsByObjectAndField',
// default: null,
// });

// //KEY = {Id-SObjectName} ex: `${record.ParentId}-${record.Field}`
// export const objectPermissionsByKey = atom<MapOf<ObjectPermissionRecord>>({
// key: 'permission-manager.objectPermissionsByKey',
// default: null,
// });

// //KEY = {Id-FieldName} ex: `${record.ParentId}-${record.Field}`
// export const fieldPermissionsByKey = atom<MapOf<FieldPermissionRecord>>({
// key: 'permission-manager.fieldPermissionsByKey',
// default: null,
// });

export const objectPermissionMap = atom<MapOf<ObjectPermissionDefinitionMap> | null>({
key: 'permission-manager.objectPermissionMap',
default: null,
Expand All @@ -79,6 +65,11 @@ export const fieldPermissionMap = atom<MapOf<FieldPermissionDefinitionMap> | nul
default: null,
});

export const tabVisibilityPermissionMap = atom<MapOf<TabVisibilityPermissionDefinitionMap> | null>({
key: 'permission-manager.tabVisibilityPermissionMap',
default: null,
});

/**
* Returns true if all selections have been made
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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[]) {
Expand All @@ -22,6 +37,7 @@ export function usePermissionRecords(selectedOrg: SalesforceOrgUi, sobjects: str

const [objectPermissionMap, setObjectPermissionMap] = useState<MapOf<ObjectPermissionDefinitionMap> | null>(null);
const [fieldPermissionMap, setFieldPermissionMap] = useState<MapOf<FieldPermissionDefinitionMap> | null>(null);
const [tabVisibilityPermissionMap, setTabVisibilityPermissionMap] = useState<MapOf<TabVisibilityPermissionDefinitionMap> | null>(null);

useEffect(() => {
isMounted.current = true;
Expand Down Expand Up @@ -50,19 +66,34 @@ export function usePermissionRecords(selectedOrg: SalesforceOrgUi, sobjects: str
queryAndCombineResults<EntityParticlePermissionsRecord>(selectedOrg, getQueryForAllPermissionableFields(sobjects), true, true),
queryAndCombineResults<ObjectPermissionRecord>(selectedOrg, getQueryObjectPermissions(sobjects, permSetIds, profilePermSetIds)),
queryAndCombineResults<FieldPermissionRecord>(selectedOrg, getQueryForFieldPermissions(sobjects, permSetIds, profilePermSetIds)),
]).then(([fieldDefinition, objectPermissions, fieldPermissions]) => {
queryAndCombineResults<TabVisibilityPermissionRecord>(
selectedOrg,
getQueryTabVisibilityPermissions(sobjects, permSetIds, profilePermSetIds)
).then((record) => record.map((item) => ({ ...item, Name: item.Name.replace('standard-', '') }))),
queryAndCombineResults<TabDefinitionRecord>(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) {
setFieldsByObject(output.fieldsByObject);
setFieldsByKey(output.fieldsByKey);
setObjectPermissionMap(output.objectPermissionMap);
setFieldPermissionMap(output.fieldPermissionMap);
setTabVisibilityPermissionMap(output.tabVisibilityPermissionMap);
}
} catch (ex) {
logger.warn('[useProfilesAndPermSets][ERROR]', ex.message);
Expand All @@ -85,6 +116,7 @@ export function usePermissionRecords(selectedOrg: SalesforceOrgUi, sobjects: str
/** permissionsByObjectAndField, objectPermissionsByKey, fieldPermissionsByKey, */
objectPermissionMap,
fieldPermissionMap,
tabVisibilityPermissionMap,
hasError,
};
}
Expand Down Expand Up @@ -226,3 +258,54 @@ function getFieldPermissionMap(
return output;
}, {});
}

function getTabVisibilityPermissionMap(
sobjects: string[],
selectedProfiles: string[],
selectedPermissionSets: string[],
permissions: TabVisibilityPermissionRecord[],
tabDefinitions: MapOf<TabDefinitionRecord>
): MapOf<TabVisibilityPermissionDefinitionMap> {
const objectPermissionsByFieldByParentId = permissions.reduce((output: MapOf<MapOf<TabVisibilityPermissionRecord>>, item) => {
output[item.Name] = output[item.Name] || {};
output[item.Name][item.ParentId] = item;
return output;
}, {});

return sobjects.reduce((output: MapOf<TabVisibilityPermissionDefinitionMap>, 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;
}, {});
}
Loading

0 comments on commit 867fbb0

Please sign in to comment.