removed a bunch more stuff that's already done

This commit is contained in:
Jake Potrebic 2021-03-28 13:11:03 -07:00
parent 57a9cd1ceb
commit 5c34e74455
No known key found for this signature in database
GPG Key ID: 7C58557EC9C421F8
43 changed files with 0 additions and 4039 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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">&times;</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>

View File

@ -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">&times;</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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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') + '&hellip;'"
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>

View File

@ -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() }}&nbsp;</span>
<span class="minor">{{ entry.versionString }}&nbsp;</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>

View File

@ -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>

View File

@ -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>

View File

@ -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">&times;</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">&times;</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">&times;</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>

View File

@ -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">&times;</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">&lt;none&gt;</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>

View File

@ -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>

View File

@ -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>

View File

@ -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');

View File

@ -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');

View File

@ -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');

View File

@ -1,4 +0,0 @@
import { createApp } from 'vue';
import ReleaseChannels from '@/components/entrypoints/versions/ReleaseChannels';
createApp(ReleaseChannels).mount('#release-channels');

View File

@ -1,6 +0,0 @@
import { createApp } from 'vue';
import Swagger from '@/components/Swagger';
require('swagger-ui');
createApp(Swagger).mount('#swagger-ui-vue');

View File

@ -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');

View File

@ -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');

View File

@ -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"]'));
}
});
},
});
});
});

View File

@ -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();
},
});
});
});

View File

@ -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")

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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 +
'}';
}
}

View File

@ -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;

View File

@ -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">&times;</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>

View File

@ -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>

View File

@ -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">&times;</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>

View File

@ -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>

View File

@ -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>

View File

@ -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" />&hellip;</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>

View File

@ -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">&times;</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">&lt;none&gt;</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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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">&times;</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>

View File

@ -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>

View File

@ -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" />&hellip;">
<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>

View File

@ -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">&times;</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>