diff --git a/atst/forms/app_settings.py b/atst/forms/app_settings.py index 20e1aa25..19801a10 100644 --- a/atst/forms/app_settings.py +++ b/atst/forms/app_settings.py @@ -2,18 +2,18 @@ from flask_wtf import FlaskForm from wtforms.fields import FieldList, FormField, HiddenField, RadioField, StringField from .forms import BaseForm -from .data import ENV_ROLES +from .data import ENV_ROLES, ENV_ROLE_NO_ACCESS as NO_ACCESS class MemberForm(FlaskForm): user_id = HiddenField() user_name = StringField() - role_name = RadioField(choices=ENV_ROLES, default="no_access") + role_name = RadioField(choices=ENV_ROLES, default=NO_ACCESS) @property def data(self): _data = super().data - if "role_name" in _data and _data["role_name"] == "no_access": + if "role_name" in _data and _data["role_name"] == NO_ACCESS: _data["role_name"] = None return _data diff --git a/atst/forms/data.py b/atst/forms/data.py index 2ddc3124..e0d4cf7f 100644 --- a/atst/forms/data.py +++ b/atst/forms/data.py @@ -217,6 +217,7 @@ REQUIRED_DISTRIBUTIONS = [ ("other", "Other as necessary"), ] +ENV_ROLE_NO_ACCESS = "No Access" ENV_ROLES = [(role.value, role.value) for role in CSPRole] + [ - ("no_access", "No access") + (ENV_ROLE_NO_ACCESS, "No access") ] diff --git a/atst/forms/team.py b/atst/forms/team.py index 6e4ce3ee..29e46874 100644 --- a/atst/forms/team.py +++ b/atst/forms/team.py @@ -1,14 +1,31 @@ from flask_wtf import FlaskForm -from wtforms.fields import FormField, FieldList, HiddenField, StringField +from wtforms.fields import FormField, FieldList, HiddenField, RadioField, StringField from wtforms.validators import Required -from .application_member import EnvironmentForm +from .application_member import EnvironmentForm as BaseEnvironmentForm +from .data import ENV_ROLES, ENV_ROLE_NO_ACCESS as NO_ACCESS from .forms import BaseForm from atst.forms.fields import SelectField from atst.domain.permission_sets import PermissionSets from atst.utils.localization import translate +class EnvironmentForm(BaseEnvironmentForm): + role = RadioField( + "Role", + choices=ENV_ROLES, + default=None, + filters=[lambda x: None if x == "None" else x], + ) + + @property + def data(self): + _data = super().data + if "role" in _data and _data["role"] == NO_ACCESS: + _data["role"] = None + return _data + + class PermissionsForm(FlaskForm): perms_team_mgmt = SelectField( translate("portfolios.applications.members.new.manage_team"), diff --git a/atst/routes/applications/settings.py b/atst/routes/applications/settings.py index b3077b77..935aaaf4 100644 --- a/atst/routes/applications/settings.py +++ b/atst/routes/applications/settings.py @@ -5,6 +5,7 @@ from atst.domain.environments import Environments from atst.domain.applications import Applications from atst.forms.app_settings import AppEnvRolesForm from atst.forms.application import ApplicationForm, EditEnvironmentForm +from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS from atst.domain.authz.decorator import user_can_access_decorator as user_can from atst.models.environment_role import CSPRole from atst.domain.exceptions import NotFoundError @@ -46,10 +47,10 @@ def sort_env_users_by_role(env): users_list = [] no_access_users = env.application.users - env.users no_access_list = [ - {"user_id": str(user.id), "user_name": user.full_name, "role_name": "no_access"} + {"user_id": str(user.id), "user_name": user.full_name, "role_name": NO_ACCESS} for user in no_access_users ] - users_list.append({"role": "no_access", "members": no_access_list}) + users_list.append({"role": NO_ACCESS, "members": no_access_list}) for role in CSPRole: users_list.append( diff --git a/atst/routes/applications/team.py b/atst/routes/applications/team.py index 97783f89..3aa988e4 100644 --- a/atst/routes/applications/team.py +++ b/atst/routes/applications/team.py @@ -6,6 +6,7 @@ from atst.domain.applications import Applications from atst.domain.application_roles import ApplicationRoles from atst.domain.authz.decorator import user_can_access_decorator as user_can from atst.domain.environment_roles import EnvironmentRoles +from atst.domain.environments import Environments from atst.domain.exceptions import AlreadyExistsError from atst.domain.permission_sets import PermissionSets from atst.domain.users import Users @@ -97,15 +98,25 @@ def update_team(application_id): form = TeamForm(http_request.form) if form.validate(): - for member in form.members: - app_role = ApplicationRoles.get(member.data["user_id"], application.id) + for member_form in form.members: + app_role = ApplicationRoles.get(member_form.user_id.data, application.id) new_perms = [ perm - for perm in member.data["permission_sets"] + for perm in member_form.data["permission_sets"] if perm != PermissionSets.VIEW_APPLICATION ] ApplicationRoles.update_permission_sets(app_role, new_perms) - flash("updated_application_members_permissions") + + for environment_role_form in member_form.environment_roles: + user = Users.get(member_form.user_id.data) + environment = Environments.get( + environment_role_form.environment_id.data + ) + Environments.update_env_role( + environment, user, environment_role_form.data.get("role") + ) + + flash("updated_application_team_settings", application_name=application.name) return redirect( url_for( diff --git a/atst/utils/flash.py b/atst/utils/flash.py index 1557a90e..0a96366a 100644 --- a/atst/utils/flash.py +++ b/atst/utils/flash.py @@ -186,10 +186,10 @@ MESSAGES = { """, "category": "success", }, - "updated_application_members_permissions": { + "updated_application_team_settings": { "title_template": translate("flash.success"), "message_template": """ -
{{ "flash.updated_application_members_permissions" | translate }}
+{{ "flash.updated_application_team_settings" | translate({"application_name": application_name}) }}
""", "category": "success", }, diff --git a/js/components/__tests__/edit_environment_role.test.js b/js/components/__tests__/edit_environment_role.test.js index 4421d58c..8242acb4 100644 --- a/js/components/__tests__/edit_environment_role.test.js +++ b/js/components/__tests__/edit_environment_role.test.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils' -import EditEnvironmentRole from '../forms/edit_environment_role' +import { NO_ACCESS, EditEnvironmentRole } from '../forms/edit_environment_role' describe('EditEnvironmentRole', () => { var initialRoleCategories, wrapper @@ -7,7 +7,7 @@ describe('EditEnvironmentRole', () => { beforeEach(() => { initialRoleCategories = [ { - role: 'no_access', + role: NO_ACCESS, members: [ { role_name: null, user_id: '123' }, { role_name: null, user_id: '456' }, @@ -41,10 +41,10 @@ describe('EditEnvironmentRole', () => { it('removes null roles to no_access', () => { let roles = wrapper.vm.sanitizeValues([ - { role: 'no_access', members: [{ role_name: null }] }, + { role: NO_ACCESS, members: [{ role_name: null }] }, ]) expect(roles).toEqual([ - { role: 'no_access', members: [{ role_name: 'no_access' }] }, + { role: NO_ACCESS, members: [{ role_name: NO_ACCESS }] }, ]) }) diff --git a/js/components/environment_role.js b/js/components/environment_role.js new file mode 100644 index 00000000..23133c25 --- /dev/null +++ b/js/components/environment_role.js @@ -0,0 +1,36 @@ +import optionsinput from './options_input' +import { emitEvent } from '../lib/emitters' + +export default { + name: 'environment-role', + + components: { + optionsinput, + }, + + props: { + initialRole: String, + }, + + data: function() { + return { + role: this.initialRole, + expanded: false, + } + }, + + methods: { + toggle: function() { + this.expanded = !this.expanded + }, + radioChange: function(e) { + this.role = e.target.value + emitEvent('field-change', this, { + value: e.target.value, + valid: true, + name: this.name, + watch: true, + }) + }, + }, +} diff --git a/js/components/forms/edit_application_roles.js b/js/components/forms/edit_application_roles.js index c4f6d490..ef5de676 100644 --- a/js/components/forms/edit_application_roles.js +++ b/js/components/forms/edit_application_roles.js @@ -1,7 +1,7 @@ import FormMixin from '../../mixins/form' import Modal from '../../mixins/modal' import toggler from '../toggler' -import EditEnvironmentRole from './edit_environment_role' +import { EditEnvironmentRole } from './edit_environment_role' export default { name: 'edit-application-roles', diff --git a/js/components/forms/edit_environment_role.js b/js/components/forms/edit_environment_role.js index cbb09457..e9081c34 100644 --- a/js/components/forms/edit_environment_role.js +++ b/js/components/forms/edit_environment_role.js @@ -5,7 +5,9 @@ import Modal from '../../mixins/modal' // https://github.com/dod-ccpo/atst/pull/799/files#r282240663 // May also want to reconsider the data structure by storing the roles and members separately -export default { +export const NO_ACCESS = 'No Access' + +export const EditEnvironmentRole = { name: 'edit-environment-role', mixins: [FormMixin], @@ -26,7 +28,7 @@ export default { roles.forEach(role => { role.members.forEach(member => { if (member.role_name === null) { - member.role_name = 'no_access' + member.role_name = NO_ACCESS } }) }) @@ -34,7 +36,7 @@ export default { }, checkNoAccess: function(role) { - return role === 'no_access' + return role === NO_ACCESS }, toggleSection: function(sectionName) { diff --git a/js/components/toggler.js b/js/components/toggler.js index e433b294..225e144b 100644 --- a/js/components/toggler.js +++ b/js/components/toggler.js @@ -1,7 +1,8 @@ -import editEnvironmentRole from './forms/edit_environment_role' +import { EditEnvironmentRole } from './forms/edit_environment_role' import FormMixin from '../mixins/form' import optionsinput from './options_input' import textinput from './text_input' +import EnvironmentRole from './environment_role' export default { name: 'toggler', @@ -16,10 +17,12 @@ export default { }, components: { - editEnvironmentRole, + EditEnvironmentRole, optionsinput, textinput, optionsinput, + EnvironmentRole, + toggler: this, }, data: function() { diff --git a/js/index.js b/js/index.js index 02a9b404..520dea03 100644 --- a/js/index.js +++ b/js/index.js @@ -18,7 +18,7 @@ import poc from './components/forms/poc' import oversight from './components/forms/oversight' import toggler from './components/toggler' import NewApplication from './components/forms/new_application' -import EditEnvironmentRole from './components/forms/edit_environment_role' +import { EditEnvironmentRole } from './components/forms/edit_environment_role' import EditApplicationRoles from './components/forms/edit_application_roles' import MultiStepModalForm from './components/forms/multi_step_modal_form' import funding from './components/forms/funding' @@ -39,6 +39,7 @@ import KoReview from './components/forms/ko_review' import BaseForm from './components/forms/base_form' import DeleteConfirmation from './components/delete_confirmation' import NewEnvironment from './components/forms/new_environment' +import EnvironmentRole from './components/environment_role' Vue.config.productionTip = false @@ -80,6 +81,7 @@ const app = new Vue({ DeleteConfirmation, nestedcheckboxinput, NewEnvironment, + EnvironmentRole, }, mounted: function() { diff --git a/styles/components/_accordion_table.scss b/styles/components/_accordion_table.scss index 1bcf3331..c5d93b09 100644 --- a/styles/components/_accordion_table.scss +++ b/styles/components/_accordion_table.scss @@ -91,6 +91,30 @@ border-bottom: 1px solid $color-gray-lighter; } } + + .accordion-table__item__action-group { + padding: 1rem 3.2rem; + background-color: $color-gray-lightest; + + button, + a { + margin: 0; + font-size: $small-font-size; + } + + .icon-link { + padding-top: 0.5rem; + float: none; + } + + > *:first-child { + padding-left: 0; + } + + > *:last-child { + float: right; + } + } } .accordion-table__item__toggler { @@ -100,6 +124,10 @@ color: $color-blue; padding: $gap; + > span { + margin-left: auto; + } + .icon { @include icon-size(12); @@ -147,6 +175,17 @@ .accordion-table__item__expanded_first { float: left; } + + .accordion-table__item__expanded-role { + .icon-link { + padding: 0; + vertical-align: text-top; + + .icon { + margin: 0 0 0 0.25rem; + } + } + } } } } diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index 60fd04c6..c555ed13 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -1,7 +1,6 @@ .portfolio-panel-container { @include media($large-screen) { @include grid-row; - min-height: 500px; } @@ -38,7 +37,6 @@ &.icon-link--disabled { color: $color-gray-dark; opacity: 1; - .icon { @include icon-color($color-gray-dark); } @@ -53,7 +51,6 @@ .icon-link { color: $color-gray-medium; pointer-events: none; - &.icon-link--disabled { opacity: 1; } @@ -63,7 +60,6 @@ .portfolio-header { flex-direction: column; - @include media($small-screen) { flex-direction: row; } @@ -103,10 +99,8 @@ .icon-link { padding: 0.8rem 1.2rem; - &.active { color: $color-gray; - .icon { @include icon-color($color-gray); } @@ -150,7 +144,6 @@ .unfunded { color: $color-red; - .icon { @include icon-color($color-red); } @@ -166,7 +159,7 @@ } .portfolio-content { - margin: (6 * $gap) $gap 0 $gap; + margin: 6 * $gap $gap 0 $gap; .panel { @include shadow-panel; @@ -175,7 +168,6 @@ .member-list { .panel { @include shadow-panel; - padding-bottom: 0; } @@ -191,7 +183,7 @@ } tr:first-child { - padding: 0 (2 * $gap) 0 (5 * $gap); + padding: 0 2 * $gap 0 5 * $gap; } td { @@ -203,14 +195,14 @@ th { background-color: $color-gray-lightest; - padding: $gap (2 * $gap); + padding: $gap 2 * $gap; border-top: none; border-bottom: none; color: $color-gray; } td:first-child { - padding: (2 * $gap) (2 * $gap) (2 * $gap) (5 * $gap); + padding: 2 * $gap 2 * $gap 2 * $gap 5 * $gap; } tbody { @@ -218,7 +210,7 @@ border-bottom: 1px solid $color-gray-lightest; font-size: 1.6rem; border-top: 0; - padding: (3 * $gap) (2 * $gap); + padding: 3 * $gap 2 * $gap; .usa-button-disabled { color: $color-gray-medium; @@ -318,6 +310,11 @@ .alert { margin: 4rem; } + + .member-list__subhead { + font-weight: $font-normal; + font-size: $base-font-size; + } } .application-content { @@ -333,7 +330,6 @@ .block-list__footer { border-bottom: none; } - .application-edit__env-list-item { label { color: $color-black; @@ -373,7 +369,6 @@ .portfolio-applications__header--actions { color: $color-blue; font-size: $small-font-size; - .icon { @include icon-color($color-blue); @include icon-size(14); @@ -384,7 +379,6 @@ .application-list { .toggle-link { background-color: $color-blue-light; - .icon { margin: $gap / 2; } @@ -402,7 +396,6 @@ .application-list-item__environment__csp_link { font-size: $small-font-size; font-weight: normal; - &:hover { background-color: $color-aqua-light; } @@ -420,7 +413,6 @@ .subheading { @include subheading; - margin-top: 6 * $gap; margin-bottom: 2 * $gap; } @@ -431,6 +423,7 @@ .pending-task-order { background-color: $color-gold-lightest; + align-items: center; margin: 0; margin-bottom: 2 * $gap; @@ -462,7 +455,6 @@ .icon--tiny { @include icon-size(10); - margin-left: 1rem; } } @@ -487,7 +479,7 @@ th { background-color: $color-gray-lightest; - padding: $gap (2 * $gap); + padding: $gap 2 * $gap; border-top: none; border-bottom: none; color: $color-gray; @@ -566,7 +558,6 @@ .panel { @include shadow-panel; - margin-bottom: 4 * $gap; } } @@ -586,7 +577,6 @@ input { max-width: 30em; } - .icon-validation { left: 30em; } @@ -615,3 +605,44 @@ } } } + +.member-list__name { + margin-top: 1rem; +} + +.member-list__role-select { + overflow: auto; + margin: 1.6rem -3.2rem -1.6rem -3.2rem; + padding: 2rem 3.2rem 2rem 5rem; + background: $color-gray-cool-light; + border-top: 1px solid $color-gray-lighter; + + > label { + font-weight: $font-bold; + margin: 0; + } + + > label:first-child + ul.member-list____role-select__radio { + display: flex; + background: $color-gray-cool-light; + + li { + border-bottom: none; + + label { + margin-top: 1rem; + margin-left: 2rem; + } + } + + li:first-child > label { + margin-left: 0; + } + } + + button { + font-size: $small-font-size; + float: right; + margin-right: 0; + } +} diff --git a/styles/core/_variables.scss b/styles/core/_variables.scss index 65153584..4463f1bd 100644 --- a/styles/core/_variables.scss +++ b/styles/core/_variables.scss @@ -73,7 +73,7 @@ $color-gray-lightest: #f1f1f1; $color-gray-warm-dark: #494440; $color-gray-warm-light: #e4e2e0; -$color-gray-cool-light: #dce4ef; +$color-gray-cool-light: #eff2f7; $color-gold-dark: #cd841b; $color-gold: #fdb81e; diff --git a/templates/components/modal.html b/templates/components/modal.html index 957aee3a..25772f53 100644 --- a/templates/components/modal.html +++ b/templates/components/modal.html @@ -12,7 +12,7 @@ {% endif %} diff --git a/templates/fragments/applications/edit_team.html b/templates/fragments/applications/edit_team.html index 1914a62a..844e22dc 100644 --- a/templates/fragments/applications/edit_team.html +++ b/templates/fragments/applications/edit_team.html @@ -11,7 +11,11 @@Members ({{ team_form.members | length }})
+