DSS-6551 Refactors privacy compliance component to support. Fixes issues with propagating ui updates to underlying data model that resulted from updates to the map props. Creates sass fw function to get only font weights associated with the default font. Adds styles for action-bar ui component that contains buttons for view action

This commit is contained in:
Seyi Adebajo 2017-03-27 18:26:09 -07:00 committed by Mars Lan
parent 854286696b
commit e3ccb71fb2
5 changed files with 187 additions and 87 deletions

View File

@ -10,126 +10,203 @@ const {
} = Ember;
const complianceListKey = 'privacyCompliancePolicy.compliancePurgeEntities';
// List of field formats for security compliance
const fieldFormats = [
'MEMBER_ID', 'SUBJECT_MEMBER_ID', 'URN', 'SUBJECT_URN', 'COMPANY_ID', 'GROUP_ID', 'CUSTOMER_ID'
];
const logicalTypes = ['id', 'number', 'urn', 'reversed urn', 'hash'].sort();
/**
* Computed macro tha checks if a dependentList has a specified privacy type
* @param {String} dependentListKey
* @param {String} type
* @returns {boolean}
* Returns a computed macro based on a provided type will return a list of
* Compliance fields that are of that identifierType or have no type
* @param {String} type string to match against identifierType
*/
const datasetHasPrivacyIdentifierType = (dependentListKey, type) => {
const prop = 'identifierType';
const complianceEntitiesMatchingType = type => {
return computed('complianceDataFields.[]', function() {
const fieldRegex = new RegExp(`${type}`, 'i');
return computed(`${dependentListKey}.@each.${prop}`, {
get() {
const list = get(this, dependentListKey) || [];
return list.filterBy(prop, type).length > 0;
}
return get(this, 'complianceDataFields').filter(({ identifierType }) => {
return fieldRegex.test(identifierType) || isBlank(identifierType);
});
});
};
export default Component.extend({
sortColumnWithName: 'name',
filterBy: 'name',
filterBy: 'identifierField',
sortDirection: 'asc',
searchTerm: '',
radioSelection: {
memberId: null,
subjectMemberId: null,
urnId: null,
orgId: null
/**
* Map of radio Group state values
* Each initially has an indeterminate state, as the user
* progresses through the prompts
* @type {Object.<Boolean, null>}
*/
userIndicatesDatasetHas: {
member: null,
org: null,
group: null
},
hasMemberId: datasetHasPrivacyIdentifierType(complianceListKey, 'MEMBER_ID'),
hasSubjectMemberId: datasetHasPrivacyIdentifierType(complianceListKey, 'SUBJECT_MEMBER_ID'),
hasUrnId: datasetHasPrivacyIdentifierType(complianceListKey, 'URN'),
hasOrgId: datasetHasPrivacyIdentifierType(complianceListKey, 'COMPANY_ID'),
// Component ui state transitions based on the userIndicatesDatasetHas map
hasUserRespondedToMemberPrompt: computed.notEmpty(
'userIndicatesDatasetHas.member'
),
showOrgPrompt: computed.bool('hasUserRespondedToMemberPrompt'),
hasUserRespondedToOrgPrompt: computed.notEmpty('userIndicatesDatasetHas.org'),
showGroupPrompt: computed.bool('hasUserRespondedToOrgPrompt'),
isMemberIdSelected: computed.notEmpty('radioSelection.memberId'),
isMemberIdSelectedTrue: computed.equal('radioSelection.memberId', true),
isSubjectMemberIdSelected: computed.notEmpty('radioSelection.subjectMemberId'),
isUrnIdSelected: computed.notEmpty('radioSelection.urnId'),
showSubjectMemberIdPrompt: computed.equal('radioSelection.memberId', false),
showUrnIdPrompt: computed.or('isSubjectMemberIdSelected', 'isMemberIdSelectedTrue'),
showOrgIdPrompt: computed.bool('isUrnIdSelected'),
identifierTypes: ['', ...fieldFormats].map(type => ({
value: type,
label: type ? type.replace(/_/g, ' ').toLowerCase().capitalize() : 'Please select'
// Map logicalTypes to options consumable by ui
logicalTypes: ['', ...logicalTypes].map(value => ({
value,
label: value ? value.capitalize() : 'Please select'
})),
complianceEntities: computed(`${complianceListKey}.[]`, function () {
return getWithDefault(this, complianceListKey, []);
}),
/**
* Lists all dataset fields found in the `columns` performs an intersection
* of fields with the currently persisted and/or updated
* privacyCompliancePolicy.compliancePurgeEntities.
* The returned list is a map of fields with current or default privacy properties
*/
complianceDataFields: computed(
`${complianceListKey}.@each.identifierType`,
`${complianceListKey}.[]`,
'schemaFieldNamesMappedToDataTypes',
function() {
const sourceEntities = getWithDefault(this, complianceListKey, []);
const complianceFieldNames = sourceEntities.mapBy('identifierField');
ownerFieldIdType(field) {
const ownerField = getWithDefault(this, complianceListKey, []).filterBy('identifierField', field).shift();
return ownerField && ownerField.identifierType;
},
const getAttributeOnField = (attribute, fieldName) => {
const sourceField = getWithDefault(this, complianceListKey, []).find(
({ identifierField }) => identifierField === fieldName
);
return sourceField ? sourceField[attribute] : null;
};
complianceFields: computed('complianceEntities', 'datasetSchemaFieldsAndTypes', function () {
const complianceFieldNames = get(this, 'complianceEntities').mapBy('identifierField');
return get(this, 'datasetSchemaFieldsAndTypes').map(({name, type}) => ({
name,
type,
hasPrivacyData: complianceFieldNames.includes(name),
format: get(this, 'ownerFieldIdType').call(this, name)
// Set default or if already in policy, retrieve current values from
// privacyCompliancePolicy.compliancePurgeEntities
return getWithDefault(this, 'schemaFieldNamesMappedToDataTypes', [
]).map(({ fieldName, dataType }) => ({
dataType,
identifierField: fieldName,
identifierType: getAttributeOnField('identifierType', fieldName),
hasPrivacyData: complianceFieldNames.includes(fieldName),
isSubject: getAttributeOnField('isSubject', fieldName),
logicalType: getAttributeOnField('logicalType', fieldName)
}));
}),
}
),
changeFieldFormat(fieldName, format) {
let field = get(this, 'complianceEntities').findBy('identifierField', fieldName);
// Compliance entities filtered for each identifierType
memberComplianceEntities: complianceEntitiesMatchingType('member'),
orgComplianceEntities: complianceEntitiesMatchingType('organization'),
groupComplianceEntities: complianceEntitiesMatchingType('group'),
if (field && fieldFormats.includes(format)) {
return set(field, 'identifierType', format);
/**
* Changes the logicalType on a field.
* Ensures that the logicalType / format is applicable to the specified field
* @param {String} fieldName the fieldName identifying the field to be updated
* @param {String} format logicalType or format te field is in
* @return {String| void}
*/
changeFieldLogicalType(fieldName, format) {
const sourceField = get(this, complianceListKey).findBy(
'identifierField',
fieldName
);
if (sourceField && logicalTypes.includes(format)) {
return set(sourceField, 'logicalType', String(format).toUpperCase());
}
},
toggleFieldOnComplianceList(identifierField, toggle) {
const complianceList = get(this, 'complianceEntities');
const op = {
/**
* Adds or removes a field onto the
* privacyCompliancePolicy.compliancePurgeEntities list.
* @param {Object} props initial props for the field to be added
* @prop {String} field.identifierField
* @param {String} identifierType the type of the field to toggle
* @param {('add'|'remove')} toggle operation to perform, can either be
* add or remove
* @return {Ember.Array|*}
*/
toggleFieldOnComplianceList(props, identifierType, toggle) {
const identifierField = get(props, 'identifierField');
const sourceEntities = get(this, complianceListKey);
if (!['add', 'remove'].includes(toggle)) {
throw new Error(`Unsupported toggle operation ${toggle}`);
}
return {
add() {
if (!complianceList.findBy('identifierField', identifierField)) {
return complianceList.setObjects([...complianceList, {identifierField}]);
// Ensure that we don't currently have this field present on the
// privacyCompliancePolicy.compliancePurgeEntities list
if (!sourceEntities.findBy('identifierField', identifierField)) {
const addPurgeEntity = { identifierField, identifierType };
return sourceEntities.setObjects([addPurgeEntity, ...sourceEntities]);
}
},
remove: () => complianceList.setObjects(complianceList.filter(item => item.identifierField !== identifierField)),
remove: () => {
// Remove the identifierType since we are removing it from the
// privacyCompliancePolicy.compliancePurgeEntities in case it
// is added back during the session
set(props, 'identifierType', null);
return sourceEntities.setObjects(
sourceEntities.filter(
item => item.identifierField !== identifierField
)
);
}
}[toggle];
return typeof op === 'function' && op();
},
ensureTypeContainsFormat: (updatedCompliance) =>
updatedCompliance.every(entity => fieldFormats.includes(get(entity, 'identifierType'))),
actions: {
onFieldFormatChange({identifierField: fieldName}, {value: format}) {
return this.changeFieldLogicalType(fieldName, format);
/**
*
* @param {String} identifierField id for the field to update
* @param {String} logicalType updated format to apply to the field
* @return {*|String|void} logicalType or void
*/
onFieldFormatChange({ identifierField }, { value: logicalType }) {
return this.changeFieldLogicalType(identifierField, logicalType);
},
onFieldPrivacyChange({identifierField: fieldName, hasPrivacyData}) {
/**
* Toggles a field on / off the compliance list
* @param {String} identifierType the type of the field to be toggled on
* the privacyCompliancePolicy.compliancePurgeEntities list
* @param {Object|Ember.Object} props containing the props to be added
* @prop {Boolean} field.hasPrivacyData checked or not checked
* @return {*}
*/
onFieldPrivacyChange(identifierType, props) {
// If checked, add, otherwise remove
const { hasPrivacyData } = props;
const toggle = !hasPrivacyData ? 'add' : 'remove';
return this.toggleFieldOnComplianceList(fieldName, toggle);
return this.toggleFieldOnComplianceList(props, identifierType, toggle);
},
/**
* Toggles the isSubject property of a member identifiable field
* @param {Object} field
* @param {Object} props the props on the member field to update
* @prop {Boolean} isSubject flag indicating this field as a subject owner
* when true
* @prop {String} identifierField unique field to update isSubject property
*/
onMemberFieldSubjectChange(field) {
const {isSubject} = field;
onMemberFieldSubjectChange(props) {
const { isSubject, identifierField: name } = props;
if (field && 'isSubject' in field) {
set(field, 'isSubject', !isSubject);
// Ensure that a flag isSubject is present on the props
if (props && 'isSubject' in props) {
const sourceField = get(this, complianceListKey).find(
({ identifierField }) => identifierField === name
);
set(sourceField, 'isSubject', !isSubject);
}
},
@ -145,17 +222,6 @@ export default Component.extend({
// server state
resetCompliance() {
this.get('onReset')();
},
didChangePrivacyIdentifiable (sectionName, isPrivacyIdentifiable) {
const section = {
'has-subject-member': 'subjectMemberId',
'has-urn': 'urnId',
'has-organization': 'orgId',
'has-member': 'memberId'
}[sectionName];
return set(this, `radioSelection.${section}`, isPrivacyIdentifiable);
}
}
});

View File

@ -41,6 +41,20 @@
@return null;
}
/// Get a font-weight from the $font-weight map
/// @param {String} $style - the font-style to look up
/// @param {Number} $weight - number for the previous style to be looked up
/// @return {Number}
/// @require $font-weights
@function fw($style, $weight) {
@if map-has-key($font-weights, $style) {
@return map-get(map-get($font-weights, $style), $weight);
}
@warn '#{$style} not found in `$font-weights` list';
@return null;
}
/// Progressively add black as you decrease the proportion of the color
/// Provides more subtle transitions than darken
/// @param {Color} $color - color to shade

View File

@ -16,6 +16,21 @@ $font-awesome-stack: (
sans-serif
);
$font-weights: (
normal:(
2: 200,
3: 300,
4: 400,
6:600
),
italic:(
2: 200,
3:300,
4:400,
6:600
)
);
/// Color Scheme
/// @type Map
/// @prop {String} key - Scheme name

View File

@ -2,6 +2,7 @@
@import "navbar";
@import "hero";
@import "dataset-author/all";
@import "dataset-compliance/all";
@import "nacho/nacho-button";
@import "nacho/nacho-global-search";

View File

@ -40,3 +40,7 @@
}
}
}
.action-bar {
margin-bottom: 20px;
}