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; } = Ember;
const complianceListKey = 'privacyCompliancePolicy.compliancePurgeEntities'; const complianceListKey = 'privacyCompliancePolicy.compliancePurgeEntities';
// List of field formats for security compliance const logicalTypes = ['id', 'number', 'urn', 'reversed urn', 'hash'].sort();
const fieldFormats = [
'MEMBER_ID', 'SUBJECT_MEMBER_ID', 'URN', 'SUBJECT_URN', 'COMPANY_ID', 'GROUP_ID', 'CUSTOMER_ID'
];
/** /**
* Computed macro tha checks if a dependentList has a specified privacy type * Returns a computed macro based on a provided type will return a list of
* @param {String} dependentListKey * Compliance fields that are of that identifierType or have no type
* @param {String} type * @param {String} type string to match against identifierType
* @returns {boolean}
*/ */
const datasetHasPrivacyIdentifierType = (dependentListKey, type) => { const complianceEntitiesMatchingType = type => {
const prop = 'identifierType'; return computed('complianceDataFields.[]', function() {
const fieldRegex = new RegExp(`${type}`, 'i');
return computed(`${dependentListKey}.@each.${prop}`, { return get(this, 'complianceDataFields').filter(({ identifierType }) => {
get() { return fieldRegex.test(identifierType) || isBlank(identifierType);
const list = get(this, dependentListKey) || []; });
return list.filterBy(prop, type).length > 0;
}
}); });
}; };
export default Component.extend({ export default Component.extend({
sortColumnWithName: 'name', sortColumnWithName: 'name',
filterBy: 'name', filterBy: 'identifierField',
sortDirection: 'asc', sortDirection: 'asc',
searchTerm: '', searchTerm: '',
radioSelection: { /**
memberId: null, * Map of radio Group state values
subjectMemberId: null, * Each initially has an indeterminate state, as the user
urnId: null, * progresses through the prompts
orgId: null * @type {Object.<Boolean, null>}
*/
userIndicatesDatasetHas: {
member: null,
org: null,
group: null
}, },
hasMemberId: datasetHasPrivacyIdentifierType(complianceListKey, 'MEMBER_ID'), // Component ui state transitions based on the userIndicatesDatasetHas map
hasSubjectMemberId: datasetHasPrivacyIdentifierType(complianceListKey, 'SUBJECT_MEMBER_ID'), hasUserRespondedToMemberPrompt: computed.notEmpty(
hasUrnId: datasetHasPrivacyIdentifierType(complianceListKey, 'URN'), 'userIndicatesDatasetHas.member'
hasOrgId: datasetHasPrivacyIdentifierType(complianceListKey, 'COMPANY_ID'), ),
showOrgPrompt: computed.bool('hasUserRespondedToMemberPrompt'),
hasUserRespondedToOrgPrompt: computed.notEmpty('userIndicatesDatasetHas.org'),
showGroupPrompt: computed.bool('hasUserRespondedToOrgPrompt'),
isMemberIdSelected: computed.notEmpty('radioSelection.memberId'), // Map logicalTypes to options consumable by ui
isMemberIdSelectedTrue: computed.equal('radioSelection.memberId', true), logicalTypes: ['', ...logicalTypes].map(value => ({
isSubjectMemberIdSelected: computed.notEmpty('radioSelection.subjectMemberId'), value,
isUrnIdSelected: computed.notEmpty('radioSelection.urnId'), label: value ? value.capitalize() : 'Please select'
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'
})), })),
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 getAttributeOnField = (attribute, fieldName) => {
const ownerField = getWithDefault(this, complianceListKey, []).filterBy('identifierField', field).shift(); const sourceField = getWithDefault(this, complianceListKey, []).find(
return ownerField && ownerField.identifierType; ({ identifierField }) => identifierField === fieldName
}, );
return sourceField ? sourceField[attribute] : null;
};
complianceFields: computed('complianceEntities', 'datasetSchemaFieldsAndTypes', function () { // Set default or if already in policy, retrieve current values from
const complianceFieldNames = get(this, 'complianceEntities').mapBy('identifierField'); // 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)
}));
}
),
return get(this, 'datasetSchemaFieldsAndTypes').map(({name, type}) => ({ // Compliance entities filtered for each identifierType
name, memberComplianceEntities: complianceEntitiesMatchingType('member'),
type, orgComplianceEntities: complianceEntitiesMatchingType('organization'),
hasPrivacyData: complianceFieldNames.includes(name), groupComplianceEntities: complianceEntitiesMatchingType('group'),
format: get(this, 'ownerFieldIdType').call(this, name)
}));
}),
changeFieldFormat(fieldName, format) { /**
let field = get(this, 'complianceEntities').findBy('identifierField', fieldName); * 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 (field && fieldFormats.includes(format)) { if (sourceField && logicalTypes.includes(format)) {
return set(field, 'identifierType', format); return set(sourceField, 'logicalType', String(format).toUpperCase());
} }
}, },
toggleFieldOnComplianceList(identifierField, toggle) { /**
const complianceList = get(this, 'complianceEntities'); * Adds or removes a field onto the
const op = { * 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() { add() {
if (!complianceList.findBy('identifierField', identifierField)) { // Ensure that we don't currently have this field present on the
return complianceList.setObjects([...complianceList, {identifierField}]); // 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]; }[toggle];
return typeof op === 'function' && op();
}, },
ensureTypeContainsFormat: (updatedCompliance) => ensureTypeContainsFormat: (updatedCompliance) =>
updatedCompliance.every(entity => fieldFormats.includes(get(entity, 'identifierType'))), updatedCompliance.every(entity => fieldFormats.includes(get(entity, 'identifierType'))),
actions: { 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'; 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 * 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) { onMemberFieldSubjectChange(props) {
const {isSubject} = field; const { isSubject, identifierField: name } = props;
if (field && 'isSubject' in field) { // Ensure that a flag isSubject is present on the props
set(field, 'isSubject', !isSubject); if (props && 'isSubject' in props) {
const sourceField = get(this, complianceListKey).find(
({ identifierField }) => identifierField === name
);
set(sourceField, 'isSubject', !isSubject);
} }
}, },
@ -143,19 +220,8 @@ export default Component.extend({
// Rolls back changes made to the compliance spec to current // Rolls back changes made to the compliance spec to current
// server state // server state
resetCompliance () { resetCompliance() {
this.get('onReset')(); 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; @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 /// Progressively add black as you decrease the proportion of the color
/// Provides more subtle transitions than darken /// Provides more subtle transitions than darken
/// @param {Color} $color - color to shade /// @param {Color} $color - color to shade

View File

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

View File

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

View File

@ -39,4 +39,8 @@
} }
} }
} }
} }
.action-bar {
margin-bottom: 20px;
}