Browse Source

SONARCLOUD-380 Rework modal styling of SC and SQ

* Update modal-fields
* Update form-fields styling
* Update modal-fields usage in extensions
* Clean css
tags/7.7
Grégoire Aubert 5 years ago
parent
commit
79a32b89b9
100 changed files with 2017 additions and 1196 deletions
  1. 1
    1
      server/sonar-server/src/main/java/org/sonar/server/user/ws/SetSettingAction.java
  2. 2
    9
      server/sonar-server/src/test/java/org/sonar/server/user/ws/SetSettingActionTest.java
  3. 0
    1
      server/sonar-web/src/main/js/app/components/StartupModal.tsx
  4. 35
    128
      server/sonar-web/src/main/js/app/styles/components/modals.css
  5. 4
    0
      server/sonar-web/src/main/js/app/styles/init/base.css
  6. 21
    8
      server/sonar-web/src/main/js/app/styles/init/forms.css
  7. 5
    1
      server/sonar-web/src/main/js/app/styles/init/type.css
  8. 1
    4
      server/sonar-web/src/main/js/app/types.d.ts
  9. 4
    4
      server/sonar-web/src/main/js/apps/account/components/Password.tsx
  10. 7
    12
      server/sonar-web/src/main/js/apps/account/components/__tests__/Password-test.tsx
  11. 90
    0
      server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Password-test.tsx.snap
  12. 1
    1
      server/sonar-web/src/main/js/apps/background-tasks/components/ScannerContext.tsx
  13. 1
    1
      server/sonar-web/src/main/js/apps/background-tasks/components/Stacktrace.tsx
  14. 1
    1
      server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/ScannerContext-test.tsx.snap
  15. 1
    1
      server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/Stacktrace-test.tsx.snap
  16. 3
    5
      server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx
  17. 6
    6
      server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx
  18. 139
    164
      server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx
  19. 2
    2
      server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx
  20. 3
    3
      server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx
  21. 1
    1
      server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx
  22. 38
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/ActivationFormModal-test.tsx
  23. 41
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/BulkChangeModal-test.tsx
  24. 39
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/CustomRuleFormModal-test.tsx
  25. 3
    15
      server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleListItem-test.tsx
  26. 101
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/ActivationFormModal-test.tsx.snap
  27. 56
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap
  28. 237
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/CustomRuleFormModal-test.tsx.snap
  29. 4
    4
      server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleListItem-test.tsx.snap
  30. 0
    9
      server/sonar-web/src/main/js/apps/coding-rules/styles.css
  31. 2
    2
      server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationModal.tsx
  32. 1
    1
      server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationModal-test.tsx.snap
  33. 11
    3
      server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx
  34. 1
    1
      server/sonar-web/src/main/js/apps/create/organization/RemoteOrganizationChoose.tsx
  35. 17
    10
      server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx
  36. 7
    7
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap
  37. 1
    0
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/RemoteOrganizationChoose-test.tsx.snap
  38. 6
    2
      server/sonar-web/src/main/js/apps/create/organization/__tests__/actions-test.ts
  39. 10
    6
      server/sonar-web/src/main/js/apps/create/organization/actions.ts
  40. 4
    3
      server/sonar-web/src/main/js/apps/custom-measures/components/Form.tsx
  41. 2
    0
      server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Form-test.tsx.snap
  42. 3
    2
      server/sonar-web/src/main/js/apps/custom-metrics/components/Form.tsx
  43. 1
    0
      server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/Form-test.tsx.snap
  44. 2
    1
      server/sonar-web/src/main/js/apps/groups/components/Form.tsx
  45. 1
    0
      server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/Form-test.tsx.snap
  46. 44
    51
      server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
  47. 13
    12
      server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap
  48. 9
    0
      server/sonar-web/src/main/js/apps/issues/styles.css
  49. 1
    1
      server/sonar-web/src/main/js/apps/organizationMembers/AddMemberForm.tsx
  50. 60
    46
      server/sonar-web/src/main/js/apps/organizationMembers/ManageMemberGroupsForm.tsx
  51. 7
    4
      server/sonar-web/src/main/js/apps/organizationMembers/MembersList.tsx
  52. 1
    1
      server/sonar-web/src/main/js/apps/organizationMembers/MembersListHeader.tsx
  53. 26
    17
      server/sonar-web/src/main/js/apps/organizationMembers/MembersListItem.tsx
  54. 59
    98
      server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx
  55. 13
    14
      server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx
  56. 53
    68
      server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx
  57. 20
    16
      server/sonar-web/src/main/js/apps/organizationMembers/__tests__/ManageMemberGroupsForm-test.tsx
  58. 12
    16
      server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersList-test.tsx
  59. 28
    53
      server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersListItem-test.tsx
  60. 3
    9
      server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersPageHeader-test.tsx
  61. 11
    2
      server/sonar-web/src/main/js/apps/organizationMembers/__tests__/OrganizationMembers-test.tsx
  62. 3
    6
      server/sonar-web/src/main/js/apps/organizationMembers/__tests__/SyncMemberForm-test.tsx
  63. 1
    1
      server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/AddMemberForm-test.tsx.snap
  64. 43
    23
      server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/ManageMemberGroupsForm-test.tsx.snap
  65. 0
    1
      server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersList-test.tsx.snap
  66. 3
    3
      server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersListHeader-test.tsx.snap
  67. 45
    35
      server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersListItem-test.tsx.snap
  68. 31
    24
      server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap
  69. 11
    2
      server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap
  70. 151
    157
      server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/SyncMemberForm-test.tsx.snap
  71. 15
    13
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationEdit.tsx
  72. 1
    1
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationEmpty.css
  73. 5
    9
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationEmpty.tsx
  74. 54
    54
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationEdit-test.tsx.snap
  75. 14
    14
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationEmpty-test.tsx.snap
  76. 4
    3
      server/sonar-web/src/main/js/apps/permission-templates/components/Form.tsx
  77. 30
    0
      server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/Form-test.tsx
  78. 12
    0
      server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/__snapshots__/Form-test.tsx.snap
  79. 5
    1
      server/sonar-web/src/main/js/apps/permissions/project/components/ApplyTemplate.tsx
  80. 53
    0
      server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/ApplyTemplate-test.tsx
  81. 83
    0
      server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/__snapshots__/ApplyTemplate-test.tsx.snap
  82. 2
    1
      server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.tsx
  83. 3
    2
      server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeEventForm.tsx
  84. 41
    0
      server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/AddEventForm-test.tsx
  85. 35
    0
      server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/ChangeEventForm-test.tsx
  86. 26
    0
      server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/__snapshots__/AddEventForm-test.tsx.snap
  87. 26
    0
      server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/__snapshots__/ChangeEventForm-test.tsx.snap
  88. 1
    1
      server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx
  89. 3
    3
      server/sonar-web/src/main/js/apps/projectBranches/components/SettingForm.tsx
  90. 3
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/RenameBranchModal-test.tsx.snap
  91. 4
    4
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/SettingForm-test.tsx.snap
  92. 5
    1
      server/sonar-web/src/main/js/apps/projectLinks/CreationModal.tsx
  93. 1
    0
      server/sonar-web/src/main/js/apps/projectLinks/__tests__/__snapshots__/CreationModal-test.tsx.snap
  94. 1
    1
      server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx
  95. 2
    1
      server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx
  96. 8
    0
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/BulkApplyTemplateModal-test.tsx.snap
  97. 6
    3
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap
  98. 4
    3
      server/sonar-web/src/main/js/apps/quality-gates/components/ConditionModal.tsx
  99. 6
    2
      server/sonar-web/src/main/js/apps/quality-gates/components/ConditionOperator.tsx
  100. 0
    0
      server/sonar-web/src/main/js/apps/quality-gates/components/CopyQualityGateForm.tsx

+ 1
- 1
server/sonar-server/src/main/java/org/sonar/server/user/ws/SetSettingAction.java View File

@@ -57,7 +57,7 @@ public class SetSettingAction implements UsersWsAction {
.setRequired(true)
.setMaximumLength(100)
.setDescription("Setting key")
.setPossibleValues("notifications.optOut", UserUpdater.NOTIFICATIONS_READ_DATE, "organizations.members.dismissSyncNotif");
.setPossibleValues("notifications.optOut", UserUpdater.NOTIFICATIONS_READ_DATE);

action.createParam(PARAM_VALUE)
.setRequired(true)

+ 2
- 9
server/sonar-server/src/test/java/org/sonar/server/user/ws/SetSettingActionTest.java View File

@@ -91,17 +91,11 @@ public class SetSettingActionTest {
.setParam("value", "true")
.execute();

ws.newRequest()
.setParam("key", "organizations.members.dismissSyncNotif")
.setParam("value", "org1,org2")
.execute();

assertThat(db.getDbClient().userPropertiesDao().selectByUser(db.getSession(), user))
.extracting(UserPropertyDto::getKey, UserPropertyDto::getValue)
.containsExactlyInAnyOrder(
tuple("notifications.readDate", "1234"),
tuple("notifications.optOut", "true"),
tuple("organizations.members.dismissSyncNotif", "org1,org2"));
tuple("notifications.optOut", "true"));
}

@Test
@@ -131,8 +125,7 @@ public class SetSettingActionTest {

assertThat(definition.param("key").possibleValues()).containsExactlyInAnyOrder(
"notifications.optOut",
"notifications.readDate",
"organizations.members.dismissSyncNotif");
"notifications.readDate");
}

}

+ 0
- 1
server/sonar-web/src/main/js/app/components/StartupModal.tsx View File

@@ -153,7 +153,6 @@ export class StartupModal extends React.PureComponent<Props, State> {
<OnboardingModal
onClose={this.closeOnboarding}
onOpenProjectOnboarding={this.openProjectOnboarding}
skipOnboarding={this.props.skipOnboarding}
/>
)}
</OnboardingContext.Provider>

+ 35
- 128
server/sonar-web/src/main/js/app/styles/components/modals.css View File

@@ -28,9 +28,6 @@
background-color: #fff;
opacity: 0;
transition: all 0.2s ease;
}

.modal.sonarcloud {
border-radius: 3px;
}

@@ -45,6 +42,11 @@
opacity: 1;
}

.modal-small {
width: 450px;
margin-left: -225px;
}

.modal-medium {
width: 830px;
margin-left: -415px;
@@ -86,116 +88,57 @@
margin-right: var(--sbw);
}

.modal-container {
max-height: 60vh;
padding: 10px;
box-sizing: border-box;
overflow: auto;
}

.modal.sonarcloud .modal-container {
border-top: 1px solid var(--barBorderColor);
margin-top: var(--pagePadding);
}

.modal.sonarcloud .modal-container > :last-child {
margin-bottom: var(--pagePadding);
}

.modal-head {
padding: 0 10px;
background-color: var(--gray94);
border-bottom: 1px solid var(--disableGrayBorder);
}

.modal.sonarcloud .modal-head {
background-color: transparent;
border-bottom: none;
padding: var(--pagePadding) calc(2 * var(--pagePadding)) 0;
padding: calc(4 * var(--gridSize));
padding-bottom: 0;
}

.modal-head h1,
.modal-head h2 {
line-height: 30px;
min-height: 30px;
}

.modal.sonarcloud .modal-head h1,
.modal.sonarcloud .modal-head h2 {
margin-top: var(--gridSize);
margin: 0;
font-size: var(--bigFontSize);
font-weight: bold;
line-height: 30px;
line-height: normal;
}

.modal-body {
padding: 10px;
padding: var(--pagePadding) calc(4 * var(--gridSize));
}

.modal.sonarcloud .modal-body {
padding: var(--pagePadding) calc(2 * var(--pagePadding));
.modal-container {
max-height: 60vh;
box-sizing: border-box;
overflow-y: scroll;
border-top: 1px solid var(--barBorderColor);
margin-top: var(--pagePadding);
padding-right: calc(4 * var(--gridSize) - var(--sbw));
}

.modal-container > :last-child {
margin-bottom: var(--pagePadding);
}

.modal-field,
.modal-large-field,
.modal-validation-field {
clear: both;
display: block;
padding: 5px 0 5px 130px;
}

.modal-large-field {
padding: 20px 40px;
}

.modal-validation-field {
padding: 3px 0 3px 130px;
padding: 0;
margin-bottom: calc(var(--gridSize) * 2);
}

.modal-field label,
.modal-validation-field label {
position: relative;
left: -140px;
display: block;
float: left;
width: 120px;
margin-right: -130px;
padding-top: 5px;
padding-bottom: 2px;
padding-left: 10px;
line-height: 1;
text-align: right;
overflow: hidden;
text-overflow: ellipsis;
}

.modal-large-field label {
display: inline-block;
padding-bottom: 15px;
font-weight: bold;
}

.modal-field .note {
line-height: var(--controlHeight);
}

.readonly-field {
padding-top: 5px;
margin-left: -5px;
line-height: 1;
padding-bottom: calc(var(--gridSize) / 2);
}

.modal-field a.icon-checkbox,
.modal-field input,
.modal-large-field input,
.modal-field select,
.modal-large-field select,
.modal-field textarea,
.modal-large-field textarea,
.modal-field .Select,
.modal-large-field .Select {
.modal-field .Select {
margin-right: 5px;
margin-bottom: 10px;
}

.modal-field a.icon-checkbox {
@@ -203,15 +146,12 @@
}

.modal-field input[type='radio'],
.modal-large-field input[type='radio'],
.modal-field input[type='checkbox'],
.modal-large-field input[type='checkbox'] {
.modal-field input[type='checkbox'] {
margin-top: 5px;
margin-bottom: 4px;
}

.modal-field > .icon-checkbox,
.modal-large-field > .icon-checkbox {
.modal-field > .icon-checkbox {
padding-top: 6px;
padding-right: 8px;
}
@@ -222,77 +162,44 @@
.modal-field textarea,
.modal-field select,
.modal-field .Select {
width: 250px;
}

.modal-field textarea {
max-width: 250px;
min-width: 250px;
max-height: 50vh;
min-height: var(--controlHeight);
}

.modal-large-field input[type='text'],
.modal-large-field input[type='email'],
.modal-large-field input[type='password'],
.modal-large-field textarea,
.modal-large-field select,
.modal-large-field .Select {
width: 100%;
}

.modal-large-field textarea {
max-width: 100%;
min-width: 100%;
max-height: 50vh;
min-height: var(--controlHeight);
}

.modal-validation-field input,
.modal-validation-field textarea,
.modal-validation-field .Select {
margin-right: 5px;
margin-right: var(--gridSize);
margin-bottom: 2px;
width: 250px;
width: calc(100% - 3 * var(--gridSize));
}

.modal-field textarea,
.modal-validation-field textarea {
max-width: 250px;
min-width: 250px;
max-width: 100%;
min-width: 100%;
max-height: 50vh;
min-height: var(--controlHeight);
}

.modal-validation-field input:not(.is-invalid),
.modal-validation-field .Select:not(.is-invalid) {
margin-bottom: 18px;
margin-bottom: calc(var(--tinyControlHeight) + 2px);
}

.modal-field-description {
padding-bottom: 4px;
line-height: 1.4;
color: var(--secondFontColor);
font-size: var(--smallFontSize);
overflow: hidden;
text-overflow: ellipsis;
}

.modal-validation-field .modal-field-description {
margin-top: 2px;
}

.modal-foot {
padding: 10px;
border-top: 1px solid var(--disableGrayBorder);
background-color: var(--gray94);
text-align: right;
}

.modal.sonarcloud .modal-foot {
padding: var(--pagePadding);
padding: var(--pagePadding) calc(4 * var(--gridSize));
border-top: 1px solid var(--barBorderColor);
background-color: var(--barBackgroundColor);
border-radius: 3px;
text-align: right;
}

.modal-foot button,

+ 4
- 0
server/sonar-web/src/main/js/app/styles/init/base.css View File

@@ -81,6 +81,10 @@ textarea {
font-family: inherit;
}

textarea {
min-height: 40px;
}

/*Remove button padding in FF*/
select::-moz-focus-inner,
input::-moz-focus-inner,

+ 21
- 8
server/sonar-web/src/main/js/app/styles/init/forms.css View File

@@ -160,16 +160,11 @@ select {
}

.input-super-large {
width: 100%;
width: 100% !important;
max-width: 300px;
min-width: 200px;
}

textarea.input-super-large {
max-width: 600px;
min-width: 300px;
}

.input-ghost {
padding: 0 !important;
border: none !important;
@@ -193,13 +188,31 @@ em.mandatory {
.form-field {
clear: both;
display: block;
padding-top: var(--gridSize);
padding-bottom: calc(2 * var(--gridSize));
}

.form-field label {
display: block;
padding-bottom: var(--gridSize);
font-weight: bold;
padding-bottom: calc(var(--gridSize) / 2);
}

.form-field-description {
line-height: 1.4;
color: var(--secondFontColor);
font-size: var(--smallFontSize);
overflow: hidden;
text-overflow: ellipsis;
margin-top: 2px;
}

.form-field input[type='text'],
.form-field input[type='email'],
.form-field input[type='password'],
.form-field textarea,
.form-field select,
.form-field .Select {
width: 250px;
}

.radio-toggle {

+ 5
- 1
server/sonar-web/src/main/js/app/styles/init/type.css View File

@@ -168,6 +168,10 @@ small,
font-size: var(--smallFontSize);
}

.medium {
font-size: var(--mediumFontSize);
}

.big {
font-size: var(--bigFontSize);
}
@@ -255,7 +259,7 @@ small,
}

.text-normal {
font-weight: normal;
font-weight: normal !important;
}

.text-muted {

+ 1
- 4
server/sonar-web/src/main/js/app/types.d.ts View File

@@ -216,10 +216,7 @@ declare namespace T {
value: string;
}

type CurrentUserSettingNames =
| 'notifications.optOut'
| 'notifications.readDate'
| 'organizations.members.dismissSyncNotif';
type CurrentUserSettingNames = 'notifications.optOut' | 'notifications.readDate';

export interface CustomMeasure {
createdAt?: string;

+ 4
- 4
server/sonar-web/src/main/js/apps/account/components/Password.tsx View File

@@ -87,7 +87,7 @@ export default class Password extends React.Component<Props, State> {
</Alert>
))}

<div className="modal-field">
<div className="form-field">
<label htmlFor="old_password">
{translate('my_profile.password.old')}
<em className="mandatory">*</em>
@@ -101,7 +101,7 @@ export default class Password extends React.Component<Props, State> {
type="password"
/>
</div>
<div className="modal-field">
<div className="form-field">
<label htmlFor="password">
{translate('my_profile.password.new')}
<em className="mandatory">*</em>
@@ -115,7 +115,7 @@ export default class Password extends React.Component<Props, State> {
type="password"
/>
</div>
<div className="modal-field">
<div className="form-field">
<label htmlFor="password_confirmation">
{translate('my_profile.password.confirm')}
<em className="mandatory">*</em>
@@ -129,7 +129,7 @@ export default class Password extends React.Component<Props, State> {
type="password"
/>
</div>
<div className="modal-field">
<div className="form-field">
<SubmitButton id="change-password">
{translate('my_profile.password.submit')}
</SubmitButton>

server/sonar-web/src/main/js/components/ui/NewInfoBox.css → server/sonar-web/src/main/js/apps/account/components/__tests__/Password-test.tsx View File

@@ -17,16 +17,11 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
.new-info-box {
display: flex;
padding: var(--gridSize);
background-color: var(--veryLightBlue);
border: 1px solid var(--alertBorderInfo);
border-radius: 2px;
}
import * as React from 'react';
import { shallow } from 'enzyme';
import Password from '../Password';
import { mockCurrentUser } from '../../../../helpers/testMocks';

.new-info-box-header {
display: flex;
justify-content: space-between;
align-items: center;
}
it('renders correctly', () => {
expect(shallow(<Password user={mockCurrentUser()} />)).toMatchSnapshot();
});

+ 90
- 0
server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Password-test.tsx.snap View File

@@ -0,0 +1,90 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders correctly 1`] = `
<section
className="boxed-group"
>
<h2
className="spacer-bottom"
>
my_profile.password.title
</h2>
<form
className="boxed-group-inner"
onSubmit={[Function]}
>
<div
className="form-field"
>
<label
htmlFor="old_password"
>
my_profile.password.old
<em
className="mandatory"
>
*
</em>
</label>
<input
autoComplete="off"
id="old_password"
name="old_password"
required={true}
type="password"
/>
</div>
<div
className="form-field"
>
<label
htmlFor="password"
>
my_profile.password.new
<em
className="mandatory"
>
*
</em>
</label>
<input
autoComplete="off"
id="password"
name="password"
required={true}
type="password"
/>
</div>
<div
className="form-field"
>
<label
htmlFor="password_confirmation"
>
my_profile.password.confirm
<em
className="mandatory"
>
*
</em>
</label>
<input
autoComplete="off"
id="password_confirmation"
name="password_confirmation"
required={true}
type="password"
/>
</div>
<div
className="form-field"
>
<SubmitButton
id="change-password"
>
my_profile.password.submit
</SubmitButton>
</div>
</form>
</section>
`;

+ 1
- 1
server/sonar-web/src/main/js/apps/background-tasks/components/ScannerContext.tsx View File

@@ -62,7 +62,7 @@ export default class ScannerContext extends React.PureComponent<Props, State> {
const { scannerContext } = this.state;

return (
<Modal contentLabel="scanner context" large={true} onRequestClose={this.props.onClose}>
<Modal contentLabel="scanner context" onRequestClose={this.props.onClose} size={'large'}>
<div className="modal-head">
<h2>
{translate('background_tasks.scanner_context')}

+ 1
- 1
server/sonar-web/src/main/js/apps/background-tasks/components/Stacktrace.tsx View File

@@ -70,7 +70,7 @@ export default class Stacktrace extends React.PureComponent<Props, State> {
const { loading, stacktrace } = this.state;

return (
<Modal contentLabel="stacktrace" large={true} onRequestClose={this.props.onClose}>
<Modal contentLabel="stacktrace" onRequestClose={this.props.onClose} size={'large'}>
<div className="modal-head">
<h2>
{translate('background_tasks.error_stacktrace')}

+ 1
- 1
server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/ScannerContext-test.tsx.snap View File

@@ -3,8 +3,8 @@
exports[`renders 1`] = `
<Modal
contentLabel="scanner context"
large={true}
onRequestClose={[MockFunction]}
size="large"
>
<div
className="modal-head"

+ 1
- 1
server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/Stacktrace-test.tsx.snap View File

@@ -3,8 +3,8 @@
exports[`renders 1`] = `
<Modal
contentLabel="stacktrace"
large={true}
onRequestClose={[MockFunction]}
size="large"
>
<div
className="modal-head"

+ 3
- 5
server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx View File

@@ -21,7 +21,7 @@ import * as React from 'react';
import Modal from '../../../components/controls/Modal';
import Select from '../../../components/controls/Select';
import SeverityHelper from '../../../components/shared/SeverityHelper';
import { activateRule, Profile as BaseProfile } from '../../../api/quality-profiles';
import { activateRule, Profile } from '../../../api/quality-profiles';
import { SEVERITIES } from '../../../helpers/constants';
import { translate } from '../../../helpers/l10n';
import { sortProfiles } from '../../quality-profiles/utils';
@@ -34,7 +34,7 @@ interface Props {
onClose: () => void;
onDone: (severity: string) => Promise<void>;
organization: string | undefined;
profiles: BaseProfile[];
profiles: Profile[];
rule: T.Rule | T.RuleDetails;
}

@@ -153,7 +153,7 @@ export default class ActivationFormModal extends React.PureComponent<Props, Stat
const isUpdateMode = !!activation;

return (
<Modal contentLabel={this.props.modalHeader} onRequestClose={this.props.onClose}>
<Modal contentLabel={this.props.modalHeader} onRequestClose={this.props.onClose} size="small">
<form onSubmit={this.handleFormSubmit}>
<div className="modal-head">
<h2>{this.props.modalHeader}</h2>
@@ -206,7 +206,6 @@ export default class ActivationFormModal extends React.PureComponent<Props, Stat
<label title={param.key}>{param.key}</label>
{param.type === 'TEXT' ? (
<textarea
className="width100"
disabled={submitting}
name={param.key}
onChange={this.handleParameterChange}
@@ -216,7 +215,6 @@ export default class ActivationFormModal extends React.PureComponent<Props, Stat
/>
) : (
<input
className="input-super-large"
disabled={submitting}
name={param.key}
onChange={this.handleParameterChange}

+ 6
- 6
server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx View File

@@ -19,13 +19,13 @@
*/
import * as React from 'react';
import { Query, serializeQuery } from '../query';
import { Profile, bulkActivateRules, bulkDeactivateRules } from '../../../api/quality-profiles';
import Modal from '../../../components/controls/Modal';
import Select from '../../../components/controls/Select';
import { Alert } from '../../../components/ui/Alert';
import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons';
import { Profile, bulkActivateRules, bulkDeactivateRules } from '../../../api/quality-profiles';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { formatMeasure } from '../../../helpers/measures';
import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons';
import { Alert } from '../../../components/ui/Alert';

interface Props {
action: string;
@@ -198,7 +198,7 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
: `${translate('coding_rules.deactivate_in_quality_profile')} (${formatMeasure(total, 'INT')} ${translate('coding_rules._rules')})`;

return (
<Modal contentLabel={header} onRequestClose={this.props.onClose}>
<Modal contentLabel={header} onRequestClose={this.props.onClose} size="small">
<form onSubmit={this.handleFormSubmit}>
<header className="modal-head">
<h2>{header}</h2>
@@ -218,11 +218,11 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
</label>
</h3>
{profile ? (
<h3 className="readonly-field">
<span>
{profile.name}
{' — '}
{translate('are_you_sure')}
</h3>
</span>
) : (
this.renderProfileSelect()
)}

+ 139
- 164
server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx View File

@@ -153,76 +153,60 @@ export default class CustomRuleFormModal extends React.PureComponent<Props, Stat
};

renderNameField = () => (
<tr className="property">
<th className="nowrap">
<h3>
{translate('name')} <em className="mandatory">*</em>
</h3>
</th>
<td>
<div className="modal-field">
<label htmlFor="coding-rules-custom-rule-creation-name">
{translate('name')} <em className="mandatory">*</em>
</label>
<input
autoFocus={true}
disabled={this.state.submitting}
id="coding-rules-custom-rule-creation-name"
onChange={this.handleNameChange}
required={true}
type="text"
value={this.state.name}
/>
</div>
);

renderKeyField = () => (
<div className="modal-field">
<label htmlFor="coding-rules-custom-rule-creation-key">
{translate('key')} {!this.props.customRule && <em className="mandatory">*</em>}
</label>

{this.props.customRule ? (
<span className="coding-rules-detail-custom-rule-key" title={this.props.customRule.key}>
{this.props.customRule.key}
</span>
) : (
<input
autoFocus={true}
className="coding-rules-name-key"
disabled={this.state.submitting}
id="coding-rules-custom-rule-creation-name"
onChange={this.handleNameChange}
id="coding-rules-custom-rule-creation-key"
onChange={this.handleKeyChange}
required={true}
type="text"
value={this.state.name}
value={this.state.key}
/>
</td>
</tr>
);

renderKeyField = () => (
<tr className="property">
<th className="nowrap">
<h3>
{translate('key')} {!this.props.customRule && <em className="mandatory">*</em>}
</h3>
</th>
<td>
{this.props.customRule ? (
<span className="coding-rules-detail-custom-rule-key" title={this.props.customRule.key}>
{this.props.customRule.key}
</span>
) : (
<input
className="coding-rules-name-key"
disabled={this.state.submitting}
id="coding-rules-custom-rule-creation-key"
onChange={this.handleKeyChange}
required={true}
type="text"
value={this.state.key}
/>
)}
</td>
</tr>
)}
</div>
);

renderDescriptionField = () => (
<tr className="property">
<th className="nowrap">
<h3>
{translate('description')} <em className="mandatory">*</em>
</h3>
</th>
<td>
<textarea
className="coding-rules-markdown-description"
disabled={this.state.submitting}
id="coding-rules-custom-rule-creation-html-description"
onChange={this.handleDescriptionChange}
required={true}
rows={5}
value={this.state.description}
/>
<span className="text-right">
<MarkdownTips />
</span>
</td>
</tr>
<div className="modal-field">
<label htmlFor="coding-rules-custom-rule-creation-html-description">
{translate('description')} <em className="mandatory">*</em>
</label>
<textarea
disabled={this.state.submitting}
id="coding-rules-custom-rule-creation-html-description"
onChange={this.handleDescriptionChange}
required={true}
rows={5}
value={this.state.description}
/>
<MarkdownTips className="modal-field-descriptor text-right" />
</div>
);

renderTypeOption = ({ value }: { value: T.RuleType }) => {
@@ -230,107 +214,99 @@ export default class CustomRuleFormModal extends React.PureComponent<Props, Stat
};

renderTypeField = () => (
<tr className="property">
<th className="nowrap">
<h3>{translate('type')}</h3>
</th>
<td>
<Select
className="input-medium"
clearable={false}
disabled={this.state.submitting}
onChange={this.handleTypeChange}
optionRenderer={this.renderTypeOption}
options={RULE_TYPES.map(type => ({
label: translate('issue.type', type),
value: type
}))}
searchable={false}
value={this.state.type}
valueRenderer={this.renderTypeOption}
/>
</td>
</tr>
<div className="modal-field flex-1 spacer-right">
<label htmlFor="coding-rules-custom-rule-type">{translate('type')}</label>
<Select
clearable={false}
disabled={this.state.submitting}
id="coding-rules-custom-rule-type"
onChange={this.handleTypeChange}
optionRenderer={this.renderTypeOption}
options={RULE_TYPES.map(type => ({
label: translate('issue.type', type),
value: type
}))}
searchable={false}
value={this.state.type}
valueRenderer={this.renderTypeOption}
/>
</div>
);

renderSeverityOption = ({ value }: { value: string }) => <SeverityHelper severity={value} />;

renderSeverityField = () => (
<tr className="property">
<th className="nowrap">
<h3>{translate('severity')}</h3>
</th>
<td>
<Select
className="input-medium"
clearable={false}
disabled={this.state.submitting}
onChange={this.handleSeverityChange}
optionRenderer={this.renderSeverityOption}
options={SEVERITIES.map(severity => ({
label: translate('severity', severity),
value: severity
}))}
searchable={false}
value={this.state.severity}
valueRenderer={this.renderSeverityOption}
/>
</td>
</tr>
<div className="modal-field flex-1 spacer-right">
<label htmlFor="coding-rules-custom-rule-severity">{translate('severity')}</label>
<Select
clearable={false}
disabled={this.state.submitting}
id="coding-rules-custom-rule-severity"
onChange={this.handleSeverityChange}
optionRenderer={this.renderSeverityOption}
options={SEVERITIES.map(severity => ({
label: translate('severity', severity),
value: severity
}))}
searchable={false}
value={this.state.severity}
valueRenderer={this.renderSeverityOption}
/>
</div>
);

renderStatusField = () => (
<tr className="property">
<th className="nowrap">
<h3>{translate('coding_rules.filters.status')}</h3>
</th>
<td>
<Select
className="input-medium"
clearable={false}
disabled={this.state.submitting}
onChange={this.handleStatusChange}
options={RULE_STATUSES.map(status => ({
label: translate('rules.status', status),
value: status
}))}
searchable={false}
value={this.state.status}
/>
</td>
</tr>
<div className="modal-field flex-1">
<label htmlFor="coding-rules-custom-rule-status">
{translate('coding_rules.filters.status')}
</label>
<Select
clearable={false}
disabled={this.state.submitting}
id="coding-rules-custom-rule-status"
onChange={this.handleStatusChange}
options={RULE_STATUSES.map(status => ({
label: translate('rules.status', status),
value: status
}))}
searchable={false}
value={this.state.status}
/>
</div>
);

renderParameterField = (param: T.RuleParameter) => (
<tr className="property" key={param.key}>
<th className="nowrap">
<h3>{param.key}</h3>
</th>
<td>
{param.type === 'TEXT' ? (
<textarea
className="width100"
disabled={this.state.submitting}
name={param.key}
onChange={this.handleParameterChange}
placeholder={param.defaultValue}
rows={3}
value={this.state.params[param.key] || ''}
/>
) : (
<input
className="input-super-large"
disabled={this.state.submitting}
name={param.key}
onChange={this.handleParameterChange}
placeholder={param.defaultValue}
type="text"
value={this.state.params[param.key] || ''}
/>
)}
<div className="note" dangerouslySetInnerHTML={{ __html: param.htmlDesc || '' }} />
</td>
</tr>
<div className="modal-field" key={param.key}>
<label className="capitalize" htmlFor={param.key}>
{param.key}
</label>

{param.type === 'TEXT' ? (
<textarea
disabled={this.state.submitting}
id={param.key}
name={param.key}
onChange={this.handleParameterChange}
placeholder={param.defaultValue}
rows={3}
value={this.state.params[param.key] || ''}
/>
) : (
<input
disabled={this.state.submitting}
id={param.key}
name={param.key}
onChange={this.handleParameterChange}
placeholder={param.defaultValue}
type="text"
value={this.state.params[param.key] || ''}
/>
)}
<div
className="modal-field-description"
dangerouslySetInnerHTML={{ __html: param.htmlDesc || '' }}
/>
</div>
);

renderSubmitButton = () => {
@@ -371,18 +347,17 @@ export default class CustomRuleFormModal extends React.PureComponent<Props, Stat
{reactivating && (
<Alert variant="warning">{translate('coding_rules.reactivate.help')}</Alert>
)}
<table>
<tbody>
{this.renderNameField()}
{this.renderKeyField()}
{this.renderDescriptionField()}
{/* do not allow to change the type of existing rule */}
{!customRule && this.renderTypeField()}
{this.renderSeverityField()}
{this.renderStatusField()}
{params.map(this.renderParameterField)}
</tbody>
</table>

{this.renderNameField()}
{this.renderKeyField()}
<div className="display-flex-space-between">
{/* do not allow to change the type of existing rule */}
{!customRule && this.renderTypeField()}
{this.renderSeverityField()}
{this.renderStatusField()}
</div>
{this.renderDescriptionField()}
{params.map(this.renderParameterField)}
</div>

<div className="modal-foot">

+ 2
- 2
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx View File

@@ -104,7 +104,7 @@ export default class RuleDetailsCustomRules extends React.PureComponent<Props, S
</td>

<td className="coding-rules-detail-list-severity">
<SeverityHelper severity={rule.severity} />
<SeverityHelper className="display-flex-center" severity={rule.severity} />
</td>

<td className="coding-rules-detail-list-parameters">
@@ -163,7 +163,7 @@ export default class RuleDetailsCustomRules extends React.PureComponent<Props, S
</CustomRuleButton>
)}

<DeferredSpinner loading={loading}>
<DeferredSpinner className="spacer-left" loading={loading}>
{rules.length > 0 && (
<table className="coding-rules-detail-list" id="coding-rules-detail-custom-rules">
<tbody>{sortBy(rules, rule => rule.name).map(this.renderRule)}</tbody>

+ 3
- 3
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx View File

@@ -127,16 +127,16 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S

renderForm = () => (
<div className="coding-rules-detail-extend-description-form">
<table className="width100">
<table className="width-100">
<tbody>
<tr>
<td className="width100" colSpan={2}>
<td colSpan={2}>
<textarea
autoFocus={true}
className="width-100 little-spacer-bottom"
id="coding-rules-detail-extend-description-text"
onChange={this.handleDescriptionChange}
rows={4}
style={{ width: '100%', marginBottom: 4 }}
value={this.state.description}
/>
</td>

+ 1
- 1
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx View File

@@ -280,7 +280,7 @@ export default class RuleDetailsProfiles extends React.PureComponent<Props, Stat

{activations.length > 0 && (
<table
className="coding-rules-detail-quality-profiles width100"
className="coding-rules-detail-quality-profiles width-100"
id="coding-rules-detail-quality-profiles">
<tbody>{activations.map(this.renderActivation)}</tbody>
</table>

+ 38
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/ActivationFormModal-test.tsx View File

@@ -0,0 +1,38 @@
/*
* SonarQube
* Copyright (C) 2009-2019 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import ActivationFormModal from '../ActivationFormModal';
import { mockQualityProfile, mockRule } from '../../../../helpers/testMocks';

it('render correctly', () => {
expect(
shallow(
<ActivationFormModal
modalHeader="title"
onClose={jest.fn()}
onDone={jest.fn()}
organization="foo"
profiles={[mockQualityProfile()]}
rule={mockRule()}
/>
)
).toMatchSnapshot();
});

+ 41
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/BulkChangeModal-test.tsx View File

@@ -0,0 +1,41 @@
/*
* SonarQube
* Copyright (C) 2009-2019 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import BulkChangeModal from '../BulkChangeModal';
import { mockQualityProfile } from '../../../../helpers/testMocks';
import { Query } from '../../query';

it('render correctly', () => {
expect(
shallow(
<BulkChangeModal
action="activate"
languages={{ js: { key: 'js', name: 'JavaScript' } }}
onClose={jest.fn()}
organization="foo"
profile={mockQualityProfile()}
query={{ languages: ['js'] } as Query}
referencedProfiles={{ foo: mockQualityProfile() }}
total={42}
/>
)
).toMatchSnapshot();
});

+ 39
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/CustomRuleFormModal-test.tsx View File

@@ -0,0 +1,39 @@
/*
* SonarQube
* Copyright (C) 2009-2019 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import CustomRuleFormModal from '../CustomRuleFormModal';
import { mockRule } from '../../../../helpers/testMocks';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
});

function shallowRender(props: Partial<CustomRuleFormModal['props']> = {}) {
return shallow(
<CustomRuleFormModal
onClose={jest.fn()}
onDone={jest.fn()}
organization={undefined}
templateRule={{ ...mockRule(), createdAt: 'date', repo: 'squid' }}
{...props}
/>
);
}

+ 3
- 15
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleListItem-test.tsx View File

@@ -20,19 +20,7 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import RuleListItem from '../RuleListItem';
import { mockEvent } from '../../../../helpers/testMocks';

const rule: T.Rule = {
key: 'foo',
lang: 'js',
langName: 'JavaScript',
name: 'Use foo',
severity: 'MAJOR',
status: 'READY',
sysTags: ['a', 'b'],
tags: ['x'],
type: 'CODE_SMELL'
};
import { mockEvent, mockRule } from '../../../../helpers/testMocks';

it('should render', () => {
expect(shallowRender()).toMatchSnapshot();
@@ -42,7 +30,7 @@ it('should open rule', () => {
const onOpen = jest.fn();
const wrapper = shallowRender({ onOpen });
wrapper.find('Link').prop<Function>('onClick')(mockEvent({ button: 0 }));
expect(onOpen).toBeCalledWith('foo');
expect(onOpen).toBeCalledWith('javascript:S1067');
});

function shallowRender(props?: Partial<RuleListItem['props']>) {
@@ -53,7 +41,7 @@ function shallowRender(props?: Partial<RuleListItem['props']>) {
onFilterChange={jest.fn()}
onOpen={jest.fn()}
organization="org"
rule={rule}
rule={mockRule()}
selected={false}
{...props}
/>

+ 101
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/ActivationFormModal-test.tsx.snap View File

@@ -0,0 +1,101 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`render correctly 1`] = `
<Modal
contentLabel="title"
onRequestClose={[MockFunction]}
size="small"
>
<form
onSubmit={[Function]}
>
<div
className="modal-head"
>
<h2>
title
</h2>
</div>
<div
className="modal-body"
>
<Alert
variant="info"
>
coding_rules.active_in_all_profiles
</Alert>
<div
className="modal-field"
>
<label>
coding_rules.quality_profile
</label>
<Select
className="js-profile"
clearable={false}
disabled={false}
onChange={[Function]}
options={Array []}
value=""
/>
</div>
<div
className="modal-field"
>
<label>
severity
</label>
<Select
className="js-severity"
clearable={false}
disabled={false}
onChange={[Function]}
optionRenderer={[Function]}
options={
Array [
Object {
"label": "severity.BLOCKER",
"value": "BLOCKER",
},
Object {
"label": "severity.CRITICAL",
"value": "CRITICAL",
},
Object {
"label": "severity.MAJOR",
"value": "MAJOR",
},
Object {
"label": "severity.MINOR",
"value": "MINOR",
},
Object {
"label": "severity.INFO",
"value": "INFO",
},
]
}
searchable={false}
value="MAJOR"
valueRenderer={[Function]}
/>
</div>
</div>
<footer
className="modal-foot"
>
<SubmitButton
disabled={true}
>
coding_rules.activate
</SubmitButton>
<ResetButtonLink
disabled={false}
onClick={[MockFunction]}
>
cancel
</ResetButtonLink>
</footer>
</form>
</Modal>
`;

+ 56
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap View File

@@ -0,0 +1,56 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`render correctly 1`] = `
<Modal
contentLabel="coding_rules.activate_in_quality_profile (42 coding_rules._rules)"
onRequestClose={[MockFunction]}
size="small"
>
<form
onSubmit={[Function]}
>
<header
className="modal-head"
>
<h2>
coding_rules.activate_in_quality_profile (42 coding_rules._rules)
</h2>
</header>
<div
className="modal-body"
>
<div
className="modal-field"
>
<h3>
<label
htmlFor="coding-rules-bulk-change-profile"
>
coding_rules.activate_in
</label>
</h3>
<span>
name
are_you_sure
</span>
</div>
</div>
<footer
className="modal-foot"
>
<SubmitButton
disabled={false}
id="coding-rules-submit-bulk-change"
>
apply
</SubmitButton>
<ResetButtonLink
onClick={[MockFunction]}
>
cancel
</ResetButtonLink>
</footer>
</form>
</Modal>
`;

+ 237
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/CustomRuleFormModal-test.tsx.snap View File

@@ -0,0 +1,237 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<Modal
contentLabel="coding_rules.create_custom_rule"
onRequestClose={[MockFunction]}
>
<form
onSubmit={[Function]}
>
<div
className="modal-head"
>
<h2>
coding_rules.create_custom_rule
</h2>
</div>
<div
className="modal-body modal-container"
>
<div
className="modal-field"
>
<label
htmlFor="coding-rules-custom-rule-creation-name"
>
name
<em
className="mandatory"
>
*
</em>
</label>
<input
autoFocus={true}
disabled={false}
id="coding-rules-custom-rule-creation-name"
onChange={[Function]}
required={true}
type="text"
value=""
/>
</div>
<div
className="modal-field"
>
<label
htmlFor="coding-rules-custom-rule-creation-key"
>
key
<em
className="mandatory"
>
*
</em>
</label>
<input
disabled={false}
id="coding-rules-custom-rule-creation-key"
onChange={[Function]}
required={true}
type="text"
value=""
/>
</div>
<div
className="display-flex-space-between"
>
<div
className="modal-field flex-1 spacer-right"
>
<label
htmlFor="coding-rules-custom-rule-type"
>
type
</label>
<Select
clearable={false}
disabled={false}
id="coding-rules-custom-rule-type"
onChange={[Function]}
optionRenderer={[Function]}
options={
Array [
Object {
"label": "issue.type.BUG",
"value": "BUG",
},
Object {
"label": "issue.type.VULNERABILITY",
"value": "VULNERABILITY",
},
Object {
"label": "issue.type.CODE_SMELL",
"value": "CODE_SMELL",
},
Object {
"label": "issue.type.SECURITY_HOTSPOT",
"value": "SECURITY_HOTSPOT",
},
Object {
"label": "issue.type.UNKNOWN",
"value": "UNKNOWN",
},
]
}
searchable={false}
value="CODE_SMELL"
valueRenderer={[Function]}
/>
</div>
<div
className="modal-field flex-1 spacer-right"
>
<label
htmlFor="coding-rules-custom-rule-severity"
>
severity
</label>
<Select
clearable={false}
disabled={false}
id="coding-rules-custom-rule-severity"
onChange={[Function]}
optionRenderer={[Function]}
options={
Array [
Object {
"label": "severity.BLOCKER",
"value": "BLOCKER",
},
Object {
"label": "severity.CRITICAL",
"value": "CRITICAL",
},
Object {
"label": "severity.MAJOR",
"value": "MAJOR",
},
Object {
"label": "severity.MINOR",
"value": "MINOR",
},
Object {
"label": "severity.INFO",
"value": "INFO",
},
]
}
searchable={false}
value="MAJOR"
valueRenderer={[Function]}
/>
</div>
<div
className="modal-field flex-1"
>
<label
htmlFor="coding-rules-custom-rule-status"
>
coding_rules.filters.status
</label>
<Select
clearable={false}
disabled={false}
id="coding-rules-custom-rule-status"
onChange={[Function]}
options={
Array [
Object {
"label": "rules.status.READY",
"value": "READY",
},
Object {
"label": "rules.status.BETA",
"value": "BETA",
},
Object {
"label": "rules.status.DEPRECATED",
"value": "DEPRECATED",
},
]
}
searchable={false}
value="READY"
/>
</div>
</div>
<div
className="modal-field"
>
<label
htmlFor="coding-rules-custom-rule-creation-html-description"
>
description
<em
className="mandatory"
>
*
</em>
</label>
<textarea
disabled={false}
id="coding-rules-custom-rule-creation-html-description"
onChange={[Function]}
required={true}
rows={5}
value=""
/>
<MarkdownTips
className="modal-field-descriptor text-right"
/>
</div>
</div>
<div
className="modal-foot"
>
<SubmitButton
disabled={false}
id="coding-rules-custom-rule-creation-create"
>
create
</SubmitButton>
<ResetButtonLink
disabled={false}
id="coding-rules-custom-rule-creation-cancel"
onClick={[MockFunction]}
>
cancel
</ResetButtonLink>
</div>
</form>
</Modal>
`;

+ 4
- 4
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleListItem-test.tsx.snap View File

@@ -3,7 +3,7 @@
exports[`should render 1`] = `
<div
className="coding-rule"
data-rule="foo"
data-rule="javascript:S1067"
>
<table
className="coding-rule-table"
@@ -23,8 +23,8 @@ exports[`should render 1`] = `
Object {
"pathname": "/organizations/org/rules",
"query": Object {
"open": "foo",
"rule_key": "foo",
"open": "javascript:S1067",
"rule_key": "javascript:S1067",
},
}
}
@@ -72,7 +72,7 @@ exports[`should render 1`] = `
onFilterChange={[MockFunction]}
rule={
Object {
"key": "foo",
"key": "javascript:S1067",
"lang": "js",
"langName": "JavaScript",
"name": "Use foo",

+ 0
- 9
server/sonar-web/src/main/js/apps/coding-rules/styles.css View File

@@ -201,15 +201,6 @@
margin-left: 10px;
}

input.coding-rules-name-key {
width: 100%;
}

textarea.coding-rules-markdown-description {
width: 100%;
margin-bottom: 4px;
}

.coding-rules-most-violated-projects td {
border-top-color: transparent;
}

+ 2
- 2
server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationModal.tsx View File

@@ -72,10 +72,10 @@ export default class UpgradeOrganizationModal extends React.PureComponent<Props,
return (
<Modal
contentLabel={header}
medium={true}
noBackdrop={this.props.insideModal}
onRequestClose={this.props.onClose}
shouldCloseOnOverlayClick={false}>
shouldCloseOnOverlayClick={false}
size={'medium'}>
<div className="modal-head">
<h2>{header}</h2>
</div>

+ 1
- 1
server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationModal-test.tsx.snap View File

@@ -3,9 +3,9 @@
exports[`should render correctly 1`] = `
<Modal
contentLabel="billing.upgrade_box.upgrade_to_paid_plan"
medium={true}
onRequestClose={[MockFunction]}
shouldCloseOnOverlayClick={false}
size="medium"
>
<div
className="modal-head"

+ 11
- 3
server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx View File

@@ -77,12 +77,17 @@ export default class AutoOrganizationCreate extends React.PureComponent<Props, S
};

handleCreateOrganization = () => {
const { organization } = this.props;
const { almApplication, almOrganization, organization } = this.props;
if (!organization) {
return Promise.reject();
}
return this.props.createOrganization({
...organization,
alm: {
key: almApplication.key,
membersSync: true,
url: almOrganization.almUrl
},
installationId: this.props.almInstallId
});
};
@@ -169,10 +174,13 @@ export default class AutoOrganizationCreate extends React.PureComponent<Props, S
)}
</p>
<a
href={getAlmMembersUrl(almOrganization.key, almOrganization.almUrl)}
href={getAlmMembersUrl(almApplication.key, almOrganization.almUrl)}
rel="noopener noreferrer"
target="_blank">
{translate('onboarding.import_organization.see_who_has_access')}
{translateWithParameters(
'organization.members.see_all_members_on_x',
translate(almKey)
)}
</a>
</Alert>
}

+ 1
- 1
server/sonar-web/src/main/js/apps/create/organization/RemoteOrganizationChoose.tsx View File

@@ -168,7 +168,7 @@ export class RemoteOrganizationChoose extends React.PureComponent<Props & WithRo
</div>
<form className="big-spacer-top big-spacer-bottom" onSubmit={this.handleSubmit}>
<div className="form-field abs-width-400">
<label htmlFor="select-unbound-installation">
<label className="text-normal" htmlFor="select-unbound-installation">
{translateWithParameters(
'onboarding.import_organization.choose_unbound_installation_x',
translate(sanitizeAlmId(almApplication.key))

+ 17
- 10
server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx View File

@@ -22,7 +22,7 @@ import { shallow } from 'enzyme';
import AutoOrganizationCreate from '../AutoOrganizationCreate';
import { Step } from '../utils';
import { bindAlmOrganization } from '../../../../api/alm-integration';
import { mockAlmOrganization } from '../../../../helpers/testMocks';
import { mockAlmOrganization, mockAlmApplication } from '../../../../helpers/testMocks';
import { waitAndUpdate, click } from '../../../../helpers/testUtils';

jest.mock('../../../../api/alm-integration', () => ({
@@ -34,7 +34,14 @@ const organization = mockAlmOrganization();
it('should render prefilled and create org', async () => {
const createOrganization = jest.fn().mockResolvedValue({ key: 'foo' });
const handleOrgDetailsFinish = jest.fn();
const wrapper = shallowRender({ createOrganization, handleOrgDetailsFinish });
const almApplication = mockAlmApplication({ key: 'github' });
const almOrganization = mockAlmOrganization({ almUrl: 'http://github.com/thing' });
const wrapper = shallowRender({
almApplication,
almOrganization,
createOrganization,
handleOrgDetailsFinish
});

expect(wrapper).toMatchSnapshot();

@@ -44,7 +51,13 @@ it('should render prefilled and create org', async () => {

wrapper.setProps({ organization });
wrapper.find('PlanStep').prop<Function>('createOrganization')();
expect(createOrganization).toBeCalledWith({ ...organization, installationId: 'id-foo' });

const alm = {
key: 'github',
membersSync: true,
url: 'http://github.com/thing'
};
expect(createOrganization).toBeCalledWith({ ...organization, alm, installationId: 'id-foo' });
});

it('should allow to cancel org import', () => {
@@ -86,13 +99,7 @@ it('should bind existing organization', async () => {
function shallowRender(props: Partial<AutoOrganizationCreate['props']> = {}) {
return shallow(
<AutoOrganizationCreate
almApplication={{
backgroundColor: '#0052CC',
iconPath: '"/static/authbitbucket/bitbucket.svg"',
installationUrl: 'https://bitbucket.org/install/app',
key: 'bitbucket',
name: 'BitBucket'
}}
almApplication={mockAlmApplication()}
almInstallId="id-foo"
almOrganization={{ ...organization, personal: false }}
createOrganization={jest.fn()}

+ 7
- 7
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap View File

@@ -121,7 +121,7 @@ exports[`should render prefilled and create org 1`] = `
"avatar": <img
alt="BitBucket"
className="little-spacer-left"
src="/images/sonarcloud/bitbucket.svg"
src="/images/sonarcloud/github.svg"
width={16}
/>,
"name": <strong>
@@ -144,21 +144,21 @@ exports[`should render prefilled and create org 1`] = `
variant="info"
>
<p>
onboarding.import_organization.members_sync_info_x.organization.bitbucket.foo.bitbucket
onboarding.import_organization.members_sync_info_x.organization.github.foo.github
</p>
<a
href="https://github.com/foo/profile/members"
href="http://github.com/orgs/thing/people"
rel="noopener noreferrer"
target="_blank"
>
onboarding.import_organization.see_who_has_access
organization.members.see_all_members_on_x.github
</a>
</Alert>
}
onContinue={[MockFunction]}
organization={
Object {
"almUrl": "https://github.com/foo",
"almUrl": "http://github.com/thing",
"avatar": "http://example.com/avatar",
"description": "description-foo",
"key": "foo",
@@ -178,13 +178,13 @@ exports[`should render prefilled and create org 1`] = `
"backgroundColor": "#0052CC",
"iconPath": "\\"/static/authbitbucket/bitbucket.svg\\"",
"installationUrl": "https://bitbucket.org/install/app",
"key": "bitbucket",
"key": "github",
"name": "BitBucket",
}
}
almOrganization={
Object {
"almUrl": "https://github.com/foo",
"almUrl": "http://github.com/thing",
"avatar": "http://example.com/avatar",
"description": "description-foo",
"key": "foo",

+ 1
- 0
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/RemoteOrganizationChoose-test.tsx.snap View File

@@ -124,6 +124,7 @@ exports[`should display unbound installations 1`] = `
className="form-field abs-width-400"
>
<label
className="text-normal"
htmlFor="select-unbound-installation"
>
onboarding.import_organization.choose_unbound_installation_x.github

+ 6
- 2
server/sonar-web/src/main/js/apps/create/organization/__tests__/actions-test.ts View File

@@ -51,9 +51,13 @@ describe('#createOrganization', () => {
});

it('should create and sync members', async () => {
const org = mockOrganizationWithAlm({}, { membersSync: true });
const { alm, ...org } = mockOrganizationWithAlm(
{},
{ key: 'github', membersSync: true, url: 'https://github.com/foo' }
);

(createOrganization as jest.Mock).mockResolvedValueOnce(org);
const promise = actions.createOrganization(org)(dispatch);
const promise = actions.createOrganization({ alm, ...org })(dispatch);

expect(createOrganization).toHaveBeenCalledWith(org);
await promise;

+ 10
- 6
server/sonar-web/src/main/js/apps/create/organization/actions.ts View File

@@ -21,17 +21,21 @@ import { Dispatch } from 'redux';
import { bindAlmOrganization } from '../../../api/alm-integration';
import * as api from '../../../api/organizations';
import * as actions from '../../../store/organizations';
import { isGithub } from '../../../helpers/almIntegrations';

export function createOrganization(organization: T.Organization & { installationId?: string }) {
export function createOrganization({
alm,
...organization
}: T.Organization & { installationId?: string }) {
return (dispatch: Dispatch) => {
return api
.createOrganization({ ...organization, name: organization.name || organization.key })
.then((organization: T.Organization) => {
dispatch(actions.createOrganization(organization));
if (organization.alm && organization.alm.membersSync) {
api.syncMembers(organization.key);
.then((newOrganization: T.Organization) => {
dispatch(actions.createOrganization({ ...newOrganization, alm }));
if (alm && alm.membersSync && isGithub(alm.key)) {
api.syncMembers(newOrganization.key);
}
return organization.key;
return newOrganization.key;
});
};
}

+ 4
- 3
server/sonar-web/src/main/js/apps/custom-measures/components/Form.tsx View File

@@ -18,13 +18,13 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { getAllMetrics } from '../../../api/metrics';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import Select from '../../../components/controls/Select';
import SimpleModal from '../../../components/controls/SimpleModal';
import { Alert } from '../../../components/ui/Alert';
import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons';
import { getAllMetrics } from '../../../api/metrics';
import { translate } from '../../../helpers/l10n';
import { Alert } from '../../../components/ui/Alert';

interface Props {
confirmButtonText: string;
@@ -145,7 +145,8 @@ export default class Form extends React.PureComponent<Props, State> {
<SimpleModal
header={this.props.header}
onClose={this.props.onClose}
onSubmit={this.handleSubmit}>
onSubmit={this.handleSubmit}
size="small">
{({ onCloseClick, onFormSubmit, submitting }) => (
<form onSubmit={onFormSubmit}>
<header className="modal-head">

+ 2
- 0
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Form-test.tsx.snap View File

@@ -4,6 +4,7 @@ exports[`should render form 1`] = `
<Modal
contentLabel="header"
onRequestClose={[MockFunction]}
size="small"
>
<form
onSubmit={[Function]}
@@ -105,6 +106,7 @@ exports[`should render form 2`] = `
<Modal
contentLabel="header"
onRequestClose={[MockFunction]}
size="small"
>
<form
onSubmit={[Function]}

+ 3
- 2
server/sonar-web/src/main/js/apps/custom-metrics/components/Form.tsx View File

@@ -20,9 +20,9 @@
import * as React from 'react';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import SimpleModal from '../../../components/controls/SimpleModal';
import { translate } from '../../../helpers/l10n';
import Select, { Creatable } from '../../../components/controls/Select';
import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons';
import { translate } from '../../../helpers/l10n';

export interface MetricProps {
description: string;
@@ -98,7 +98,8 @@ export default class Form extends React.PureComponent<Props, State> {
<SimpleModal
header={this.props.header}
onClose={this.props.onClose}
onSubmit={this.handleSubmit}>
onSubmit={this.handleSubmit}
size="small">
{({ onCloseClick, onFormSubmit, submitting }) => (
<form onSubmit={onFormSubmit}>
<header className="modal-head">

+ 1
- 0
server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/__snapshots__/Form-test.tsx.snap View File

@@ -4,6 +4,7 @@ exports[`should render form 1`] = `
<Modal
contentLabel="header"
onRequestClose={[MockFunction]}
size="small"
>
<form
onSubmit={[Function]}

+ 2
- 1
server/sonar-web/src/main/js/apps/groups/components/Form.tsx View File

@@ -64,7 +64,8 @@ export default class Form extends React.PureComponent<Props, State> {
<SimpleModal
header={this.props.header}
onClose={this.props.onClose}
onSubmit={this.handleSubmit}>
onSubmit={this.handleSubmit}
size="small">
{({ onCloseClick, onFormSubmit, submitting }) => (
<form onSubmit={onFormSubmit}>
<header className="modal-head">

+ 1
- 0
server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/Form-test.tsx.snap View File

@@ -4,6 +4,7 @@ exports[`should render form 1`] = `
<Modal
contentLabel="header"
onRequestClose={[MockFunction]}
size="small"
>
<form
onSubmit={[Function]}

+ 44
- 51
server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx View File

@@ -18,23 +18,25 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import * as classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import { pickBy, sortBy } from 'lodash';
import { searchAssignees } from '../utils';
import { searchIssueTags, bulkChangeIssues } from '../../../api/issues';
import throwGlobalError from '../../../app/utils/throwGlobalError';
import MarkdownTips from '../../../components/common/MarkdownTips';
import SearchSelect from '../../../components/controls/SearchSelect';
import Avatar from '../../../components/ui/Avatar';
import Checkbox from '../../../components/controls/Checkbox';
import HelpTooltip from '../../../components/controls/HelpTooltip';
import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
import MarkdownTips from '../../../components/common/MarkdownTips';
import Modal from '../../../components/controls/Modal';
import Radio from '../../../components/controls/Radio';
import SearchSelect from '../../../components/controls/SearchSelect';
import Select from '../../../components/controls/Select';
import HelpTooltip from '../../../components/controls/HelpTooltip';
import SeverityHelper from '../../../components/shared/SeverityHelper';
import Avatar from '../../../components/ui/Avatar';
import throwGlobalError from '../../../app/utils/throwGlobalError';
import { Alert } from '../../../components/ui/Alert';
import { searchIssueTags, bulkChangeIssues } from '../../../api/issues';
import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons';
import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Alert } from '../../../components/ui/Alert';
import { isLoggedIn } from '../../../helpers/users';

interface AssigneeOption {
@@ -188,10 +190,12 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
}
};

handleFieldChange = (field: 'comment' | 'transition') => (
event: React.SyntheticEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
this.setState<keyof FormFields>({ [field]: event.currentTarget.value });
handleRadioTransitionChange = (transition: string) => {
this.setState({ transition });
};

handleCommentChange = (event: React.SyntheticEvent<HTMLTextAreaElement>) => {
this.setState({ comment: event.currentTarget.value });
};

handleSelectFieldChange = (field: 'severity' | 'type') => (data: { value: string } | null) => {
@@ -269,14 +273,6 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
</div>
);

renderCheckbox = (field: keyof FormFields, id?: string) => (
<Checkbox
checked={this.state[field] !== undefined}
id={id}
onCheck={this.handleFieldCheck(field)}
/>
);

renderAffected = (affected: number) => (
<div className="pull-right note">
({translateWithParameters('issue_bulk_change.x_issues', affected)})
@@ -316,6 +312,7 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {

const input = (
<AssigneeSelect
className="input-super-large"
clearable={true}
defaultOptions={this.getDefaultAssignee()}
onSearch={this.handleAssigneeSearch}
@@ -348,6 +345,7 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {

const input = (
<Select
className="input-super-large"
clearable={true}
onChange={this.handleSelectFieldChange('type')}
optionRenderer={optionRenderer}
@@ -376,6 +374,7 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {

const input = (
<Select
className="input-super-large"
clearable={true}
onChange={this.handleSelectFieldChange('severity')}
optionRenderer={(option: { value: string }) => <SeverityHelper severity={option.value} />}
@@ -404,6 +403,7 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
const input = (
<TagSelect
canCreate={allowCreate}
className="input-super-large"
clearable={true}
defaultOptions={this.state.initialTags}
minimumQueryLength={0}
@@ -431,22 +431,16 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
<div className="modal-field">
<label>{translate('issue.transition')}</label>
{transitions.map(transition => (
<span className="clearfix" key={transition.transition}>
<input
<span
className="bulk-change-radio-button display-flex-center display-flex-space-between"
key={transition.transition}>
<Radio
checked={this.state.transition === transition.transition}
id={`transition-${transition.transition}`}
name="do_transition.transition"
onChange={this.handleFieldChange('transition')}
type="radio"
value={transition.transition}
/>
<label
htmlFor={`transition-${transition.transition}`}
style={{ float: 'none', display: 'inline', left: 0, cursor: 'pointer' }}>
onCheck={this.handleRadioTransitionChange}
value={transition.transition}>
{translate('issue.transition', transition.transition)}
</label>
</Radio>
{this.renderAffected(transition.count)}
<br />
</span>
))}
</div>
@@ -469,27 +463,26 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
overlay={translate('issue_bulk_change.comment.help')}
/>
</label>
<div>
<textarea
id="comment"
onChange={this.handleFieldChange('comment')}
rows={4}
style={{ width: '100%' }}
value={this.state.comment || ''}
/>
</div>
<div className="pull-right">
<MarkdownTips />
</div>
<textarea
id="comment"
onChange={this.handleCommentChange}
rows={4}
value={this.state.comment || ''}
/>
<MarkdownTips className="modal-field-descriptor text-right" />
</div>
);
};

renderNotificationsField = () => (
<div className="modal-field">
<label htmlFor="send-notifications">{translate('issue.send_notifications')}</label>
{this.renderCheckbox('notifications', 'send-notifications')}
</div>
<Checkbox
checked={this.state.notifications !== undefined}
className="display-inline-block spacer-top"
id="send-notifications"
onCheck={this.handleFieldCheck('notifications')}
right={true}>
<strong className="little-spacer-right">{translate('issue.send_notifications')}</strong>
</Checkbox>
);

renderForm = () => {
@@ -503,7 +496,7 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
<h2>{translateWithParameters('issue_bulk_change.form.title', issues.length)}</h2>
</div>

<div className="modal-body">
<div className={classNames('modal-body', { 'modal-container': limitReached })}>
{limitReached && (
<Alert variant="warning">
<FormattedMessage
@@ -540,7 +533,7 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {

render() {
return (
<Modal contentLabel="modal" onRequestClose={this.props.onClose}>
<Modal contentLabel="modal" onRequestClose={this.props.onClose} size={'small'}>
{this.state.loading ? this.renderLoading() : this.renderForm()}
</Modal>
);

+ 13
- 12
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap View File

@@ -4,6 +4,7 @@ exports[`should display error message when no issues available 1`] = `
<Modal
contentLabel="modal"
onRequestClose={[Function]}
size="small"
>
<form
id="bulk-change-form"
@@ -48,6 +49,7 @@ exports[`should display form when issues are present 1`] = `
<Modal
contentLabel="modal"
onRequestClose={[Function]}
size="small"
>
<form
id="bulk-change-form"
@@ -63,21 +65,20 @@ exports[`should display form when issues are present 1`] = `
<div
className="modal-body"
>
<div
className="modal-field"
<Checkbox
checked={false}
className="display-inline-block spacer-top"
id="send-notifications"
onCheck={[Function]}
right={true}
thirdState={false}
>
<label
htmlFor="send-notifications"
<strong
className="little-spacer-right"
>
issue.send_notifications
</label>
<Checkbox
checked={false}
id="send-notifications"
onCheck={[Function]}
thirdState={false}
/>
</div>
</strong>
</Checkbox>
</div>
<div
className="modal-foot"

+ 9
- 0
server/sonar-web/src/main/js/apps/issues/styles.css View File

@@ -270,3 +270,12 @@
width: auto;
margin-right: 4px;
}

.bulk-change-radio-button {
margin: 0 calc(- var(--gridSize) / 2);
padding: 0 calc(var(--gridSize) / 2);
}

.bulk-change-radio-button:hover {
background-color: var(--barBackgroundColor);
}

+ 1
- 1
server/sonar-web/src/main/js/apps/organizationMembers/AddMemberForm.tsx View File

@@ -77,7 +77,7 @@ export default class AddMemberForm extends React.PureComponent<Props, State> {
</header>
<form onSubmit={this.handleSubmit}>
<div className="modal-body">
<div className="modal-large-field">
<div className="modal-field">
<label>{translate('users.search_description')}</label>
<UsersSelectSearch
autoFocus={true}

+ 60
- 46
server/sonar-web/src/main/js/apps/organizationMembers/ManageMemberGroupsForm.tsx View File

@@ -18,19 +18,24 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { keyBy, pickBy } from 'lodash';
import { getUserGroups, UserGroup } from '../../api/users';
import Modal from '../../components/controls/Modal';
import { translate, translateWithParameters } from '../../helpers/l10n';
import { keyBy, pickBy, some } from 'lodash';
import OrganizationGroupCheckbox from '../organizations/components/OrganizationGroupCheckbox';
import SimpleModal from '../../components/controls/SimpleModal';
import { SubmitButton, ResetButtonLink } from '../../components/ui/buttons';
import { getUserGroups, UserGroup } from '../../api/users';
import { translate, translateWithParameters } from '../../helpers/l10n';
import DeferredSpinner from '../../components/common/DeferredSpinner';

interface Props {
onClose: () => void;
member: T.OrganizationMember;
organization: T.Organization;
organizationGroups: T.Group[];
updateMemberGroups: (member: T.OrganizationMember, add: string[], remove: string[]) => void;
updateMemberGroups: (
member: T.OrganizationMember,
add: string[],
remove: string[]
) => Promise<void>;
}

interface State {
@@ -81,7 +86,7 @@ export default class ManageMemberGroupsForm extends React.PureComponent<Props, S

onCheck = (groupName: string, checked: boolean) => {
this.setState((prevState: State) => {
const userGroups = prevState.userGroups || {};
const { userGroups = {} } = prevState;
const group = userGroups[groupName] || {};
let status = '';
if (group.selected && !checked) {
@@ -93,53 +98,62 @@ export default class ManageMemberGroupsForm extends React.PureComponent<Props, S
});
};

handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
this.props.updateMemberGroups(
this.props.member,
Object.keys(pickBy(this.state.userGroups, group => group.status === 'add')),
Object.keys(pickBy(this.state.userGroups, group => group.status === 'remove'))
);
this.props.onClose();
handleSubmit = () => {
return this.props
.updateMemberGroups(
this.props.member,
Object.keys(pickBy(this.state.userGroups, group => group.status === 'add')),
Object.keys(pickBy(this.state.userGroups, group => group.status === 'remove'))
)
.then(this.props.onClose);
};

render() {
const { loading, userGroups = {} } = this.state;
const header = translate('organization.members.manage_groups');
const hasChanges = some(userGroups, group => group.status !== undefined);
return (
<Modal contentLabel={header} onRequestClose={this.props.onClose}>
<header className="modal-head">
<h2>{header}</h2>
</header>
<form onSubmit={this.handleSubmit}>
<div className="modal-body modal-container">
<strong>
{translateWithParameters(
'organization.members.members_groups',
this.props.member.name
<SimpleModal header={header} onClose={this.props.onClose} onSubmit={this.handleSubmit}>
{({ onCloseClick, onFormSubmit, submitting }) => (
<form onSubmit={onFormSubmit}>
<header className="modal-head">
<h2>{header}</h2>
</header>
<div className="modal-body modal-container">
<p>
<strong>
{translateWithParameters(
'organization.members.members_groups',
this.props.member.name
)}
</strong>
</p>
{loading ? (
<DeferredSpinner className="spacer-top" />
) : (
<ul className="list-spaced">
{this.props.organizationGroups.map(group => (
<OrganizationGroupCheckbox
checked={this.isGroupSelected(group.name)}
group={group}
key={group.id}
onCheck={this.onCheck}
/>
))}
</ul>
)}
</strong>{' '}
{this.state.loading && <i className="spinner" />}
{!this.state.loading && (
<ul className="list-spaced">
{this.props.organizationGroups.map(group => (
<OrganizationGroupCheckbox
checked={this.isGroupSelected(group.name)}
group={group}
key={group.id}
onCheck={this.onCheck}
/>
))}
</ul>
)}
</div>
<footer className="modal-foot">
<div>
<SubmitButton>{translate('save')}</SubmitButton>
<ResetButtonLink onClick={this.props.onClose}>{translate('cancel')}</ResetButtonLink>
</div>
</footer>
</form>
</Modal>

<footer className="modal-foot">
<DeferredSpinner className="spacer-right" loading={submitting} />
<SubmitButton disabled={submitting || !hasChanges}>{translate('save')}</SubmitButton>
<ResetButtonLink disabled={submitting} onClick={onCloseClick}>
{translate('cancel')}
</ResetButtonLink>
</footer>
</form>
)}
</SimpleModal>
);
}
}

+ 7
- 4
server/sonar-web/src/main/js/apps/organizationMembers/MembersList.tsx View File

@@ -23,20 +23,21 @@ import MembersListItem from './MembersListItem';
import { translate } from '../../helpers/l10n';

interface Props {
currentUser: T.LoggedInUser;
members: T.OrganizationMember[];
organizationGroups: T.Group[];
organization: T.Organization;
removeMember: (member: T.OrganizationMember) => void;
removeMember?: (member: T.OrganizationMember) => void;
updateMemberGroups: (
member: T.OrganizationMember,
add: Array<string>,
remove: Array<string>
) => void;
) => Promise<void>;
}

export default class MembersList extends React.PureComponent<Props> {
render() {
const { members } = this.props;
const { currentUser, members } = this.props;

if (!members.length) {
return <div className="note">{translate('no_results')}</div>;
@@ -53,7 +54,9 @@ export default class MembersList extends React.PureComponent<Props> {
member={member}
organization={this.props.organization}
organizationGroups={this.props.organizationGroups}
removeMember={this.props.removeMember}
removeMember={
currentUser.login !== member.login ? this.props.removeMember : undefined
}
updateMemberGroups={this.props.updateMemberGroups}
/>
))}

+ 1
- 1
server/sonar-web/src/main/js/apps/organizationMembers/MembersListHeader.tsx View File

@@ -52,7 +52,7 @@ export default function MembersListHeader({
<HelpTooltip
className="spacer-left"
overlay={
<div className="abs-width-300 markdown cut-margins">
<div className="abs-width-300 markdown cut-margins">
<p>
{translate(
'organization.members.auto_sync_total_help',

+ 26
- 17
server/sonar-web/src/main/js/apps/organizationMembers/MembersListItem.tsx View File

@@ -32,8 +32,12 @@ interface Props {
member: T.OrganizationMember;
organization: T.Organization;
organizationGroups: T.Group[];
removeMember: (member: T.OrganizationMember) => void;
updateMemberGroups: (member: T.OrganizationMember, add: string[], remove: string[]) => void;
removeMember?: (member: T.OrganizationMember) => void;
updateMemberGroups: (
member: T.OrganizationMember,
add: string[],
remove: string[]
) => Promise<void>;
}

interface State {
@@ -76,7 +80,7 @@ export default class MembersListItem extends React.PureComponent<Props, State> {
};

render() {
const { member, organization } = this.props;
const { member, organization, removeMember } = this.props;
const { actions = {} } = organization;
return (
<tr>
@@ -96,16 +100,20 @@ export default class MembersListItem extends React.PureComponent<Props, State> {
</td>
)}
{actions.admin && (
<React.Fragment>
<>
<td className="nowrap text-middle text-right">
<ActionsDropdown>
<ActionsDropdownItem onClick={this.handleManageGroupsClick}>
{translate('organization.members.manage_groups')}
</ActionsDropdownItem>
<ActionsDropdownDivider />
<ActionsDropdownItem destructive={true} onClick={this.handleRemoveMemberClick}>
{translate('organization.members.remove')}
</ActionsDropdownItem>
{removeMember && (
<>
<ActionsDropdownDivider />
<ActionsDropdownItem destructive={true} onClick={this.handleRemoveMemberClick}>
{translate('organization.members.remove')}
</ActionsDropdownItem>
</>
)}
</ActionsDropdown>
</td>

@@ -119,15 +127,16 @@ export default class MembersListItem extends React.PureComponent<Props, State> {
/>
)}

{this.state.removeMemberForm && (
<RemoveMemberForm
member={this.props.member}
onClose={this.closeRemoveMemberForm}
organization={this.props.organization}
removeMember={this.props.removeMember}
/>
)}
</React.Fragment>
{removeMember &&
this.state.removeMemberForm && (
<RemoveMemberForm
member={this.props.member}
onClose={this.closeRemoveMemberForm}
organization={this.props.organization}
removeMember={removeMember}
/>
)}
</>
)}
</tr>
);

+ 59
- 98
server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx View File

@@ -18,121 +18,82 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import AddMemberForm from './AddMemberForm';
import SyncMemberForm from './SyncMemberForm';
import DeferredSpinner from '../../components/common/DeferredSpinner';
import DocTooltip from '../../components/docs/DocTooltip';
import NewInfoBox from '../../components/ui/NewInfoBox';
import { sanitizeAlmId } from '../../helpers/almIntegrations';
import { translate, translateWithParameters } from '../../helpers/l10n';
import { getCurrentUserSetting, Store } from '../../store/rootReducer';
import { setCurrentUserSetting } from '../../store/users';
import { Alert } from '../../components/ui/Alert';

interface Props {
dismissSyncNotifOrg: string[];
export interface Props {
handleAddMember: (member: T.OrganizationMember) => void;
loading: boolean;
members?: T.OrganizationMember[];
organization: T.Organization;
refreshMembers: () => Promise<void>;
setCurrentUserSetting: (setting: T.CurrentUserSetting) => void;
}

export class MembersPageHeader extends React.PureComponent<Props> {
handleDismissSyncNotif = () => {
const { dismissSyncNotifOrg, organization } = this.props;
this.props.setCurrentUserSetting({
key: 'organizations.members.dismissSyncNotif',
value: [...dismissSyncNotifOrg, organization.key].join(',')
});
};
export default function MembersPageHeader(props: Props) {
const { members, organization, refreshMembers } = props;
const memberLogins = members ? members.map(member => member.login) : [];
const isAdmin = organization.actions && organization.actions.admin;
const almKey = organization.alm && sanitizeAlmId(organization.alm.key);
const hasMemberSync = organization.alm && organization.alm.membersSync;
const showSyncNotif = isAdmin && organization.alm && !hasMemberSync;

render() {
const { dismissSyncNotifOrg, members, organization, refreshMembers } = this.props;
const memberLogins = members ? members.map(member => member.login) : [];
const isAdmin = organization.actions && organization.actions.admin;
const almKey = organization.alm && sanitizeAlmId(organization.alm.key);
const hasMemberSync = organization.alm && organization.alm.membersSync;
const showSyncNotif =
isAdmin &&
organization.alm &&
!hasMemberSync &&
!dismissSyncNotifOrg.some(orgKey => orgKey === organization.key);

return (
<header className="page-header">
<h1 className="page-title">{translate('organization.members.page')}</h1>
<DeferredSpinner loading={this.props.loading} />
{isAdmin && (
<div className="page-actions text-right">
{almKey &&
!showSyncNotif && (
<SyncMemberForm organization={organization} refreshMembers={refreshMembers} />
)}
{!hasMemberSync && (
<div className="display-inline-block spacer-left spacer-bottom">
<AddMemberForm
addMember={this.props.handleAddMember}
memberLogins={memberLogins}
organization={organization}
/>
<DocTooltip
className="spacer-left"
doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/organizations/add-organization-member.md')}
/>
</div>
return (
<header className="page-header">
<h1 className="page-title">{translate('organization.members.page')}</h1>
<DeferredSpinner loading={props.loading} />
{isAdmin && (
<div className="page-actions text-right">
{almKey &&
!showSyncNotif && (
<SyncMemberForm organization={organization} refreshMembers={refreshMembers} />
)}
{almKey &&
showSyncNotif && (
<NewInfoBox
description={translateWithParameters(
'organization.members.auto_sync_members_from_org_x',
translate(almKey)
)}
onClose={this.handleDismissSyncNotif}
title={translateWithParameters(
'organization.members.auto_sync_with_x',
translate(almKey)
)}>
<SyncMemberForm
dismissSyncNotif={this.handleDismissSyncNotif}
organization={organization}
refreshMembers={refreshMembers}
/>
</NewInfoBox>
)}
</div>
)}
<div className="page-description">
<FormattedMessage
defaultMessage={translate('organization.members.page.description')}
id="organization.members.page.description"
values={{
link: (
<Link to="/documentation/organizations/manage-team/">
{translate('organization.members.manage_a_team')}
</Link>
)
}}
/>
{!hasMemberSync && (
<div className="display-inline-block spacer-left spacer-bottom">
<AddMemberForm
addMember={props.handleAddMember}
memberLogins={memberLogins}
organization={organization}
/>
<DocTooltip
className="spacer-left"
doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/organizations/add-organization-member.md')}
/>
</div>
)}
</div>
</header>
);
}
)}
<div className="page-description">
<FormattedMessage
defaultMessage={translate('organization.members.page.description')}
id="organization.members.page.description"
values={{
link: (
<Link target="_blank" to="/documentation/organizations/manage-team/">
{translate('organization.members.manage_a_team')}
</Link>
)
}}
/>
{almKey &&
showSyncNotif && (
<Alert className="spacer-top" display="inline" variant="info">
{translateWithParameters(
'organization.members.auto_sync_members_from_org_x',
translate('organization', almKey)
)}
<span className="spacer-left">
<SyncMemberForm organization={organization} refreshMembers={refreshMembers} />
</span>
</Alert>
)}
</div>
</header>
);
}

const mapStateToProps = (state: Store) => ({
dismissSyncNotifOrg: (
getCurrentUserSetting(state, 'organizations.members.dismissSyncNotif') || ''
).split(',')
});

const mapDispatchToProps = { setCurrentUserSetting };

export default connect(
mapStateToProps,
mapDispatchToProps
)(MembersPageHeader);

+ 13
- 14
server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx View File

@@ -182,22 +182,20 @@ export default class OrganizationMembers extends React.PureComponent<Props, Stat
removeUserFromGroup({ name, login, organization: this.props.organization.key })
)
];
Promise.all(promises).then(
() => {
if (this.mounted) {
this.updateGroup(login, member => ({
...member,
groupCount: (member.groupCount || 0) + add.length - remove.length
}));
}
},
() => {}
);
return Promise.all(promises).then(() => {
if (this.mounted) {
this.updateGroup(login, member => ({
...member,
groupCount: (member.groupCount || 0) + add.length - remove.length
}));
}
});
};

render() {
const { organization } = this.props;
const { currentUser, organization } = this.props;
const { groups, loading, members, paging } = this.state;
const hasMemberSync = organization.alm && organization.alm.membersSync;
return (
<div className="page page-limited">
<Helmet title={translate('organization.members.page')} />
@@ -213,16 +211,17 @@ export default class OrganizationMembers extends React.PureComponent<Props, Stat
paging !== undefined && (
<>
<MembersListHeader
currentUser={this.props.currentUser}
currentUser={currentUser}
handleSearch={this.handleSearchMembers}
organization={organization}
total={paging.total}
/>
<MembersList
currentUser={currentUser}
members={members}
organization={organization}
organizationGroups={groups}
removeMember={this.handleRemoveMember}
removeMember={hasMemberSync ? undefined : this.handleRemoveMember}
updateMemberGroups={this.updateMemberGroups}
/>
{paging.total !== 0 && (

+ 53
- 68
server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx View File

@@ -30,7 +30,6 @@ import { translate, translateWithParameters } from '../../helpers/l10n';
import { fetchOrganization } from '../../store/rootActions';

interface Props {
dismissSyncNotif?: () => void;
fetchOrganization: (key: string) => void;
organization: T.Organization;
refreshMembers: () => Promise<void>;
@@ -49,13 +48,9 @@ export class SyncMemberForm extends React.PureComponent<Props, State> {
}

handleConfirm = () => {
const { dismissSyncNotif, organization } = this.props;
const { organization } = this.props;
const { membersSync } = this.state;

if (dismissSyncNotif) {
dismissSyncNotif();
}

return setOrganizationMemberSync({
organization: organization.key,
enabled: membersSync
@@ -99,63 +94,55 @@ export class SyncMemberForm extends React.PureComponent<Props, State> {
const { organization } = this.props;
const almKey = organization.alm && sanitizeAlmId(organization.alm.key);
return (
<>
<div className="display-flex-stretch big-spacer-top">
<RadioCard
onClick={this.handleManualClick}
selected={!membersSync}
title={translate('organization.members.management.manual')}>
<div className="spacer-left">
<ul className="big-spacer-left note">
<li className="spacer-bottom">
{translate('organization.members.management.manual.add_members_manually')}
</li>
<li>
{translate('organization.members.management.manual.choose_members_permissions')}
</li>
</ul>
</div>
</RadioCard>
<RadioCard
onClick={this.handleAutoClick}
selected={membersSync}
title={translateWithParameters(
'organization.members.management.automatic',
translate(almKey || '')
)}>
<div className="spacer-left">
<ul className="big-spacer-left note">
{almKey && (
<>
<li className="spacer-bottom">
{translateWithParameters(
'organization.members.management.automatic.synchronized_from_x',
translate(almKey)
)}
</li>
<li className="spacer-bottom">
{translate(
'organization.members.management.automatic.members_changes_reflected',
almKey
)}
</li>
</>
)}
<li>
{translate(
'organization.members.management.automatic.still_choose_members_permissions'
)}
</li>
</ul>
</div>
{(!organization.alm || !organization.alm.membersSync) && (
<Alert className="big-spacer-top" variant="warning">
{translate('organization.members.management.automatic.warning')}
</Alert>
)}
</RadioCard>
</div>
</>
<div className="display-flex-stretch big-spacer-top">
<RadioCard
onClick={this.handleManualClick}
selected={!membersSync}
title={translate('organization.members.management.manual')}>
<div className="spacer-left">
<ul className="big-spacer-left note">
<li className="spacer-bottom">
{translate('organization.members.management.manual.add_members_manually')}
</li>
<li>{translate('organization.members.management.choose_members_permissions')}</li>
</ul>
</div>
</RadioCard>
<RadioCard
onClick={this.handleAutoClick}
selected={membersSync}
title={translateWithParameters(
'organization.members.management.automatic',
translate(almKey || '')
)}>
<div className="spacer-left">
<ul className="big-spacer-left note">
{almKey && (
<>
<li className="spacer-bottom">
{translateWithParameters(
'organization.members.management.automatic.synchronized_from_x',
translate('organization', almKey)
)}
</li>
<li className="spacer-bottom">
{translate(
'organization.members.management.automatic.members_changes_reflected',
almKey
)}
</li>
</>
)}
<li>{translate('organization.members.management.choose_members_permissions')}</li>
</ul>
</div>
{(!organization.alm || !organization.alm.membersSync) && (
<Alert className="big-spacer-top" variant="warning">
{translate('organization.members.management.automatic.warning')}
</Alert>
)}
</RadioCard>
</div>
);
};

@@ -167,14 +154,12 @@ export class SyncMemberForm extends React.PureComponent<Props, State> {
cancelButtonText={translate('close')}
confirmButtonText={translate('save')}
confirmDisable={this.state.membersSync === orgMemberSync}
medium={true}
modalBody={this.renderModalBody()}
modalHeader={translate('organization.members.management.title')}
modalHeaderDescription={this.renderModalDescription()}
onConfirm={this.handleConfirm}>
{({ onClick }) => (
<Button onClick={onClick}>{translate('organization.members.config_synchro')}</Button>
)}
onConfirm={this.handleConfirm}
size={'medium'}>
{({ onClick }) => <Button onClick={onClick}>{translate('configure')}</Button>}
</ConfirmButton>
);
}

+ 20
- 16
server/sonar-web/src/main/js/apps/organizationMembers/__tests__/ManageMemberGroupsForm-test.tsx View File

@@ -20,7 +20,6 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import ManageMemberGroupsForm from '../ManageMemberGroupsForm';
import { mockEvent } from '../../../helpers/testMocks';

const member = { login: 'admin', name: 'Admin Istrator', avatar: '', groupCount: 3 };
const organization = { name: 'MyOrg', key: 'myorg' };
@@ -45,11 +44,17 @@ const organizationGroups = [
}
];
const userGroups = {
11: { id: 11, name: 'pull-request-analysers', description: 'Technical accounts', selected: true }
'11': {
default: false,
id: 11,
name: 'pull-request-analysers',
description: 'Technical accounts',
selected: true
}
};

function getMountedForm(updateFunc = jest.fn()) {
const wrapper = shallow(
function getMountedForm(updateFunc = jest.fn().mockResolvedValue({})) {
const wrapper = shallow<ManageMemberGroupsForm>(
<ManageMemberGroupsForm
member={member}
onClose={jest.fn()}
@@ -75,27 +80,26 @@ it('should render', () => {
/>
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.dive()).toMatchSnapshot();
});

it('should correctly select the groups', () => {
const form = getMountedForm();
const instance = form.instance as ManageMemberGroupsForm;
expect(instance.isGroupSelected('11')).toBeTruthy();
expect(instance.isGroupSelected('7')).toBeFalsy();
instance.onCheck('11', false);
instance.onCheck('7', true);
expect(form.instance.isGroupSelected('11')).toBeTruthy();
expect(form.instance.isGroupSelected('7')).toBeFalsy();
form.instance.onCheck('11', false);
form.instance.onCheck('7', true);
expect(form.wrapper.state('userGroups')).toMatchSnapshot();
expect(instance.isGroupSelected('11')).toBeFalsy();
expect(instance.isGroupSelected('7')).toBeTruthy();
expect(form.instance.isGroupSelected('11')).toBeFalsy();
expect(form.instance.isGroupSelected('7')).toBeTruthy();
});

it('should correctly handle the submit event and close the modal', () => {
const updateMemberGroups = jest.fn();
const updateMemberGroups = jest.fn().mockResolvedValue({});
const form = getMountedForm(updateMemberGroups);
const instance = form.instance as ManageMemberGroupsForm;
instance.onCheck('11', false);
instance.onCheck('7', true);
instance.handleSubmit(mockEvent());
form.instance.onCheck('11', false);
form.instance.onCheck('7', true);
form.instance.handleSubmit();
expect(updateMemberGroups.mock.calls).toMatchSnapshot();
expect(form.wrapper.state()).toMatchSnapshot();
});

+ 12
- 16
server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersList-test.tsx View File

@@ -20,35 +20,31 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import MembersList from '../MembersList';
import { mockOrganization, mockCurrentUser } from '../../../helpers/testMocks';

const organization = { key: 'foo', name: 'Foo' };
const members = [
{ login: 'admin', name: 'Admin Istrator', avatar: '', groupCount: 3 },
{ login: 'john', name: 'John Doe', avatar: '7daf6c79d4802916d83f6266e24850af', groupCount: 1 }
];

it('should render a list of members of an organization', () => {
const wrapper = shallow(
<MembersList
members={members}
organization={organization}
organizationGroups={[]}
removeMember={jest.fn()}
updateMemberGroups={jest.fn()}
/>
);
expect(wrapper).toMatchSnapshot();
expect(shallowRender()).toMatchSnapshot();
});

it('should render "no results"', () => {
const wrapper = shallow(
expect(shallowRender({ members: [] })).toMatchSnapshot();
});

function shallowRender(props: Partial<MembersList['props']> = {}) {
return shallow(
<MembersList
members={[]}
organization={organization}
currentUser={mockCurrentUser({ login: 'admin' })}
members={members}
organization={mockOrganization()}
organizationGroups={[]}
removeMember={jest.fn()}
updateMemberGroups={jest.fn()}
{...props}
/>
);
expect(wrapper).toMatchSnapshot();
});
}

+ 28
- 53
server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersListItem-test.tsx View File

@@ -21,60 +21,30 @@ import * as React from 'react';
import { shallow } from 'enzyme';
import MembersListItem from '../MembersListItem';
import { click } from '../../../helpers/testUtils';

const organization = { key: 'foo', name: 'Foo' };
const admin = { login: 'admin', name: 'Admin Istrator', avatar: '', groupCount: 3 };
const john = { login: 'john', name: 'John Doe', avatar: '7daf6c79d4802916d83f6266e24850af' };
import { mockOrganizationWithAdminActions, mockOrganization } from '../../../helpers/testMocks';

it('should not render actions and groups for non admin', () => {
const wrapper = shallow(
<MembersListItem
member={admin}
organization={organization}
organizationGroups={[]}
removeMember={jest.fn()}
updateMemberGroups={jest.fn()}
/>
);
expect(wrapper).toMatchSnapshot();
expect(shallowRender({ organization: mockOrganization() })).toMatchSnapshot();
});

it('should render actions and groups for admin', () => {
const wrapper = shallow(
<MembersListItem
member={admin}
organization={{ ...organization, actions: { admin: true } }}
organizationGroups={[]}
removeMember={jest.fn()}
updateMemberGroups={jest.fn()}
/>
);
expect(wrapper).toMatchSnapshot();
expect(shallowRender()).toMatchSnapshot();
});

it('should groups at 0 if the groupCount field is not defined (just added user)', () => {
const wrapper = shallow(
<MembersListItem
member={john}
organization={{ ...organization, actions: { admin: true } }}
organizationGroups={[]}
removeMember={jest.fn()}
updateMemberGroups={jest.fn()}
/>
);
expect(wrapper).toMatchSnapshot();
it('should show groups at 0 if the groupCount field is not defined (just added user)', () => {
expect(
shallowRender({
member: { login: 'john', name: 'John Doe', avatar: '7daf6c79d4802916d83f6266e24850af' }
})
).toMatchSnapshot();
});

it('should not display the remove member action', () => {
expect(shallowRender({ removeMember: undefined }).find('ActionsDropdown')).toMatchSnapshot();
});

it('should open groups form', () => {
const wrapper = shallow(
<MembersListItem
member={admin}
organization={{ ...organization, actions: { admin: true } }}
organizationGroups={[]}
removeMember={jest.fn()}
updateMemberGroups={jest.fn()}
/>
);
const wrapper = shallowRender();

click(wrapper.find('ActionsDropdownItem').first());
expect(wrapper.find('ManageMemberGroupsForm').exists()).toBe(true);
@@ -85,15 +55,7 @@ it('should open groups form', () => {
});

it('should open remove member form', () => {
const wrapper = shallow(
<MembersListItem
member={admin}
organization={{ ...organization, actions: { admin: true } }}
organizationGroups={[]}
removeMember={jest.fn()}
updateMemberGroups={jest.fn()}
/>
);
const wrapper = shallowRender();

click(wrapper.find('ActionsDropdownItem').last());
expect(wrapper.find('RemoveMemberForm').exists()).toBe(true);
@@ -102,3 +64,16 @@ it('should open remove member form', () => {
wrapper.update();
expect(wrapper.find('RemoveMemberForm').exists()).toBe(false);
});

function shallowRender(props: Partial<MembersListItem['props']> = {}) {
return shallow(
<MembersListItem
member={{ login: 'admin', name: 'Admin Istrator', avatar: '', groupCount: 3 }}
organization={mockOrganizationWithAdminActions()}
organizationGroups={[]}
removeMember={jest.fn()}
updateMemberGroups={jest.fn()}
{...props}
/>
);
}

+ 3
- 9
server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersPageHeader-test.tsx View File

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import { MembersPageHeader } from '../MembersPageHeader';
import MembersPageHeader, { Props } from '../MembersPageHeader';
import {
mockOrganization,
mockOrganizationWithAlm,
@@ -39,10 +39,6 @@ it('should render for admin', () => {
it('should render for bound organization without sync', () => {
const organization = mockOrganizationWithAlm(mockOrganizationWithAdminActions());
expect(shallowRender({ organization })).toMatchSnapshot();

const wrapper = shallowRender({ organization, dismissSyncNotifOrg: [organization.key] });
expect(wrapper.find('Connect(SyncMemberForm)').exists()).toBe(true);
expect(wrapper.find('NewInfoBox').exists()).toBe(false);
});

it('should render for bound organization with sync', () => {
@@ -52,19 +48,17 @@ it('should render for bound organization with sync', () => {
const wrapper = shallowRender({ organization });
expect(wrapper.find('Connect(SyncMemberForm)').exists()).toBe(true);
expect(wrapper.find('AddMemberForm').exists()).toBe(false);
expect(wrapper.find('NewInfoBox').exists()).toBe(false);
expect(wrapper.find('Alert').exists()).toBe(false);
});

function shallowRender(props: Partial<MembersPageHeader['props']> = {}) {
function shallowRender(props: Partial<Props> = {}) {
return shallow(
<MembersPageHeader
dismissSyncNotifOrg={[]}
handleAddMember={jest.fn()}
loading={false}
members={[]}
organization={mockOrganization()}
refreshMembers={jest.fn()}
setCurrentUserSetting={jest.fn()}
{...props}
/>
);

+ 11
- 2
server/sonar-web/src/main/js/apps/organizationMembers/__tests__/OrganizationMembers-test.tsx View File

@@ -25,7 +25,8 @@ import { searchUsersGroups, addUserToGroup, removeUserFromGroup } from '../../..
import {
mockOrganization,
mockCurrentUser,
mockOrganizationWithAdminActions
mockOrganizationWithAdminActions,
mockOrganizationWithAlm
} from '../../../helpers/testMocks';
import { waitAndUpdate } from '../../../helpers/testUtils';

@@ -112,7 +113,7 @@ it('should refresh members', async () => {
it('should add new member', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
wrapper.find('Connect(MembersPageHeader)').prop<Function>('handleAddMember')({ login: 'bar' });
wrapper.find('MembersPageHeader').prop<Function>('handleAddMember')({ login: 'bar' });
await waitAndUpdate(wrapper);
expect(
wrapper
@@ -160,6 +161,14 @@ it('should update groups', async () => {
expect(removeUserFromGroup).toBeCalledWith({ login: 'john', name: 'birds', organization: 'foo' });
});

it('should not allow to remove members when in sync mode', async () => {
const wrapper = shallowRender({
organization: mockOrganizationWithAlm(mockOrganizationWithAdminActions(), { membersSync: true })
});
await waitAndUpdate(wrapper);
expect(wrapper.find('MembersList').prop('removeMember')).toBeUndefined();
});

function shallowRender(props: Partial<OrganizationMembers['props']> = {}) {
return shallow<OrganizationMembers>(
<OrganizationMembers

+ 3
- 6
server/sonar-web/src/main/js/apps/organizationMembers/__tests__/SyncMemberForm-test.tsx View File

@@ -30,15 +30,13 @@ jest.mock('../../../api/organizations', () => ({
}));

beforeEach(() => {
(setOrganizationMemberSync as jest.Mock).mockClear();
(syncMembers as jest.Mock).mockClear();
jest.clearAllMocks();
});

it('should allow to switch to automatic mode with github', async () => {
const dismissSyncNotif = jest.fn();
const fetchOrganization = jest.fn();
const refreshMembers = jest.fn().mockResolvedValue({});
const wrapper = shallowRender({ dismissSyncNotif, fetchOrganization, refreshMembers });
const wrapper = shallowRender({ fetchOrganization, refreshMembers });
expect(wrapper).toMatchSnapshot();

wrapper.setState({ membersSync: true });
@@ -48,8 +46,7 @@ it('should allow to switch to automatic mode with github', async () => {
await waitAndUpdate(wrapper);
expect(fetchOrganization).toHaveBeenCalledWith('foo');
expect(syncMembers).toHaveBeenCalledWith('foo');
expect(refreshMembers).toBeCalledTimes(1);
expect(dismissSyncNotif).toBeCalledTimes(1);
expect(refreshMembers).toBeCalled();
});

it('should allow to switch to automatic mode with bitbucket', async () => {

+ 1
- 1
server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/AddMemberForm-test.tsx.snap View File

@@ -38,7 +38,7 @@ exports[`should render and open the modal 2`] = `
className="modal-body"
>
<div
className="modal-large-field"
className="modal-field"
>
<label>
users.search_description

+ 43
- 23
server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/ManageMemberGroupsForm-test.tsx.snap View File

@@ -24,6 +24,7 @@ Object {
"loading": false,
"userGroups": Object {
"11": Object {
"default": false,
"description": "Technical accounts",
"id": 11,
"name": "pull-request-analysers",
@@ -40,6 +41,7 @@ Object {
exports[`should correctly select the groups 1`] = `
Object {
"11": Object {
"default": false,
"description": "Technical accounts",
"id": 11,
"name": "pull-request-analysers",
@@ -53,44 +55,62 @@ Object {
`;

exports[`should render 1`] = `
<SimpleModal
header="organization.members.manage_groups"
onClose={[MockFunction]}
onSubmit={[Function]}
>
<Component />
</SimpleModal>
`;

exports[`should render 2`] = `
<Modal
contentLabel="organization.members.manage_groups"
onRequestClose={[MockFunction]}
>
<header
className="modal-head"
>
<h2>
organization.members.manage_groups
</h2>
</header>
<form
onSubmit={[Function]}
>
<header
className="modal-head"
>
<h2>
organization.members.manage_groups
</h2>
</header>
<div
className="modal-body modal-container"
>
<strong>
organization.members.members_groups.Admin Istrator
</strong>
<i
className="spinner"
<p>
<strong>
organization.members.members_groups.Admin Istrator
</strong>
</p>
<DeferredSpinner
className="spacer-top"
timeout={100}
/>
</div>
<footer
className="modal-foot"
>
<div>
<SubmitButton>
save
</SubmitButton>
<ResetButtonLink
onClick={[MockFunction]}
>
cancel
</ResetButtonLink>
</div>
<DeferredSpinner
className="spacer-right"
loading={false}
timeout={100}
/>
<SubmitButton
disabled={true}
>
save
</SubmitButton>
<ResetButtonLink
disabled={false}
onClick={[Function]}
>
cancel
</ResetButtonLink>
</footer>
</form>
</Modal>

+ 0
- 1
server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersList-test.tsx.snap View File

@@ -33,7 +33,6 @@ exports[`should render a list of members of an organization 1`] = `
}
}
organizationGroups={Array []}
removeMember={[MockFunction]}
updateMemberGroups={[MockFunction]}
/>
<MembersListItem

+ 3
- 3
server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersListHeader-test.tsx.snap View File

@@ -5,7 +5,7 @@ exports[`should not render link in help tooltip 1`] = `
className="spacer-left"
overlay={
<div
className="abs-width-300 markdown cut-margins"
className="abs-width-300 markdown cut-margins"
>
<p>
organization.members.auto_sync_total_help.github
@@ -20,7 +20,7 @@ exports[`should render a help tooltip 1`] = `
className="spacer-left"
overlay={
<div
className="abs-width-300 markdown cut-margins"
className="abs-width-300 markdown cut-margins"
>
<p>
organization.members.auto_sync_total_help.github
@@ -47,7 +47,7 @@ exports[`should render a help tooltip 2`] = `
className="spacer-left"
overlay={
<div
className="abs-width-300 markdown cut-margins"
className="abs-width-300 markdown cut-margins"
>
<p>
organization.members.auto_sync_total_help.bitbucket

+ 45
- 35
server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersListItem-test.tsx.snap View File

@@ -1,13 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should groups at 0 if the groupCount field is not defined (just added user) 1`] = `
exports[`should not display the remove member action 1`] = `
<ActionsDropdown>
<ActionsDropdownItem
onClick={[Function]}
>
organization.members.manage_groups
</ActionsDropdownItem>
</ActionsDropdown>
`;

exports[`should not render actions and groups for non admin 1`] = `
<tr>
<td
className="thin nowrap"
>
<Connect(Avatar)
hash="7daf6c79d4802916d83f6266e24850af"
name="John Doe"
hash=""
name="Admin Istrator"
size={36}
/>
</td>
@@ -15,41 +25,18 @@ exports[`should groups at 0 if the groupCount field is not defined (just added u
className="nowrap text-middle"
>
<strong>
John Doe
Admin Istrator
</strong>
<span
className="note little-spacer-left"
>
john
admin
</span>
</td>
<td
className="text-right text-middle"
>
organization.members.x_groups.0
</td>
<td
className="nowrap text-middle text-right"
>
<ActionsDropdown>
<ActionsDropdownItem
onClick={[Function]}
>
organization.members.manage_groups
</ActionsDropdownItem>
<ActionsDropdownDivider />
<ActionsDropdownItem
destructive={true}
onClick={[Function]}
>
organization.members.remove
</ActionsDropdownItem>
</ActionsDropdown>
</td>
</tr>
`;

exports[`should not render actions and groups for non admin 1`] = `
exports[`should render actions and groups for admin 1`] = `
<tr>
<td
className="thin nowrap"
@@ -72,17 +59,40 @@ exports[`should not render actions and groups for non admin 1`] = `
admin
</span>
</td>
<td
className="text-right text-middle"
>
organization.members.x_groups.3
</td>
<td
className="nowrap text-middle text-right"
>
<ActionsDropdown>
<ActionsDropdownItem
onClick={[Function]}
>
organization.members.manage_groups
</ActionsDropdownItem>
<ActionsDropdownDivider />
<ActionsDropdownItem
destructive={true}
onClick={[Function]}
>
organization.members.remove
</ActionsDropdownItem>
</ActionsDropdown>
</td>
</tr>
`;

exports[`should render actions and groups for admin 1`] = `
exports[`should show groups at 0 if the groupCount field is not defined (just added user) 1`] = `
<tr>
<td
className="thin nowrap"
>
<Connect(Avatar)
hash=""
name="Admin Istrator"
hash="7daf6c79d4802916d83f6266e24850af"
name="John Doe"
size={36}
/>
</td>
@@ -90,18 +100,18 @@ exports[`should render actions and groups for admin 1`] = `
className="nowrap text-middle"
>
<strong>
Admin Istrator
John Doe
</strong>
<span
className="note little-spacer-left"
>
admin
john
</span>
</td>
<td
className="text-right text-middle"
>
organization.members.x_groups.3
organization.members.x_groups.0
</td>
<td
className="nowrap text-middle text-right"

+ 31
- 24
server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap View File

@@ -24,6 +24,7 @@ exports[`should render correctly 1`] = `
"link": <Link
onlyActiveOnIndex={false}
style={Object {}}
target="_blank"
to="/documentation/organizations/manage-team/"
>
organization.members.manage_a_team
@@ -84,6 +85,7 @@ exports[`should render for admin 1`] = `
"link": <Link
onlyActiveOnIndex={false}
style={Object {}}
target="_blank"
to="/documentation/organizations/manage-team/"
>
organization.members.manage_a_team
@@ -137,30 +139,6 @@ exports[`should render for bound organization without sync 1`] = `
doc={Promise {}}
/>
</div>
<NewInfoBox
description="organization.members.auto_sync_members_from_org_x.github"
onClose={[Function]}
title="organization.members.auto_sync_with_x.github"
>
<Connect(SyncMemberForm)
dismissSyncNotif={[Function]}
organization={
Object {
"actions": Object {
"admin": true,
},
"alm": Object {
"key": "github",
"membersSync": false,
"url": "https://github.com/foo",
},
"key": "foo",
"name": "Foo",
}
}
refreshMembers={[MockFunction]}
/>
</NewInfoBox>
</div>
<div
className="page-description"
@@ -173,6 +151,7 @@ exports[`should render for bound organization without sync 1`] = `
"link": <Link
onlyActiveOnIndex={false}
style={Object {}}
target="_blank"
to="/documentation/organizations/manage-team/"
>
organization.members.manage_a_team
@@ -180,6 +159,34 @@ exports[`should render for bound organization without sync 1`] = `
}
}
/>
<Alert
className="spacer-top"
display="inline"
variant="info"
>
organization.members.auto_sync_members_from_org_x.organization.github
<span
className="spacer-left"
>
<Connect(SyncMemberForm)
organization={
Object {
"actions": Object {
"admin": true,
},
"alm": Object {
"key": "github",
"membersSync": false,
"url": "https://github.com/foo",
},
"key": "foo",
"name": "Foo",
}
}
refreshMembers={[MockFunction]}
/>
</span>
</Alert>
</div>
</header>
`;

+ 11
- 2
server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap View File

@@ -12,7 +12,7 @@ exports[`should fetch members and render for non-admin 1`] = `
<Suggestions
suggestions="organization_members"
/>
<Connect(MembersPageHeader)
<MembersPageHeader
handleAddMember={[Function]}
loading={true}
organization={
@@ -38,7 +38,7 @@ exports[`should fetch members and render for non-admin 2`] = `
<Suggestions
suggestions="organization_members"
/>
<Connect(MembersPageHeader)
<MembersPageHeader
handleAddMember={[Function]}
loading={false}
members={
@@ -85,6 +85,15 @@ exports[`should fetch members and render for non-admin 2`] = `
total={3}
/>
<MembersList
currentUser={
Object {
"groups": Array [],
"isLoggedIn": true,
"login": "luke",
"name": "Skywalker",
"scmAccounts": Array [],
}
}
members={
Array [
Object {

+ 151
- 157
server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/SyncMemberForm-test.tsx.snap View File

@@ -5,71 +5,68 @@ exports[`should allow to switch to automatic mode with bitbucket 1`] = `
cancelButtonText="close"
confirmButtonText="save"
confirmDisable={true}
medium={true}
modalBody={
<React.Fragment>
<div
className="display-flex-stretch big-spacer-top"
<div
className="display-flex-stretch big-spacer-top"
>
<RadioCard
onClick={[Function]}
selected={true}
title="organization.members.management.manual"
>
<RadioCard
onClick={[Function]}
selected={true}
title="organization.members.management.manual"
<div
className="spacer-left"
>
<div
className="spacer-left"
<ul
className="big-spacer-left note"
>
<ul
className="big-spacer-left note"
<li
className="spacer-bottom"
>
organization.members.management.manual.add_members_manually
</li>
<li>
organization.members.management.choose_members_permissions
</li>
</ul>
</div>
</RadioCard>
<RadioCard
onClick={[Function]}
selected={false}
title="organization.members.management.automatic.bitbucket"
>
<div
className="spacer-left"
>
<ul
className="big-spacer-left note"
>
<React.Fragment>
<li
className="spacer-bottom"
>
organization.members.management.manual.add_members_manually
organization.members.management.automatic.synchronized_from_x.organization.bitbucket
</li>
<li>
organization.members.management.manual.choose_members_permissions
<li
className="spacer-bottom"
>
organization.members.management.automatic.members_changes_reflected.bitbucket
</li>
</ul>
</div>
</RadioCard>
<RadioCard
onClick={[Function]}
selected={false}
title="organization.members.management.automatic.bitbucket"
</React.Fragment>
<li>
organization.members.management.choose_members_permissions
</li>
</ul>
</div>
<Alert
className="big-spacer-top"
variant="warning"
>
<div
className="spacer-left"
>
<ul
className="big-spacer-left note"
>
<React.Fragment>
<li
className="spacer-bottom"
>
organization.members.management.automatic.synchronized_from_x.bitbucket
</li>
<li
className="spacer-bottom"
>
organization.members.management.automatic.members_changes_reflected.bitbucket
</li>
</React.Fragment>
<li>
organization.members.management.automatic.still_choose_members_permissions
</li>
</ul>
</div>
<Alert
className="big-spacer-top"
variant="warning"
>
organization.members.management.automatic.warning
</Alert>
</RadioCard>
</div>
</React.Fragment>
organization.members.management.automatic.warning
</Alert>
</RadioCard>
</div>
}
modalHeader="organization.members.management.title"
modalHeaderDescription={
@@ -93,6 +90,7 @@ exports[`should allow to switch to automatic mode with bitbucket 1`] = `
</p>
}
onConfirm={[Function]}
size="medium"
>
<Component />
</ConfirmButton>
@@ -103,71 +101,68 @@ exports[`should allow to switch to automatic mode with github 1`] = `
cancelButtonText="close"
confirmButtonText="save"
confirmDisable={true}
medium={true}
modalBody={
<React.Fragment>
<div
className="display-flex-stretch big-spacer-top"
<div
className="display-flex-stretch big-spacer-top"
>
<RadioCard
onClick={[Function]}
selected={true}
title="organization.members.management.manual"
>
<RadioCard
onClick={[Function]}
selected={true}
title="organization.members.management.manual"
<div
className="spacer-left"
>
<div
className="spacer-left"
<ul
className="big-spacer-left note"
>
<ul
className="big-spacer-left note"
<li
className="spacer-bottom"
>
organization.members.management.manual.add_members_manually
</li>
<li>
organization.members.management.choose_members_permissions
</li>
</ul>
</div>
</RadioCard>
<RadioCard
onClick={[Function]}
selected={false}
title="organization.members.management.automatic.github"
>
<div
className="spacer-left"
>
<ul
className="big-spacer-left note"
>
<React.Fragment>
<li
className="spacer-bottom"
>
organization.members.management.manual.add_members_manually
organization.members.management.automatic.synchronized_from_x.organization.github
</li>
<li>
organization.members.management.manual.choose_members_permissions
<li
className="spacer-bottom"
>
organization.members.management.automatic.members_changes_reflected.github
</li>
</ul>
</div>
</RadioCard>
<RadioCard
onClick={[Function]}
selected={false}
title="organization.members.management.automatic.github"
</React.Fragment>
<li>
organization.members.management.choose_members_permissions
</li>
</ul>
</div>
<Alert
className="big-spacer-top"
variant="warning"
>
<div
className="spacer-left"
>
<ul
className="big-spacer-left note"
>
<React.Fragment>
<li
className="spacer-bottom"
>
organization.members.management.automatic.synchronized_from_x.github
</li>
<li
className="spacer-bottom"
>
organization.members.management.automatic.members_changes_reflected.github
</li>
</React.Fragment>
<li>
organization.members.management.automatic.still_choose_members_permissions
</li>
</ul>
</div>
<Alert
className="big-spacer-top"
variant="warning"
>
organization.members.management.automatic.warning
</Alert>
</RadioCard>
</div>
</React.Fragment>
organization.members.management.automatic.warning
</Alert>
</RadioCard>
</div>
}
modalHeader="organization.members.management.title"
modalHeaderDescription={
@@ -191,6 +186,7 @@ exports[`should allow to switch to automatic mode with github 1`] = `
</p>
}
onConfirm={[Function]}
size="medium"
>
<Component />
</ConfirmButton>
@@ -201,65 +197,62 @@ exports[`should allow to switch to manual mode 1`] = `
cancelButtonText="close"
confirmButtonText="save"
confirmDisable={true}
medium={true}
modalBody={
<React.Fragment>
<div
className="display-flex-stretch big-spacer-top"
<div
className="display-flex-stretch big-spacer-top"
>
<RadioCard
onClick={[Function]}
selected={false}
title="organization.members.management.manual"
>
<RadioCard
onClick={[Function]}
selected={false}
title="organization.members.management.manual"
<div
className="spacer-left"
>
<div
className="spacer-left"
<ul
className="big-spacer-left note"
>
<ul
className="big-spacer-left note"
<li
className="spacer-bottom"
>
organization.members.management.manual.add_members_manually
</li>
<li>
organization.members.management.choose_members_permissions
</li>
</ul>
</div>
</RadioCard>
<RadioCard
onClick={[Function]}
selected={true}
title="organization.members.management.automatic.github"
>
<div
className="spacer-left"
>
<ul
className="big-spacer-left note"
>
<React.Fragment>
<li
className="spacer-bottom"
>
organization.members.management.manual.add_members_manually
</li>
<li>
organization.members.management.manual.choose_members_permissions
organization.members.management.automatic.synchronized_from_x.organization.github
</li>
</ul>
</div>
</RadioCard>
<RadioCard
onClick={[Function]}
selected={true}
title="organization.members.management.automatic.github"
>
<div
className="spacer-left"
>
<ul
className="big-spacer-left note"
>
<React.Fragment>
<li
className="spacer-bottom"
>
organization.members.management.automatic.synchronized_from_x.github
</li>
<li
className="spacer-bottom"
>
organization.members.management.automatic.members_changes_reflected.github
</li>
</React.Fragment>
<li>
organization.members.management.automatic.still_choose_members_permissions
<li
className="spacer-bottom"
>
organization.members.management.automatic.members_changes_reflected.github
</li>
</ul>
</div>
</RadioCard>
</div>
</React.Fragment>
</React.Fragment>
<li>
organization.members.management.choose_members_permissions
</li>
</ul>
</div>
</RadioCard>
</div>
}
modalHeader="organization.members.management.title"
modalHeaderDescription={
@@ -283,6 +276,7 @@ exports[`should allow to switch to manual mode 1`] = `
</p>
}
onConfirm={[Function]}
size="medium"
>
<Component />
</ConfirmButton>

+ 15
- 13
server/sonar-web/src/main/js/apps/organizations/components/OrganizationEdit.tsx View File

@@ -111,12 +111,13 @@ export class OrganizationEdit extends React.PureComponent<Props, State> {

<div className="boxed-group boxed-group-inner">
<form onSubmit={this.handleSubmit}>
<div className="modal-field">
<div className="form-field">
<label htmlFor="organization-name">
{translate('organization.name')}
<em className="mandatory">*</em>
</label>
<input
className="input-super-large"
disabled={this.state.loading}
id="organization-name"
maxLength={255}
@@ -126,13 +127,14 @@ export class OrganizationEdit extends React.PureComponent<Props, State> {
type="text"
value={this.state.name}
/>
<div className="modal-field-description">
<div className="form-field-description">
{translate('organization.name.description')}
</div>
</div>
<div className="modal-field">
<div className="form-field">
<label htmlFor="organization-avatar">{translate('organization.avatar')}</label>
<input
className="input-super-large"
disabled={this.state.loading}
id="organization-avatar"
maxLength={256}
@@ -142,11 +144,11 @@ export class OrganizationEdit extends React.PureComponent<Props, State> {
type="text"
value={this.state.avatar}
/>
<div className="modal-field-description">
<div className="form-field-description">
{translate('organization.avatar.description')}
</div>
{(this.state.avatarImage || this.state.name) && (
<div className="spacer-top spacer-bottom">
<div className="spacer-top">
<div className="little-spacer-bottom">
{translate('organization.avatar.preview')}
{':'}
@@ -160,9 +162,10 @@ export class OrganizationEdit extends React.PureComponent<Props, State> {
</div>
)}
</div>
<div className="modal-field">
<div className="form-field">
<label htmlFor="organization-description">{translate('description')}</label>
<textarea
className="input-super-large"
disabled={this.state.loading}
id="organization-description"
maxLength={256}
@@ -171,13 +174,14 @@ export class OrganizationEdit extends React.PureComponent<Props, State> {
rows={3}
value={this.state.description}
/>
<div className="modal-field-description">
<div className="form-field-description">
{translate('organization.description.description')}
</div>
</div>
<div className="modal-field">
<div className="form-field">
<label htmlFor="organization-url">{translate('organization.url')}</label>
<input
className="input-super-large"
disabled={this.state.loading}
id="organization-url"
maxLength={256}
@@ -186,14 +190,12 @@ export class OrganizationEdit extends React.PureComponent<Props, State> {
type="text"
value={this.state.url}
/>
<div className="modal-field-description">
<div className="form-field-description">
{translate('organization.url.description')}
</div>
</div>
<div className="modal-field">
<SubmitButton disabled={this.state.loading}>{translate('save')}</SubmitButton>
{this.state.loading && <i className="spinner spacer-left" />}
</div>
<SubmitButton disabled={this.state.loading}>{translate('save')}</SubmitButton>
{this.state.loading && <i className="spinner spacer-left" />}
</form>
</div>
</div>

+ 1
- 1
server/sonar-web/src/main/js/apps/organizations/components/OrganizationEmpty.css View File

@@ -19,5 +19,5 @@
*/
.organization-empty {
margin: 100px auto 0;
width: 800px;
width: 472px;
}

+ 5
- 9
server/sonar-web/src/main/js/apps/organizations/components/OrganizationEmpty.tsx View File

@@ -50,19 +50,15 @@ export class OrganizationEmpty extends React.PureComponent<Props> {
return (
<div className="organization-empty">
<h3 className="text-center">{translate('onboarding.create_organization.ready')}</h3>
<div className="onboarding-choices">
<Button className="onboarding-choice" onClick={this.handleNewProjectClick}>
<div className="display-flex-space-around huge-spacer-top">
<Button className="button-huge" onClick={this.handleNewProjectClick}>
<OnboardingProjectIcon className="big-spacer-bottom" />
<h6 className="onboarding-choice-name">
{translate('provisioning.analyze_new_project')}
</h6>
<p className="medium spacer-top">{translate('provisioning.analyze_new_project')}</p>
</Button>
{!memberSyncActivated && (
<Button className="onboarding-choice" onClick={this.handleAddMembersClick}>
<Button className="button-huge" onClick={this.handleAddMembersClick}>
<OnboardingAddMembersIcon />
<h6 className="onboarding-choice-name">
{translate('organization.members.add.multiple')}
</h6>
<p className="medium spacer-top">{translate('organization.members.add.multiple')}</p>
</Button>
)}
</div>

+ 54
- 54
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationEdit-test.tsx.snap View File

@@ -25,7 +25,7 @@ exports[`smoke test 1`] = `
onSubmit={[Function]}
>
<div
className="modal-field"
className="form-field"
>
<label
htmlFor="organization-name"
@@ -38,6 +38,7 @@ exports[`smoke test 1`] = `
</em>
</label>
<input
className="input-super-large"
disabled={false}
id="organization-name"
maxLength={255}
@@ -48,13 +49,13 @@ exports[`smoke test 1`] = `
value="Foo"
/>
<div
className="modal-field-description"
className="form-field-description"
>
organization.name.description
</div>
</div>
<div
className="modal-field"
className="form-field"
>
<label
htmlFor="organization-avatar"
@@ -62,6 +63,7 @@ exports[`smoke test 1`] = `
organization.avatar
</label>
<input
className="input-super-large"
disabled={false}
id="organization-avatar"
maxLength={256}
@@ -72,12 +74,12 @@ exports[`smoke test 1`] = `
value=""
/>
<div
className="modal-field-description"
className="form-field-description"
>
organization.avatar.description
</div>
<div
className="spacer-top spacer-bottom"
className="spacer-top"
>
<div
className="little-spacer-bottom"
@@ -96,7 +98,7 @@ exports[`smoke test 1`] = `
</div>
</div>
<div
className="modal-field"
className="form-field"
>
<label
htmlFor="organization-description"
@@ -104,6 +106,7 @@ exports[`smoke test 1`] = `
description
</label>
<textarea
className="input-super-large"
disabled={false}
id="organization-description"
maxLength={256}
@@ -113,13 +116,13 @@ exports[`smoke test 1`] = `
value=""
/>
<div
className="modal-field-description"
className="form-field-description"
>
organization.description.description
</div>
</div>
<div
className="modal-field"
className="form-field"
>
<label
htmlFor="organization-url"
@@ -127,6 +130,7 @@ exports[`smoke test 1`] = `
organization.url
</label>
<input
className="input-super-large"
disabled={false}
id="organization-url"
maxLength={256}
@@ -136,20 +140,16 @@ exports[`smoke test 1`] = `
value=""
/>
<div
className="modal-field-description"
className="form-field-description"
>
organization.url.description
</div>
</div>
<div
className="modal-field"
<SubmitButton
disabled={false}
>
<SubmitButton
disabled={false}
>
save
</SubmitButton>
</div>
save
</SubmitButton>
</form>
</div>
</div>
@@ -180,7 +180,7 @@ exports[`smoke test 2`] = `
onSubmit={[Function]}
>
<div
className="modal-field"
className="form-field"
>
<label
htmlFor="organization-name"
@@ -193,6 +193,7 @@ exports[`smoke test 2`] = `
</em>
</label>
<input
className="input-super-large"
disabled={false}
id="organization-name"
maxLength={255}
@@ -203,13 +204,13 @@ exports[`smoke test 2`] = `
value="New Foo"
/>
<div
className="modal-field-description"
className="form-field-description"
>
organization.name.description
</div>
</div>
<div
className="modal-field"
className="form-field"
>
<label
htmlFor="organization-avatar"
@@ -217,6 +218,7 @@ exports[`smoke test 2`] = `
organization.avatar
</label>
<input
className="input-super-large"
disabled={false}
id="organization-avatar"
maxLength={256}
@@ -227,12 +229,12 @@ exports[`smoke test 2`] = `
value="foo-avatar"
/>
<div
className="modal-field-description"
className="form-field-description"
>
organization.avatar.description
</div>
<div
className="spacer-top spacer-bottom"
className="spacer-top"
>
<div
className="little-spacer-bottom"
@@ -251,7 +253,7 @@ exports[`smoke test 2`] = `
</div>
</div>
<div
className="modal-field"
className="form-field"
>
<label
htmlFor="organization-description"
@@ -259,6 +261,7 @@ exports[`smoke test 2`] = `
description
</label>
<textarea
className="input-super-large"
disabled={false}
id="organization-description"
maxLength={256}
@@ -268,13 +271,13 @@ exports[`smoke test 2`] = `
value="foo-description"
/>
<div
className="modal-field-description"
className="form-field-description"
>
organization.description.description
</div>
</div>
<div
className="modal-field"
className="form-field"
>
<label
htmlFor="organization-url"
@@ -282,6 +285,7 @@ exports[`smoke test 2`] = `
organization.url
</label>
<input
className="input-super-large"
disabled={false}
id="organization-url"
maxLength={256}
@@ -291,20 +295,16 @@ exports[`smoke test 2`] = `
value="foo-url"
/>
<div
className="modal-field-description"
className="form-field-description"
>
organization.url.description
</div>
</div>
<div
className="modal-field"
<SubmitButton
disabled={false}
>
<SubmitButton
disabled={false}
>
save
</SubmitButton>
</div>
save
</SubmitButton>
</form>
</div>
</div>
@@ -335,7 +335,7 @@ exports[`smoke test 3`] = `
onSubmit={[Function]}
>
<div
className="modal-field"
className="form-field"
>
<label
htmlFor="organization-name"
@@ -348,6 +348,7 @@ exports[`smoke test 3`] = `
</em>
</label>
<input
className="input-super-large"
disabled={true}
id="organization-name"
maxLength={255}
@@ -358,13 +359,13 @@ exports[`smoke test 3`] = `
value="New Foo"
/>
<div
className="modal-field-description"
className="form-field-description"
>
organization.name.description
</div>
</div>
<div
className="modal-field"
className="form-field"
>
<label
htmlFor="organization-avatar"
@@ -372,6 +373,7 @@ exports[`smoke test 3`] = `
organization.avatar
</label>
<input
className="input-super-large"
disabled={true}
id="organization-avatar"
maxLength={256}
@@ -382,12 +384,12 @@ exports[`smoke test 3`] = `
value="foo-avatar"
/>
<div
className="modal-field-description"
className="form-field-description"
>
organization.avatar.description
</div>
<div
className="spacer-top spacer-bottom"
className="spacer-top"
>
<div
className="little-spacer-bottom"
@@ -406,7 +408,7 @@ exports[`smoke test 3`] = `
</div>
</div>
<div
className="modal-field"
className="form-field"
>
<label
htmlFor="organization-description"
@@ -414,6 +416,7 @@ exports[`smoke test 3`] = `
description
</label>
<textarea
className="input-super-large"
disabled={true}
id="organization-description"
maxLength={256}
@@ -423,13 +426,13 @@ exports[`smoke test 3`] = `
value="foo-description"
/>
<div
className="modal-field-description"
className="form-field-description"
>
organization.description.description
</div>
</div>
<div
className="modal-field"
className="form-field"
>
<label
htmlFor="organization-url"
@@ -437,6 +440,7 @@ exports[`smoke test 3`] = `
organization.url
</label>
<input
className="input-super-large"
disabled={true}
id="organization-url"
maxLength={256}
@@ -446,23 +450,19 @@ exports[`smoke test 3`] = `
value="foo-url"
/>
<div
className="modal-field-description"
className="form-field-description"
>
organization.url.description
</div>
</div>
<div
className="modal-field"
<SubmitButton
disabled={true}
>
<SubmitButton
disabled={true}
>
save
</SubmitButton>
<i
className="spinner spacer-left"
/>
</div>
save
</SubmitButton>
<i
className="spinner spacer-left"
/>
</form>
</div>
</div>

+ 14
- 14
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationEmpty-test.tsx.snap View File

@@ -10,20 +10,20 @@ exports[`should hide add members button when member sync activated 1`] = `
onboarding.create_organization.ready
</h3>
<div
className="onboarding-choices"
className="display-flex-space-around huge-spacer-top"
>
<Button
className="onboarding-choice"
className="button-huge"
onClick={[Function]}
>
<OnboardingProjectIcon
className="big-spacer-bottom"
/>
<h6
className="onboarding-choice-name"
<p
className="medium spacer-top"
>
provisioning.analyze_new_project
</h6>
</p>
</Button>
</div>
</div>
@@ -39,31 +39,31 @@ exports[`should render 1`] = `
onboarding.create_organization.ready
</h3>
<div
className="onboarding-choices"
className="display-flex-space-around huge-spacer-top"
>
<Button
className="onboarding-choice"
className="button-huge"
onClick={[Function]}
>
<OnboardingProjectIcon
className="big-spacer-bottom"
/>
<h6
className="onboarding-choice-name"
<p
className="medium spacer-top"
>
provisioning.analyze_new_project
</h6>
</p>
</Button>
<Button
className="onboarding-choice"
className="button-huge"
onClick={[Function]}
>
<OnboardingAddMembersIcon />
<h6
className="onboarding-choice-name"
<p
className="medium spacer-top"
>
organization.members.add.multiple
</h6>
</p>
</Button>
</div>
</div>

+ 4
- 3
server/sonar-web/src/main/js/apps/permission-templates/components/Form.tsx View File

@@ -20,17 +20,17 @@
import * as React from 'react';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import SimpleModal from '../../../components/controls/SimpleModal';
import { translate } from '../../../helpers/l10n';
import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons';
import { translate } from '../../../helpers/l10n';

interface Props {
confirmButtonText: string;
header: string;
permissionTemplate?: { description?: string; name: string; projectKeyPattern?: string };
onClose: () => void;
onSubmit: (
data: { description: string; name: string; projectKeyPattern: string }
) => Promise<void>;
permissionTemplate?: { description?: string; name: string; projectKeyPattern?: string };
}

interface State {
@@ -79,7 +79,8 @@ export default class Form extends React.PureComponent<Props, State> {
<SimpleModal
header={this.props.header}
onClose={this.props.onClose}
onSubmit={this.handleSubmit}>
onSubmit={this.handleSubmit}
size="small">
{({ onCloseClick, onFormSubmit, submitting }) => (
<form id="permission-template-form" onSubmit={onFormSubmit}>
<header className="modal-head">

+ 30
- 0
server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/Form-test.tsx View File

@@ -0,0 +1,30 @@
/*
* SonarQube
* Copyright (C) 2009-2019 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import Form from '../Form';

it('render correctly', () => {
expect(
shallow(
<Form confirmButtonText="confirm" header="title" onClose={jest.fn()} onSubmit={jest.fn()} />
)
).toMatchSnapshot();
});

+ 12
- 0
server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/__snapshots__/Form-test.tsx.snap View File

@@ -0,0 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`render correctly 1`] = `
<SimpleModal
header="title"
onClose={[MockFunction]}
onSubmit={[Function]}
size="small"
>
<Component />
</SimpleModal>
`;

+ 5
- 1
server/sonar-web/src/main/js/apps/permissions/project/components/ApplyTemplate.tsx View File

@@ -98,7 +98,11 @@ export default class ApplyTemplate extends React.PureComponent<Props, State> {
);

return (
<SimpleModal header={header} onClose={this.props.onClose} onSubmit={this.handleSubmit}>
<SimpleModal
header={header}
onClose={this.props.onClose}
onSubmit={this.handleSubmit}
size="small">
{({ onCloseClick, onFormSubmit, submitting }) => (
<form id="project-permissions-apply-template-form" onSubmit={onFormSubmit}>
<header className="modal-head">

+ 53
- 0
server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/ApplyTemplate-test.tsx View File

@@ -0,0 +1,53 @@
/*
* SonarQube
* Copyright (C) 2009-2019 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import ApplyTemplate from '../ApplyTemplate';
import { waitAndUpdate } from '../../../../../helpers/testUtils';

jest.mock('../../../../../api/permissions', () => ({
getPermissionTemplates: jest.fn().mockResolvedValue({
permissionTemplates: [
{
id: 'tmp1',
name: 'SonarSource projects',
createdAt: '2015-11-27T15:20:32+0100',
permissions: [
{ key: 'admin', usersCount: 0, groupsCount: 3 },
{ key: 'codeviewer', usersCount: 0, groupsCount: 6 }
]
}
],
defaultTemplates: [{ templateId: 'tmp1', qualifier: 'TRK' }],
permissions: [
{ key: 'admin', name: 'Administer', description: 'Administer access' },
{ key: 'codeviewer', name: 'See Source Code', description: 'View code' }
]
})
}));

it('render correctly', async () => {
const wrapper = shallow(
<ApplyTemplate onClose={jest.fn()} organization="foo" project={{ key: 'foo', name: 'Foo' }} />
);
expect(wrapper).toMatchSnapshot();
await waitAndUpdate(wrapper);
expect(wrapper.dive()).toMatchSnapshot();
});

+ 83
- 0
server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/__snapshots__/ApplyTemplate-test.tsx.snap View File

@@ -0,0 +1,83 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`render correctly 1`] = `
<SimpleModal
header="projects_role.apply_template_to_xxx.Foo"
onClose={[MockFunction]}
onSubmit={[Function]}
size="small"
>
<Component />
</SimpleModal>
`;

exports[`render correctly 2`] = `
<Modal
contentLabel="projects_role.apply_template_to_xxx.Foo"
onRequestClose={[MockFunction]}
size="small"
>
<form
id="project-permissions-apply-template-form"
onSubmit={[Function]}
>
<header
className="modal-head"
>
<h2>
projects_role.apply_template_to_xxx.Foo
</h2>
</header>
<div
className="modal-body"
>
<div
className="modal-field"
>
<label
htmlFor="project-permissions-template"
>
template
<em
className="mandatory"
>
*
</em>
</label>
<Select
clearable={false}
id="project-permissions-template"
onChange={[Function]}
options={
Array [
Object {
"label": "SonarSource projects",
"value": "tmp1",
},
]
}
/>
</div>
</div>
<footer
className="modal-foot"
>
<DeferredSpinner
className="spacer-right"
loading={false}
timeout={100}
/>
<SubmitButton
disabled={true}
>
apply
</SubmitButton>
<ResetButtonLink
onClick={[Function]}
>
cancel
</ResetButtonLink>
</footer>
</form>
</Modal>
`;

+ 2
- 1
server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.tsx View File

@@ -51,7 +51,8 @@ export default class AddEventForm extends React.PureComponent<Props, State> {
confirmDisable={!this.state.name}
header={translate(this.props.addEventButtonText)}
onClose={this.props.onClose}
onConfirm={this.handleSubmit}>
onConfirm={this.handleSubmit}
size="small">
<div className="modal-field">
<label>{translate('name')}</label>
<input

+ 3
- 2
server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeEventForm.tsx View File

@@ -23,8 +23,8 @@ import ConfirmModal from '../../../../components/controls/ConfirmModal';

interface Props {
changeEvent: (event: string, name: string) => Promise<void>;
header: string;
event: T.AnalysisEvent;
header: string;
onClose: () => void;
}

@@ -54,7 +54,8 @@ export default class ChangeEventForm extends React.PureComponent<Props, State> {
confirmDisable={!name || name === this.props.event.name}
header={this.props.header}
onClose={this.props.onClose}
onConfirm={this.handleSubmit}>
onConfirm={this.handleSubmit}
size="small">
<div className="modal-field">
<label>{translate('name')}</label>
<input autoFocus={true} onChange={this.changeInput} type="text" value={name} />

+ 41
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/AddEventForm-test.tsx View File

@@ -0,0 +1,41 @@
/*
* SonarQube
* Copyright (C) 2009-2019 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import AddEventForm from '../AddEventForm';

it('should render correctly', () => {
expect(
shallow(
<AddEventForm
addEvent={jest.fn()}
addEventButtonText="add"
analysis={{
key: '1',
date: new Date('2019-01-14T15:44:51.000Z'),
events: [{ key: '2', category: 'VERSION', name: '1.0' }],
codePeriodVersion: '1.0',
manualNewCodePeriodBaseline: false
}}
onClose={jest.fn()}
/>
)
).toMatchSnapshot();
});

+ 35
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/ChangeEventForm-test.tsx View File

@@ -0,0 +1,35 @@
/*
* SonarQube
* Copyright (C) 2009-2019 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import ChangeEventForm from '../ChangeEventForm';

it('should render correctly', () => {
expect(
shallow(
<ChangeEventForm
changeEvent={jest.fn()}
event={{ category: 'VERSION', key: '1', name: '1.0' }}
header="change"
onClose={jest.fn()}
/>
)
).toMatchSnapshot();
});

+ 26
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/__snapshots__/AddEventForm-test.tsx.snap View File

@@ -0,0 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<ConfirmModal
confirmButtonText="save"
confirmDisable={true}
header="add"
onClose={[MockFunction]}
onConfirm={[Function]}
size="small"
>
<div
className="modal-field"
>
<label>
name
</label>
<input
autoFocus={true}
onChange={[Function]}
type="text"
value=""
/>
</div>
</ConfirmModal>
`;

+ 26
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/__snapshots__/ChangeEventForm-test.tsx.snap View File

@@ -0,0 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<ConfirmModal
confirmButtonText="change_verb"
confirmDisable={true}
header="change"
onClose={[MockFunction]}
onConfirm={[Function]}
size="small"
>
<div
className="modal-field"
>
<label>
name
</label>
<input
autoFocus={true}
onChange={[Function]}
type="text"
value="1.0"
/>
</div>
</ConfirmModal>
`;

+ 1
- 1
server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx View File

@@ -79,7 +79,7 @@ export default class RenameBranchModal extends React.PureComponent<Props, State>
this.state.loading || !this.state.name || this.state.name === branch.name;

return (
<Modal contentLabel={header} onRequestClose={this.props.onClose}>
<Modal contentLabel={header} onRequestClose={this.props.onClose} size="small">
<header className="modal-head">
<h2>{header}</h2>
</header>

+ 3
- 3
server/sonar-web/src/main/js/apps/projectBranches/components/SettingForm.tsx View File

@@ -98,7 +98,7 @@ export default class SettingForm extends React.PureComponent<Props, State> {
className="big-spacer-bottom markdown"
dangerouslySetInnerHTML={{ __html: translate(`property.${setting.key}.description`) }}
/>
<div className="big-spacer-bottom">
<div className="modal-field">
<input
autoFocus={true}
className="input-super-large"
@@ -108,11 +108,11 @@ export default class SettingForm extends React.PureComponent<Props, State> {
value={this.state.value}
/>
{setting.inherited && (
<div className="note spacer-top">{translate('settings._default')}</div>
<div className="modal-field-description">{translate('settings._default')}</div>
)}
{!setting.inherited &&
setting.parentValue && (
<div className="note spacer-top">
<div className="modal-field-description">
{translateWithParameters('settings.default_x', setting.parentValue)}
</div>
)}

+ 3
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/RenameBranchModal-test.tsx.snap View File

@@ -4,6 +4,7 @@ exports[`renders 1`] = `
<Modal
contentLabel="branches.rename"
onRequestClose={[MockFunction]}
size="small"
>
<header
className="modal-head"
@@ -66,6 +67,7 @@ exports[`renders 2`] = `
<Modal
contentLabel="branches.rename"
onRequestClose={[MockFunction]}
size="small"
>
<header
className="modal-head"
@@ -128,6 +130,7 @@ exports[`renders 3`] = `
<Modal
contentLabel="branches.rename"
onRequestClose={[MockFunction]}
size="small"
>
<header
className="modal-head"

+ 4
- 4
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/SettingForm-test.tsx.snap View File

@@ -16,7 +16,7 @@ exports[`changes value 1`] = `
}
/>
<div
className="big-spacer-bottom"
className="modal-field"
>
<input
autoFocus={true}
@@ -27,7 +27,7 @@ exports[`changes value 1`] = `
value="release-.*"
/>
<div
className="note spacer-top"
className="modal-field-description"
>
settings._default
</div>
@@ -66,7 +66,7 @@ exports[`resets value 1`] = `
}
/>
<div
className="big-spacer-bottom"
className="modal-field"
>
<input
autoFocus={true}
@@ -77,7 +77,7 @@ exports[`resets value 1`] = `
value="release-.*"
/>
<div
className="note spacer-top"
className="modal-field-description"
>
settings.default_x.branch-.*
</div>

+ 5
- 1
server/sonar-web/src/main/js/apps/projectLinks/CreationModal.tsx View File

@@ -52,7 +52,11 @@ export default class CreationModal extends React.PureComponent<Props, State> {
const header = translate('project_links.create_new_project_link');

return (
<SimpleModal header={header} onClose={this.props.onClose} onSubmit={this.handleSubmit}>
<SimpleModal
header={header}
onClose={this.props.onClose}
onSubmit={this.handleSubmit}
size="small">
{({ onCloseClick, onFormSubmit, submitting }) => (
<form onSubmit={onFormSubmit}>
<header className="modal-head">

+ 1
- 0
server/sonar-web/src/main/js/apps/projectLinks/__tests__/__snapshots__/CreationModal-test.tsx.snap View File

@@ -4,6 +4,7 @@ exports[`should create link 1`] = `
<Modal
contentLabel="project_links.create_new_project_link"
onRequestClose={[MockFunction]}
size="small"
>
<form
onSubmit={[Function]}

+ 1
- 1
server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx View File

@@ -152,7 +152,7 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S
const header = translate('permission_templates.bulk_apply_permission_template');

return (
<Modal contentLabel={header} onRequestClose={this.props.onClose}>
<Modal contentLabel={header} onRequestClose={this.props.onClose} size="small">
<header className="modal-head">
<h2>{header}</h2>
</header>

+ 2
- 1
server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx View File

@@ -199,8 +199,9 @@ export default class CreateProjectForm extends React.PureComponent<Props, State>
{organization.actions &&
organization.actions.admin &&
!organization.canUpdateProjectsVisibilityToPrivate && (
<div className="spacer-top display-flex-space-around">
<div className="spacer-top">
<UpgradeOrganizationBox
className="width-100"
insideModal={true}
onOrganizationUpgrade={this.props.onOrganizationUpgrade}
organization={organization}

+ 8
- 0
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/BulkApplyTemplateModal-test.tsx.snap View File

@@ -4,6 +4,7 @@ exports[`bulk applies template to all results 1`] = `
<Modal
contentLabel="permission_templates.bulk_apply_permission_template"
onRequestClose={[MockFunction]}
size="small"
>
<header
className="modal-head"
@@ -36,6 +37,7 @@ exports[`bulk applies template to all results 2`] = `
<Modal
contentLabel="permission_templates.bulk_apply_permission_template"
onRequestClose={[MockFunction]}
size="small"
>
<header
className="modal-head"
@@ -106,6 +108,7 @@ exports[`bulk applies template to all results 3`] = `
<Modal
contentLabel="permission_templates.bulk_apply_permission_template"
onRequestClose={[MockFunction]}
size="small"
>
<header
className="modal-head"
@@ -179,6 +182,7 @@ exports[`bulk applies template to all results 4`] = `
<Modal
contentLabel="permission_templates.bulk_apply_permission_template"
onRequestClose={[MockFunction]}
size="small"
>
<header
className="modal-head"
@@ -213,6 +217,7 @@ exports[`bulk applies template to selected results 1`] = `
<Modal
contentLabel="permission_templates.bulk_apply_permission_template"
onRequestClose={[MockFunction]}
size="small"
>
<header
className="modal-head"
@@ -245,6 +250,7 @@ exports[`bulk applies template to selected results 2`] = `
<Modal
contentLabel="permission_templates.bulk_apply_permission_template"
onRequestClose={[MockFunction]}
size="small"
>
<header
className="modal-head"
@@ -315,6 +321,7 @@ exports[`bulk applies template to selected results 3`] = `
<Modal
contentLabel="permission_templates.bulk_apply_permission_template"
onRequestClose={[MockFunction]}
size="small"
>
<header
className="modal-head"
@@ -388,6 +395,7 @@ exports[`bulk applies template to selected results 4`] = `
<Modal
contentLabel="permission_templates.bulk_apply_permission_template"
onRequestClose={[MockFunction]}
size="small"
>
<header
className="modal-head"

+ 6
- 3
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap View File

@@ -79,9 +79,10 @@ exports[`creates project 1`] = `
/>
</div>
<div
className="spacer-top display-flex-space-around"
className="spacer-top"
>
<UpgradeOrganizationBox
className="width-100"
insideModal={true}
onOrganizationUpgrade={[MockFunction]}
organization={
@@ -196,9 +197,10 @@ exports[`creates project 2`] = `
/>
</div>
<div
className="spacer-top display-flex-space-around"
className="spacer-top"
>
<UpgradeOrganizationBox
className="width-100"
insideModal={true}
onOrganizationUpgrade={[MockFunction]}
organization={
@@ -313,9 +315,10 @@ exports[`creates project 3`] = `
/>
</div>
<div
className="spacer-top display-flex-space-around"
className="spacer-top"
>
<UpgradeOrganizationBox
className="width-100"
insideModal={true}
onOrganizationUpgrade={[MockFunction]}
organization={

+ 4
- 3
server/sonar-web/src/main/js/apps/quality-gates/components/ConditionModal.tsx View File

@@ -107,7 +107,8 @@ export default class ConditionModal extends React.PureComponent<Props, State> {
confirmDisable={metric === undefined}
header={header}
onClose={onClose}
onConfirm={this.handleFormSubmit}>
onConfirm={this.handleFormSubmit}
size="small">
{this.state.errorMessage && <Alert variant="error">{this.state.errorMessage}</Alert>}
<div className="modal-field">
<label htmlFor="condition-metric">{translate('quality_gates.conditions.metric')}</label>
@@ -118,7 +119,7 @@ export default class ConditionModal extends React.PureComponent<Props, State> {
</div>
{metric && (
<>
<div className="modal-field">
<div className="modal-field display-inline-block">
<label htmlFor="condition-operator">
{translate('quality_gates.conditions.operator')}
</label>
@@ -128,7 +129,7 @@ export default class ConditionModal extends React.PureComponent<Props, State> {
op={op}
/>
</div>
<div className="modal-field">
<div className="modal-field display-inline-block spacer-left">
<label htmlFor="condition-threshold">
{translate('quality_gates.conditions.error')}
</label>

+ 6
- 2
server/sonar-web/src/main/js/apps/quality-gates/components/ConditionOperator.tsx View File

@@ -23,9 +23,9 @@ import { translate } from '../../../helpers/l10n';
import { getPossibleOperators } from '../utils';

interface Props {
op?: string;
metric: T.Metric;
onOperatorChange: (op: string) => void;
op?: string;
}

export default class ConditionOperator extends React.PureComponent<Props> {
@@ -62,7 +62,11 @@ export default class ConditionOperator extends React.PureComponent<Props> {
/>
);
} else {
return <span className="note">{this.getLabel(operators, this.props.metric)}</span>;
return (
<span className="display-inline-block note abs-width-150">
{this.getLabel(operators, this.props.metric)}
</span>
);
}
}
}

+ 0
- 0
server/sonar-web/src/main/js/apps/quality-gates/components/CopyQualityGateForm.tsx View File


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save