Skip to content

Commit

Permalink
Merge pull request #669 from jetstreamapp/feat/663
Browse files Browse the repository at this point in the history
Add Tab Visibility to Permissions Editor
  • Loading branch information
paustint authored Dec 10, 2023
2 parents e49991b + 867fbb0 commit 985a9e5
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 985a9e5

Please sign in to comment.