Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
heptapod
heptapod
Commits
93b9c98994cb
Commit
03b14b48
authored
Sep 07, 2017
by
Mike Greiling
Committed by
Phil Hughes
Sep 07, 2017
Browse files
Resolve "Make project and features visibility settings less confusing"
parent
c763efd25a92
Changes
21
Hide whitespace changes
Inline
Side-by-side
app/assets/javascripts/dispatcher.js
View file @
93b9c989
...
...
@@ -577,6 +577,9 @@ import initChangesDropdown from './init_changes_dropdown';
case
'
edit
'
:
shortcut_handler
=
new
ShortcutsNavigation
();
new
ProjectNew
();
import
(
/* webpackChunkName: 'project_permissions' */
'
./projects/permissions
'
)
.
then
(
permissions
=>
permissions
.
default
())
.
catch
(()
=>
{});
break
;
case
'
new
'
:
new
ProjectNew
();
...
...
app/assets/javascripts/projects/permissions/components/project_feature_setting.vue
0 → 100644
View file @
93b9c989
<
script
>
import
projectFeatureToggle
from
'
./project_feature_toggle.vue
'
;
export
default
{
props
:
{
name
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
options
:
{
type
:
Array
,
required
:
false
,
default
:
()
=>
[],
},
value
:
{
type
:
Number
,
required
:
false
,
default
:
0
,
},
disabledInput
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
},
components
:
{
projectFeatureToggle
,
},
computed
:
{
featureEnabled
()
{
return
this
.
value
!==
0
;
},
displayOptions
()
{
if
(
this
.
featureEnabled
)
{
return
this
.
options
;
}
return
[
[
0
,
'
Enable feature to choose access level
'
],
];
},
displaySelectInput
()
{
return
this
.
disabledInput
||
!
this
.
featureEnabled
||
this
.
displayOptions
.
length
<
2
;
},
},
model
:
{
prop
:
'
value
'
,
event
:
'
change
'
,
},
methods
:
{
toggleFeature
(
featureEnabled
)
{
if
(
featureEnabled
===
false
||
this
.
options
.
length
<
1
)
{
this
.
$emit
(
'
change
'
,
0
);
}
else
{
const
[
firstOptionValue
]
=
this
.
options
[
this
.
options
.
length
-
1
];
this
.
$emit
(
'
change
'
,
firstOptionValue
);
}
},
selectOption
(
e
)
{
this
.
$emit
(
'
change
'
,
Number
(
e
.
target
.
value
));
},
},
};
</
script
>
<
template
>
<div
class=
"project-feature-controls"
:data-for=
"name"
>
<input
v-if=
"name"
type=
"hidden"
:name=
"name"
:value=
"value"
/>
<project-feature-toggle
:value=
"featureEnabled"
@
change=
"toggleFeature"
:disabledInput=
"disabledInput"
/>
<div
class=
"select-wrapper"
>
<select
class=
"form-control project-repo-select select-control"
@
change=
"selectOption"
:disabled=
"displaySelectInput"
>
<option
v-for=
"[optionValue, optionName] in displayOptions"
:key=
"optionValue"
:value=
"optionValue"
:selected=
"optionValue === value"
>
{{
optionName
}}
</option>
</select>
<i
aria-hidden=
"true"
class=
"fa fa-chevron-down"
></i>
</div>
</div>
</
template
>
app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue
0 → 100644
View file @
93b9c989
<
script
>
export
default
{
props
:
{
name
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
value
:
{
type
:
Boolean
,
required
:
true
,
},
disabledInput
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
},
model
:
{
prop
:
'
value
'
,
event
:
'
change
'
,
},
methods
:
{
toggleFeature
()
{
if
(
!
this
.
disabledInput
)
this
.
$emit
(
'
change
'
,
!
this
.
value
);
},
},
};
</
script
>
<
template
>
<label
class=
"toggle-wrapper"
>
<input
v-if=
"name"
type=
"hidden"
:name=
"name"
:value=
"value"
/>
<button
type=
"button"
aria-label=
"Toggle"
class=
"project-feature-toggle"
data-enabled-text=
"Enabled"
data-disabled-text=
"Disabled"
:class=
"
{ checked: value, disabled: disabledInput }"
@click="toggleFeature"
/>
</label>
</
template
>
app/assets/javascripts/projects/permissions/components/project_setting_row.vue
0 → 100644
View file @
93b9c989
<
script
>
export
default
{
props
:
{
label
:
{
type
:
String
,
required
:
false
,
default
:
null
,
},
helpPath
:
{
type
:
String
,
required
:
false
,
default
:
null
,
},
helpText
:
{
type
:
String
,
required
:
false
,
default
:
null
,
},
},
};
</
script
>
<
template
>
<div
class=
"project-feature-row"
>
<label
v-if=
"label"
class=
"label-light"
>
{{
label
}}
<a
v-if=
"helpPath"
:href=
"helpPath"
target=
"_blank"
>
<i
aria-hidden=
"true"
data-hidden=
"true"
class=
"fa fa-question-circle"
></i>
</a>
</label>
<span
v-if=
"helpText"
class=
"help-block"
>
{{
helpText
}}
</span>
<slot
/>
</div>
</
template
>
app/assets/javascripts/projects/permissions/components/settings_panel.vue
0 → 100644
View file @
93b9c989
<
script
>
import
projectFeatureSetting
from
'
./project_feature_setting.vue
'
;
import
projectFeatureToggle
from
'
./project_feature_toggle.vue
'
;
import
projectSettingRow
from
'
./project_setting_row.vue
'
;
import
{
visibilityOptions
,
visibilityLevelDescriptions
}
from
'
../constants
'
;
import
{
toggleHiddenClassBySelector
}
from
'
../external
'
;
export
default
{
props
:
{
currentSettings
:
{
type
:
Object
,
required
:
true
,
},
canChangeVisibilityLevel
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
allowedVisibilityOptions
:
{
type
:
Array
,
required
:
false
,
default
:
()
=>
[
0
,
10
,
20
],
},
lfsAvailable
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
registryAvailable
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
visibilityHelpPath
:
{
type
:
String
,
required
:
false
,
},
lfsHelpPath
:
{
type
:
String
,
required
:
false
,
},
registryHelpPath
:
{
type
:
String
,
required
:
false
,
},
},
data
()
{
const
defaults
=
{
visibilityOptions
,
visibilityLevel
:
visibilityOptions
.
PUBLIC
,
issuesAccessLevel
:
20
,
repositoryAccessLevel
:
20
,
mergeRequestsAccessLevel
:
20
,
buildsAccessLevel
:
20
,
wikiAccessLevel
:
20
,
snippetsAccessLevel
:
20
,
containerRegistryEnabled
:
true
,
lfsEnabled
:
true
,
requestAccessEnabled
:
true
,
highlightChangesClass
:
false
,
};
return
{
...
defaults
,
...
this
.
currentSettings
};
},
components
:
{
projectFeatureSetting
,
projectFeatureToggle
,
projectSettingRow
,
},
computed
:
{
featureAccessLevelOptions
()
{
const
options
=
[
[
10
,
'
Only Project Members
'
],
];
if
(
this
.
visibilityLevel
!==
visibilityOptions
.
PRIVATE
)
{
options
.
push
([
20
,
'
Everyone With Access
'
]);
}
return
options
;
},
repoFeatureAccessLevelOptions
()
{
return
this
.
featureAccessLevelOptions
.
filter
(
([
value
])
=>
value
<=
this
.
repositoryAccessLevel
,
);
},
repositoryEnabled
()
{
return
this
.
repositoryAccessLevel
>
0
;
},
visibilityLevelDescription
()
{
return
visibilityLevelDescriptions
[
this
.
visibilityLevel
];
},
},
methods
:
{
highlightChanges
()
{
this
.
highlightChangesClass
=
true
;
this
.
$nextTick
(()
=>
{
this
.
highlightChangesClass
=
false
;
});
},
visibilityAllowed
(
option
)
{
return
this
.
allowedVisibilityOptions
.
includes
(
option
);
},
},
watch
:
{
visibilityLevel
(
value
,
oldValue
)
{
if
(
value
===
visibilityOptions
.
PRIVATE
)
{
// when private, features are restricted to "only team members"
this
.
issuesAccessLevel
=
Math
.
min
(
10
,
this
.
issuesAccessLevel
);
this
.
repositoryAccessLevel
=
Math
.
min
(
10
,
this
.
repositoryAccessLevel
);
this
.
mergeRequestsAccessLevel
=
Math
.
min
(
10
,
this
.
mergeRequestsAccessLevel
);
this
.
buildsAccessLevel
=
Math
.
min
(
10
,
this
.
buildsAccessLevel
);
this
.
wikiAccessLevel
=
Math
.
min
(
10
,
this
.
wikiAccessLevel
);
this
.
snippetsAccessLevel
=
Math
.
min
(
10
,
this
.
snippetsAccessLevel
);
this
.
highlightChanges
();
}
else
if
(
oldValue
===
visibilityOptions
.
PRIVATE
)
{
// if changing away from private, make enabled features more permissive
if
(
this
.
issuesAccessLevel
>
0
)
this
.
issuesAccessLevel
=
20
;
if
(
this
.
repositoryAccessLevel
>
0
)
this
.
repositoryAccessLevel
=
20
;
if
(
this
.
mergeRequestsAccessLevel
>
0
)
this
.
mergeRequestsAccessLevel
=
20
;
if
(
this
.
buildsAccessLevel
>
0
)
this
.
buildsAccessLevel
=
20
;
if
(
this
.
wikiAccessLevel
>
0
)
this
.
wikiAccessLevel
=
20
;
if
(
this
.
snippetsAccessLevel
>
0
)
this
.
snippetsAccessLevel
=
20
;
this
.
highlightChanges
();
}
},
repositoryAccessLevel
(
value
,
oldValue
)
{
if
(
value
<
oldValue
)
{
// sub-features cannot have more premissive access level
this
.
mergeRequestsAccessLevel
=
Math
.
min
(
this
.
mergeRequestsAccessLevel
,
value
);
this
.
buildsAccessLevel
=
Math
.
min
(
this
.
buildsAccessLevel
,
value
);
if
(
value
===
0
)
{
this
.
containerRegistryEnabled
=
false
;
this
.
lfsEnabled
=
false
;
}
}
else
if
(
oldValue
===
0
)
{
this
.
mergeRequestsAccessLevel
=
value
;
this
.
buildsAccessLevel
=
value
;
this
.
containerRegistryEnabled
=
true
;
this
.
lfsEnabled
=
true
;
}
},
issuesAccessLevel
(
value
,
oldValue
)
{
if
(
value
===
0
)
toggleHiddenClassBySelector
(
'
.issues-feature
'
,
true
);
else
if
(
oldValue
===
0
)
toggleHiddenClassBySelector
(
'
.issues-feature
'
,
false
);
},
mergeRequestsAccessLevel
(
value
,
oldValue
)
{
if
(
value
===
0
)
toggleHiddenClassBySelector
(
'
.merge-requests-feature
'
,
true
);
else
if
(
oldValue
===
0
)
toggleHiddenClassBySelector
(
'
.merge-requests-feature
'
,
false
);
},
buildsAccessLevel
(
value
,
oldValue
)
{
if
(
value
===
0
)
toggleHiddenClassBySelector
(
'
.builds-feature
'
,
true
);
else
if
(
oldValue
===
0
)
toggleHiddenClassBySelector
(
'
.builds-feature
'
,
false
);
},
},
};
</
script
>
<
template
>
<div>
<div
class=
"project-visibility-setting"
>
<project-setting-row
label=
"Project visibility"
:help-path=
"visibilityHelpPath"
>
<div
class=
"project-feature-controls"
>
<div
class=
"select-wrapper"
>
<select
name=
"project[visibility_level]"
v-model=
"visibilityLevel"
class=
"form-control select-control"
:disabled=
"!canChangeVisibilityLevel"
>
<option
:value=
"visibilityOptions.PRIVATE"
:disabled=
"!visibilityAllowed(visibilityOptions.PRIVATE)"
>
Private
</option>
<option
:value=
"visibilityOptions.INTERNAL"
:disabled=
"!visibilityAllowed(visibilityOptions.INTERNAL)"
>
Internal
</option>
<option
:value=
"visibilityOptions.PUBLIC"
:disabled=
"!visibilityAllowed(visibilityOptions.PUBLIC)"
>
Public
</option>
</select>
<i
aria-hidden=
"true"
data-hidden=
"true"
class=
"fa fa-chevron-down"
></i>
</div>
</div>
<span
class=
"help-block"
>
{{
visibilityLevelDescription
}}
</span>
<label
v-if=
"visibilityLevel !== visibilityOptions.PUBLIC"
class=
"request-access"
>
<input
type=
"hidden"
name=
"project[request_access_enabled]"
:value=
"requestAccessEnabled"
/>
<input
type=
"checkbox"
v-model=
"requestAccessEnabled"
/>
Allow users to request access
</label>
</project-setting-row>
</div>
<div
class=
"project-feature-settings"
:class=
"
{ 'highlight-changes': highlightChangesClass }">
<project-setting-row
label=
"Issues"
help-text=
"Lightweight issue tracking system for this project"
>
<project-feature-setting
name=
"project[project_feature_attributes][issues_access_level]"
:options=
"featureAccessLevelOptions"
v-model=
"issuesAccessLevel"
/>
</project-setting-row>
<project-setting-row
label=
"Repository"
help-text=
"View and edit files in this project"
>
<project-feature-setting
name=
"project[project_feature_attributes][repository_access_level]"
:options=
"featureAccessLevelOptions"
v-model=
"repositoryAccessLevel"
/>
</project-setting-row>
<div
class=
"project-feature-setting-group"
>
<project-setting-row
label=
"Merge requests"
help-text=
"Submit changes to be merged upstream"
>
<project-feature-setting
name=
"project[project_feature_attributes][merge_requests_access_level]"
:options=
"repoFeatureAccessLevelOptions"
v-model=
"mergeRequestsAccessLevel"
:disabledInput=
"!repositoryEnabled"
/>
</project-setting-row>
<project-setting-row
label=
"Pipelines"
help-text=
"Build, test, and deploy your changes"
>
<project-feature-setting
name=
"project[project_feature_attributes][builds_access_level]"
:options=
"repoFeatureAccessLevelOptions"
v-model=
"buildsAccessLevel"
:disabledInput=
"!repositoryEnabled"
/>
</project-setting-row>
<project-setting-row
v-if=
"registryAvailable"
label=
"Container registry"
:help-path=
"registryHelpPath"
help-text=
"Every project can have its own space to store its Docker images"
>
<project-feature-toggle
name=
"project[container_registry_enabled]"
v-model=
"containerRegistryEnabled"
:disabledInput=
"!repositoryEnabled"
/>
</project-setting-row>
<project-setting-row
v-if=
"lfsAvailable"
label=
"Git Large File Storage"
:help-path=
"lfsHelpPath"
help-text=
"Manages large files such as audio, video, and graphics files"
>
<project-feature-toggle
name=
"project[lfs_enabled]"
v-model=
"lfsEnabled"
:disabledInput=
"!repositoryEnabled"
/>
</project-setting-row>
</div>
<project-setting-row
label=
"Wiki"
help-text=
"Pages for project documentation"
>
<project-feature-setting
name=
"project[project_feature_attributes][wiki_access_level]"
:options=
"featureAccessLevelOptions"
v-model=
"wikiAccessLevel"
/>
</project-setting-row>
<project-setting-row
label=
"Snippets"
help-text=
"Share code pastes with others out of Git repository"
>
<project-feature-setting
name=
"project[project_feature_attributes][snippets_access_level]"
:options=
"featureAccessLevelOptions"
v-model=
"snippetsAccessLevel"
/>
</project-setting-row>
</div>
</div>
</
template
>
app/assets/javascripts/projects/permissions/constants.js
0 → 100644
View file @
93b9c989
export
const
visibilityOptions
=
{
PRIVATE
:
0
,
INTERNAL
:
10
,
PUBLIC
:
20
,
};
export
const
visibilityLevelDescriptions
=
{
[
visibilityOptions
.
PRIVATE
]:
'
The project is accessible only by members of the project. Access must be granted explicitly to each user.
'
,
[
visibilityOptions
.
INTERNAL
]:
'
The project can be accessed by any user who is logged in.
'
,
[
visibilityOptions
.
PUBLIC
]:
'
The project can be accessed by anyone, regardless of authentication.
'
,
};
app/assets/javascripts/projects/permissions/external.js
0 → 100644
View file @
93b9c989
const
selectorCache
=
[];
// workaround since we don't have a polyfill for classList.toggle 2nd parameter
export
function
toggleHiddenClass
(
element
,
hidden
)
{
if
(
hidden
)
{
element
.
classList
.
add
(
'
hidden
'
);
}
else
{
element
.
classList
.
remove
(
'
hidden
'
);
}
}
// hide external feature-specific settings when a given feature is disabled
export
function
toggleHiddenClassBySelector
(
selector
,
hidden
)
{
if
(
!
selectorCache
[
selector
])
{
selectorCache
[
selector
]
=
document
.
querySelectorAll
(
selector
);
}
selectorCache
[
selector
].
forEach
(
elm
=>
toggleHiddenClass
(
elm
,
hidden
));
}
app/assets/javascripts/projects/permissions/index.js
0 → 100644
View file @
93b9c989
import
Vue
from
'
vue
'
;
import
settingsPanel
from
'
./components/settings_panel.vue
'
;
export
default
function
initProjectPermissionsSettings
()
{
const
mountPoint
=
document
.
querySelector
(
'
.js-project-permissions-form
'
);
const
componentPropsEl
=
document
.
querySelector
(
'
.js-project-permissions-form-data
'
);
const
componentProps
=
JSON
.
parse
(
componentPropsEl
.
innerHTML
);
return
new
Vue
({
el
:
mountPoint
,
render
:
createElement
=>
createElement
(
settingsPanel
,
{
props
:
{
...
componentProps
}
}),
});
}
app/assets/stylesheets/framework/variables.scss
View file @
93b9c989
...
...
@@ -579,6 +579,11 @@ $project-breadcrumb-color: #999;
$project-private-forks-notice-odd
:
$green-600
;
$project-network-controls-color
:
#888
;
$feature-toggle-color
:
#fff
;
$feature-toggle-text-color
:
#fff
;
$feature-toggle-color-disabled
:
#999
;
$feature-toggle-color-enabled
:
#4a8bee
;
/*
* Runners
*/
...
...
app/assets/stylesheets/pages/projects.scss
View file @
93b9c989
...
...
@@ -10,41 +10,6 @@
.edit-project
,
.import-project
{