Skip to content
Snippets Groups Projects
Commit 31dd86b6 authored by Francisco Javier López's avatar Francisco Javier López Committed by Douwe Maan
Browse files

Projects and groups badges settings UI

parent dd552d06
No related branches found
No related tags found
No related merge requests found
Showing
with 1017 additions and 2 deletions
<script>
import Icon from '~/vue_shared/components/icon.vue';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip';
export default {
name: 'Badge',
components: {
Icon,
LoadingIcon,
Tooltip,
},
directives: {
Tooltip,
},
props: {
imageUrl: {
type: String,
required: true,
},
linkUrl: {
type: String,
required: true,
},
},
data() {
return {
hasError: false,
isLoading: true,
numRetries: 0,
};
},
computed: {
imageUrlWithRetries() {
if (this.numRetries === 0) {
return this.imageUrl;
}
return `${this.imageUrl}#retries=${this.numRetries}`;
},
},
watch: {
imageUrl() {
this.hasError = false;
this.isLoading = true;
this.numRetries = 0;
},
},
methods: {
onError() {
this.isLoading = false;
this.hasError = true;
},
onLoad() {
this.isLoading = false;
},
reloadImage() {
this.hasError = false;
this.isLoading = true;
this.numRetries += 1;
},
},
};
</script>
<template>
<div>
<a
v-show="!isLoading && !hasError"
:href="linkUrl"
target="_blank"
rel="noopener noreferrer"
>
<img
class="project-badge"
:src="imageUrlWithRetries"
@load="onLoad"
@error="onError"
aria-hidden="true"
/>
</a>
<loading-icon
v-show="isLoading"
:inline="true"
/>
<div
v-show="hasError"
class="btn-group"
>
<div class="btn btn-default btn-xs disabled">
<icon
class="prepend-left-8 append-right-8"
name="doc_image"
:size="16"
aria-hidden="true"
/>
</div>
<div
class="btn btn-default btn-xs disabled"
>
<span class="prepend-left-8 append-right-8">{{ s__('Badges|No badge image') }}</span>
</div>
</div>
<button
v-show="hasError"
class="btn btn-transparent btn-xs text-primary"
type="button"
v-tooltip
:title="s__('Badges|Reload badge image')"
@click="reloadImage"
>
<icon
name="retry"
:size="16"
/>
</button>
</div>
</template>
<script>
import _ from 'underscore';
import { mapActions, mapState } from 'vuex';
import createFlash from '~/flash';
import { s__, sprintf } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import createEmptyBadge from '../empty_badge';
import Badge from './badge.vue';
const badgePreviewDelayInMilliseconds = 1500;
export default {
name: 'BadgeForm',
components: {
Badge,
LoadingButton,
LoadingIcon,
},
props: {
isEditing: {
type: Boolean,
required: true,
},
},
computed: {
...mapState([
'badgeInAddForm',
'badgeInEditForm',
'docsUrl',
'isRendering',
'isSaving',
'renderedBadge',
]),
badge() {
if (this.isEditing) {
return this.badgeInEditForm;
}
return this.badgeInAddForm;
},
canSubmit() {
return (
this.badge !== null &&
this.badge.imageUrl &&
this.badge.imageUrl.trim() !== '' &&
this.badge.linkUrl &&
this.badge.linkUrl.trim() !== '' &&
!this.isSaving
);
},
helpText() {
const placeholders = ['project_path', 'project_id', 'default_branch', 'commit_sha']
.map(placeholder => `<code>%{${placeholder}}</code>`)
.join(', ');
return sprintf(
s__('Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}'),
{
docsLinkEnd: '</a>',
docsLinkStart: `<a href="${_.escape(this.docsUrl)}">`,
placeholders,
},
false,
);
},
renderedImageUrl() {
return this.renderedBadge ? this.renderedBadge.renderedImageUrl : '';
},
renderedLinkUrl() {
return this.renderedBadge ? this.renderedBadge.renderedLinkUrl : '';
},
imageUrl: {
get() {
return this.badge ? this.badge.imageUrl : '';
},
set(imageUrl) {
const badge = this.badge || createEmptyBadge();
this.updateBadgeInForm({
...badge,
imageUrl,
});
},
},
linkUrl: {
get() {
return this.badge ? this.badge.linkUrl : '';
},
set(linkUrl) {
const badge = this.badge || createEmptyBadge();
this.updateBadgeInForm({
...badge,
linkUrl,
});
},
},
submitButtonLabel() {
if (this.isEditing) {
return s__('Badges|Save changes');
}
return s__('Badges|Add badge');
},
},
methods: {
...mapActions(['addBadge', 'renderBadge', 'saveBadge', 'stopEditing', 'updateBadgeInForm']),
debouncedPreview: _.debounce(function preview() {
this.renderBadge();
}, badgePreviewDelayInMilliseconds),
onCancel() {
this.stopEditing();
},
onSubmit() {
if (!this.canSubmit) {
return Promise.resolve();
}
if (this.isEditing) {
return this.saveBadge()
.then(() => {
createFlash(s__('Badges|The badge was saved.'), 'notice');
})
.catch(error => {
createFlash(
s__('Badges|Saving the badge failed, please check the entered URLs and try again.'),
);
throw error;
});
}
return this.addBadge()
.then(() => {
createFlash(s__('Badges|A new badge was added.'), 'notice');
})
.catch(error => {
createFlash(
s__('Badges|Adding the badge failed, please check the entered URLs and try again.'),
);
throw error;
});
},
},
badgeImageUrlPlaceholder:
'https://example.gitlab.com/%{project_path}/badges/%{default_branch}/<badge>.svg',
badgeLinkUrlPlaceholder: 'https://example.gitlab.com/%{project_path}',
};
</script>
<template>
<form
class="prepend-top-default append-bottom-default"
@submit.prevent.stop="onSubmit"
>
<div class="form-group">
<label for="badge-link-url">{{ s__('Badges|Link') }}</label>
<input
id="badge-link-url"
type="text"
class="form-control"
v-model="linkUrl"
:placeholder="$options.badgeLinkUrlPlaceholder"
@input="debouncedPreview"
/>
<span
class="help-block"
v-html="helpText"
></span>
</div>
<div class="form-group">
<label for="badge-image-url">{{ s__('Badges|Badge image URL') }}</label>
<input
id="badge-image-url"
type="text"
class="form-control"
v-model="imageUrl"
:placeholder="$options.badgeImageUrlPlaceholder"
@input="debouncedPreview"
/>
<span
class="help-block"
v-html="helpText"
></span>
</div>
<div class="form-group">
<label for="badge-preview">{{ s__('Badges|Badge image preview') }}</label>
<badge
id="badge-preview"
v-show="renderedBadge && !isRendering"
:image-url="renderedImageUrl"
:link-url="renderedLinkUrl"
/>
<p v-show="isRendering">
<loading-icon
:inline="true"
/>
</p>
<p
v-show="!renderedBadge && !isRendering"
class="disabled-content"
>{{ s__('Badges|No image to preview') }}</p>
</div>
<div class="row-content-block">
<loading-button
type="submit"
container-class="btn btn-success"
:disabled="!canSubmit"
:loading="isSaving"
:label="submitButtonLabel"
/>
<button
class="btn btn-cancel"
type="button"
v-if="isEditing"
@click="onCancel"
>{{ __('Cancel') }}</button>
</div>
</form>
</template>
<script>
import { mapState } from 'vuex';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import BadgeListRow from './badge_list_row.vue';
import { GROUP_BADGE } from '../constants';
export default {
name: 'BadgeList',
components: {
BadgeListRow,
LoadingIcon,
},
computed: {
...mapState(['badges', 'isLoading', 'kind']),
hasNoBadges() {
return !this.isLoading && (!this.badges || !this.badges.length);
},
isGroupBadge() {
return this.kind === GROUP_BADGE;
},
},
};
</script>
<template>
<div class="panel panel-default">
<div class="panel-heading">
{{ s__('Badges|Your badges') }}
<span
v-show="!isLoading"
class="badge"
>{{ badges.length }}</span>
</div>
<loading-icon
v-show="isLoading"
class="panel-body"
size="2"
/>
<div
v-if="hasNoBadges"
class="panel-body"
>
<span v-if="isGroupBadge">{{ s__('Badges|This group has no badges') }}</span>
<span v-else>{{ s__('Badges|This project has no badges') }}</span>
</div>
<div
v-else
class="panel-body"
>
<badge-list-row
v-for="badge in badges"
:key="badge.id"
:badge="badge"
/>
</div>
</div>
</template>
<script>
import { mapActions, mapState } from 'vuex';
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import { PROJECT_BADGE } from '../constants';
import Badge from './badge.vue';
export default {
name: 'BadgeListRow',
components: {
Badge,
Icon,
LoadingIcon,
},
props: {
badge: {
type: Object,
required: true,
},
},
computed: {
...mapState(['kind']),
badgeKindText() {
if (this.badge.kind === PROJECT_BADGE) {
return s__('Badges|Project Badge');
}
return s__('Badges|Group Badge');
},
canEditBadge() {
return this.badge.kind === this.kind;
},
},
methods: {
...mapActions(['editBadge', 'updateBadgeInModal']),
},
};
</script>
<template>
<div class="gl-responsive-table-row-layout gl-responsive-table-row">
<badge
class="table-section section-30"
:image-url="badge.renderedImageUrl"
:link-url="badge.renderedLinkUrl"
/>
<span class="table-section section-50 str-truncated">{{ badge.linkUrl }}</span>
<div class="table-section section-10">
<span class="badge">{{ badgeKindText }}</span>
</div>
<div class="table-section section-10 table-button-footer">
<div
v-if="canEditBadge"
class="table-action-buttons">
<button
class="btn btn-default append-right-8"
type="button"
:disabled="badge.isDeleting"
@click="editBadge(badge)"
>
<icon
name="pencil"
:size="16"
:aria-label="__('Edit')"
/>
</button>
<button
class="btn btn-danger"
type="button"
data-toggle="modal"
data-target="#delete-badge-modal"
:disabled="badge.isDeleting"
@click="updateBadgeInModal(badge)"
>
<icon
name="remove"
:size="16"
:aria-label="__('Delete')"
/>
</button>
<loading-icon
v-show="badge.isDeleting"
:inline="true"
/>
</div>
</div>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import Badge from './badge.vue';
import BadgeForm from './badge_form.vue';
import BadgeList from './badge_list.vue';
export default {
name: 'BadgeSettings',
components: {
Badge,
BadgeForm,
BadgeList,
GlModal,
},
computed: {
...mapState(['badgeInModal', 'isEditing']),
deleteModalText() {
return s__(
'Badges|You are going to delete this badge. Deleted badges <strong>cannot</strong> be restored.',
);
},
},
methods: {
...mapActions(['deleteBadge']),
onSubmitModal() {
this.deleteBadge(this.badgeInModal)
.then(() => {
createFlash(s__('Badges|The badge was deleted.'), 'notice');
})
.catch(error => {
createFlash(s__('Badges|Deleting the badge failed, please try again.'));
throw error;
});
},
},
};
</script>
<template>
<div class="badge-settings">
<gl-modal
id="delete-badge-modal"
:header-title-text="s__('Badges|Delete badge?')"
footer-primary-button-variant="danger"
:footer-primary-button-text="s__('Badges|Delete badge')"
@submit="onSubmitModal">
<div class="well">
<badge
:image-url="badgeInModal ? badgeInModal.renderedImageUrl : ''"
:link-url="badgeInModal ? badgeInModal.renderedLinkUrl : ''"
/>
</div>
<p v-html="deleteModalText"></p>
</gl-modal>
<badge-form
v-show="isEditing"
:is-editing="true"
/>
<badge-form
v-show="!isEditing"
:is-editing="false"
/>
<badge-list v-show="!isEditing" />
</div>
</template>
export const GROUP_BADGE = 'group';
export const PROJECT_BADGE = 'project';
export default () => ({
imageUrl: '',
isDeleting: false,
linkUrl: '',
renderedImageUrl: '',
renderedLinkUrl: '',
});
import axios from '~/lib/utils/axios_utils';
import types from './mutation_types';
export const transformBackendBadge = badge => ({
id: badge.id,
imageUrl: badge.image_url,
kind: badge.kind,
linkUrl: badge.link_url,
renderedImageUrl: badge.rendered_image_url,
renderedLinkUrl: badge.rendered_link_url,
isDeleting: false,
});
export default {
requestNewBadge({ commit }) {
commit(types.REQUEST_NEW_BADGE);
},
receiveNewBadge({ commit }, newBadge) {
commit(types.RECEIVE_NEW_BADGE, newBadge);
},
receiveNewBadgeError({ commit }) {
commit(types.RECEIVE_NEW_BADGE_ERROR);
},
addBadge({ dispatch, state }) {
const newBadge = state.badgeInAddForm;
const endpoint = state.apiEndpointUrl;
dispatch('requestNewBadge');
return axios
.post(endpoint, {
image_url: newBadge.imageUrl,
link_url: newBadge.linkUrl,
})
.catch(error => {
dispatch('receiveNewBadgeError');
throw error;
})
.then(res => {
dispatch('receiveNewBadge', transformBackendBadge(res.data));
});
},
requestDeleteBadge({ commit }, badgeId) {
commit(types.REQUEST_DELETE_BADGE, badgeId);
},
receiveDeleteBadge({ commit }, badgeId) {
commit(types.RECEIVE_DELETE_BADGE, badgeId);
},
receiveDeleteBadgeError({ commit }, badgeId) {
commit(types.RECEIVE_DELETE_BADGE_ERROR, badgeId);
},
deleteBadge({ dispatch, state }, badge) {
const badgeId = badge.id;
dispatch('requestDeleteBadge', badgeId);
const endpoint = `${state.apiEndpointUrl}/${badgeId}`;
return axios
.delete(endpoint)
.catch(error => {
dispatch('receiveDeleteBadgeError', badgeId);
throw error;
})
.then(() => {
dispatch('receiveDeleteBadge', badgeId);
});
},
editBadge({ commit }, badge) {
commit(types.START_EDITING, badge);
},
requestLoadBadges({ commit }, data) {
commit(types.REQUEST_LOAD_BADGES, data);
},
receiveLoadBadges({ commit }, badges) {
commit(types.RECEIVE_LOAD_BADGES, badges);
},
receiveLoadBadgesError({ commit }) {
commit(types.RECEIVE_LOAD_BADGES_ERROR);
},
loadBadges({ dispatch, state }, data) {
dispatch('requestLoadBadges', data);
const endpoint = state.apiEndpointUrl;
return axios
.get(endpoint)
.catch(error => {
dispatch('receiveLoadBadgesError');
throw error;
})
.then(res => {
dispatch('receiveLoadBadges', res.data.map(transformBackendBadge));
});
},
requestRenderedBadge({ commit }) {
commit(types.REQUEST_RENDERED_BADGE);
},
receiveRenderedBadge({ commit }, renderedBadge) {
commit(types.RECEIVE_RENDERED_BADGE, renderedBadge);
},
receiveRenderedBadgeError({ commit }) {
commit(types.RECEIVE_RENDERED_BADGE_ERROR);
},
renderBadge({ dispatch, state }) {
const badge = state.isEditing ? state.badgeInEditForm : state.badgeInAddForm;
const { linkUrl, imageUrl } = badge;
if (!linkUrl || linkUrl.trim() === '' || !imageUrl || imageUrl.trim() === '') {
return Promise.resolve(badge);
}
dispatch('requestRenderedBadge');
const parameters = [
`link_url=${encodeURIComponent(linkUrl)}`,
`image_url=${encodeURIComponent(imageUrl)}`,
].join('&');
const renderEndpoint = `${state.apiEndpointUrl}/render?${parameters}`;
return axios
.get(renderEndpoint)
.catch(error => {
dispatch('receiveRenderedBadgeError');
throw error;
})
.then(res => {
dispatch('receiveRenderedBadge', transformBackendBadge(res.data));
});
},
requestUpdatedBadge({ commit }) {
commit(types.REQUEST_UPDATED_BADGE);
},
receiveUpdatedBadge({ commit }, updatedBadge) {
commit(types.RECEIVE_UPDATED_BADGE, updatedBadge);
},
receiveUpdatedBadgeError({ commit }) {
commit(types.RECEIVE_UPDATED_BADGE_ERROR);
},
saveBadge({ dispatch, state }) {
const badge = state.badgeInEditForm;
const endpoint = `${state.apiEndpointUrl}/${badge.id}`;
dispatch('requestUpdatedBadge');
return axios
.put(endpoint, {
image_url: badge.imageUrl,
link_url: badge.linkUrl,
})
.catch(error => {
dispatch('receiveUpdatedBadgeError');
throw error;
})
.then(res => {
dispatch('receiveUpdatedBadge', transformBackendBadge(res.data));
});
},
stopEditing({ commit }) {
commit(types.STOP_EDITING);
},
updateBadgeInForm({ commit }, badge) {
commit(types.UPDATE_BADGE_IN_FORM, badge);
},
updateBadgeInModal({ commit }, badge) {
commit(types.UPDATE_BADGE_IN_MODAL, badge);
},
};
import Vue from 'vue';
import Vuex from 'vuex';
import createState from './state';
import actions from './actions';
import mutations from './mutations';
Vue.use(Vuex);
export default new Vuex.Store({
state: createState(),
actions,
mutations,
});
export default {
RECEIVE_DELETE_BADGE: 'RECEIVE_DELETE_BADGE',
RECEIVE_DELETE_BADGE_ERROR: 'RECEIVE_DELETE_BADGE_ERROR',
RECEIVE_LOAD_BADGES: 'RECEIVE_LOAD_BADGES',
RECEIVE_LOAD_BADGES_ERROR: 'RECEIVE_LOAD_BADGES_ERROR',
RECEIVE_NEW_BADGE: 'RECEIVE_NEW_BADGE',
RECEIVE_NEW_BADGE_ERROR: 'RECEIVE_NEW_BADGE_ERROR',
RECEIVE_RENDERED_BADGE: 'RECEIVE_RENDERED_BADGE',
RECEIVE_RENDERED_BADGE_ERROR: 'RECEIVE_RENDERED_BADGE_ERROR',
RECEIVE_UPDATED_BADGE: 'RECEIVE_UPDATED_BADGE',
RECEIVE_UPDATED_BADGE_ERROR: 'RECEIVE_UPDATED_BADGE_ERROR',
REQUEST_DELETE_BADGE: 'REQUEST_DELETE_BADGE',
REQUEST_LOAD_BADGES: 'REQUEST_LOAD_BADGES',
REQUEST_NEW_BADGE: 'REQUEST_NEW_BADGE',
REQUEST_RENDERED_BADGE: 'REQUEST_RENDERED_BADGE',
REQUEST_UPDATED_BADGE: 'REQUEST_UPDATED_BADGE',
START_EDITING: 'START_EDITING',
STOP_EDITING: 'STOP_EDITING',
UPDATE_BADGE_IN_FORM: 'UPDATE_BADGE_IN_FORM',
UPDATE_BADGE_IN_MODAL: 'UPDATE_BADGE_IN_MODAL',
};
import types from './mutation_types';
import { PROJECT_BADGE } from '../constants';
const reorderBadges = badges =>
badges.sort((a, b) => {
if (a.kind !== b.kind) {
return a.kind === PROJECT_BADGE ? 1 : -1;
}
return a.id - b.id;
});
export default {
[types.RECEIVE_NEW_BADGE](state, newBadge) {
Object.assign(state, {
badgeInAddForm: null,
badges: reorderBadges(state.badges.concat(newBadge)),
isSaving: false,
renderedBadge: null,
});
},
[types.RECEIVE_NEW_BADGE_ERROR](state) {
Object.assign(state, {
isSaving: false,
});
},
[types.REQUEST_NEW_BADGE](state) {
Object.assign(state, {
isSaving: true,
});
},
[types.RECEIVE_UPDATED_BADGE](state, updatedBadge) {
const badges = state.badges.map(badge => {
if (badge.id === updatedBadge.id) {
return updatedBadge;
}
return badge;
});
Object.assign(state, {
badgeInEditForm: null,
badges,
isEditing: false,
isSaving: false,
renderedBadge: null,
});
},
[types.RECEIVE_UPDATED_BADGE_ERROR](state) {
Object.assign(state, {
isSaving: false,
});
},
[types.REQUEST_UPDATED_BADGE](state) {
Object.assign(state, {
isSaving: true,
});
},
[types.RECEIVE_LOAD_BADGES](state, badges) {
Object.assign(state, {
badges: reorderBadges(badges),
isLoading: false,
});
},
[types.RECEIVE_LOAD_BADGES_ERROR](state) {
Object.assign(state, {
isLoading: false,
});
},
[types.REQUEST_LOAD_BADGES](state, data) {
Object.assign(state, {
kind: data.kind, // project or group
apiEndpointUrl: data.apiEndpointUrl,
docsUrl: data.docsUrl,
isLoading: true,
});
},
[types.RECEIVE_DELETE_BADGE](state, badgeId) {
const badges = state.badges.filter(badge => badge.id !== badgeId);
Object.assign(state, {
badges,
});
},
[types.RECEIVE_DELETE_BADGE_ERROR](state, badgeId) {
const badges = state.badges.map(badge => {
if (badge.id === badgeId) {
return {
...badge,
isDeleting: false,
};
}
return badge;
});
Object.assign(state, {
badges,
});
},
[types.REQUEST_DELETE_BADGE](state, badgeId) {
const badges = state.badges.map(badge => {
if (badge.id === badgeId) {
return {
...badge,
isDeleting: true,
};
}
return badge;
});
Object.assign(state, {
badges,
});
},
[types.RECEIVE_RENDERED_BADGE](state, renderedBadge) {
Object.assign(state, { isRendering: false, renderedBadge });
},
[types.RECEIVE_RENDERED_BADGE_ERROR](state) {
Object.assign(state, { isRendering: false });
},
[types.REQUEST_RENDERED_BADGE](state) {
Object.assign(state, { isRendering: true });
},
[types.START_EDITING](state, badge) {
Object.assign(state, {
badgeInEditForm: { ...badge },
isEditing: true,
renderedBadge: { ...badge },
});
},
[types.STOP_EDITING](state) {
Object.assign(state, {
badgeInEditForm: null,
isEditing: false,
renderedBadge: null,
});
},
[types.UPDATE_BADGE_IN_FORM](state, badge) {
if (state.isEditing) {
Object.assign(state, {
badgeInEditForm: badge,
});
} else {
Object.assign(state, {
badgeInAddForm: badge,
});
}
},
[types.UPDATE_BADGE_IN_MODAL](state, badge) {
Object.assign(state, {
badgeInModal: badge,
});
},
};
export default () => ({
apiEndpointUrl: null,
badgeInAddForm: null,
badgeInEditForm: null,
badgeInModal: null,
badges: [],
docsUrl: null,
renderedBadge: null,
isEditing: false,
isLoading: false,
isRendering: false,
isSaving: false,
});
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import { GROUP_BADGE } from '~/badges/constants';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => {
mountBadgeSettings(GROUP_BADGE);
});
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import { PROJECT_BADGE } from '~/badges/constants';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => {
mountBadgeSettings(PROJECT_BADGE);
});
import Vue from 'vue';
import BadgeSettings from '~/badges/components/badge_settings.vue';
import store from '~/badges/store';
export default kind => {
const badgeSettingsElement = document.getElementById('badge-settings');
store.dispatch('loadBadges', {
kind,
apiEndpointUrl: badgeSettingsElement.dataset.apiEndpointUrl,
docsUrl: badgeSettingsElement.dataset.docsUrl,
});
return new Vue({
el: badgeSettingsElement,
store,
components: {
BadgeSettings,
},
render(createElement) {
return createElement(BadgeSettings);
},
});
};
Loading
Loading
@@ -39,7 +39,7 @@
.table-section {
white-space: nowrap;
 
$section-widths: 10 15 20 25 30 40 100;
$section-widths: 10 15 20 25 30 40 50 100;
@each $width in $section-widths {
&.section-#{$width} {
flex: 0 0 #{$width + '%'};
Loading
Loading
Loading
Loading
@@ -1143,3 +1143,11 @@ pre.light-well {
white-space: pre-wrap;
}
}
.project-badge {
opacity: 0.9;
&:hover {
opacity: 1;
}
}
module Groups
module Settings
class BadgesController < Groups::ApplicationController
include GrapeRouteHelpers::NamedRouteMatcher
before_action :authorize_admin_group!
def index
@badge_api_endpoint = api_v4_groups_badges_path(id: @group.id)
end
end
end
end
module Projects
module Settings
class BadgesController < Projects::ApplicationController
include GrapeRouteHelpers::NamedRouteMatcher
before_action :authorize_admin_project!
def index
@badge_api_endpoint = api_v4_projects_badges_path(id: @project.id)
end
end
end
end
module GroupsHelper
def group_nav_link_paths
%w[groups#projects groups#edit ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index]
%w[groups#projects groups#edit badges#index ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index]
end
 
def group_sidebar_links
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment