mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-03-01 15:17:07 +08:00
removed a bunch more stuff that's already done
This commit is contained in:
parent
57a9cd1ceb
commit
5c34e74455
@ -1,448 +0,0 @@
|
||||
<template>
|
||||
<button
|
||||
class="btn btn-primary btn-block"
|
||||
type="button"
|
||||
data-toggle="collapse"
|
||||
data-target="#dep-collapse"
|
||||
aria-expanded="false"
|
||||
aria-controls="dep-collapse"
|
||||
>
|
||||
Manage Platforms/Dependencies
|
||||
</button>
|
||||
<div id="dep-collapse" class="collapse">
|
||||
<div v-if="!loading" class="container-fluid">
|
||||
<template v-for="(platform, platformKey) in platformInfo" :key="platformKey">
|
||||
<div
|
||||
:id="`${platformKey}-row`"
|
||||
class="row platform-row align-items-center"
|
||||
:style="{
|
||||
backgroundColor: !platforms[platformKey] ? '#00000022' : platform.tag.background + '45',
|
||||
}"
|
||||
>
|
||||
<div class="platform-row-overlay" :style="{ zIndex: !platforms[platformKey] ? 5 : -10 }"></div>
|
||||
<div class="col-1 d-flex align-items-center" style="z-index: 10">
|
||||
<input
|
||||
:id="`${platformKey}-is-enabled`"
|
||||
type="checkbox"
|
||||
:checked="platforms[platformKey]"
|
||||
:disabled="freezePlatforms"
|
||||
class="mr-2"
|
||||
@change="selectPlatform(platformKey)"
|
||||
/>
|
||||
<label :for="`${platformKey}-is-enabled`">
|
||||
<span
|
||||
class="platform-header p-1 rounded"
|
||||
:style="{
|
||||
color: platform.tag.foreground,
|
||||
backgroundColor: platform.tag.background,
|
||||
borderColor: platform.tag.background,
|
||||
}"
|
||||
>
|
||||
{{ platform.name }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="clearfix"></div>
|
||||
<div>
|
||||
<div class="row no-gutters">
|
||||
<!--is there a better way of making two columns?-->
|
||||
<div
|
||||
v-for="(versionList, index) in [
|
||||
platform.possibleVersions.slice(0, Math.ceil(platform.possibleVersions.length / 2)),
|
||||
platform.possibleVersions.slice(Math.ceil(platform.possibleVersions.length / 2)),
|
||||
]"
|
||||
:key="index"
|
||||
class="col-6 text-right d-flex flex-column justify-content-start"
|
||||
>
|
||||
<div v-for="version in versionList" :key="version">
|
||||
<label :for="`${platformKey}-version-${version}`">
|
||||
{{ version }}
|
||||
</label>
|
||||
<input
|
||||
:id="`${platformKey}-version-${version}`"
|
||||
type="checkbox"
|
||||
:value="version"
|
||||
v-model="platforms[platformKey]"
|
||||
class="mr-2"
|
||||
:disabled="!platforms[platformKey]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-8 text-center">
|
||||
<form :ref="`${platformKey.toLowerCase()}DepForm`" autocomplete="off">
|
||||
<table
|
||||
v-if="(dependencies[platformKey] && dependencies[platformKey].length) || !freezePlatforms"
|
||||
class="table table-bordered table-dark dependency-table"
|
||||
>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Required</th>
|
||||
<th>Link</th>
|
||||
</tr>
|
||||
<tr v-for="dep in dependencies[platformKey]" :key="dep.name">
|
||||
<td class="text-center">
|
||||
<div class="w-100">
|
||||
{{ dep.name }}
|
||||
</div>
|
||||
|
||||
<button v-if="!freezePlatforms" class="btn btn-danger" @click="removeDepFromTable(platformKey, dep.name)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<span :style="{ fontSize: '3rem', color: dep.required ? 'green' : 'red' }">
|
||||
<i class="fas" :class="`${dep.required ? 'fa-check-circle' : 'fa-times-circle'}`"></i>
|
||||
</span>
|
||||
</td>
|
||||
<td :id="`${platformKey}-${dep.name}-link-cell`">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">
|
||||
<input
|
||||
type="radio"
|
||||
value="Hangar"
|
||||
aria-label="Select Hangar Project"
|
||||
v-model="dependencyLinking[platformKey][dep.name]"
|
||||
@change="linkingClick('external_url', platformKey, dep.name)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
:id="`${platformKey}-${dep.name}-project-input`"
|
||||
type="text"
|
||||
class="form-control"
|
||||
aria-label="Hangar Project Name"
|
||||
placeholder="Search for project..."
|
||||
:required="dependencyLinking[platformKey][dep.name] !== 'External'"
|
||||
:disabled="dependencyLinking[platformKey][dep.name] !== 'Hangar'"
|
||||
@focus="toggleFocus(platformKey, dep.name, true)"
|
||||
@blur="toggleFocus(platformKey, dep.name, false)"
|
||||
@input="projectSearch($event.target, platformKey, dep.name)"
|
||||
/>
|
||||
<ul :id="`${platformKey}-${dep.name}-project-dropdown`" class="search-dropdown list-group" style="display: none">
|
||||
<li
|
||||
v-for="project in searchResults"
|
||||
:key="`${project.namespace.owner}/${project.namespace.slug}`"
|
||||
class="list-group-item list-group-item-dark"
|
||||
@mousedown.prevent=""
|
||||
@click="selectProject(platformKey, dep.name, project)"
|
||||
>
|
||||
{{ project.namespace.owner }} /
|
||||
{{ project.namespace.slug }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="input-group mt-1">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">
|
||||
<input
|
||||
type="radio"
|
||||
value="External"
|
||||
aria-label="External Link"
|
||||
v-model="dependencyLinking[platformKey][dep.name]"
|
||||
@change="linkingClick('namespace', platformKey, dep.name)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
:id="`${platformKey}-${dep.name}-external-input`"
|
||||
type="text"
|
||||
class="form-control"
|
||||
aria-label="Hangar Project Name"
|
||||
placeholder="Paste an external link"
|
||||
:required="dependencyLinking[platformKey][dep.name] !== 'Hangar'"
|
||||
:disabled="dependencyLinking[platformKey][dep.name] !== 'External'"
|
||||
@change="setExternalUrl($event.target.value, platformKey, dep.name)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!freezePlatforms">
|
||||
<td colspan="3">
|
||||
<div class="input-group">
|
||||
<input
|
||||
:id="`${platformKey}-new-dep`"
|
||||
type="text"
|
||||
placeholder="Dependency Name"
|
||||
class="form-control"
|
||||
aria-label="New Dependency Name"
|
||||
v-model="addDependency[platformKey].name"
|
||||
/>
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text">
|
||||
<input
|
||||
:id="`${platformKey}-new-dep-required`"
|
||||
class="mr-2"
|
||||
type="checkbox"
|
||||
aria-label="Dependency is required?"
|
||||
v-model="addDependency[platformKey].required"
|
||||
/>
|
||||
<label :for="`${platformKey}-new-dep-required`" style="position: relative; top: 1px"> Required? </label>
|
||||
</div>
|
||||
<button
|
||||
:disabled="!addDependency[platformKey].name || addDependency[platformKey].name.trim().length === 0"
|
||||
class="btn btn-info"
|
||||
type="button"
|
||||
@click="addDepToTable(platformKey)"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<i v-else class="dark-gray">No dependencies</i>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { nextTick } from 'vue';
|
||||
import $ from 'jquery';
|
||||
import 'bootstrap/js/dist/collapse';
|
||||
import { isEmpty, remove, union } from 'lodash-es';
|
||||
|
||||
import { API } from '@/api';
|
||||
|
||||
export default {
|
||||
name: 'DependencySelection',
|
||||
emits: ['update:platformsProp', 'update:dependenciesProp'],
|
||||
props: {
|
||||
platformsProp: Object,
|
||||
dependenciesProp: Object,
|
||||
prevVersion: Object,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
freezePlatforms: false,
|
||||
loading: true,
|
||||
dependencyLinking: {},
|
||||
searchResults: [],
|
||||
platformInfo: {},
|
||||
addDependency: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
platforms: {
|
||||
get() {
|
||||
return this.platformsProp;
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:platformsProp', val);
|
||||
},
|
||||
},
|
||||
dependencies: {
|
||||
get() {
|
||||
return this.dependenciesProp;
|
||||
},
|
||||
set() {
|
||||
this.$emit('update:dependenciesProp');
|
||||
},
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
const data = await API.request('platforms');
|
||||
for (const platformObj of data) {
|
||||
this.platformInfo[platformObj.name.toUpperCase()] = platformObj;
|
||||
this.dependencyLinking[platformObj.name.toUpperCase()] = {};
|
||||
this.addDependency[platformObj.name.toUpperCase()] = {
|
||||
name: '',
|
||||
required: false,
|
||||
};
|
||||
}
|
||||
this.loading = false;
|
||||
await nextTick();
|
||||
|
||||
if (!isEmpty(this.platforms)) {
|
||||
this.freezePlatforms = true;
|
||||
for (const platformName in this.platforms) {
|
||||
if (this.prevVersion && this.prevVersion.platforms.find((plat) => plat.name === platformName)) {
|
||||
this.platforms[platformName] = union(
|
||||
this.platforms[platformName],
|
||||
this.prevVersion.platforms.find((plat) => plat.name === platformName).versions
|
||||
);
|
||||
}
|
||||
|
||||
if (this.prevVersion && this.prevVersion.dependencies[platformName] && this.prevVersion.dependencies[platformName].length) {
|
||||
this.setDependencyLinks(this.prevVersion.dependencies[platformName], platformName);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$('#dep-collapse').collapse('show');
|
||||
if (this.prevVersion) {
|
||||
for (const platformObj of this.prevVersion.platforms) {
|
||||
const platformName = platformObj.name.toUpperCase();
|
||||
this.platforms[platformName] = platformObj.versions;
|
||||
}
|
||||
for (const platform in this.prevVersion.dependencies) {
|
||||
this.dependencies[platform.toUpperCase()] = this.prevVersion.dependencies[platform];
|
||||
}
|
||||
await nextTick();
|
||||
for (const platform in this.dependencies) {
|
||||
this.setDependencyLinks(this.dependencies[platform], platform);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setDependencyLinks(deps, platformName) {
|
||||
for (const dep of deps) {
|
||||
if (dep.namespace) {
|
||||
this.dependencyLinking[platformName][dep.name] = 'Hangar';
|
||||
API.request(`projects/${dep.namespace.owner}/${dep.namespace.slug}`).then((res) => {
|
||||
this.selectProject(platformName, dep.name, res);
|
||||
});
|
||||
} else if (dep.external_url) {
|
||||
$(`#${platformName}-${dep.name}-external-input`).val(dep.external_url);
|
||||
this.dependencyLinking[platformName][dep.name] = 'External';
|
||||
this.setExternalUrl(dep.external_url, platformName, dep.name);
|
||||
}
|
||||
}
|
||||
},
|
||||
isEmpty,
|
||||
linkingClick(toReset, platformKey, depName) {
|
||||
this.dependencies[platformKey].find((dep) => dep.name === depName)[toReset] = null;
|
||||
},
|
||||
toggleFocus(platformKey, depName, showDropdown) {
|
||||
if (showDropdown) {
|
||||
$(`#${platformKey}-${depName}-project-dropdown`).show();
|
||||
} else {
|
||||
$(`#${platformKey}-${depName}-project-dropdown`).hide();
|
||||
}
|
||||
},
|
||||
projectSearch(target, platformKey, depName) {
|
||||
if (target.value) {
|
||||
const inputVal = target.value.trim();
|
||||
const input = $(target);
|
||||
if (input.data('namespace') !== inputVal) {
|
||||
// reset on changing
|
||||
input.data('namespace', '');
|
||||
input.data('id', '');
|
||||
this.dependencies[platformKey].find((dep) => dep.name === depName).namespace = null;
|
||||
}
|
||||
|
||||
API.request(`projects?relevance=true&limit=25&offset=0&q=${inputVal}`).then((res) => {
|
||||
if (res.result.length) {
|
||||
$(`#${platformKey}-${depName}-project-dropdown`).show();
|
||||
this.searchResults = res.result;
|
||||
} else {
|
||||
$(`#${platformKey}-${depName}-project-dropdown`).hide();
|
||||
this.searchResults = [];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$(target).parent().find('.search-dropdown').hide();
|
||||
}
|
||||
},
|
||||
selectProject(platformKey, depName, project) {
|
||||
$(`#${platformKey}-${depName}-project-dropdown`).hide();
|
||||
const input = $(`#${platformKey}-${depName}-project-input`);
|
||||
let namespace = '';
|
||||
if (project.namespace) {
|
||||
namespace = `${project.namespace.owner}/${project.namespace.slug}`;
|
||||
} else {
|
||||
namespace = `${project.author}/${project.slug}`;
|
||||
}
|
||||
input.data('id', project.id);
|
||||
input.data('namespace', namespace);
|
||||
this.dependencies[platformKey].find((dep) => dep.name === depName).namespace = namespace;
|
||||
input.val(namespace);
|
||||
},
|
||||
setExternalUrl(value, platformKey, depName) {
|
||||
this.dependencies[platformKey].find((dep) => dep.name === depName).external_url = value;
|
||||
},
|
||||
selectPlatform(platformKey) {
|
||||
if (!this.platforms[platformKey]) {
|
||||
this.platforms[platformKey] = [];
|
||||
this.dependencies[platformKey] = [];
|
||||
} else {
|
||||
delete this.platforms[platformKey];
|
||||
delete this.dependencies[platformKey];
|
||||
}
|
||||
},
|
||||
addDepToTable(platformKey) {
|
||||
if (!this.dependencies[platformKey]) {
|
||||
this.dependencies[platformKey] = [];
|
||||
}
|
||||
this.dependencies[platformKey].push({
|
||||
name: this.addDependency[platformKey].name,
|
||||
required: this.addDependency[platformKey].required,
|
||||
namespace: null,
|
||||
external_url: null,
|
||||
});
|
||||
this.addDependency[platformKey].name = '';
|
||||
this.addDependency[platformKey].required = false;
|
||||
},
|
||||
removeDepFromTable(platformKey, depName) {
|
||||
remove(this.dependencies[platformKey], (dep) => dep.name === depName);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.search-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
max-height: 300px;
|
||||
z-index: 100;
|
||||
width: 100%;
|
||||
|
||||
li {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.platform-row:not(:first-of-type) {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.platform-row {
|
||||
position: relative;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.platform-row-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.platform-select-div {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.platform-header {
|
||||
border: #aaa 1px solid;
|
||||
box-shadow: #2a2a2a;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.dependency-table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.input-group > input[type='text'].form-control {
|
||||
width: 1%;
|
||||
}
|
||||
|
||||
.input-group input[type='text'] {
|
||||
width: unset;
|
||||
}
|
||||
</style>
|
@ -1,24 +0,0 @@
|
||||
<template>
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-center align-items-center">
|
||||
<div class="col-5 divider divider-before"></div>
|
||||
<div class="divider-text"><slot></slot></div>
|
||||
<div class="col-5 divider divider-after"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Divider',
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.divider-text {
|
||||
padding: 0 5px;
|
||||
}
|
||||
.divider {
|
||||
height: 2px;
|
||||
background-color: #00000082;
|
||||
}
|
||||
</style>
|
@ -1,258 +0,0 @@
|
||||
<template>
|
||||
<template v-if="filteredMembers.length">
|
||||
<div class="card" style="z-index: 2">
|
||||
<div class="card-header">
|
||||
<h3 class="float-left card-title" v-text="$t('project.settings.members')"></h3>
|
||||
<div v-if="canManageMembers" class="float-right">
|
||||
<a v-if="!editable && settingsCall" :href="settingsCall" class="btn bg-warning btn-sm">
|
||||
<i class="fas fa-pencil-alt"></i>
|
||||
</a>
|
||||
<button
|
||||
v-if="saveCall && (Object.keys(form.updates).length || Object.keys(form.additions).length)"
|
||||
class="btn-members-save btn btn-card btn-sm"
|
||||
data-tooltip-toggle
|
||||
data-placement="top"
|
||||
:data-title="$t('org.users.save')"
|
||||
@click.prevent="save"
|
||||
>
|
||||
<i class="fas fa-save"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="list-members list-group">
|
||||
<transition name="slide-down">
|
||||
<li v-show="error" class="list-group-item" style="z-index: -1">
|
||||
<div class="alert alert-danger" role="alert" v-text="error"></div>
|
||||
</li>
|
||||
</transition>
|
||||
<li v-for="({ key: role, value: user }, index) in filteredMembers" :key="index" class="list-group-item">
|
||||
<UserAvatar :user-name="user.name" :avatar-url="avatarUrl(user.name)" clazz="user-avatar-xs"></UserAvatar>
|
||||
<a :href="ROUTES.parse('USERS_SHOW_PROJECTS', user.name)" class="username" v-text="user.name"></a>
|
||||
<template v-if="editable && canManageMembers && (!role.role || (role.role.permissions & isJoinableOwnerPerm) !== isJoinableOwnerPerm)">
|
||||
<a v-if="!role.isEditing" href="#" @click.prevent title="Remove Member" @click="userToDelete = user.name">
|
||||
<i class="ml-1 fas fa-trash fa-xs text-danger" data-toggle="modal" data-target="#modal-user-delete"></i>
|
||||
</a>
|
||||
<a v-if="!role.isEditing" href="#" @click.prevent="edit(role, user)" title="Change Role"><i class="ml-1 fas fa-edit fa-xs"></i></a>
|
||||
<a v-else href="#" @click.prevent="cancel(role, user)" :title="`Cancel ${role.isNew ? 'New' : 'Edit'}`">
|
||||
<i class="ml-1 fas fa-times fa-sm"></i>
|
||||
</a>
|
||||
</template>
|
||||
<span v-if="!role.isEditing" class="minor float-right">
|
||||
<template v-if="!role.isAccepted">
|
||||
<span class="minor">(Invited as {{ role.role.title }})</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ role.role.title }}
|
||||
</template>
|
||||
</span>
|
||||
<select
|
||||
v-if="
|
||||
editable &&
|
||||
canManageMembers &&
|
||||
(!role.role || (role.role.permissions & isJoinableOwnerPerm) !== isJoinableOwnerPerm) &&
|
||||
role.isEditing &&
|
||||
!role.isNew
|
||||
"
|
||||
aria-label="Role Selection"
|
||||
v-model="form.updates[user.name].role"
|
||||
>
|
||||
<option v-for="role in roles" :key="role.value" :value="role.value">{{ role.title }}</option>
|
||||
</select>
|
||||
<select
|
||||
v-if="
|
||||
editable &&
|
||||
canManageMembers &&
|
||||
(!role.role || (role.role.permissions & isJoinableOwnerPerm) !== isJoinableOwnerPerm) &&
|
||||
role.isEditing &&
|
||||
role.isNew
|
||||
"
|
||||
aria-label="Role Selection"
|
||||
v-model="form.additions[user.name].role"
|
||||
>
|
||||
<option v-for="role in roles" :key="role.value" :value="role.value">{{ role.title }}</option>
|
||||
</select>
|
||||
</li>
|
||||
<li v-if="editable && canManageMembers" class="list-group-item">
|
||||
<UserSearch @add-user="addUser"></UserSearch>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<teleport to="body">
|
||||
<div class="modal fade" id="modal-user-delete" tabindex="-1" role="dialog" aria-labelledby="label-user-delete">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="label-user-delete" v-text="$t('project.removeMember._')"></h4>
|
||||
<button type="button" class="close" data-dismiss="modal" :aria-label="$t('general.close')">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" v-text="$t('project.removeMember.confirm')"></div>
|
||||
<div class="modal-footer">
|
||||
<HangarForm :action="removeCall" method="POST" clazz="form-inline">
|
||||
<input v-model="userToDelete" type="hidden" name="username" />
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal" v-text="$t('general.close')"></button>
|
||||
<button type="submit" class="btn btn-danger" v-text="$t('general.remove')"></button>
|
||||
</div>
|
||||
</HangarForm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { remove } from 'lodash-es';
|
||||
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
import UserSearch from '@/components/UserSearch';
|
||||
import HangarForm from '@/components/HangarForm';
|
||||
|
||||
export default {
|
||||
name: 'MemberList',
|
||||
components: {
|
||||
HangarForm,
|
||||
UserSearch,
|
||||
UserAvatar,
|
||||
},
|
||||
props: {
|
||||
filteredMembersProp: Array,
|
||||
canManageMembers: Boolean,
|
||||
editable: Boolean,
|
||||
removeCall: String,
|
||||
settingsCall: String,
|
||||
saveCall: String,
|
||||
roles: Array,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ROUTES: window.ROUTES,
|
||||
filteredMembers: [],
|
||||
isJoinableOwnerPerm: window.ORG_OWNER_PERM || window.PROJECT_OWNER_PERM,
|
||||
form: {
|
||||
updates: {},
|
||||
additions: {},
|
||||
},
|
||||
error: null,
|
||||
userToDelete: null,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.filteredMembers = [...this.filteredMembersProp];
|
||||
},
|
||||
methods: {
|
||||
avatarUrl(username) {
|
||||
return window.AVATAR_URL.replace('%s', username);
|
||||
},
|
||||
edit(role, user) {
|
||||
role.isEditing = true;
|
||||
if (role.isNew) {
|
||||
this.form.additions[user.name] = { role: null, id: user.id };
|
||||
} else {
|
||||
this.form.updates[user.name] = { role: role.role.value, id: user.id };
|
||||
}
|
||||
},
|
||||
save() {
|
||||
const data = {
|
||||
updates: [],
|
||||
additions: [],
|
||||
};
|
||||
for (const name in this.form.updates) {
|
||||
data.updates.push({
|
||||
role: this.form.updates[name].role,
|
||||
id: this.form.updates[name].id,
|
||||
});
|
||||
}
|
||||
for (const name in this.form.additions) {
|
||||
data.additions.push({
|
||||
role: this.form.additions[name].role,
|
||||
id: this.form.additions[name].id,
|
||||
});
|
||||
}
|
||||
axios
|
||||
.post(this.saveCall, this.getForm(), window.ajaxSettings)
|
||||
.then(() => {
|
||||
location.reload();
|
||||
})
|
||||
.catch(() => {
|
||||
console.error('Error updating roles');
|
||||
this.error = 'Error updating roles';
|
||||
setTimeout(
|
||||
(self) => {
|
||||
self.error = null;
|
||||
},
|
||||
5000,
|
||||
this
|
||||
);
|
||||
this.filteredMembers = [...this.filteredMembersProp];
|
||||
this.resetForm();
|
||||
});
|
||||
},
|
||||
resetForm() {
|
||||
this.form = {
|
||||
updates: {},
|
||||
additions: {},
|
||||
};
|
||||
},
|
||||
getForm() {
|
||||
const data = {
|
||||
updates: [],
|
||||
additions: [],
|
||||
};
|
||||
for (const name in this.form.updates) {
|
||||
data.updates.push({
|
||||
role: this.form.updates[name].role,
|
||||
id: this.form.updates[name].id,
|
||||
});
|
||||
}
|
||||
for (const name in this.form.additions) {
|
||||
data.additions.push({
|
||||
role: this.form.additions[name].role,
|
||||
id: this.form.additions[name].id,
|
||||
});
|
||||
}
|
||||
return data;
|
||||
},
|
||||
cancel(role, user) {
|
||||
if (role.isNew) {
|
||||
remove(this.filteredMembers, (mem) => mem.value.id === user.id);
|
||||
delete this.form.additions[user.name];
|
||||
} else {
|
||||
role.isEditing = false;
|
||||
delete this.form.updates[user.name];
|
||||
}
|
||||
},
|
||||
addUser(user) {
|
||||
const role = { isNew: true };
|
||||
const newUser = { ...user };
|
||||
this.filteredMembers.push({
|
||||
key: role,
|
||||
value: newUser,
|
||||
});
|
||||
this.edit(role, newUser);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.slide-down-enter-active,
|
||||
.slide-down-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.slide-down-enter-from,
|
||||
.slide-down-leave-to {
|
||||
margin-top: -60px;
|
||||
transform: scaleY(0);
|
||||
transform-origin: top;
|
||||
}
|
||||
|
||||
.alert {
|
||||
margin-bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
@ -1,186 +0,0 @@
|
||||
<template>
|
||||
<HangarModal target-id="channel-settings" label-id="settings-label">
|
||||
<template v-slot:activator="slotProps">
|
||||
<slot name="activator" v-bind:targetId="slotProps.targetId"></slot>
|
||||
</template>
|
||||
<template v-slot:modal-content>
|
||||
<div class="modal-header">
|
||||
<h4 v-if="!edit" class="modal-title">New Channel</h4>
|
||||
<h4 v-else class="modal-title">Edit Channel</h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Cancel">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p style="font-size: 12px" class="text-center">
|
||||
Name must be: unique; alphanumeric, have no spaces, and max {{ maxChannelNameLen }} characters.
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<div id="channel-input" class="input-group">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
:maxlength="`${maxChannelNameLen}`"
|
||||
v-model="name"
|
||||
aria-label="Channel Name"
|
||||
placeholder="Channel Name"
|
||||
/>
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text color-popover-container">
|
||||
<div v-show="showPopover" class="color-popover-arrow"></div>
|
||||
<div v-show="showPopover" class="color-popover">
|
||||
<table>
|
||||
<tr v-for="(colorArr, index) in colorTable" :key="index">
|
||||
<td
|
||||
v-for="{ value, hex } in colorArr"
|
||||
:key="value"
|
||||
@click.prevent="closePopover(hex, value)"
|
||||
:style="{ color: hex }"
|
||||
class="color-circle"
|
||||
>
|
||||
<i class="fas fa-circle fa-lg"></i>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<a
|
||||
id="color-popover-open"
|
||||
href="#"
|
||||
:style="{ color: color.hex || 'black', cursor: 'pointer' }"
|
||||
@click.prevent="showPopover = !showPopover"
|
||||
>
|
||||
<span v-show="!color.hex">
|
||||
<i class="fas fa-lg fa-question-circle"></i>
|
||||
</span>
|
||||
<span v-show="color.hex">
|
||||
<i class="fas fa-lg fa-circle"></i>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input type="checkbox" class="form-check-input" id="new-channel-exclude-input" v-model="nonReviewed" />
|
||||
<label for="new-channel-exclude-input" class="form-check-label">Exclude from moderation review queue?</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="close-modal" type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" @click="finalizeChannel">{{ edit ? 'Edit' : 'Create' }}</button>
|
||||
</div>
|
||||
</template>
|
||||
</HangarModal>
|
||||
</template>
|
||||
<script>
|
||||
import $ from 'jquery';
|
||||
import 'bootstrap/js/dist/modal';
|
||||
import HangarModal from '@/components/HangarModal';
|
||||
|
||||
const colors2dArray = [];
|
||||
for (let i = 0; i < window.COLORS.length; i += 4) {
|
||||
colors2dArray.push(window.COLORS.slice(i, i + 4));
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'NewChannel',
|
||||
components: {
|
||||
HangarModal,
|
||||
},
|
||||
emits: ['saved', 'update:nameProp', 'update:nonReviewedProp', 'update:colorProp'],
|
||||
props: {
|
||||
edit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
nameProp: String,
|
||||
nonReviewedProp: Boolean,
|
||||
colorProp: Object,
|
||||
},
|
||||
computed: {
|
||||
name: {
|
||||
get: function () {
|
||||
return this.nameProp;
|
||||
},
|
||||
set: function (val) {
|
||||
this.$emit('update:nameProp', val);
|
||||
},
|
||||
},
|
||||
nonReviewed: {
|
||||
get: function () {
|
||||
return this.nonReviewedProp;
|
||||
},
|
||||
set: function (val) {
|
||||
this.$emit('update:nonReviewedProp', val);
|
||||
},
|
||||
},
|
||||
color: {
|
||||
get: function () {
|
||||
return this.colorProp;
|
||||
},
|
||||
set: function (val) {
|
||||
this.$emit('update:colorProp', val);
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
colorTable: colors2dArray,
|
||||
showPopover: false,
|
||||
maxChannelNameLen: window.MAX_CHANNEL_NAME_LEN,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async closePopover(hex, value) {
|
||||
if (hex && typeof value !== 'undefined') {
|
||||
this.color.hex = hex;
|
||||
this.color.value = value;
|
||||
}
|
||||
this.showPopover = false;
|
||||
},
|
||||
finalizeChannel() {
|
||||
$('.invalid-input').removeClass('invalid-input');
|
||||
if (!this.name || this.name.trim().length === 0 || !/^[a-zA-Z0-9]+$/.test(this.name) || this.name.length > this.maxChannelNameLen) {
|
||||
$('#channel-input').addClass('invalid-input');
|
||||
return;
|
||||
} else if (!this.color.hex || !/^#[0-9a-f]{6}$/i.test(this.color.hex)) {
|
||||
$('#color-popover-open').addClass('invalid-input');
|
||||
return;
|
||||
}
|
||||
this.$emit('saved', this.edit ? 'EDIT' : 'NEW');
|
||||
$('#channel-settings').modal('hide');
|
||||
$(document.body).removeClass('modal-open');
|
||||
$('.modal-backdrop').remove();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.color-popover-container {
|
||||
position: relative;
|
||||
|
||||
.color-popover-arrow {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: #a6a6a6;
|
||||
box-shadow: 3px -3px 2px 2px #00000066;
|
||||
transform: rotate(45deg);
|
||||
right: -14px;
|
||||
}
|
||||
|
||||
.color-popover {
|
||||
position: absolute;
|
||||
background-color: #ddd;
|
||||
box-shadow: 3px 3px 2px 2px #00000066;
|
||||
left: 110%;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
|
||||
.color-circle {
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,125 +0,0 @@
|
||||
<template>
|
||||
<ul class="pagination">
|
||||
<li :class="{ disabled: !hasPrevious }">
|
||||
<a @click="previous">«</a>
|
||||
</li>
|
||||
<li v-if="current >= 3">
|
||||
<a @click="jump(1)">{{ 1 }}</a>
|
||||
</li>
|
||||
<li class="disabled" v-if="current >= 4 && total > 4">
|
||||
<a>...</a>
|
||||
</li>
|
||||
<li v-if="current === total && current - 3 > 0">
|
||||
<a @click="jump(current - 2)">{{ current - 2 }}</a>
|
||||
</li>
|
||||
<li v-if="current - 1 > 0">
|
||||
<a @click="jump(current - 1)">{{ current - 1 }}</a>
|
||||
</li>
|
||||
<li class="active">
|
||||
<a>{{ current }}</a>
|
||||
</li>
|
||||
<li v-if="current + 1 <= total">
|
||||
<a @click="jump(current + 1)">{{ current + 1 }}</a>
|
||||
</li>
|
||||
<li v-if="current === 1 && total > 3">
|
||||
<a @click="jump(current + 2)">{{ current + 2 }}</a>
|
||||
</li>
|
||||
<li class="disabled" v-if="total - current >= 3 && total > 4">
|
||||
<a>...</a>
|
||||
</li>
|
||||
<li v-if="total - current >= 2">
|
||||
<a @click="jump(total)">{{ total }}</a>
|
||||
</li>
|
||||
<li :class="{ disabled: !hasNext }" @click="next">
|
||||
<a>»</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
emits: ['prev', 'next', 'jump-to'],
|
||||
props: {
|
||||
current: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hasPrevious: function () {
|
||||
return this.current - 1 >= 1;
|
||||
},
|
||||
hasNext: function () {
|
||||
return this.current + 1 <= this.total;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
previous: function () {
|
||||
if (this.hasPrevious) {
|
||||
this.$emit('prev');
|
||||
}
|
||||
},
|
||||
next: function () {
|
||||
if (this.hasNext) {
|
||||
this.$emit('next');
|
||||
}
|
||||
},
|
||||
jump: function (page) {
|
||||
if (page > 0 <= this.total) {
|
||||
this.$emit('jump-to', page);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import './../scss/variables';
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 0.5rem;
|
||||
|
||||
> li {
|
||||
margin-right: 1rem;
|
||||
cursor: pointer;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&.disabled a,
|
||||
&.disabled a:hover {
|
||||
background: transparent;
|
||||
border: 1px solid #ddd;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
border: 1px solid #ddd;
|
||||
padding: 0.85rem 1.6rem;
|
||||
background: #ffffff;
|
||||
color: $sponge_grey;
|
||||
|
||||
&:first-child,
|
||||
&:last-child {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
> a,
|
||||
> a:hover {
|
||||
cursor: pointer;
|
||||
color: darken($sponge_yellow, 30);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,29 +0,0 @@
|
||||
<template>
|
||||
<div class="setting">
|
||||
<div class="setting-description">
|
||||
<slot name="description" :setting-name="name" :desc="desc">
|
||||
<h4 :class="{ danger }">
|
||||
{{ name }} <i v-if="optional">({{ $t('general.optional') }})</i>
|
||||
</h4>
|
||||
<p v-if="desc" :id="`${name.toLowerCase()}-desc`" v-text="desc"></p>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="setting-content">
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
<slot></slot>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Setting',
|
||||
props: {
|
||||
name: String,
|
||||
desc: String,
|
||||
optional: Boolean,
|
||||
danger: Boolean,
|
||||
},
|
||||
};
|
||||
</script>
|
@ -1,86 +0,0 @@
|
||||
<template>
|
||||
<div id="swagger-ui"></div>
|
||||
</template>
|
||||
<script>
|
||||
import SwaggerUIBundle from 'swagger-ui';
|
||||
import { API } from '@/api';
|
||||
|
||||
export default {
|
||||
name: 'SwaggerUI',
|
||||
mounted() {
|
||||
window.onload = () => {
|
||||
window.ui = SwaggerUIBundle({
|
||||
url: '/v2/api-docs',
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset],
|
||||
plugins: [SwaggerUIBundle.plugins.DownloadUrl],
|
||||
layout: 'BaseLayout',
|
||||
requestInterceptor: (req) => {
|
||||
if (!req.loadSpec) {
|
||||
const promise = API.getSession().then((session) => {
|
||||
req.headers.authorization = 'HangarApi session="' + session + '"';
|
||||
return req;
|
||||
});
|
||||
// Workaround for fixing the curl URL
|
||||
// https://github.com/swagger-api/swagger-ui/issues/4778#issuecomment-456403631
|
||||
promise.url = req.url;
|
||||
return promise;
|
||||
} else {
|
||||
return req;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@use '~swagger-ui/dist/swagger-ui';
|
||||
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.swagger-ui .topbar .download-url-wrapper,
|
||||
.swagger-ui .info hgroup.main a {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.swagger-ui .info {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.swagger-ui .info .title small pre {
|
||||
background-color: unset;
|
||||
border: unset;
|
||||
}
|
||||
|
||||
.model-container,
|
||||
.responses-inner {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.swagger-ui .info .description h2 {
|
||||
padding-top: 1.5rem;
|
||||
margin: 1.5rem 0 0;
|
||||
border-top: 3px solid #333333;
|
||||
}
|
||||
|
||||
.swagger-ui .scheme-container {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
</style>
|
@ -1,103 +0,0 @@
|
||||
<template>
|
||||
<div class="input-group" style="position: relative">
|
||||
<input
|
||||
v-model.trim="input"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:placeholder="$t('org.users.add') + '…'"
|
||||
aria-label="Username"
|
||||
@input="search"
|
||||
@focus="inputFocused = true"
|
||||
@blur="inputFocused = false"
|
||||
@keyup.enter="addUser"
|
||||
/>
|
||||
<ul v-show="inputFocused" class="search-dropdown list-group">
|
||||
<li v-for="user in userResults" :key="user.id" class="list-group-item list-group-item-action" @mousedown.prevent @click="selectUser(user)">
|
||||
{{ user.name }}
|
||||
</li>
|
||||
</ul>
|
||||
<div class="input-group-append input-group-btn">
|
||||
<button :disabled="!selectedUser || loading" type="button" class="btn btn-default" title="Add User" @click.prevent="addUser">
|
||||
<i v-show="loading" class="fas fa-spinner fa-spin"></i>
|
||||
<i v-show="!loading" class="fas fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { API } from '@/api';
|
||||
|
||||
export default {
|
||||
name: 'UserSearch',
|
||||
emits: ['add-user'],
|
||||
data() {
|
||||
return {
|
||||
input: '',
|
||||
inputFocused: false,
|
||||
userResults: [],
|
||||
selectedUser: null,
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
search() {
|
||||
this.selectedUser = null;
|
||||
this.loading = true;
|
||||
API.request(`users?q=${this.input}`).then((res) => {
|
||||
this.loading = false;
|
||||
if (res.result.length === 1 && res.result[0].name.toLowerCase() === this.input.toLowerCase()) {
|
||||
this.selectUser(res.result[0]);
|
||||
} else {
|
||||
this.userResults = res.result;
|
||||
}
|
||||
});
|
||||
},
|
||||
selectUser(user) {
|
||||
this.reset();
|
||||
this.input = user.name;
|
||||
this.selectedUser = user;
|
||||
},
|
||||
addUser() {
|
||||
if (this.userResults.length === 1) {
|
||||
this.selectedUser = this.userResults[0];
|
||||
}
|
||||
if (!this.selectedUser) {
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
API.request(`users/${this.selectedUser.name}`)
|
||||
.then(() => {
|
||||
this.$emit('add-user', this.selectedUser);
|
||||
this.reset();
|
||||
})
|
||||
.catch(() => {
|
||||
this.reset();
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
reset() {
|
||||
this.selectedUser = null;
|
||||
this.input = '';
|
||||
this.inputFocused = false;
|
||||
this.userResults = [];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.search-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
max-height: 300px;
|
||||
overflow-y: hidden;
|
||||
z-index: 100;
|
||||
width: 100%;
|
||||
|
||||
li {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,145 +0,0 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title" v-text="$t('user.queue.progress')"></h3>
|
||||
</div>
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Project version</th>
|
||||
<th>Queued by</th>
|
||||
<th style="text-align: right; max-width: 40px"></th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="!underReview.length">
|
||||
<th rowspan="5">
|
||||
<h4 v-text="$t('queue.review.none')"></h4>
|
||||
</th>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="entry in underReview"
|
||||
:key="entry.namespace + '/' + entry.versionStringUrl"
|
||||
:class="{ warning: entry.unfinished && entry.reviewerId === currentUser.id }"
|
||||
>
|
||||
<td>
|
||||
<a :href="ROUTES.parse('VERSIONS_SHOW', entry.author, entry.slug, entry.versionStringUrl)" v-text="entry.namespace"></a>
|
||||
<br />
|
||||
{{ entry.versionString }}
|
||||
<span class="channel" :style="{ backgroundColor: entry.channelColor.hex }" v-text="entry.channelName"></span>
|
||||
</td>
|
||||
<td>
|
||||
<a v-if="entry.versionAuthor" :href="`https://papermc.io/forums/users/${entry.versionAuthor}`" v-text="entry.versionAuthor"></a>
|
||||
<span v-else>Unknown</span>
|
||||
<br />
|
||||
<span class="faint" v-text="new Date(entry.versionCreatedAt).toLocaleDateString()"></span>
|
||||
</td>
|
||||
<td v-if="entry.unfinished" style="text-align: right; max-width: 40px">
|
||||
<i v-if="currentUser.id === entry.reviewerId" class="status fas fa-fw fa-play-circle fa-2x" style="color: green"></i>
|
||||
<i v-else class="status fas fa-fw fa-cog fa-2x" style="color: black"></i>
|
||||
</td>
|
||||
<td v-else style="text-align: right; max-width: 40px">
|
||||
<i class="status fas fa-fw fa-pause-circle fa-2x" style="color: orange"></i>
|
||||
</td>
|
||||
<td v-if="entry.unfinished" style="color: darkred">
|
||||
{{ entry.reviewerName }}
|
||||
<br />
|
||||
<span v-if="Date.now() - new Date(entry.reviewStarted) >= maxReviewTime">pastdue </span>
|
||||
<span v-else>started </span>
|
||||
<span>{{ ((Date.now() - new Date(entry.reviewStarted)) / 1000 / 60 / 60).toFixed(2) }} hours ago</span>
|
||||
</td>
|
||||
<td v-else>
|
||||
<span v-text="entry.reviewerName" style="text-decoration: line-through"></span>
|
||||
<br />
|
||||
<span v-if="Date.now() - new Date(entry.reviewStarted) >= maxReviewTime">pastdue </span>
|
||||
<span>abandoned </span>
|
||||
<span>{{ ((Date.now() - new Date(entry.reviewStarted)) / 1000 / 60 / 60).toFixed(2) }} hours ago</span>
|
||||
</td>
|
||||
<td style="vertical-align: middle; text-align: right; padding-right: 15px">
|
||||
<a :href="ROUTES.parse('REVIEWS_SHOW_REVIEWS', entry.author, entry.slug, entry.versionStringUrl)">
|
||||
<i class="fas fa-2x fa-fw fa-info"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title" v-text="$t('user.queue.open')"></h3>
|
||||
</div>
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Project</th>
|
||||
<th>Version</th>
|
||||
<th>Queued by</th>
|
||||
<th style="text-align: right">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="!notStarted.length">
|
||||
<th rowspan="5">
|
||||
<h4><i class="fas fa-thumbs-o-up"></i> {{ $t('user.queue.none') }}</h4>
|
||||
</th>
|
||||
</tr>
|
||||
<tr v-for="entry in notStarted" :key="entry.namespace + '/' + entry.versionStringUrl">
|
||||
<td>
|
||||
<UserAvatar :user-name="entry.author" clazz="user-avatar-xs" />
|
||||
</td>
|
||||
<td>
|
||||
<a :href="ROUTES.parse('VERSIONS_SHOW', entry.author, entry.slug, entry.versionStringUrl)" v-text="entry.namespace"></a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="faint">{{ new Date(entry.versionCreatedAt).toLocaleDateString() }} </span>
|
||||
<span class="minor">{{ entry.versionString }} </span>
|
||||
<span class="channel" :style="{ backgroundColor: entry.channelColor.hex }">{{ entry.channelName }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<a v-if="entry.versionAuthor" :href="`https://papermc.io/forums/users/${entry.versionAuthor}`" v-text="entry.versionAuthor"></a>
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
class="btn btn-success float-right"
|
||||
:href="ROUTES.parse('REVIEWS_SHOW_REVIEWS', entry.author, entry.slug, entry.versionStringUrl)"
|
||||
>
|
||||
Start Review
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
|
||||
export default {
|
||||
name: 'VersionQueue',
|
||||
components: { UserAvatar },
|
||||
data() {
|
||||
return {
|
||||
ROUTES: window.ROUTES,
|
||||
maxReviewTime: window.MAX_REVIEW_TIME,
|
||||
currentUser: window.CURRENT_USER,
|
||||
notStarted: window.NOT_STARTED.sort((a, b) => new Date(a.versionCreatedAt) - new Date(b.versionCreatedAt)),
|
||||
underReview: window.UNDER_REVIEW,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
console.log(this.maxReviewTime);
|
||||
},
|
||||
};
|
||||
</script>
|
@ -1,227 +0,0 @@
|
||||
<template>
|
||||
<HangarForm :action="ROUTES.parse('PROJECTS_CREATE_PROJECT')" method="post" no-autocomplete>
|
||||
<div class="row">
|
||||
<div class="col-12 col-xl-5">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<label for="create-as-selector" class="input-group-text"> Create as... </label>
|
||||
</div>
|
||||
<select v-model="form.createAs" id="create-as-selector" name="owner" class="custom-select">
|
||||
<option :value="currentUser.id" v-text="currentUser.name"></option>
|
||||
<option v-for="org in organizations" :key="org.id" :value="org.id" v-text="org.name"></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-7">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<label for="project-name" class="input-group-text" v-text="$t('project.create.input.name')"></label>
|
||||
</div>
|
||||
<input v-model.trim="form.projectName" type="text" id="project-name" name="name" class="form-control" required @input="projectNameInput" />
|
||||
<div class="input-group-append">
|
||||
<div v-show="success.projectName" class="input-group-text text-success">
|
||||
<i class="fas fa-check-circle fa-lg"></i>
|
||||
</div>
|
||||
<div v-show="!success.projectName" class="input-group-text text-danger">
|
||||
<i class="fas fa-times-circle fa-lg"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<label for="project-description" class="input-group-text" v-text="$t('project.create.input.description')"></label>
|
||||
</div>
|
||||
<input v-model.trim="form.description" type="text" name="description" class="form-control" id="project-description" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<label for="category-input" class="input-group-text" v-text="$t('project.create.input.category')"></label>
|
||||
</div>
|
||||
<select id="category-input" name="category" class="custom-select" required>
|
||||
<option v-for="cat in categories" :key="cat.id" :value="cat.name" v-text="cat.name"></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<input type="hidden" name="pageContent" v-model="form.pageContent" />
|
||||
<BBCodeConverter v-model:proj-page-content="form.pageContent"></BBCodeConverter>
|
||||
</div>
|
||||
<div v-if="form.pageContent" class="col-12">
|
||||
<div class="alert alert-info">Your converted project description will now be displayed on your main project page.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row collapse" id="additional-settings">
|
||||
<Divider>Links</Divider>
|
||||
<div class="col-12">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<label for="homepage-input" class="input-group-text"> <i class="fas fa-home"></i> Homepage </label>
|
||||
</div>
|
||||
<input id="homepage-input" name="homepageUrl" type="text" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<label for="issue-tracker-input" class="input-group-text"> <i class="fas fa-bug"></i> Issue Tracker </label>
|
||||
</div>
|
||||
<input id="issue-tracker-input" name="issueTrackerUrl" type="text" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<label for="source-input" class="input-group-text"> <i class="fas fa-code"></i> Source Code </label>
|
||||
</div>
|
||||
<input id="source-input" name="sourceUrl" type="text" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<label for="external-support-input" class="input-group-text"> <i class="fas fa-question"></i> External Support </label>
|
||||
</div>
|
||||
<input id="external-support-input" name="externalSupportUrl" type="text" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<Divider>License</Divider>
|
||||
<div class="col-12">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<label for="license-type-input" class="input-group-text">Type</label>
|
||||
</div>
|
||||
<select v-model="form.licenseType" name="licenseType" id="license-type-input" class="custom-select">
|
||||
<option v-text="$t('licenses.mit')"></option>
|
||||
<option v-text="$t('licenses.apache2.0')"></option>
|
||||
<option v-text="$t('licenses.gpl')"></option>
|
||||
<option v-text="$t('licenses.lgpl')"></option>
|
||||
<option v-text="$t('licenses.custom')"></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<label for="license-url-input" class="input-group-text">
|
||||
<i class="fas fa-gavel"></i> {{ form.licenseType === $t('licenses.custom') ? 'Name/' : '' }}URL
|
||||
</label>
|
||||
</div>
|
||||
<div v-show="form.licenseType === $t('licenses.custom')" class="input-group-prepend">
|
||||
<input
|
||||
v-model.trim="form.customName"
|
||||
type="text"
|
||||
id="license-name-input"
|
||||
name="licenseName"
|
||||
class="form-control"
|
||||
placeholder="Custom Name"
|
||||
aria-label="License Name"
|
||||
/>
|
||||
</div>
|
||||
<input id="license-url-input" name="licenseUrl" class="form-control" type="text" placeholder="URL" />
|
||||
</div>
|
||||
</div>
|
||||
<Divider>SEO</Divider>
|
||||
<div class="col-12">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<label for="keywords-input" class="input-group-text"> <i class="fas fa-key"></i> Keywords </label>
|
||||
</div>
|
||||
<input id="keywords-input" name="keywords" type="text" class="form-control" placeholder="(separated by spaces)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-info float-left"
|
||||
data-toggle="collapse"
|
||||
data-target="#additional-settings"
|
||||
aria-expanded="false"
|
||||
aria-controls="additional-settings"
|
||||
>
|
||||
Additional Settings (optional)
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary float-right"
|
||||
:disabled="!success.projectName || !form.description || (form.licenseType === $t('licenses.custom') && !form.customName)"
|
||||
>
|
||||
Create project
|
||||
</button>
|
||||
</HangarForm>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import HangarForm from '@/components/HangarForm';
|
||||
import { Category } from '@/enums';
|
||||
import BBCodeConverter from '@/components/BBCodeConverter';
|
||||
import Divider from '@/components/Divider';
|
||||
|
||||
export default {
|
||||
name: 'CreateProject',
|
||||
components: {
|
||||
Divider,
|
||||
BBCodeConverter,
|
||||
HangarForm,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ROUTES: window.ROUTES,
|
||||
organizations: window.ORGANIZAITONS,
|
||||
currentUser: window.CURRENT_USER,
|
||||
success: {
|
||||
projectName: false,
|
||||
},
|
||||
form: {
|
||||
createAs: window.CURRENT_USER.id,
|
||||
projectName: '',
|
||||
description: '',
|
||||
licenseType: '',
|
||||
customName: '',
|
||||
pageContent: '',
|
||||
},
|
||||
categories: Category.values,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
projectNameInput() {
|
||||
if (!this.form.createAs) {
|
||||
this.success.projectName = false;
|
||||
} else {
|
||||
axios
|
||||
.post(
|
||||
window.ROUTES.parse('PROJECTS_VALIDATE_NAME'),
|
||||
{
|
||||
user: this.form.createAs,
|
||||
name: this.form.projectName,
|
||||
},
|
||||
window.ajaxSettings
|
||||
)
|
||||
.then(() => {
|
||||
this.success.projectName = true;
|
||||
})
|
||||
.catch(() => {
|
||||
this.success.projectName = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.row > * {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
label > svg {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
#license-name-input {
|
||||
border-radius: 0;
|
||||
}
|
||||
</style>
|
@ -1,137 +0,0 @@
|
||||
<template>
|
||||
<div class="float-right project-controls">
|
||||
<span v-if="flagReported" class="flag-msg"> <i class="fas fa-thumbs-up"></i> Flag submitted for review </span>
|
||||
<template v-if="visibility !== 'softDelete'">
|
||||
<template v-if="!isOwner">
|
||||
<button class="btn btn-default" @click="star">
|
||||
<span v-show="value.starred"><i class="fas fa-star"></i></span>
|
||||
<span v-show="!value.starred"><i class="far fa-star"></i></span>
|
||||
<span v-text="' ' + value.starCount"></span>
|
||||
</button>
|
||||
<button class="btn btn-default" @click="watch">
|
||||
<span v-show="value.watching"><i class="fas fa-eye-slash"></i></span>
|
||||
<span v-show="!value.watching"><i class="fas fa-eye"></i></span>
|
||||
<span v-text="value.watching ? ' Unwatch' : ' Watch'"></span>
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="minor stars-static"> <i class="fas fa-star"></i> {{ value.starCount }} </span>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="hasUser && !isOwner && !hasUserFlags && visibility !== 'softDelete'">
|
||||
<button class="btn btn-default" data-toggle="modal" data-target="#modal-flag"><i class="fa-flag fas"></i> {{ $t('project.flag._') }}</button>
|
||||
</template>
|
||||
<template v-if="hasUser && (hasModNotes || hasViewLogs)">
|
||||
<button class="btn btn-alert dropdown-toggle" type="button" id="admin-actions" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Admin Actions
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="admin-actions">
|
||||
<a v-if="hasModNotes" :href="ROUTES.parse('PROJECTS_SHOW_FLAGS', ownerName, projectSlug)" class="dropdown-item">
|
||||
Flag history ({{ flagCount }})
|
||||
</a>
|
||||
<a v-if="hasModNotes" :href="ROUTES.parse('PROJECTS_SHOW_NOTES', ownerName, projectSlug)" class="dropdown-item">
|
||||
Staff notes ({{ noteCount }})
|
||||
</a>
|
||||
<a v-if="hasViewLogs" :href="`${ROUTES.parse('SHOW_LOG')}?projectFilter=${ownerName}/${projectSlug}`" class="dropdown-item">
|
||||
User Action Logs
|
||||
</a>
|
||||
<a :href="`https://papermc.io/forums/${ownerName}`" class="dropdown-item">
|
||||
Forum <i class="fas fa-external-link-alt" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import $ from 'jquery';
|
||||
|
||||
$.ajaxSetup(window.ajaxSettings);
|
||||
|
||||
export default {
|
||||
name: 'ProjectControls',
|
||||
props: {
|
||||
visibility: String,
|
||||
hasUser: Boolean,
|
||||
isOwner: Boolean,
|
||||
isStarred: Boolean,
|
||||
isWatching: Boolean,
|
||||
hasUserFlags: Boolean,
|
||||
starCount: Number,
|
||||
flagCount: Number,
|
||||
noteCount: Number,
|
||||
hasModNotes: Boolean,
|
||||
hasViewLogs: Boolean,
|
||||
flagReported: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ROUTES: window.ROUTES,
|
||||
ownerName: window.PROJECT.ownerName,
|
||||
projectSlug: window.PROJECT.slug,
|
||||
value: {
|
||||
starred: false,
|
||||
watching: false,
|
||||
starCount: 0,
|
||||
},
|
||||
starIncrement: 0,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
watch() {
|
||||
if (!this.checkHasUser()) return;
|
||||
this.value.watching = !this.value.watching;
|
||||
$.ajax({
|
||||
type: 'post',
|
||||
url: window.ROUTES.parse('PROJECTS_SET_WATCHING', this.ownerName, this.projectSlug, this.value.watching),
|
||||
});
|
||||
},
|
||||
star() {
|
||||
if (!this.checkHasUser()) return;
|
||||
this.value.starCount += this.starIncrement;
|
||||
this.value.starred = this.starIncrement > 0;
|
||||
$.ajax({
|
||||
type: 'post',
|
||||
url: window.ROUTES.parse('PROJECTS_TOGGLE_STARRED', this.ownerName, this.projectSlug),
|
||||
});
|
||||
this.starIncrement *= -1;
|
||||
},
|
||||
checkHasUser() {
|
||||
if (!this.hasUser) {
|
||||
// TODO some alert or modal?
|
||||
alert('Please login first');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.value.starred = this.isStarred;
|
||||
this.value.watching = this.isWatching;
|
||||
this.value.starCount = this.starCount;
|
||||
this.starIncrement = this.isStarred ? -1 : 1;
|
||||
},
|
||||
mounted() {
|
||||
const flagList = $('#list-flags');
|
||||
if (flagList.length) {
|
||||
flagList.find('li').on('click', function () {
|
||||
flagList.find(':checked').removeAttr('checked');
|
||||
$(this).find('input').prop('checked', true);
|
||||
});
|
||||
}
|
||||
const flagMsg = $('.flag-msg');
|
||||
if (flagMsg.length) {
|
||||
flagMsg.hide().fadeIn(1000).delay(2000).fadeOut(1000);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.project-controls > * {
|
||||
margin-left: 4px;
|
||||
|
||||
.stars-static {
|
||||
color: gray;
|
||||
padding-right: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,561 +0,0 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card card-settings">
|
||||
<div class="card-header sticky">
|
||||
<h3 class="card-title float-left" v-text="$t('project.settings._')"></h3>
|
||||
<template v-if="permissions.seeHidden">
|
||||
<BtnHide :namespace="project.namespace" :project-visibility="project.visibility"></BtnHide>
|
||||
</template>
|
||||
<button type="button" class="btn btn-success float-right" @click="save">
|
||||
<i class="fas fa-check"></i>
|
||||
Save changes
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<Setting :name="$t('project.settings.category._')" :desc="$t('project.settings.category.info', [categories.length])">
|
||||
<template v-slot:content>
|
||||
<label for="category" class="sr-only">{{ $t('project.settings.category._') }}</label>
|
||||
<select aria-labelledby="category-desc" v-model="form.category" class="form-control" id="category" name="category">
|
||||
<option v-for="category in categories" :key="category.id" :value="category.id">
|
||||
{{ category.name }}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
||||
</Setting>
|
||||
<Setting optional :name="$t('project.settings.keywords._')" :desc="$t('project.settings.keywords.info')">
|
||||
<label for="keywords-input" class="sr-only">{{ $t('project.settings.keywords._') }}</label>
|
||||
<input
|
||||
v-model.trim="form.keywords"
|
||||
aria-labelledby="keywords-desc"
|
||||
type="text"
|
||||
id="keywords-input"
|
||||
class="form-control"
|
||||
placeholder="sponge server plugins mods"
|
||||
/>
|
||||
</Setting>
|
||||
<Setting optional :name="$t('project.settings.homepage._')" :desc="$t('project.settings.homepage.info')">
|
||||
<label for="homepage-input" class="sr-only">{{ $t('project.settings.homepage._') }}</label>
|
||||
<input
|
||||
v-model.trim="form.links.homepage"
|
||||
aria-labelledby="homepage-desc"
|
||||
type="url"
|
||||
class="form-control"
|
||||
id="homepage-input"
|
||||
placeholder="https://papermc.io"
|
||||
/>
|
||||
</Setting>
|
||||
<Setting optional :name="$t('project.settings.issues._')" :desc="$t('project.settings.issues.info')">
|
||||
<label for="issues-input" class="sr-only">{{ $t('project.settings.issues._') }}</label>
|
||||
<input
|
||||
v-model.trim="form.links.issues"
|
||||
aria-labelledby="issues-desc"
|
||||
type="url"
|
||||
class="form-control"
|
||||
id="issues-input"
|
||||
placeholder="https://github.com/MiniDigger/Hangar/issues"
|
||||
/>
|
||||
<div class="clearfix"></div>
|
||||
</Setting>
|
||||
<Setting optional :name="$t('project.settings.source._')" :desc="$t('project.settings.source.info')">
|
||||
<label for="source-input" class="sr-only">{{ $t('project.settings.source._') }}</label>
|
||||
<input
|
||||
v-model.trim="form.links.source"
|
||||
aria-labelledby="source-desc"
|
||||
type="url"
|
||||
class="form-control"
|
||||
id="source-input"
|
||||
placeholder="https://github.com/MiniDigger/Hangar"
|
||||
/>
|
||||
</Setting>
|
||||
<Setting optional :name="$t('project.settings.externalSupport._')" :desc="$t('project.settings.externalSupport.info')">
|
||||
<label for="external-input" class="sr-only">{{ $t('project.settings.externalSupport._') }}</label>
|
||||
<input
|
||||
v-model.trim="form.links.support"
|
||||
aria-labelledby="external-support-desc"
|
||||
type="url"
|
||||
class="form-control"
|
||||
id="external-input"
|
||||
placeholder="https://discord.gg/papermc"
|
||||
/>
|
||||
</Setting>
|
||||
<Setting optional :name="$t('project.settings.license._')" :desc="$t('project.settings.license.info')">
|
||||
<div class="input-group pb-2 float-left">
|
||||
<div class="input-group-prepend">
|
||||
<label for="license-type-input" class="input-group-text">Type</label>
|
||||
</div>
|
||||
<select v-model="form.license.type" name="licenseType" id="license-type-input" class="custom-select">
|
||||
<option v-text="$t('licenses.mit')"></option>
|
||||
<option v-text="$t('licenses.apache2.0')"></option>
|
||||
<option v-text="$t('licenses.gpl')"></option>
|
||||
<option v-text="$t('licenses.lgpl')"></option>
|
||||
<option v-text="$t('licenses.custom')"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<label for="license-url-input" class="input-group-text">
|
||||
<i class="fas fa-gavel"></i> {{ form.licenseType === $t('licenses.custom') ? 'Name/' : '' }}URL
|
||||
</label>
|
||||
</div>
|
||||
<div v-show="form.license.type === $t('licenses.custom')" class="input-group-prepend">
|
||||
<input
|
||||
v-model.trim.trim="form.license.name"
|
||||
type="text"
|
||||
id="license-name-input"
|
||||
name="licenseName"
|
||||
class="form-control"
|
||||
placeholder="Custom Name"
|
||||
aria-label="License Name"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
v-model.trim="form.license.url"
|
||||
id="license-url-input"
|
||||
name="licenseUrl"
|
||||
class="form-control"
|
||||
type="text"
|
||||
placeholder="URL"
|
||||
/>
|
||||
</div>
|
||||
</Setting>
|
||||
<Setting :name="$t('project.settings.forumSync._')" :desc="$t('project.settings.forumSync.info')">
|
||||
<template v-slot:content>
|
||||
<label>
|
||||
<input v-model="form.forumSync" type="checkbox" id="forum-sync" />
|
||||
</label>
|
||||
</template>
|
||||
</Setting>
|
||||
<Setting :name="$t('project.settings.description._')" :desc="$t('project.settings.description.info')">
|
||||
<label for="description-input" class="sr-only">{{ $t('project.settings.description._') }}</label>
|
||||
<input
|
||||
v-model.trim="form.description"
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="description-input"
|
||||
maxlength="120"
|
||||
:placeholder="$t('version.create.noDescription')"
|
||||
/>
|
||||
</Setting>
|
||||
<transition name="slide-down">
|
||||
<div v-show="showUploadMsg" class="alert alert-info">Don't forget to save changes!</div>
|
||||
</transition>
|
||||
<Setting :name="$t('project.settings.icon._')">
|
||||
<template v-slot:description>
|
||||
<h4 v-text="$t('project.settings.icon._')"></h4>
|
||||
<UserAvatar :img-src="icon.previewSrc" :user-name="project.project.ownerName" clazz="user-avatar-md"></UserAvatar>
|
||||
<input type="file" id="icon-upload-input" class="form-control-static mt-2" @change="handleFileChange($event.target)" />
|
||||
</template>
|
||||
<template v-slot:content>
|
||||
<div class="icon-description">
|
||||
<p v-text="$t('project.settings.icon.info')"></p>
|
||||
<div class="btn-group float-right">
|
||||
<button class="btn btn-default" @click.prevent="resetIcon">Reset</button>
|
||||
<button class="btn btn-info float-right" :disabled="!icon.file" @click.prevent="uploadIcon">
|
||||
<i v-show="loading.upload" class="fas fa-spinner fa-spin"></i>
|
||||
<i v-show="!loading.upload" class="fas fa-upload"></i> Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Setting>
|
||||
<Setting v-if="permissions.editApiKeys" :name="$t('project.settings.deployKey._')" :desc="$t('project.settings.deployKey.info')">
|
||||
<template v-slot:description="props">
|
||||
<h4 v-text="props['setting-name']"></h4>
|
||||
<p>
|
||||
{{ props.desc }}
|
||||
<!-- TODO I think this link is supposed to show some info or something -->
|
||||
<a href="#" @click.prevent class="ml-1"><i class="fas fa-question-circle"></i></a>
|
||||
</p>
|
||||
<input
|
||||
v-if="deploymentKey"
|
||||
aria-label="Deployment Key"
|
||||
class="form-control input-key"
|
||||
type="text"
|
||||
:value="deploymentKey.value"
|
||||
readonly
|
||||
/>
|
||||
<input v-else aria-label="Deployment Key" class="form-control input-key" type="text" value="" readonly />
|
||||
</template>
|
||||
<template v-slot:content>
|
||||
<button v-if="deploymentKey" class="btn btn-danger btn-block" @click="genKey">
|
||||
<span v-show="loading.genKey" class="spinner" style="display: none"><i class="fas fa-spinner fa-spin"></i></span>
|
||||
<span class="text" v-text="$t('project.settings.revokeKey')"></span>
|
||||
</button>
|
||||
<button v-else class="btn btn-info btn-block" @click="revokeKey">
|
||||
<span v-show="loading.revokeKey" class="spinner"><i class="fas fa-spinner fa-spin"></i></span>
|
||||
<span class="text" v-text="$t('project.settings.genKey')"></span>
|
||||
</button>
|
||||
</template>
|
||||
</Setting>
|
||||
<HangarForm id="rename-form" method="post" :action="ROUTES.parse('PROJECTS_RENAME', project.project.ownerName, project.project.slug)">
|
||||
<Setting :name="$t('project.rename._')" :desc="$t('project.rename.info')">
|
||||
<template v-slot:content>
|
||||
<input
|
||||
v-model.trim="renameForm.value"
|
||||
name="name"
|
||||
class="form-control mb-2"
|
||||
type="text"
|
||||
maxlength="25"
|
||||
@keydown.enter.prevent
|
||||
@keyup.enter.prevent="$refs.renameButton.click()"
|
||||
/>
|
||||
<button
|
||||
ref="renameButton"
|
||||
type="button"
|
||||
id="btn-rename"
|
||||
data-toggle="modal"
|
||||
data-target="#modal-rename"
|
||||
class="btn btn-warning"
|
||||
:disabled="renameForm.value === project.project.name"
|
||||
>
|
||||
{{ $t('project.rename._') }}
|
||||
</button>
|
||||
</template>
|
||||
</Setting>
|
||||
<HangarModal target-id="modal-rename" label-id="label-rename">
|
||||
<template v-slot:modal-content>
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="label-rename" v-text="$t('project.rename.title')"></h4>
|
||||
<button type="button" class="close" data-dismiss="modal" :aria-label="$t('general.cancel')">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" v-text="$t('project.rename.info')"></div>
|
||||
<div class="modal-footer">
|
||||
<div class="form-inline">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal" v-text="$t('channel.edit.close')"></button>
|
||||
<button type="submit" form="rename-form" class="btn btn-warning" v-text="$t('project.rename._')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</HangarModal>
|
||||
</HangarForm>
|
||||
<HangarForm
|
||||
v-if="permissions.deleteProject"
|
||||
id="delete-form"
|
||||
method="post"
|
||||
:action="ROUTES.parse('PROJECTS_SOFT_DELETE', project.project.ownerName, project.project.slug)"
|
||||
>
|
||||
<Setting name="Danger" desc="Once you delete a project, it cannot be recovered." danger>
|
||||
<template v-slot:content>
|
||||
<button type="button" class="btn btn-delete btn-danger" data-toggle="modal" data-target="#modal-delete">Delete</button>
|
||||
</template>
|
||||
</Setting>
|
||||
<HangarModal target-id="modal-delete" label-id="label-delete">
|
||||
<template v-slot:modal-content>
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="label-delete" v-text="$t('project.delete.title')"></h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Cancel">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{ $t('project.delete.info._') }}
|
||||
<br />
|
||||
<textarea
|
||||
v-model.trim="deleteForm.value"
|
||||
form="delete-form"
|
||||
name="comment"
|
||||
class="textarea-delete-comment form-control"
|
||||
rows="3"
|
||||
></textarea>
|
||||
<br />
|
||||
<div class="alert alert-warning" v-text="$t('project.delete.info.uniqueid', [project.project.name])"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="form-inline">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal" v-text="$t('channel.edit.close')"></button>
|
||||
<button
|
||||
type="submit"
|
||||
form="delete-form"
|
||||
class="btn btn-danger"
|
||||
v-text="$t('general.delete')"
|
||||
:disabled="!deleteForm.value"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</HangarModal>
|
||||
</HangarForm>
|
||||
<HangarForm
|
||||
id="hard-delete-form"
|
||||
v-if="permissions.hardDeleteProject"
|
||||
method="post"
|
||||
:action="ROUTES.parse('PROJECTS_DELETE', project.project.ownerName, project.project.slug)"
|
||||
>
|
||||
<Setting name="Hard Delete" desc="Once you delete a project, it cannot be recovered." class="striped" danger>
|
||||
<template v-slot:content>
|
||||
<button type="button" class="btn btn-delete btn-danger" data-toggle="modal" data-target="#hard-delete-modal">
|
||||
<strong>Hard Delete</strong>
|
||||
</button>
|
||||
</template>
|
||||
</Setting>
|
||||
<HangarModal target-id="hard-delete-modal" label-id="label-hard-delete">
|
||||
<template v-slot:modal-content>
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="label-hard-delete">Hard Delete Project?</h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Cancel">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">Are you sure you want to permanently delete this project?</div>
|
||||
<div class="modal-footer">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal" v-text="$t('channel.edit.close')"></button>
|
||||
<button type="submit" form="hard-delete-form" class="btn btn-danger" v-text="$t('general.delete')" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</HangarModal>
|
||||
</HangarForm>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<transition name="slide-down">
|
||||
<div v-show="error" id="project-settings-error" class="alert alert-danger" v-text="error"></div>
|
||||
</transition>
|
||||
<button type="button" class="btn btn-success float-right" @click="save">
|
||||
<i class="fas fa-check"></i>
|
||||
Save changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<MemberList
|
||||
ref="memberList"
|
||||
:filtered-members-prop="memberList.filteredMembers"
|
||||
:can-manage-members="permissions.manageMembers"
|
||||
:roles="memberList.roles"
|
||||
editable
|
||||
:remove-call="ROUTES.parse('PROJECTS_REMOVE_MEMBER', project.project.ownerName, project.project.slug)"
|
||||
:settings-call="ROUTES.parse('PROJECTS_SHOW_SETTINGS', project.project.ownerName, project.project.slug)"
|
||||
></MemberList>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
|
||||
import { Category } from '@/enums';
|
||||
import BtnHide from '@/components/BtnHide';
|
||||
import MemberList from '@/components/MemberList';
|
||||
import HangarForm from '@/components/HangarForm';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
import Setting from '@/components/Setting';
|
||||
import HangarModal from '@/components/HangarModal';
|
||||
|
||||
const LICENSES = ['MIT', 'Apache 2.0', 'GNU General Public License (GPL)', 'GNU Lesser General Public License (LGPL)'];
|
||||
const VALIDATIONS = [
|
||||
{ name: 'category', el: 'category' },
|
||||
{ name: 'description', el: 'description-input' },
|
||||
];
|
||||
|
||||
export default {
|
||||
name: 'ProjectSettings',
|
||||
components: { HangarModal, Setting, UserAvatar, HangarForm, MemberList, BtnHide },
|
||||
data() {
|
||||
return {
|
||||
ROUTES: window.ROUTES,
|
||||
error: null,
|
||||
loading: {
|
||||
upload: false,
|
||||
genKey: false,
|
||||
revokeKey: false,
|
||||
},
|
||||
showUploadMsg: false,
|
||||
project: window.PROJECT,
|
||||
deploymentKey: window.DEPLOYMENT_KEY,
|
||||
permissions: {
|
||||
seeHidden: window.PERMISSIONS.SEE_HIDDEN,
|
||||
manageMembers: window.PERMISSIONS.MANAGE_MEMBERS,
|
||||
editApiKeys: window.PERMISSIONS.EDIT_API_KEYS,
|
||||
deleteProject: window.PERMISSIONS.DELETE_PROJECT,
|
||||
hardDeleteProject: window.PERMISSIONS.HARD_DELETE_PROJECT,
|
||||
},
|
||||
memberList: {
|
||||
filteredMembers: window.FILTERED_MEMBERS,
|
||||
roles: window.POSSIBLE_ROLES,
|
||||
},
|
||||
categories: Category.values,
|
||||
form: {
|
||||
category: window.PROJECT.project.category,
|
||||
keywords: window.PROJECT.settings.keywords.length ? window.PROJECT.settings.keywords.join(' ') : null,
|
||||
links: {
|
||||
homepage: window.PROJECT.settings.homepage,
|
||||
issues: window.PROJECT.settings.issues,
|
||||
source: window.PROJECT.settings.source,
|
||||
support: window.PROJECT.settings.support,
|
||||
},
|
||||
license: {
|
||||
type: LICENSES.indexOf(window.PROJECT.settings.licenseName) > -1 ? window.PROJECT.settings.licenseName : 'Custom',
|
||||
name: LICENSES.indexOf(window.PROJECT.settings.licenseName) > -1 ? null : window.PROJECT.settings.licenseName,
|
||||
url: window.PROJECT.settings.licenseUrl,
|
||||
},
|
||||
forumSync: window.PROJECT.settings.forumSync,
|
||||
description: window.PROJECT.project.description,
|
||||
},
|
||||
renameForm: {
|
||||
value: window.PROJECT.project.name,
|
||||
},
|
||||
deleteForm: {
|
||||
value: null,
|
||||
},
|
||||
icon: {
|
||||
file: null,
|
||||
previewSrc: window.PROJECT.iconUrl,
|
||||
hasChanged: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
showError(id) {
|
||||
document.getElementById(id).classList.add('invalid-input');
|
||||
document.getElementById(id).scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'center',
|
||||
});
|
||||
},
|
||||
save() {
|
||||
Array.from(document.getElementsByClassName('invalid-input')).forEach((el) => {
|
||||
el.classList.remove('invalid-input');
|
||||
});
|
||||
const data = {
|
||||
category: this.form.category,
|
||||
keywords: typeof this.form.keywords === 'string' ? this.form.keywords.split(' ') : !Array.isArray(this.form.keywords) ? [] : this.form.keywords,
|
||||
links: { ...this.form.links },
|
||||
license: {
|
||||
name: this.form.license.type !== 'Custom' ? this.form.license.type : this.form.license.name,
|
||||
url: this.form.license.url,
|
||||
},
|
||||
forumSync: this.form.forumSync,
|
||||
description: this.form.description,
|
||||
iconChange: this.icon.hasChanged,
|
||||
members: this.$refs.memberList.getForm(),
|
||||
};
|
||||
|
||||
for (const validation of VALIDATIONS) {
|
||||
if (!validation.name.split('.').reduce((o, i) => o[i], data)) {
|
||||
this.showError(validation.el);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (data.license.url && !data.license.name) {
|
||||
this.showError('license-name-input');
|
||||
return;
|
||||
}
|
||||
const self = this;
|
||||
axios
|
||||
.post(this.ROUTES.parse('PROJECTS_SAVE', this.project.project.ownerName, this.project.project.slug), data, window.ajaxSettings)
|
||||
.then(() => {
|
||||
location.href = this.ROUTES.parse('PROJECTS_SHOW', this.project.project.ownerName, this.project.project.slug);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.response.headers['content-type'] === 'application/json') {
|
||||
this.error = this.$t(err.response.data.messageKey, err.response.data.messageArgs);
|
||||
} else {
|
||||
this.error = 'Error while saving, ' + err.message; // TODO move to i18n
|
||||
}
|
||||
document.getElementById('project-settings-error').scrollIntoView({
|
||||
inline: 'nearest',
|
||||
block: 'nearest',
|
||||
behavior: 'smooth',
|
||||
});
|
||||
setTimeout(
|
||||
() => {
|
||||
self.error = null;
|
||||
},
|
||||
5000,
|
||||
this
|
||||
);
|
||||
});
|
||||
},
|
||||
handleFileChange(target) {
|
||||
if (target.files.length) {
|
||||
this.icon.file = target.files[0];
|
||||
} else {
|
||||
this.icon.file = null;
|
||||
}
|
||||
},
|
||||
uploadIcon() {
|
||||
if (!this.icon.file) return;
|
||||
this.loading.upload = true;
|
||||
const formData = new FormData();
|
||||
formData.append('icon', this.icon.file);
|
||||
axios
|
||||
.post(this.ROUTES.parse('PROJECTS_UPLOAD_ICON', this.project.project.ownerName, this.project.project.slug), formData, {
|
||||
headers: {
|
||||
[window.csrfInfo.headerName]: window.csrfInfo.token,
|
||||
'content-type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.icon.previewSrc =
|
||||
this.ROUTES.parse('PROJECTS_SHOW_PENDING_ICON', this.project.project.ownerName, this.project.project.slug) + '?' + performance.now();
|
||||
this.loading.upload = false;
|
||||
this.icon.file = null;
|
||||
document.getElementById('icon-upload-input').value = null;
|
||||
this.showUploadMsg = true;
|
||||
this.icon.hasChanged = true;
|
||||
});
|
||||
},
|
||||
resetIcon() {
|
||||
document.getElementById('icon-upload-input').value = null;
|
||||
axios
|
||||
.post(this.ROUTES.parse('PROJECTS_RESET_ICON', this.project.project.ownerName, this.project.project.slug), {}, window.ajaxSettings)
|
||||
.then(() => {
|
||||
this.icon.previewSrc =
|
||||
this.ROUTES.parse('PROJECTS_SHOW_ICON', this.project.project.ownerName, this.project.project.slug) + '?' + performance.now();
|
||||
this.icon.file = null;
|
||||
this.showUploadMsg = false;
|
||||
this.icon.hasChanged = false;
|
||||
});
|
||||
},
|
||||
genKey() {
|
||||
this.loading.genKey = true;
|
||||
axios
|
||||
.post(`/api/old/v1/projects/${this.project.namespace}/keys/new`, { 'key-type': 0 }, window.ajaxSettings)
|
||||
.then(({ data }) => {
|
||||
this.deploymentKey = data;
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading.genKey = false;
|
||||
});
|
||||
},
|
||||
revokeKey() {
|
||||
this.loading.revokeKey = true;
|
||||
axios
|
||||
.post(`/api/old/v1/projects/${this.project.namespace}/keys/revoke`, { id: this.deploymentKey.id }, window.ajaxSettings)
|
||||
.then(() => {
|
||||
this.deploymentKey = null;
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading.revokeKey = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.slide-down-enter-active,
|
||||
.slide-down-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.slide-down-enter-from,
|
||||
.slide-down-leave-to {
|
||||
margin-top: -60px;
|
||||
transform: scaleY(0);
|
||||
transform-origin: top;
|
||||
}
|
||||
|
||||
.alert {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
@ -1,333 +0,0 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col-lg-8 col-12" style="z-index: 1">
|
||||
<Editor
|
||||
:save-call="ROUTES.parse('PAGES_SAVE', project.ownerName, project.slug, page.slug)"
|
||||
:delete-call="ROUTES.parse('PAGES_DELETE', project.ownerName, project.slug, page.slug)"
|
||||
:deletable="page.deletable"
|
||||
:enabled="canEditPages"
|
||||
:raw="page.contents"
|
||||
subject="Page"
|
||||
:extra-form-value="page.name"
|
||||
:open="editorOpen"
|
||||
></Editor>
|
||||
</div>
|
||||
<div class="col-lg-4 col-12">
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12">
|
||||
<div v-if="project.recommendedVersionId" class="btn-group btn-download">
|
||||
<a
|
||||
:href="ROUTES.parse('VERSIONS_DOWNLOAD_RECOMMENDED', project.ownerName, project.slug)"
|
||||
:title="$t('project.download.recommend._')"
|
||||
data-tooltip-toggle
|
||||
data-placement="bottom"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
<i class="fas fa-download"></i>
|
||||
{{ $t('general.download') }}
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary copy-url"
|
||||
data-tooltip-toggle
|
||||
:title="$t('project.download.copyURL')"
|
||||
:data-clipboard-text="baseUrl + ROUTES.parse('VERSIONS_DOWNLOAD_RECOMMENDED', project.ownerName, project.slug)"
|
||||
>
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- todo: make donation button toggleable in settings, get email and stuff into modal, translate -->
|
||||
<div v-if="true">
|
||||
<DonationModal
|
||||
donation-email="minidigger-author@hangar.minidigger.me"
|
||||
donation-target="paper/Test"
|
||||
return-url="http://localhost:8080/paper/Test?donation=success"
|
||||
cancel-return-url="http://localhost:8080/paper/Test?donation=failure"
|
||||
>
|
||||
<template v-slot:activator="slotProps">
|
||||
<a
|
||||
:title="$t('general.donate')"
|
||||
data-tooltip-toggle
|
||||
data-placement="bottom"
|
||||
class="btn btn-primary btn-donate"
|
||||
data-toggle="modal"
|
||||
:data-target="`#${slotProps.targetId}`"
|
||||
@click.prevent
|
||||
>
|
||||
<i class="fas fa-hand-holding-usd"></i>
|
||||
{{ $t('general.donate') }}
|
||||
</a>
|
||||
</template>
|
||||
</DonationModal>
|
||||
</div>
|
||||
<div class="stats minor">
|
||||
<p>{{ $t('project.category.info', [formatCategory(project.category)]) }}</p>
|
||||
<p>{{ $t('project.publishDate', [moment(project.createdAt).format('MMM D, YYYY')]) }}</p>
|
||||
<p v-if="apiProject">
|
||||
<span id="view-count">{{ apiProject.stats.views }} views</span>
|
||||
</p>
|
||||
<p v-if="apiProject">
|
||||
<span id="star-count">{{ apiProject.stats.stars }}</span>
|
||||
<a :href="ROUTES.parse('PROJECTS_SHOW_STARGAZERS', project.ownerName, project.slug)">
|
||||
{{ apiProject.stats.views !== 1 ? ' stars' : ' star' }}
|
||||
</a>
|
||||
</p>
|
||||
<p v-if="apiProject">
|
||||
<span id="watcher-count">{{ apiProject.stats.watchers }}</span>
|
||||
<a :href="ROUTES.parse('PROJECTS_SHOW_WATCHERS', project.ownerName, project.slug)">
|
||||
{{ apiProject.stats.views !== 1 ? ' watchers' : ' watcher' }}
|
||||
</a>
|
||||
</p>
|
||||
<p v-if="apiProject">
|
||||
<span id="download-count">{{ apiProject.stats.downloads }} total download{{ apiProject.stats.downloads !== 1 ? 's' : '' }}</span>
|
||||
</p>
|
||||
<p v-if="project.licenseName && project.licenseUrl">
|
||||
{{ $t('project.license.link') }}
|
||||
<a :href="project.licenseUrl" target="_blank" ref="noopener">{{ project.licenseName }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-12 col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title" v-text="$t('project.promotedVersions')"></h3>
|
||||
</div>
|
||||
<div v-if="apiProject" class="list-group promoted-list">
|
||||
<a
|
||||
v-for="(version, index) in apiProject.promoted_versions"
|
||||
:key="`${index}-${version.version}`"
|
||||
class="list-group-item list-group-item-action"
|
||||
:href="ROUTES.parse('VERSIONS_SHOW', project.ownerName, project.slug, version.version)"
|
||||
>
|
||||
{{ version.version.substring(0, version.version.lastIndexOf('.')) }}
|
||||
<Tag v-for="(tag, index) in version.tags" :key="index" :color="tag.color" :data="tag.display_data" :name="tag.name"></Tag>
|
||||
</a>
|
||||
</div>
|
||||
<div v-else class="text-center py-4">
|
||||
<i class="fas fa-spinner fa-spin fa-3x"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="float-left card-title" v-text="$t('page.plural')"></h3>
|
||||
<button
|
||||
v-if="canEditPages"
|
||||
data-toggle="modal"
|
||||
data-target="#new-page"
|
||||
title="New"
|
||||
class="new-page btn btn-primary btn-sm float-right"
|
||||
>
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
<ul class="list-group">
|
||||
<li
|
||||
v-for="{ key: pg, value: pages } in rootPages"
|
||||
:key="pg.id"
|
||||
class="list-group-item"
|
||||
style="position: relative; overflow: hidden"
|
||||
>
|
||||
<template v-if="pages.length">
|
||||
<a
|
||||
v-if="expanded[pg.name]"
|
||||
class="toggle-collapse page-collapse position-relative"
|
||||
@click.prevent="expanded[pg.name] = false"
|
||||
>
|
||||
<i class="far fa-minus-square"></i>
|
||||
</a>
|
||||
<a v-else class="toggle-collapse position-relative" @click.prevent="expanded[pg.name] = true">
|
||||
<i class="far fa-plus-square"></i>
|
||||
</a>
|
||||
</template>
|
||||
<div class="d-inline-block position-relative" style="z-index: 2">
|
||||
<a :href="ROUTES.parse('PAGES_SHOW', project.ownerName, project.slug, pg.slug)" class="href" v-text="pg.name"></a>
|
||||
</div>
|
||||
|
||||
<transition v-if="pages.length" name="collapse">
|
||||
<ul v-show="pages.length && expanded[pg.name]" class="list-group sub-page-group">
|
||||
<li v-for="subpage in pages" :key="subpage.id" class="list-group-item">
|
||||
<a
|
||||
:href="ROUTES.parse('PAGES_SHOW', project.ownerName, project.slug, subpage.slug)"
|
||||
class="href"
|
||||
v-text="subpage.name"
|
||||
></a>
|
||||
</li>
|
||||
</ul>
|
||||
</transition>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-4">
|
||||
<MemberList
|
||||
:filtered-members-prop="filteredMembers"
|
||||
:can-manage-members="canManageMembers"
|
||||
:settings-call="ROUTES.parse('PROJECTS_SHOW_SETTINGS', project.ownerName, project.slug)"
|
||||
></MemberList>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<HangarModal target-id="new-page" label-id="new-page-label" modal-class="modal-lg">
|
||||
<template v-slot:modal-content>
|
||||
<div class="modal-header">
|
||||
<h4 v-show="!newPage.error" class="modal-title" id="new-page-label" v-text="$t('page.new.title')"></h4>
|
||||
<h4 v-show="newPage.error" class="modal-title" id="new-page-label-error" v-text="$t('page.new.error')"></h4>
|
||||
<button type="button" class="close" data-dismiss="modal" :aria-label="$t('general.close')">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body input-group">
|
||||
<div class="setting">
|
||||
<div class="setting-description">
|
||||
<h4 v-text="$t('project.page.name._')"></h4>
|
||||
<p v-text="$t('project.page.name.info')"></p>
|
||||
</div>
|
||||
<div class="setting-content">
|
||||
<label for="page-name" class="sr-only">Page Name</label>
|
||||
<input class="form-control" type="text" id="page-name" v-model="newPage.pageName" />
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="setting setting-no-border">
|
||||
<div class="setting-description">
|
||||
<h4 v-text="$t('project.page.parent._')"></h4>
|
||||
<p v-text="$t('project.page.parent.info')"></p>
|
||||
</div>
|
||||
<div class="setting-content">
|
||||
<label for="new-page-parent-select" class="sr-only"></label>
|
||||
<select class="form-control select-parent" id="new-page-parent-select" v-model="newPage.parentPage">
|
||||
<option disabled hidden :value="null"><none></option>
|
||||
<option v-for="pg in noHomePage(rootPages)" :key="pg.id" :value="{ id: pg.id, slug: pg.slug }" v-text="pg.name"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal" v-text="$t('general.close')"></button>
|
||||
<button id="continue-page" type="button" class="btn btn-primary" v-text="$t('general.continue')" @click="createPage"></button>
|
||||
</div>
|
||||
</template>
|
||||
</HangarModal>
|
||||
</template>
|
||||
<script>
|
||||
import Editor from '@/components/Editor';
|
||||
import Tag from '@/components/Tag';
|
||||
import HangarModal from '@/components/HangarModal';
|
||||
import moment from 'moment';
|
||||
import { API } from '@/api';
|
||||
import { go, slugify } from '@/utils';
|
||||
import $ from 'jquery';
|
||||
import MemberList from '@/components/MemberList';
|
||||
import DonationModal from '@/components/donation/DonationModal';
|
||||
|
||||
$.ajaxSetup(window.ajaxSettings);
|
||||
|
||||
export default {
|
||||
name: 'ProjectView',
|
||||
components: { DonationModal, MemberList, Editor, Tag, HangarModal },
|
||||
data() {
|
||||
return {
|
||||
ROUTES: window.ROUTES,
|
||||
project: window.PROJECT,
|
||||
rootPages: window.ROOT_PAGES,
|
||||
page: window.PAGE,
|
||||
canEditPages: window.CAN_EDIT_PAGES,
|
||||
apiProject: null,
|
||||
newPage: {
|
||||
error: false,
|
||||
pageName: null,
|
||||
parentPage: null,
|
||||
},
|
||||
expanded: {},
|
||||
editorOpen: window.EDITOR_OPEN,
|
||||
canManageMembers: window.CAN_MANAGE_MEMBERS,
|
||||
filteredMembers: window.FILTERED_MEMBERS,
|
||||
baseUrl: window.location.origin,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
API.request(`projects/${this.project.ownerName}/${this.project.slug}`).then((res) => {
|
||||
this.apiProject = res;
|
||||
});
|
||||
this.expanded[this.page.name] = true;
|
||||
},
|
||||
methods: {
|
||||
moment,
|
||||
formatCategory(apiName) {
|
||||
const formatted = apiName.replace('_', ' ');
|
||||
return this.capitalize(formatted);
|
||||
},
|
||||
capitalize(input) {
|
||||
return input
|
||||
.toLowerCase()
|
||||
.split(' ')
|
||||
.map((s) => s.charAt(0).toUpperCase() + s.substring(1))
|
||||
.join(' ');
|
||||
},
|
||||
noHomePage(pages) {
|
||||
return pages.filter((pg) => pg.key.name !== 'Home').map((pg) => pg.key);
|
||||
},
|
||||
createPage() {
|
||||
let url = `/${this.project.ownerName}/${this.project.slug}/pages/`;
|
||||
let parentId = null;
|
||||
if (this.newPage.parentPage && this.newPage.parentPage.id !== -1) {
|
||||
parentId = this.newPage.parentPage.id;
|
||||
url += `${this.newPage.parentPage.slug}/${slugify(this.newPage.pageName)}/edit`;
|
||||
} else {
|
||||
url += `${slugify(this.newPage.pageName)}/edit`;
|
||||
}
|
||||
$.ajax({
|
||||
method: 'post',
|
||||
url,
|
||||
data: {
|
||||
'parent-id': parentId,
|
||||
content: '# ' + this.newPage.pageName + '\n',
|
||||
name: this.newPage.pageName,
|
||||
},
|
||||
success() {
|
||||
go(url);
|
||||
},
|
||||
error(err) {
|
||||
console.error(err);
|
||||
this.newPage.error = true;
|
||||
setTimeout((self) => (self.newPage.error = false), 2000, this);
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.toggle-collapse {
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.sub-page-group {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-top: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.collapse-enter-active,
|
||||
.collapse-leave-active {
|
||||
transition: margin-top 0.7s ease-out, opacity 0.3s ease-out;
|
||||
}
|
||||
|
||||
.collapse-enter-from {
|
||||
margin-top: -50%;
|
||||
}
|
||||
|
||||
//.collapse-enter-from,
|
||||
.collapse-leave-to {
|
||||
margin-top: -50%;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
@ -1,20 +0,0 @@
|
||||
<template>
|
||||
<ProjectList :owner="user" :offset="(page - 1) * limit" :limit="limit" @prevPage="page--" @nextPage="page++" @jumpToPage="page = $event"></ProjectList>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ProjectList from '@/components/ProjectList';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ProjectList,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
page: 1,
|
||||
limit: 5,
|
||||
user: window.USERNAME,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
@ -1,155 +0,0 @@
|
||||
<template>
|
||||
<table class="table table-dark">
|
||||
<thead class="thead-dark text-center">
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<i class="fas fa-tag"></i>
|
||||
Channel Name
|
||||
</th>
|
||||
<th scope="col">
|
||||
<i class="fas fa-list-ol"></i>
|
||||
Version Count
|
||||
</th>
|
||||
<th scope="col">
|
||||
<i class="fas fa-edit"></i>
|
||||
Edit
|
||||
</th>
|
||||
<th scope="col">
|
||||
<i class="fas fa-trash"></i>
|
||||
Trash
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="{ versionCount, channel } in channels" :key="channel.id">
|
||||
<td>
|
||||
<div class="channel" :style="{ backgroundColor: channel.color.hex }">{{ channel.name }}</div>
|
||||
</td>
|
||||
<td class="version-count" :style="{ color: versionCount ? 'var(--danger)' : 'var(--success)' }">
|
||||
{{ versionCount }}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn btn-sm btn-warning" @click="editChannel(channel)">Edit</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn btn-sm" :class="versionCount ? 'btn-danger' : 'btn-warning'" @click.prevent="delChannel(channel, versionCount)">Delete</div>
|
||||
<div>
|
||||
<small>not implemented</small
|
||||
><!--TODO implement-->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<button type="button" class="btn btn-sm btn-block btn-primary" @click="addChannel">
|
||||
<i class="fas fa-plus"></i>
|
||||
Add Channel
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<NewChannel
|
||||
:edit="edit"
|
||||
v-model:name-prop="newChannel.name"
|
||||
v-model:color-prop="newChannel.color"
|
||||
v-model:non-reviewed-prop="newChannel.nonReviewed"
|
||||
@saved="editFinal($event)"
|
||||
></NewChannel>
|
||||
<form ref="edit-form" :action="ROUTES.parse('CHANNELS_SAVE', ownerName, projectSlug, newChannel.oldName)" method="post" class="d-none">
|
||||
<input type="hidden" name="name" :value="newChannel.name" class="d-none" required />
|
||||
<input type="hidden" name="color" :value="newChannel.color.hex" class="d-none" required />
|
||||
<input type="hidden" name="nonReviewed" :value="newChannel.nonReviewed" class="d-none" required />
|
||||
<input type="hidden" :name="CSRF_INFO.parameterName" :value="CSRF_INFO.token" required />
|
||||
</form>
|
||||
</template>
|
||||
<script>
|
||||
import $ from 'jquery';
|
||||
import 'bootstrap/js/dist/modal';
|
||||
import NewChannel from '@/components/NewChannel';
|
||||
|
||||
export default {
|
||||
name: 'ReleaseChannels.vue',
|
||||
components: {
|
||||
NewChannel,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
CSRF_INFO: window.csrfInfo,
|
||||
ROUTES: window.ROUTES,
|
||||
ownerName: window.OWNER_NAME,
|
||||
projectSlug: window.PROJECT_SLUG,
|
||||
channels: window.CHANNELS,
|
||||
newChannel: {
|
||||
name: null,
|
||||
color: {
|
||||
hex: null,
|
||||
value: null,
|
||||
},
|
||||
nonReviewed: false,
|
||||
oldName: null,
|
||||
},
|
||||
edit: true,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
editChannel(channel) {
|
||||
this.edit = true;
|
||||
this.newChannel.oldName = channel.name;
|
||||
this.newChannel.name = `${channel.name}`;
|
||||
this.newChannel.color.hex = channel.color.hex;
|
||||
this.newChannel.color.value = channel.color.value;
|
||||
this.newChannel.nonReviewed = channel.isNonReviewed;
|
||||
$('#channel-settings').modal('show');
|
||||
},
|
||||
delChannel(channel, versionCount) {
|
||||
$('#delete-form').attr('action', this.ROUTES.parse('CHANNELS_DELETE', this.ownerName, this.projectSlug, channel.name));
|
||||
if (versionCount > 0) {
|
||||
$('#version-count-on-delete').text(versionCount);
|
||||
$('#modal-delete').modal('show');
|
||||
} else {
|
||||
$('#delete-form').submit();
|
||||
}
|
||||
},
|
||||
editFinal(state) {
|
||||
if (state === 'EDIT') {
|
||||
if (!this.$refs['edit-form'].reportValidity()) {
|
||||
location.reload();
|
||||
}
|
||||
this.$refs['edit-form'].submit();
|
||||
} else if (state === 'NEW') {
|
||||
this.$refs['edit-form'].action = this.ROUTES.parse('CHANNELS_CREATE', this.ownerName, this.projectSlug);
|
||||
if (!this.$refs['edit-form'].reportValidity()) {
|
||||
location.reload();
|
||||
}
|
||||
this.$refs['edit-form'].submit();
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
},
|
||||
addChannel() {
|
||||
this.edit = false;
|
||||
this.newChannel.oldName = null;
|
||||
this.newChannel.name = null;
|
||||
this.newChannel.color.hex = null;
|
||||
this.newChannel.color.value = null;
|
||||
this.newChannel.nonReviewed = false;
|
||||
$('#channel-settings').modal('show');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
table {
|
||||
text-align: center;
|
||||
|
||||
th {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.version-count {
|
||||
font-weight: bold;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,6 +0,0 @@
|
||||
import { createApp } from 'vue';
|
||||
import CreateProject from '@/components/entrypoints/projects/CreateProject';
|
||||
import { setupI18n } from '@/plugins/i18n';
|
||||
|
||||
const i18n = setupI18n();
|
||||
createApp(CreateProject).use(i18n).mount('#create-project');
|
@ -1,6 +0,0 @@
|
||||
import { createApp } from 'vue';
|
||||
import ProjectSettings from '@/components/entrypoints/projects/ProjectSettings';
|
||||
import { setupI18n } from '@/plugins/i18n';
|
||||
|
||||
const i18n = setupI18n();
|
||||
createApp(ProjectSettings).use(i18n).mount('#project-settings');
|
@ -1,9 +0,0 @@
|
||||
import { createApp } from 'vue';
|
||||
import ProjectView from '@/components/entrypoints/projects/ProjectView';
|
||||
import { setupI18n } from '@/plugins/i18n';
|
||||
import DonationResult from '@/components/donation/DonationResult';
|
||||
|
||||
const i18n = setupI18n();
|
||||
createApp(ProjectView).use(i18n).mount('#project-view');
|
||||
|
||||
createApp(DonationResult).use(i18n).mount('#donation-result');
|
@ -1,4 +0,0 @@
|
||||
import { createApp } from 'vue';
|
||||
import ReleaseChannels from '@/components/entrypoints/versions/ReleaseChannels';
|
||||
|
||||
createApp(ReleaseChannels).mount('#release-channels');
|
@ -1,6 +0,0 @@
|
||||
import { createApp } from 'vue';
|
||||
import Swagger from '@/components/Swagger';
|
||||
|
||||
require('swagger-ui');
|
||||
|
||||
createApp(Swagger).mount('#swagger-ui-vue');
|
@ -1,7 +0,0 @@
|
||||
import { createApp } from 'vue';
|
||||
import $ from 'jquery';
|
||||
import UserProfile from '@/components/entrypoints/users/UserProfile';
|
||||
|
||||
$.ajaxSetup(window.ajaxSettings);
|
||||
|
||||
createApp(UserProfile).mount('#user-profile');
|
@ -1,6 +0,0 @@
|
||||
import { createApp } from 'vue';
|
||||
import { setupI18n } from '@/plugins/i18n';
|
||||
import VersionQueue from '@/components/entrypoints/admin/VersionQueue';
|
||||
|
||||
const i18n = setupI18n();
|
||||
createApp(VersionQueue).use(i18n).mount('#version-queue');
|
@ -1,29 +0,0 @@
|
||||
import $ from 'jquery';
|
||||
import { clearUnread, toggleSpinner } from '@/utils';
|
||||
|
||||
//=====> DOCUMENT READY
|
||||
|
||||
$(function () {
|
||||
$('.btn-resolve').click(function () {
|
||||
var listItem = $(this).closest('li');
|
||||
var flagId = listItem.data('flag-id');
|
||||
toggleSpinner($(this).find('[data-fa-i2svg]').removeClass('fa-check'));
|
||||
|
||||
$.ajax({
|
||||
url: '/admin/flags/' + flagId + '/resolve/true',
|
||||
complete: function () {
|
||||
toggleSpinner($('.btn-resolve').find('[data-fa-i2svg]').addClass('fa-check'));
|
||||
},
|
||||
success: function () {
|
||||
$.when(listItem.fadeOut('slow')).done(function () {
|
||||
listItem.remove();
|
||||
if (!$('.list-flags-admin').find('li').length) {
|
||||
resolveAll.fadeOut(); // eslint-disable-line no-undef
|
||||
$('.no-flags').fadeIn();
|
||||
clearUnread($('a[href="/admin/flags"]'));
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
@ -1,21 +0,0 @@
|
||||
import $ from 'jquery';
|
||||
import { toggleSpinner } from '@/utils';
|
||||
|
||||
//=====> DOCUMENT READY
|
||||
|
||||
$(function () {
|
||||
$('.btn-note-addmessage-submit').click(function () {
|
||||
var panel = $(this).parent().parent().parent();
|
||||
var textarea = panel.find('textarea');
|
||||
textarea.attr('disabled', 'disabled');
|
||||
toggleSpinner($(this).find('[data-fa-i2svg]').toggleClass('fa-save'));
|
||||
$.ajax({
|
||||
type: 'post',
|
||||
url: '/' + window.resourcePath + '/notes/addmessage',
|
||||
data: { content: textarea.val() },
|
||||
success: function () {
|
||||
location.reload();
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
@ -23,7 +23,6 @@ import io.papermc.hangar.modelold.Role;
|
||||
import io.papermc.hangar.modelold.viewhelpers.Activity;
|
||||
import io.papermc.hangar.modelold.viewhelpers.LoggedActionViewModel;
|
||||
import io.papermc.hangar.modelold.viewhelpers.OrganizationData;
|
||||
import io.papermc.hangar.modelold.viewhelpers.ReviewQueueEntry;
|
||||
import io.papermc.hangar.modelold.viewhelpers.UnhealthyProject;
|
||||
import io.papermc.hangar.modelold.viewhelpers.UserData;
|
||||
import io.papermc.hangar.securityold.annotations.GlobalPermission;
|
||||
@ -122,19 +121,6 @@ public class ApplicationController extends HangarController {
|
||||
return fillModel(mv);
|
||||
}
|
||||
|
||||
@GlobalPermission(NamedPermission.REVIEWER)
|
||||
@Secured("ROLE_USER")
|
||||
@GetMapping("/admin/approval/versions")
|
||||
public ModelAndView showQueue() {
|
||||
ModelAndView mv = new ModelAndView("users/admin/queue");
|
||||
List<ReviewQueueEntry> reviewQueueEntries = new ArrayList<>();
|
||||
List<ReviewQueueEntry> notStartedQueueEntries = new ArrayList<>();
|
||||
versionService.getReviewQueue().forEach(entry -> (entry.hasReviewer() ? reviewQueueEntries : notStartedQueueEntries).add(entry));
|
||||
mv.addObject("underReview", reviewQueueEntries);
|
||||
mv.addObject("versions", notStartedQueueEntries);
|
||||
return fillModel(mv);
|
||||
}
|
||||
|
||||
@GlobalPermission(NamedPermission.VIEW_HEALTH)
|
||||
@Secured("ROLE_USER")
|
||||
@GetMapping("/admin/health")
|
||||
|
@ -1,61 +0,0 @@
|
||||
package io.papermc.hangar.controllerold;
|
||||
|
||||
import io.papermc.hangar.db.customtypes.LoggedActionType;
|
||||
import io.papermc.hangar.db.customtypes.LoggedActionType.VersionContext;
|
||||
import io.papermc.hangar.db.modelold.ProjectVersionsTable;
|
||||
import io.papermc.hangar.db.modelold.UsersTable;
|
||||
import io.papermc.hangar.model.common.NamedPermission;
|
||||
import io.papermc.hangar.model.common.projects.ReviewState;
|
||||
import io.papermc.hangar.securityold.annotations.GlobalPermission;
|
||||
import io.papermc.hangar.serviceold.UserActionLogService;
|
||||
import io.papermc.hangar.serviceold.VersionService;
|
||||
import io.papermc.hangar.util.Routes;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.annotation.Secured;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@Controller
|
||||
public class ReviewsController extends HangarController {
|
||||
|
||||
private final VersionService versionService;
|
||||
private final UserActionLogService userActionLogService;
|
||||
|
||||
private final HttpServletRequest request;
|
||||
private final Supplier<ProjectVersionsTable> projectVersionsTable;
|
||||
|
||||
@Autowired
|
||||
public ReviewsController(VersionService versionService, UserActionLogService userActionLogService, HttpServletRequest request, Supplier<Optional<UsersTable>> currentUser, Supplier<ProjectVersionsTable> projectVersionsTable) {
|
||||
this.versionService = versionService;
|
||||
this.userActionLogService = userActionLogService;
|
||||
this.request = request;
|
||||
this.projectVersionsTable = projectVersionsTable;
|
||||
this.currentUser = currentUser;
|
||||
}
|
||||
|
||||
@GlobalPermission(NamedPermission.REVIEWER)
|
||||
@Secured("ROLE_USER")
|
||||
@PostMapping("/{author}/{slug}/versions/{version}/reviews/reviewtoggle")
|
||||
public ModelAndView backlogToggle(@PathVariable String author, @PathVariable String slug, @PathVariable String version) {
|
||||
ProjectVersionsTable versionsTable = projectVersionsTable.get();
|
||||
if (versionsTable.getReviewState() != ReviewState.BACKLOG && versionsTable.getReviewState() != ReviewState.UNREVIEWED) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid state for toggle backlog");
|
||||
}
|
||||
ReviewState oldState = versionsTable.getReviewState();
|
||||
ReviewState newState = oldState == ReviewState.BACKLOG ? ReviewState.UNREVIEWED : ReviewState.BACKLOG;
|
||||
versionsTable.setReviewState(newState);
|
||||
|
||||
userActionLogService.version(request, LoggedActionType.VERSION_REVIEW_STATE_CHANGED.with(VersionContext.of(versionsTable.getProjectId(), versionsTable.getId())), newState.name(), oldState.name());
|
||||
versionService.update(versionsTable);
|
||||
return Routes.REVIEWS_SHOW_REVIEWS.getRedirect(author, slug, version);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,6 @@
|
||||
package io.papermc.hangar.db.daoold;
|
||||
|
||||
import io.papermc.hangar.db.modelold.ProjectVersionsTable;
|
||||
import io.papermc.hangar.model.common.projects.ReviewState;
|
||||
import io.papermc.hangar.modelold.viewhelpers.ReviewQueueEntry;
|
||||
import org.jdbi.v3.core.enums.EnumByOrdinal;
|
||||
import org.jdbi.v3.sqlobject.config.RegisterBeanMapper;
|
||||
import org.jdbi.v3.sqlobject.customizer.BindBean;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlQuery;
|
||||
@ -25,48 +22,6 @@ public interface ProjectVersionDao {
|
||||
@SqlUpdate("DELETE FROM project_versions WHERE id = :id")
|
||||
void deleteVersion(long id);
|
||||
|
||||
@RegisterBeanMapper(ReviewQueueEntry.class)
|
||||
@SqlQuery("SELECT sq.project_author," +
|
||||
" sq.project_slug," +
|
||||
" sq.project_name," +
|
||||
" sq.version_string," +
|
||||
" sq.version_string_url," +
|
||||
" sq.version_created_at," +
|
||||
" sq.channel_name," +
|
||||
" sq.channel_color," +
|
||||
" sq.version_author," +
|
||||
" sq.reviewer_id," +
|
||||
" sq.reviewer_name," +
|
||||
" sq.review_started," +
|
||||
" sq.review_ended" +
|
||||
" FROM (SELECT pu.name AS project_author," +
|
||||
" p.name AS project_name," +
|
||||
" p.slug AS project_slug," +
|
||||
" v.version_string," +
|
||||
" v.version_string || '.' || v.id AS version_string_url," +
|
||||
" v.created_at AS version_created_at," +
|
||||
" c.name AS channel_name," +
|
||||
" c.color AS channel_color," +
|
||||
" vu.name AS version_author," +
|
||||
" r.user_id AS reviewer_id," +
|
||||
" ru.name AS reviewer_name," +
|
||||
" r.created_at AS review_started," +
|
||||
" r.ended_at AS review_ended," +
|
||||
" row_number() OVER (PARTITION BY (p.id, v.id) ORDER BY r.created_at DESC) AS row" +
|
||||
" FROM project_versions v" +
|
||||
" LEFT JOIN users vu ON v.author_id = vu.id" +
|
||||
" INNER JOIN project_channels c ON v.channel_id = c.id" +
|
||||
" INNER JOIN projects p ON v.project_id = p.id" +
|
||||
" INNER JOIN users pu ON p.owner_id = pu.id" +
|
||||
" LEFT JOIN project_version_reviews r ON v.id = r.version_id" +
|
||||
" LEFT JOIN users ru ON ru.id = r.user_id" +
|
||||
" WHERE v.review_state = :reviewState" +
|
||||
" AND p.visibility != 4" +
|
||||
" AND v.visibility != 4) sq" +
|
||||
" WHERE row = 1" +
|
||||
" ORDER BY sq.project_name DESC, sq.version_string DESC")
|
||||
List<ReviewQueueEntry> getQueue(@EnumByOrdinal ReviewState reviewState);
|
||||
|
||||
@SqlQuery("SELECT * FROM project_versions WHERE project_id = :projectId ORDER BY created_at DESC")
|
||||
List<ProjectVersionsTable> getProjectVersions(long projectId);
|
||||
|
||||
|
@ -1,193 +0,0 @@
|
||||
package io.papermc.hangar.modelold.viewhelpers;
|
||||
|
||||
import io.papermc.hangar.model.common.Color;
|
||||
import org.jdbi.v3.core.enums.EnumByOrdinal;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
public class ReviewQueueEntry {
|
||||
|
||||
private String projectAuthor;
|
||||
private String projectName;
|
||||
private String projectSlug;
|
||||
private String versionString;
|
||||
private String versionStringUrl;
|
||||
private OffsetDateTime versionCreatedAt;
|
||||
private String channelName;
|
||||
private Color channelColor;
|
||||
private String versionAuthor;
|
||||
|
||||
private Long reviewerId;
|
||||
private String reviewerName;
|
||||
private OffsetDateTime reviewStarted;
|
||||
private OffsetDateTime reviewEnded;
|
||||
|
||||
public ReviewQueueEntry() {
|
||||
}
|
||||
|
||||
public ReviewQueueEntry(String projectAuthor, String projectName, String projectSlug, String versionString, String versionStringUrl, OffsetDateTime versionCreatedAt, String channelName, Color channelColor, String versionAuthor, @Nullable Long reviewerId, @Nullable String reviewerName, @Nullable OffsetDateTime reviewStarted, @Nullable OffsetDateTime reviewEnded) {
|
||||
this.projectAuthor = projectAuthor;
|
||||
this.projectName = projectName;
|
||||
this.projectSlug = projectSlug;
|
||||
this.versionString = versionString;
|
||||
this.versionStringUrl = versionStringUrl;
|
||||
this.versionCreatedAt = versionCreatedAt;
|
||||
this.channelName = channelName;
|
||||
this.channelColor = channelColor;
|
||||
this.versionAuthor = versionAuthor;
|
||||
this.reviewerId = reviewerId;
|
||||
this.reviewerName = reviewerName;
|
||||
this.reviewStarted = reviewStarted;
|
||||
this.reviewEnded = reviewEnded;
|
||||
}
|
||||
|
||||
public boolean isUnfinished() {
|
||||
return reviewEnded == null;
|
||||
}
|
||||
|
||||
public boolean hasReviewer() {
|
||||
return reviewerId != null;
|
||||
}
|
||||
|
||||
public String getNamespace() {
|
||||
return projectAuthor + "/" + projectSlug;
|
||||
}
|
||||
|
||||
public String getAuthor() {
|
||||
return projectAuthor;
|
||||
}
|
||||
|
||||
public String getProjectAuthor() {
|
||||
return projectAuthor;
|
||||
}
|
||||
|
||||
public String getProjectName() {
|
||||
return projectName;
|
||||
}
|
||||
|
||||
public String getSlug() {
|
||||
return projectSlug;
|
||||
}
|
||||
|
||||
public String getProjectSlug() {
|
||||
return projectSlug;
|
||||
}
|
||||
|
||||
public String getVersionString() {
|
||||
return versionString;
|
||||
}
|
||||
|
||||
public String getVersionStringUrl() {
|
||||
return versionStringUrl;
|
||||
}
|
||||
|
||||
public OffsetDateTime getVersionCreatedAt() {
|
||||
return versionCreatedAt;
|
||||
}
|
||||
|
||||
public String getChannelName() {
|
||||
return channelName;
|
||||
}
|
||||
|
||||
@EnumByOrdinal
|
||||
public Color getChannelColor() {
|
||||
return channelColor;
|
||||
}
|
||||
|
||||
public String getVersionAuthor() {
|
||||
return versionAuthor;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Long getReviewerId() {
|
||||
return reviewerId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getReviewerName() {
|
||||
return reviewerName;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public OffsetDateTime getReviewStarted() {
|
||||
return reviewStarted;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public OffsetDateTime getReviewEnded() {
|
||||
return reviewEnded;
|
||||
}
|
||||
|
||||
public void setProjectAuthor(String author) {
|
||||
this.projectAuthor = author;
|
||||
}
|
||||
|
||||
public void setProjectName(String projectName) {
|
||||
this.projectName = projectName;
|
||||
}
|
||||
|
||||
public void setProjectSlug(String projectSlug) {
|
||||
this.projectSlug = projectSlug;
|
||||
}
|
||||
|
||||
public void setVersionString(String versionString) {
|
||||
this.versionString = versionString;
|
||||
}
|
||||
|
||||
public void setVersionStringUrl(String versionStringUrl) {
|
||||
this.versionStringUrl = versionStringUrl;
|
||||
}
|
||||
|
||||
public void setVersionCreatedAt(OffsetDateTime versionCreatedAt) {
|
||||
this.versionCreatedAt = versionCreatedAt;
|
||||
}
|
||||
|
||||
public void setChannelName(String channelName) {
|
||||
this.channelName = channelName;
|
||||
}
|
||||
|
||||
@EnumByOrdinal
|
||||
public void setChannelColor(Color channelColor) {
|
||||
this.channelColor = channelColor;
|
||||
}
|
||||
|
||||
public void setVersionAuthor(String versionAuthor) {
|
||||
this.versionAuthor = versionAuthor;
|
||||
}
|
||||
|
||||
public void setReviewerId(Long reviewerId) {
|
||||
this.reviewerId = reviewerId;
|
||||
}
|
||||
|
||||
public void setReviewerName(String reviewerName) {
|
||||
this.reviewerName = reviewerName;
|
||||
}
|
||||
|
||||
public void setReviewStarted(OffsetDateTime reviewStarted) {
|
||||
this.reviewStarted = reviewStarted;
|
||||
}
|
||||
|
||||
public void setReviewEnded(OffsetDateTime reviewEnded) {
|
||||
this.reviewEnded = reviewEnded;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ReviewQueueEntry{" +
|
||||
"projectAuthor='" + projectAuthor + '\'' +
|
||||
", projectName='" + projectName + '\'' +
|
||||
", projectSlug='" + projectSlug + '\'' +
|
||||
", versionString='" + versionString + '\'' +
|
||||
", versionStringUrl='" + versionStringUrl + '\'' +
|
||||
", versionCreatedAt=" + versionCreatedAt +
|
||||
", channelName='" + channelName + '\'' +
|
||||
", channelColor=" + channelColor +
|
||||
", versionAuthor='" + versionAuthor + '\'' +
|
||||
", reviewerId=" + reviewerId +
|
||||
", reviewerName='" + reviewerName + '\'' +
|
||||
", reviewStarted=" + reviewStarted +
|
||||
", reviewEnded=" + reviewEnded +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -10,10 +10,8 @@ import io.papermc.hangar.db.modelold.ProjectVersionsTable;
|
||||
import io.papermc.hangar.db.modelold.ProjectsTable;
|
||||
import io.papermc.hangar.model.api.project.version.PluginDependency;
|
||||
import io.papermc.hangar.model.common.Platform;
|
||||
import io.papermc.hangar.model.common.projects.ReviewState;
|
||||
import io.papermc.hangar.model.common.projects.Visibility;
|
||||
import io.papermc.hangar.modelold.viewhelpers.ProjectData;
|
||||
import io.papermc.hangar.modelold.viewhelpers.ReviewQueueEntry;
|
||||
import io.papermc.hangar.modelold.viewhelpers.UserData;
|
||||
import io.papermc.hangar.modelold.viewhelpers.VersionData;
|
||||
import io.papermc.hangar.service.internal.versions.VersionDependencyService;
|
||||
@ -34,7 +32,6 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
@ -99,10 +96,6 @@ public class VersionService extends HangarService {
|
||||
return getVersion(projectsTable.getId(), versionId);
|
||||
}
|
||||
|
||||
public void update(ProjectVersionsTable projectVersionsTable) {
|
||||
versionDao.get().update(projectVersionsTable);
|
||||
}
|
||||
|
||||
public void deleteVersion(long versionId) {
|
||||
versionDao.get().deleteVersion(versionId);
|
||||
}
|
||||
@ -117,10 +110,6 @@ public class VersionService extends HangarService {
|
||||
versionDao.get().update(versionData.getV());
|
||||
}
|
||||
|
||||
public List<ReviewQueueEntry> getReviewQueue() {
|
||||
return versionDao.get().getQueue(ReviewState.UNREVIEWED);
|
||||
}
|
||||
|
||||
public VersionData getVersionData(ProjectData projectData, ProjectVersionsTable projectVersion) {
|
||||
ProjectChannelsTable projectChannel = channelService.getProjectChannel(projectData.getProject().getId(), projectVersion.getChannelId());
|
||||
String approvedBy = null;
|
||||
|
@ -1,56 +0,0 @@
|
||||
<#import "/spring.ftl" as spring />
|
||||
<#import "*/utils/hangar.ftlh" as hangar />
|
||||
<#import "*/projects/channels/helper/popoverColorPicker.ftlh" as popoverColorPicker />
|
||||
<#import "*/utils/form.ftlh" as form>
|
||||
<#import "*/utils/csrf.ftlh" as csrf>
|
||||
|
||||
<#macro modalManage>
|
||||
<div class="modal fade" id="channel-settings" tabindex="-1" role="dialog" aria-labelledBy="settings-label">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title"></h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="<@spring.message "general.cancel" />">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<@form.form action=Routes.SHOW_HOME.getRouteUrl() method="GET">
|
||||
<@csrf.formField />
|
||||
<div class="modal-body">
|
||||
<div class="form-inline">
|
||||
<label for="channel-input"><@spring.message "channel.name" /></label>
|
||||
<input class="form-control channel-input" name="channel-input" type="text" value=""
|
||||
maxlength="${config.channels.maxNameLen}"/>
|
||||
<input type="hidden" name="channel-color-input" class="channel-color-input" value="" />
|
||||
<a href="#">
|
||||
<span id="channel-color-picker" class="color-picker" data-toggle="popover" data-placement="right" data-trigger="hover">
|
||||
<i class="fas fa-circle channel-id" style=""></i>
|
||||
</span>
|
||||
</a>
|
||||
<@popoverColorPicker.popoverColorPicker />
|
||||
<span class="float-right channel preview" style="display: none;"></span>
|
||||
<p class="help-block"><@spring.message "channel.nameRequirements" /></p>
|
||||
<br/>
|
||||
<span class="minor"><@spring.message "channel.nonReviewed" /></span>
|
||||
<input class="form-control non-reviewed"
|
||||
name="non-reviewed"
|
||||
type="checkbox"
|
||||
value="true" />
|
||||
<a href="#">
|
||||
<i class="fas fa-question-circle"
|
||||
title="<@spring.message "channel.nonReviewed.info" />"
|
||||
data-tooltip-toggle></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
<@spring.message "channel.edit.close" />
|
||||
</button>
|
||||
<input type="submit" value="" class="btn btn-primary" disabled />
|
||||
</div>
|
||||
</@form.form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</#macro>
|
@ -1,52 +0,0 @@
|
||||
<#import "/spring.ftl" as spring />
|
||||
<#import "*/utils/hangar.ftlh" as hangar />
|
||||
|
||||
<#macro popoverColorPicker>
|
||||
<#assign Color=@helper["io.papermc.hangar.model.Color"]>
|
||||
<table class="popover-color-picker" style="display: none;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding: 3px;">
|
||||
<i class="fas fa-circle channel-id" style="color: ${Color.getById(0).getHex()}"></i>
|
||||
</td>
|
||||
<td style="padding: 3px;">
|
||||
<i class="fas fa-circle channel-id" style="color: ${Color.getById(1).getHex()}"></i>
|
||||
</td>
|
||||
<td style="padding: 3px;">
|
||||
<i class="fas fa-circle channel-id" style="color: ${Color.getById(2).getHex()}"></i>
|
||||
</td>
|
||||
<td style="padding: 3px;">
|
||||
<i class="fas fa-circle channel-id" style="color: ${Color.getById(3).getHex()}"></i>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 3px;">
|
||||
<i class="fas fa-circle channel-id" style="color: ${Color.getById(6).getHex()}"></i>
|
||||
</td>
|
||||
<td style="padding: 3px;">
|
||||
<i class="fas fa-circle channel-id" style="color: ${Color.getById(7).getHex()}"></i>
|
||||
</td>
|
||||
<td style="padding: 3px;">
|
||||
<i class="fas fa-circle channel-id" style="color: ${Color.getById(8).getHex()}"></i>
|
||||
</td>
|
||||
<td style="padding: 3px;">
|
||||
<i class="fas fa-circle channel-id" style="color: ${Color.getById(9).getHex()}"></i>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 3px;">
|
||||
<i class="fas fa-circle channel-id" style="color: ${Color.getById(10).getHex()}"></i>
|
||||
</td>
|
||||
<td style="padding: 3px;">
|
||||
<i class="fas fa-circle channel-id" style="color: ${Color.getById(11).getHex()}"></i>
|
||||
</td>
|
||||
<td style="padding: 3px;">
|
||||
<i class="fas fa-circle channel-id" style="color: ${Color.getById(13).getHex()}"></i>
|
||||
</td>
|
||||
<td style="padding: 3px;">
|
||||
<i class="fas fa-circle channel-id" style="color: ${Color.getById(14).getHex()}"></i>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</#macro>
|
@ -1,76 +0,0 @@
|
||||
<#import "/spring.ftl" as spring />
|
||||
<#import "*/utils/hangar.ftlh" as hangar />
|
||||
<#import "*/layout/base.ftlh" as base />
|
||||
<#import "*/projects/channels/helper/modalManage.ftlh" as modalManage />
|
||||
<#import "*/utils/form.ftlh" as form>
|
||||
<#import "*/utils/csrf.ftlh" as csrf>
|
||||
|
||||
<#assign scriptsVar>
|
||||
<script nonce="${nonce}">
|
||||
<#outputformat "JavaScript">
|
||||
window.OWNER_NAME = '${p.project.ownerName}';
|
||||
window.PROJECT_SLUG = '${p.project.slug}';
|
||||
window.COLORS = ${mapper.valueToTree(@helper["io.papermc.hangar.model.Color"].getNonTransparentValues())};
|
||||
window.MAX_CHANNEL_NAME_LEN = ${config.channels.maxNameLen};
|
||||
window.CHANNELS = ${mapper.valueToTree(channels)};
|
||||
</#outputformat>
|
||||
</script>
|
||||
<script type="text/javascript" src="${hangar.url("js/release-channels.js")}"></script>
|
||||
</#assign>
|
||||
<#assign styleVar>
|
||||
<link rel="stylesheet" href="${hangar.url("css/release-channels.css")}">
|
||||
</#assign>
|
||||
|
||||
<#assign message><@spring.messageArgs code="channel.list.title" args=[p.project.ownerName, p.project.slug] /></#assign>
|
||||
<@base.base title=message additionalScripts=scriptsVar additionalStyling=styleVar>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><@spring.message "channel.list.title" /></h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="minor create-blurb">
|
||||
<@spring.message "channel.list.description" />
|
||||
</p>
|
||||
<div id="release-channels"></div>
|
||||
|
||||
<p class="minor create-blurb">
|
||||
<a href="/${p.project.ownerName}/${p.project.slug}/versions"><@spring.message "project.back" /></a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="modal-delete" tabindex="-1" role="dialog" aria-labelledby="label-delete">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="label-delete"><@spring.message "channel.delete" /></h4>
|
||||
<button type="button" class="close" data-dismiss="modal"
|
||||
aria-label="<@spring.message "general.close" />">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p><@spring.message "channel.delete.info" /></p>
|
||||
<p class="minor">
|
||||
<strong id="version-count-on-delete"></strong>
|
||||
<i><@spring.message "channel.delete.info.versions" /></i>
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
<@spring.message "general.cancel" />
|
||||
</button>
|
||||
<form id="delete-form" method="post" action="#" class="form-channel-delete">
|
||||
<@csrf.formField />
|
||||
<button type="submit" class="btn btn-danger"><@spring.message "channel.delete" /></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</@base.base>
|
@ -1,46 +0,0 @@
|
||||
<#import "/spring.ftl" as spring />
|
||||
<#import "*/utils/hangar.ftlh" as hangar />
|
||||
<#import "*/layout/base.ftlh" as base />
|
||||
|
||||
<#--
|
||||
Page used for uploading and creating new projects.
|
||||
-->
|
||||
<#assign scriptsVar>
|
||||
<script nonce="${nonce}">
|
||||
<#outputformat "JavaScript">
|
||||
window.CURRENT_USER = ${mapper.valueToTree(cu)};
|
||||
window.ORGANIZAITONS = ${mapper.valueToTree(createProjectOrgas)};
|
||||
</#outputformat>
|
||||
</script>
|
||||
<script type="text/javascript" src="${hangar.url("js/create-project.js")}"></script>
|
||||
</#assign>
|
||||
<#assign styleVar>
|
||||
<link rel="stylesheet" href="${hangar.url("css/create-project.css")}">
|
||||
</#assign>
|
||||
|
||||
<#assign message><@spring.message "project.create" /></#assign>
|
||||
<@base.base title="${message}" additionalScripts=scriptsVar additionalStyling=styleVar>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
<@spring.message "project.create.title" />
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="card-body project-body">
|
||||
<div class="minor create-blurb">
|
||||
<p><@spring.message "project.create.infoText.head" /></p>
|
||||
<p><@spring.message "project.create.infoText.guidelines" /></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div id="create-project"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</@base.base>
|
@ -1,35 +0,0 @@
|
||||
<#import "/spring.ftl" as spring />
|
||||
<#import "*/utils/hangar.ftlh" as hangar />
|
||||
|
||||
<#function stepState s step>
|
||||
<#if s == step>
|
||||
<#return "step-active">
|
||||
<#elseif s < step>
|
||||
<#return "step-complete">
|
||||
<#else>
|
||||
<#return "">
|
||||
</#if>
|
||||
</#function>
|
||||
|
||||
<#function stepIcon s defaultIcon step>
|
||||
<#if s < step>
|
||||
<#return "fa-check-square">
|
||||
<#else>
|
||||
<#return defaultIcon>
|
||||
</#if>
|
||||
</#function>
|
||||
|
||||
<#macro createSteps step>
|
||||
<div class="project-create-steps col-md-2">
|
||||
<div class="project-create-step ${stepState(1, step)}">
|
||||
<div class="step-content">
|
||||
<i class="fas ${stepIcon(1, "fa-upload", step)}"></i> <strong>Upload version</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="project-create-step ${stepState(2, step)}">
|
||||
<div class="step-content">
|
||||
<i class="fas ${stepIcon(2, "fa-paper-plane", step)}"></i> <strong>Publish version</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</#macro>
|
@ -1,128 +0,0 @@
|
||||
<#import "/spring.ftl" as spring />
|
||||
<#import "*/utils/hangar.ftlh" as hangar />
|
||||
|
||||
<#macro inputSettings form homepage="" issues="" source="" support="" licenseName="" licenseUrl="" selected=@helper["io.papermc.hangar.model.Category"].UNDEFINED forumSync=true keywords=[]>
|
||||
<div class="setting">
|
||||
<div class="setting-description">
|
||||
<h4>Category</h4>
|
||||
<p>
|
||||
Categorize your project into one of ${@helper["io.papermc.hangar.modelold.Category"].visible()?size}
|
||||
categories. Appropriately categorizing your
|
||||
project makes it easier for people to find.
|
||||
</p>
|
||||
</div>
|
||||
<div class="setting-content">
|
||||
<select class="form-control" id="category" name="category" form="${form}">
|
||||
<#-- @ftlvariable name="category" type="io.papermc.hangar.model.common.projects.Category" -->
|
||||
<#list @helper["io.papermc.hangar.model.Category"].values() as category>
|
||||
<#if category.isVisible()>
|
||||
<option <#if selected?? && selected.equals(category)> selected </#if> >
|
||||
${category.title}
|
||||
</option>
|
||||
</#if>
|
||||
</#list>
|
||||
</select>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<div class="setting">
|
||||
<div class="setting-description">
|
||||
<h4>Keywords <i>(optional)</i></h4>
|
||||
<p>
|
||||
These are special words that will return your project when people add them to their searches. Max 5.
|
||||
</p>
|
||||
</div>
|
||||
<input <#if keywords?size gt 0> value="${keywords?join(" ")}" </#if> form="${form}" type="text" class="form-control" id="keywords"
|
||||
name="keywords" placeholder="sponge server plugins mods" />
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<div class="setting">
|
||||
<div class="setting-description">
|
||||
<h4>Homepage <i>(optional)</i></h4>
|
||||
<p>
|
||||
Having a custom homepage for your project helps you look more proper, official, and gives you another place
|
||||
to gather information about your project.
|
||||
</p>
|
||||
</div>
|
||||
<input <#if homepage?has_content> value="${homepage}" </#if> form="${form}" type="url" class="form-control" id="homepage" name="homepage" placeholder="https://papermc.io" />
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<div class="setting">
|
||||
<div class="setting-description">
|
||||
<h4>Issue tracker <i>(optional)</i></h4>
|
||||
<p>
|
||||
Providing an issue tracker helps your users get support more easily and provides you with an easy way to
|
||||
track bugs.
|
||||
</p>
|
||||
</div>
|
||||
<input <#if issues?has_content> value="${issues}" </#if> form="${form}" type="url" class="form-control" id="issues"
|
||||
name="issues" placeholder="https://github.com/MiniDigger/Hangar/issues" />
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<div class="setting">
|
||||
<div class="setting-description">
|
||||
<h4>Source code <i>(optional)</i></h4>
|
||||
<p>Support the community of developers by making your project open source!</p>
|
||||
</div>
|
||||
<input <#if source?has_content> value="${source}" </#if> form="${form}" type="url" class="form-control" id="source" name="source" placeholder="https://github.com/MiniDigger/Hangar" />
|
||||
</div>
|
||||
|
||||
<div class="setting">
|
||||
<div class="setting-description">
|
||||
<h4>External support <i>(optional)</i></h4>
|
||||
<p>
|
||||
An external place where you can offer support to your users. Could be a forum, a Discord server, or
|
||||
somewhere else.
|
||||
</p>
|
||||
</div>
|
||||
<input <#if support?has_content> value="${support}" </#if> form="${form}" type="url" class="form-control" id="support" name="support" placeholder="https://discord.gg/papermc" />
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<div class="setting">
|
||||
<div class="setting-description">
|
||||
<h4><@spring.message "project.settings.license" /> <i>(<@spring.message "general.optional" />)</i></h4>
|
||||
<p><@spring.message "project.settings.license.info" /></p>
|
||||
</div>
|
||||
<div class="input-group float-left">
|
||||
<div class="input-group-prepend input-group-btn">
|
||||
<button type="button" class="btn btn-default btn-license dropdown-toggle" data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false">
|
||||
<span class="license"><#if licenseName?has_content>${licenseName}<#else><@spring.message "licenses.mit" /></#if></span>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-license">
|
||||
<a class="dropdown-item"><@spring.message "licenses.mit" /></a>
|
||||
<a class="dropdown-item"><@spring.message "licenses.apache2.0" /></a>
|
||||
<a class="dropdown-item"><@spring.message "licenses.gpl" /></a>
|
||||
<a class="dropdown-item"><@spring.message "licenses.lgpl" /></a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="license-custom dropdown-item"><@spring.message "licenses.custom" />…</a></li>
|
||||
</div>
|
||||
</div>
|
||||
<input type="text" class="form-control" style="display: none;" name="license-name" form="${form}"
|
||||
value="<#if licenseName?has_content>${licenseName}<#else><@spring.message "licenses.mit" /></#if>" />
|
||||
<input type="text" name="license-url" class="form-control" form="${form}"
|
||||
placeholder="https://github.com/MiniDigger/Hangar/LICENSE.txt" value="${licenseUrl}">
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<div class="setting">
|
||||
<div class="setting-description">
|
||||
<h4><@spring.message "project.settings.forumSync" /></h4>
|
||||
<p><@spring.message "project.settings.forumSync.info" /></p>
|
||||
</div>
|
||||
<div class="setting-content">
|
||||
<label>
|
||||
<input <#if forumSync> checked </#if> value="true" form="${form}" type="checkbox" id="forum-sync" name="forum-sync">
|
||||
Make forum posts
|
||||
</label>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</#macro>
|
||||
|
@ -1,64 +0,0 @@
|
||||
<#import "/spring.ftl" as spring />
|
||||
<#import "*/utils/hangar.ftlh" as hangar />
|
||||
<#import "*/utils/csrf.ftlh" as csrf />
|
||||
|
||||
<#macro pageCreate project, rootPages>
|
||||
<script nonce="${nonce}">
|
||||
<#outputformat "JavaScript">
|
||||
PROJECT_OWNER = '${project.ownerName}';
|
||||
PROJECT_SLUG = '${project.slug}';
|
||||
</#outputformat>
|
||||
</script>
|
||||
|
||||
<div class="modal fade" id="new-page" tabindex="-1" role="dialog" aria-labelledby="new-page-label">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="new-page-label"><@spring.message "page.new.title" /></h4>
|
||||
<h4 class="modal-title" id="new-page-label-error" style="display: none;
|
||||
color: red">
|
||||
<@spring.message "page.new.error" />
|
||||
</h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="<@spring.message "general.close" />">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body input-group">
|
||||
<div class="setting">
|
||||
<div class="setting-description">
|
||||
<h4><@spring.message "project.page.name" /></h4>
|
||||
<p><@spring.message "project.page.name.info" /></p>
|
||||
</div>
|
||||
<div class="setting-content">
|
||||
<label for="page-name" class="sr-only">Page Name</label>
|
||||
<input class="form-control" type="text" id="page-name" name="page-name">
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="setting setting-no-border">
|
||||
<div class="setting-description">
|
||||
<h4><@spring.message "project.page.parent" /></h4>
|
||||
<p><@spring.message "project.page.parent.info" /></p>
|
||||
</div>
|
||||
<div class="setting-content">
|
||||
<label for="new-page-parent-select" class="sr-only"></label>
|
||||
<select class="form-control select-parent" id="new-page-parent-select">
|
||||
<option selected value="-1"><none></option>
|
||||
<#list rootPages?keys?filter(x -> x.name != config.pages.home.name) as singlePage>
|
||||
<option value="${singlePage.id}" data-slug="${singlePage.slug}">${singlePage.name}</option>
|
||||
</#list>
|
||||
</select>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<@csrf.formField />
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal"><@spring.message "general.close" /></button>
|
||||
<button id="continue-page" type="button" class="btn btn-primary"><@spring.message "general.continue" /></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</#macro>
|
||||
|
@ -1,47 +0,0 @@
|
||||
<#-- @ftlvariable name="pageCount" type="java.lang.Integer" -->
|
||||
<#-- @ftlvariable name="editorOpen" type="java.lang.Boolean" -->
|
||||
<#-- @ftlvariable name="rootPages" type="java.util.Map<ProjectPage, java.util.List<ProjectPage>>" -->
|
||||
<#import "/spring.ftl" as spring />
|
||||
<#import "*/utils/hangar.ftlh" as hangar />
|
||||
<#import "*/projects/view.ftlh" as projectView />
|
||||
<#import "*/users/memberList.ftlh" as memberList />
|
||||
<#import "*/utils/editor.ftlh" as editor />
|
||||
<#import "*/pages/modalPageCreate.ftlh" as pageCreate />
|
||||
|
||||
<#--
|
||||
Documentation page within Project overview.
|
||||
-->
|
||||
|
||||
<#assign Permission=@helper["io.papermc.hangar.model.Permission"]>
|
||||
|
||||
<#assign scriptsVar>
|
||||
<script nonce="${nonce}">
|
||||
<#outputformat "JavaScript">
|
||||
window.PAGE = ${mapper.valueToTree(projectPage)};
|
||||
window.ROOT_PAGES = ${utils.serializeMap(rootPages)};
|
||||
window.PAGE_COUNT = ${pageCount};
|
||||
window.EDITOR_OPEN = ${(editorOpen!false)?c};
|
||||
|
||||
window.FILTERED_MEMBERS = ${utils.serializeMap(p.filteredMembers(headerData))};
|
||||
window.CAN_MANAGE_MEMBERS = ${sp.permissions.has(Permission.ManageSubjectMembers)?c};
|
||||
</#outputformat>
|
||||
</script>
|
||||
<script type="text/javascript" src="${hangar.url("js/project-view.js")}"></script>
|
||||
</#assign>
|
||||
|
||||
<#assign styleVar>
|
||||
<link rel="stylesheet" href="${hangar.url("css/project-view.css")}">
|
||||
</#assign>
|
||||
|
||||
<@projectView.view p=p sp=sp active="#docs" additionalScripts=scriptsVar additionalStyling=styleVar>
|
||||
<div id="project-view">
|
||||
<div class="row">
|
||||
<div class="col-lg-8 col-12">
|
||||
<div class="page-preview page-rendered"><#outputformat "plainText">${markdownService.render(projectPage.contents)}</#outputformat></div>
|
||||
</div>
|
||||
<div class="col-lg-4 col-12">
|
||||
<@memberList.memberList project=p perms=sp.permissions settingsCall=Routes.PROJECTS_SHOW_SETTINGS.getRouteUrl(p.project.ownerName, p.project.slug) />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</@projectView.view>
|
@ -1,40 +0,0 @@
|
||||
<#import "/spring.ftl" as spring />
|
||||
<#import "*/utils/hangar.ftlh" as hangar />
|
||||
<#import "*/utils/form.ftlh" as form>
|
||||
<#import "*/utils/csrf.ftlh" as csrf>
|
||||
<#import "*/projects/view.ftlh" as projects />
|
||||
<#import "*/projects/helper/btnHide.ftlh" as btnHide />
|
||||
<#import "*/projects/helper/inputSettings.ftlh" as inputSettings />
|
||||
<#import "*/utils/userAvatar.ftlh" as userAvatar />
|
||||
<#import "*/users/memberList.ftlh" as memberList />
|
||||
|
||||
<#assign Permission=@helper["io.papermc.hangar.model.Permission"] />
|
||||
<#assign Role=@helper["io.papermc.hangar.model.Role"] />
|
||||
<#assign scriptsVar>
|
||||
<script nonce="${nonce}">
|
||||
<#outputformat "JavaScript">
|
||||
window.PROJECT = ${mapper.valueToTree(p)};
|
||||
window.DEPLOYMENT_KEY = ${mapper.valueToTree(deploymentKey)};
|
||||
|
||||
window.FILTERED_MEMBERS = ${utils.serializeMap(p.filteredMembers(headerData))};
|
||||
window.PERMISSIONS = {
|
||||
SEE_HIDDEN: ${headerData.globalPerm(Permission.SeeHidden)?c},
|
||||
MANAGE_MEMBERS: ${sp.perms(Permission.ManageSubjectMembers)?c},
|
||||
EDIT_API_KEYS: ${sp.perms(Permission.EditApiKeys)?c},
|
||||
DELETE_PROJECT: ${sp.perms(Permission.DeleteProject)?c},
|
||||
HARD_DELETE_PROJECT: ${headerData.globalPerm(Permission.HardDeleteProject)?c},
|
||||
}
|
||||
window.POSSIBLE_ROLES = ${mapper.valueToTree(Role.values()?filter(role -> role.category == p.roleCategory && role.isAssignable())?sort_by("permissions")?reverse)};
|
||||
window.PROJECT_OWNER_PERM = ${Permission.IsSubjectOwner.value};
|
||||
</#outputformat>
|
||||
</script>
|
||||
<script type="text/javascript" src="${hangar.url("js/project-settings.js")}"></script>
|
||||
</#assign>
|
||||
<#assign styleVar>
|
||||
<link rel="stylesheet" href="${hangar.url("css/project-settings.css")}">
|
||||
</#assign>
|
||||
|
||||
<@projects.view p=p sp=sp active="#settings" additionalScripts=scriptsVar additionalStyling=styleVar>
|
||||
<div id="project-settings">
|
||||
</div>
|
||||
</@projects.view>
|
@ -1,20 +0,0 @@
|
||||
<#import "/spring.ftl" as spring />
|
||||
<#import "*/utils/hangar.ftlh" as hangar />
|
||||
<#import "*/layout/base.ftlh" as base />
|
||||
|
||||
<#assign scriptsVar>
|
||||
<script nonce="${nonce}">
|
||||
<#outputformat "JavaScript">
|
||||
window.MAX_REVIEW_TIME = ${mapper.valueToTree(config.queue.maxReviewTime.toMillis())};
|
||||
window.CURRENT_USER = ${mapper.valueToTree(cu)};
|
||||
window.UNDER_REVIEW = ${mapper.valueToTree(underReview)};
|
||||
window.NOT_STARTED = ${mapper.valueToTree(versions)};
|
||||
</#outputformat>
|
||||
</script>
|
||||
<script type="text/javascript" src="${hangar.url("js/version-queue.js")}"></script>
|
||||
</#assign>
|
||||
|
||||
<#assign message><@spring.message "user.queue" /></#assign>
|
||||
<@base.base title="${message}" additionalScripts=scriptsVar>
|
||||
<div id="version-queue"></div>
|
||||
</@base.base>
|
@ -1,67 +0,0 @@
|
||||
<#import "/spring.ftl" as spring />
|
||||
<#import "*/utils/hangar.ftlh" as hangar />
|
||||
<#import "*/utils/userAvatar.ftlh" as userAvatar />
|
||||
<#import "*/users/invite/userSearch.ftlh" as userSearch />
|
||||
<#import "*/users/invite/roleSelect.ftlh" as roleSelect />
|
||||
|
||||
<#macro form owner roleCategory="" loadedUsers=[]>
|
||||
<#-- @ftlvariable name="owner" type="io.papermc.hangar.db.modelold.UsersTable" -->
|
||||
<#-- Template row -->
|
||||
<table style="display: none;">
|
||||
<tbody>
|
||||
<tr id="result-row">
|
||||
<td>
|
||||
<input type="hidden"/>
|
||||
<@userAvatar.userAvatar clazz = "user-avatar-xs"/>
|
||||
<i class="fas fa-times user-cancel"></i>
|
||||
<a class="username" target="_blank" rel="noopener" href=""></a>
|
||||
<span><@roleSelect.roleSelect roleCategory=roleCategory /></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<#-- User not found alert (hidden) -->
|
||||
<div class="alert alert-danger alert-dismissible" role="alert" style="display: none;">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<@spring.message "user.notFound" /> "<span class="error-username"></span>"
|
||||
</div>
|
||||
|
||||
<div class="member-table-container">
|
||||
<table class="table table-members">
|
||||
<tbody>
|
||||
<#-- Owner (not submitted) -->
|
||||
<tr>
|
||||
<td>
|
||||
<@userAvatar.userAvatar userName=owner.name avatarUrl=utils.avatarUrl(owner.name) clazz="user-avatar-xs" />
|
||||
<#if owner.name?has_content>
|
||||
<strong>${owner.name}</strong>
|
||||
</#if>
|
||||
<span><i class="minor"><@spring.message "project.owner" /></i></span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<#list loadedUsers as user>
|
||||
<tr>
|
||||
<td>
|
||||
<input form="form-continue" type="hidden" value="${user.id}"/>
|
||||
<@userAvatar.userAvatar userName=user.name avatarUrl=user.avatarUrl clazz="user-avatar-xs" />
|
||||
<a target="_blank" rel="noopener" href="${Routes.USERS_SHOW_PROJECTS.getRouteUrl(user.name)}">
|
||||
${user.name}
|
||||
</a>
|
||||
<span><@roleSelect.roleSelect roleCategory=roleCategory /></span>
|
||||
</td>
|
||||
</tr>
|
||||
</#list>
|
||||
|
||||
<#-- User search -->
|
||||
<tr>
|
||||
<td><@userSearch.userSearch /></td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</#macro>
|
@ -1,12 +0,0 @@
|
||||
<#import "/spring.ftl" as spring />
|
||||
<#import "*/utils/hangar.ftlh" as hangar />
|
||||
|
||||
<#macro roleSelect roleCategory form="form-continue" id="" classes="" hidden=false>
|
||||
<#assign Role=@helper["io.papermc.hangar.model.Role"]>
|
||||
<#assign roles=Role.values()?filter(role -> role.category == roleCategory && role.isAssignable())?sort_by("permissions")?reverse>
|
||||
<select id="${id}" form="${form}" class="${classes}" <#if hidden> style="display: none;" </#if>>
|
||||
<#list roles as roleType>
|
||||
<option value="${roleType.value}">${roleType.title}</option>
|
||||
</#list>
|
||||
</select>
|
||||
</#macro>
|
@ -1,12 +0,0 @@
|
||||
<#import "/spring.ftl" as spring />
|
||||
<#import "*/utils/hangar.ftlh" as hangar />
|
||||
|
||||
<#macro userSearch>
|
||||
<div class="input-group input-group user-search">
|
||||
<input type="text" class="form-control" placeholder="<@spring.message "org.users.add" />…">
|
||||
<div class="input-group-append input-group-btn">
|
||||
<button disabled class="btn btn-default btn-search"><i class="fas fa-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</#macro>
|
||||
|
@ -1,129 +0,0 @@
|
||||
<#import "/spring.ftl" as spring />
|
||||
<#import "*/utils/hangar.ftlh" as hangar />
|
||||
<#import "*/utils/form.ftlh" as form>
|
||||
<#import "*/utils/csrf.ftlh" as csrf>
|
||||
<#import "*/utils/userAvatar.ftlh" as userAvatar>
|
||||
<#import "*/users/invite/userSearch.ftlh" as userSearch>
|
||||
<#import "*/users/invite/roleSelect.ftlh" as roleSelect>
|
||||
|
||||
<!-- TODO: Pagination -->
|
||||
<#-- @ftlvariable name="perms" type="io.papermc.hangar.model.common.Permission" -->
|
||||
<#assign Permission=@helper["io.papermc.hangar.model.Permission"]>
|
||||
<#macro memberList project perms editable=false removeCall="" settingsCall="" saveCall="" includeModal=true>
|
||||
<#-- @ftlvariable name="project" type="io.papermc.hangar.modelold.viewhelpers.JoinableData" -->
|
||||
<#if project.members?size != 0>
|
||||
|
||||
<#if editable && perms.has(Permission.ManageSubjectMembers)>
|
||||
<@roleSelect.roleSelect roleCategory=project.roleCategory id="select-role" classes="float-right" hidden=true />
|
||||
|
||||
<!-- Row template -->
|
||||
<ul style="display: none;">
|
||||
<li id="row-user" class="list-group-item">
|
||||
<input type="hidden" />
|
||||
<@userAvatar.userAvatar clazz="user-avatar-xs"></@userAvatar.userAvatar>
|
||||
<a class="username"></a>
|
||||
<i class="fas fa-times user-cancel"></i>
|
||||
<@roleSelect.roleSelect roleCategory=project.roleCategory classes="float-right" />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Delete modal -->
|
||||
<#if includeModal>
|
||||
<div class="modal fade" id="modal-user-delete" tabindex="-1" role="dialog" aria-labelledby="label-user-delete">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="label-user-delete"><@spring.message "project.removeMember" /></h4>
|
||||
<button type="button" class="close" data-dismiss="modal"
|
||||
aria-label="<@spring.message "general.close" />">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body"><@spring.message "project.removeMember.confirm" /></div>
|
||||
<div class="modal-footer">
|
||||
<@form.form action=removeCall method="POST" class="form-inline">
|
||||
<@csrf.formField />
|
||||
<input type="hidden" name="username" />
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
<@spring.message "general.close" />
|
||||
</button>
|
||||
<button type="submit" class="btn btn-danger"><@spring.message "general.remove" /></button>
|
||||
</@form.form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</#if>
|
||||
</#if>
|
||||
|
||||
<div class="alert alert-danger member-error" style="display: none">
|
||||
<span>error</span>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="float-left card-title"><@spring.message "project.settings.members" /></h3>
|
||||
|
||||
<#if perms.has(Permission.ManageSubjectMembers)>
|
||||
<div class="float-right">
|
||||
<#if !editable && settingsCall?has_content>
|
||||
<a href="${settingsCall}"
|
||||
class="btn btn-warning btn-sm">
|
||||
<i class="fas fa-pencil-alt">Edit</i>
|
||||
</a>
|
||||
</#if>
|
||||
|
||||
<#if saveCall?has_content>
|
||||
<@form.form action=saveCall method="POST" id="save">
|
||||
<@csrf.formField />
|
||||
<button class="btn-members-save btn btn-card btn-sm" data-tooltip-toggle
|
||||
data-placement="top" data-title="<@spring.message "org.users.save" />" style="display: none;">
|
||||
<i class="fas fa-save"></i>
|
||||
</button>
|
||||
</@form.form>
|
||||
</#if>
|
||||
</div>
|
||||
</#if>
|
||||
</div>
|
||||
|
||||
<ul class="list-members list-group">
|
||||
<!-- Member list -->
|
||||
<#list project.filteredMembers(headerData) as subjectRole, user>
|
||||
<#-- @ftlvariable name="subjectRole" type="io.papermc.hangar.db.modelold.RoleTable" -->
|
||||
<#-- @ftlvariable name="user" type="io.papermc.hangar.db.modelold.UsersTable" -->
|
||||
<li class="list-group-item">
|
||||
<@userAvatar.userAvatar userName=user.name avatarUrl=utils.avatarUrl(user.name) clazz="user-avatar-xs"></@userAvatar.userAvatar>
|
||||
<a class="username" href="${Routes.USERS_SHOW_PROJECTS.getRouteUrl(user.name)}">
|
||||
${user.name}
|
||||
</a>
|
||||
<p style="display: none;" class="role-id">${subjectRole.role.roleId}</p>
|
||||
<#if editable && perms.has(Permission.ManageSubjectMembers) && !subjectRole.role.permissions.has(Permission.IsOrganizationOwner)>
|
||||
<a href="#">
|
||||
<i style="padding-left:5px" class="fas fa-trash" data-toggle="modal" data-target="#modal-user-delete"></i>
|
||||
</a>
|
||||
<a href="#"><i style="padding-left:5px" class="fas fa-edit"></i></a>
|
||||
</#if>
|
||||
|
||||
<span class="minor float-right">
|
||||
<#if !subjectRole.isAccepted>
|
||||
<span class="minor">(Invited as ${subjectRole.role.title})</span>
|
||||
<#else>
|
||||
${subjectRole.role.title}
|
||||
</#if>
|
||||
</span>
|
||||
</li>
|
||||
</#list>
|
||||
|
||||
<!-- User search -->
|
||||
<#if perms.has(@helper["io.papermc.hangar.model.Permission"].ManageSubjectMembers) && editable>
|
||||
<li class="list-group-item">
|
||||
<@userSearch.userSearch />
|
||||
</li>
|
||||
</#if>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</#if>
|
||||
</#macro>
|
||||
|
Loading…
Reference in New Issue
Block a user