Channel creation/manage views (#167)

* most work done, 1 tiny issue

* fixed the stupid issue
This commit is contained in:
Jake Potrebic 2020-10-04 14:04:35 -07:00 committed by GitHub
parent 630692127d
commit 2ab9d77232
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 990 additions and 1038 deletions

View File

@ -11,5 +11,8 @@ module.exports = {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-unused-vars': 'warn',
'vue/no-unused-vars': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
'vue/no-unused-components': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
'vue/no-mutating-props': 'off',
},
};

View File

@ -1,101 +1,122 @@
<template>
<template v-if="pendingVersion">
<div class="plugin-meta">
<table class="plugin-meta-table">
<tr>
<td><strong>Version</strong></td>
<td>
<div class="form-group">
<label for="version-string-input" class="sr-only">Version String</label>
<input
type="text"
id="version-string-input"
class="form-control"
required
v-model="payload.versionString"
:disabled="pendingVersion.versionString"
/>
</div>
</td>
</tr>
<div class="row justify-content-around">
<div class="input-group meta-info col-xl-3 col-lg-3 col-12">
<div class="input-group-prepend">
<label for="version-string-input" class="input-group-text">Version</label>
</div>
<input
type="text"
id="version-string-input"
class="form-control"
required
v-model="payload.versionString"
:disabled="pendingVersion.versionString"
/>
</div>
<template v-if="pendingVersion.fileName && !pendingVersion.externalUrl">
<tr>
<td><strong>File name</strong></td>
<td>{{ pendingVersion.fileName }}</td>
</tr>
<tr>
<td><strong>File size</strong></td>
<td>{{ filesize(pendingVersion.fileSize) }}</td>
</tr>
<div class="input-group meta-info col-xl-6 col-lg-5 col-12">
<div class="input-group-prepend">
<label for="file-name-input" class="input-group-text">File name</label>
</div>
<input id="file-name-input" type="text" class="form-control" :value="pendingVersion.fileName" disabled />
</div>
<div class="input-group meta-info col-xl-3 col-lg-4 col-12">
<div class="input-group-prepend">
<label for="file-size-input" class="input-group-text">File size</label>
</div>
<input id="file-size-input" type="text" class="form-control" :value="filesize(pendingVersion.fileSize)" disabled />
</div>
</template>
<template v-else>
<tr>
<td><strong>External URL</strong></td>
<td>
<div class="form-group">
<label for="external-url-input" class="sr-only"></label>
<input id="external-url-input" class="form-control" type="text" required v-model="payload.externalUrl" />
</div>
</td>
</tr>
</template>
<tr>
<td><strong>Channel</strong></td>
<td class="form-inline">
<select id="select-channel" class="form-control" v-model="payload.channel.name" style="margin-right: 10px">
<option v-for="channel in channels" :key="channel.id" :value="channel.name" :data-color="channel.color.hex">
{{ channel.name }}
</option>
</select>
<a href="#">
<i id="channel-new" class="fas fa-plus" data-toggle="modal" data-target="#channel-settings"></i>
</a>
</td>
</tr>
<DependencySelection
v-model:platforms-prop="payload.platforms"
v-model:dependencies-prop="payload.dependencies"
:prev-version="pendingVersion.prevVersion"
></DependencySelection>
<tr>
<td>
<label for="is-unstable-version" class="form-check-label">
<strong>Mark this version as unstable</strong>
<div v-else class="col-xl-9 col-lg-9 col-12 input-group meta-info">
<div class="input-group-prepend">
<label for="external-url-input" class="input-group-text">External URL</label>
</div>
<input id="external-url-input" class="form-control" type="text" required v-model="payload.externalUrl" />
</div>
</div>
<div class="row justify-content-around">
<div class="col-xl-4 col-lg-9 col-md-6 col-12 input-group">
<div class="input-group-prepend">
<label for="select-channel" class="input-group-text">Channel</label>
</div>
<select id="select-channel" class="custom-select" v-model="payload.channel" :style="{ backgroundColor: payload.channel.color.hex }">
<option
v-for="channel in channels"
:key="channel.id"
:value="{ name: channel.name, color: channel.color, nonReviewed: channel.nonReviewed }"
>
{{ channel.name }}
</option>
</select>
<div class="input-group-append">
<NewChannel
v-model:name-prop="createdChannel.name"
v-model:color-prop="createdChannel.color"
v-model:non-reviewed-prop="createdChannel.nonReviewed"
@channel-created="setChannel"
>
<template v-slot:activator="slotProps">
<button
class="btn btn-outline-primary"
type="button"
data-toggle="modal"
:data-target="`#${slotProps.targetId}`"
@click.prevent
>
<i class="fas fa-plus"></i>
</button>
</template>
</NewChannel>
</div>
</div>
<div class="input-group meta-info meta-info-checkbox col-xl-2 col-md-4 col-6">
<div class="input-group-prepend">
<label for="is-unstable-version" class="input-group-text">
<span>Unstable</span>
</label>
</td>
<td class="rv">
<div class="form-check">
</div>
<div class="input-group-append">
<div class="input-group-text">
<input id="is-unstable-version" class="form-check-input" type="checkbox" v-model="payload.unstable" />
</div>
<div class="clearfix"></div>
</td>
</tr>
<tr>
<td>
<label for="is-recommended-version" class="form-check-label">
<strong>Recommended</strong>
</div>
</div>
<div class="input-group meta-info meta-info-checkbox col-xl-3 col-md-4 col-6">
<div class="input-group-prepend">
<label for="is-recommended-version" class="input-group-text">
<span>Recommended</span>
</label>
</td>
<td class="rv">
<div class="form-check">
</div>
<div class="input-group-append">
<div class="input-group-text">
<input id="is-recommended-version" class="form-check-input" type="checkbox" v-model="payload.recommended" />
</div>
<div class="clearfix"></div>
</td>
</tr>
<tr>
<td>
<label for="create-forum-post-version" class="form-check-label"></label>
<strong>Create forum post</strong>
</td>
<td class="rv">
<div class="form-check">
</div>
</div>
<div class="input-group meta-info meta-info-checkbox col-xl-2 col-md-4 col-6">
<div class="input-group-prepend">
<label for="create-forum-post-version" class="input-group-text">
<span>Forum Post</span>
</label>
</div>
<div class="input-group-append">
<div class="input-group-text">
<input id="create-forum-post-version" class="form-check-input" type="checkbox" v-model="payload.forumSync" />
</div>
<div class="clearfix"></div>
</td>
</tr>
</table>
</div>
</div>
</div>
<div id="deps-management" class="row">
<div class="col-12">
<DependencySelection
v-model:platforms-prop="payload.platforms"
v-model:dependencies-prop="payload.dependencies"
:prev-version="pendingVersion.prevVersion"
></DependencySelection>
</div>
</div>
</div>
<div class="release-bulletin">
@ -114,27 +135,22 @@
enctype="multipart/form-data"
clazz="form-inline"
>
<div class="input-group float-left" style="width: 50%">
<label for="pluginFile" style="flex-wrap: wrap">
<span style="flex: 0 0 100%; margin-bottom: 10px" v-if="!pendingVersion">Either upload a file...</span>
<div :class="'btn btn-primary' + (pendingVersion ? ' mt-1': '')" style="flex: 0 0 100%">
<template v-if="!pendingVersion">
Upload
</template>
<template v-else>
Change file
</template>
</div>
</label>
<input
type="file"
id="pluginFile"
name="pluginFile"
accept=".jar,.zip"
style="display: none;"
@change="fileUploaded($event.target.name, $event.target.files)"
/>
<label for="pluginFile" style="flex-wrap: wrap">
<span style="flex: 0 0 100%; margin-bottom: 10px" v-if="!pendingVersion">Either upload a file...</span>
<div :class="'btn btn-primary' + (pendingVersion ? ' mt-1' : '')" style="flex: 0 0 100%">
<template v-if="!pendingVersion"> Upload</template>
<template v-else> Change file</template>
</div>
</label>
<input
type="file"
id="pluginFile"
name="pluginFile"
accept=".jar,.zip"
style="display: none"
@change="fileUploaded($event.target.name, $event.target.files)"
/>
</div>
<div class="alert-file file-project float-right" style="display: none">
<div class="alert alert-info float-left">
@ -174,44 +190,63 @@
</template>
<script>
import HangarForm from '@/components/HangarForm';
// import PlatformChoice from '@/PlatformChoice';
import DependencySelection from '@/components/DependencySelection';
// import PlatformTags from '@/components/PlatformTags';
import Editor from '@/components/Editor';
import NewChannel from '@/components/NewChannel';
import 'bootstrap/js/dist/tooltip';
import $ from 'jquery';
import 'bootstrap/js/dist/collapse';
import filesize from 'filesize';
import axios from 'axios';
import { remove } from 'lodash-es';
const channels = [];
for (const channel of window.CHANNELS) {
channels.push({
color: channel.color,
nonReviewed: channel.isNonReviewed,
name: channel.name,
});
}
export default {
name: 'CreateVersion',
components: {
NewChannel,
HangarForm,
// PlatformChoice,
DependencySelection,
// PlatformTags,
Editor,
},
props: {
defaultColor: String,
pendingVersion: Object,
ownerName: String,
projectSlug: String,
channels: Array,
forumSync: Boolean,
},
data() {
return {
channels,
MAX_FILE_SIZE: 20971520,
ROUTES: window.ROUTES,
createdChannel: {
name: null,
color: {
hex: null,
value: null,
},
nonReviewed: false,
},
payload: {
versionString: null,
description: null,
externalUrl: null,
channel: {
name: null,
color: null,
color: {
hex: null,
value: null,
},
nonReviewed: false,
},
platforms: {},
dependencies: {},
@ -224,7 +259,6 @@ export default {
},
created() {
if (this.pendingVersion) {
console.log(this.pendingVersion);
if (this.pendingVersion.versionString) {
this.payload.versionString = this.pendingVersion.versionString;
this.payload.description = this.pendingVersion.description;
@ -237,7 +271,11 @@ export default {
this.payload.externalUrl = this.pendingVersion.externalUrl;
}
this.payload.channel.name = this.pendingVersion.channelName;
this.payload.channel.color = this.pendingVersion.channelColor;
this.payload.channel.color.hex = this.pendingVersion.channelColor.hex;
this.payload.channel.color.value = this.pendingVersion.channelColor.value;
this.payload.channel.nonReviewed = this.channels.find(
(ch) => ch.name === this.pendingVersion.channelName && ch.color.hex === this.pendingVersion.channelColor.hex
).nonReviewed;
this.payload.forumSync = this.forumSync;
}
},
@ -349,6 +387,25 @@ export default {
600
);
},
setChannel() {
remove(this.channels, (ch) => !window.CHANNELS.find((och) => ch.name === och.name));
this.channels.push({
color: {
value: this.createdChannel.color.value,
hex: this.createdChannel.color.hex,
},
nonReviewed: this.createdChannel.nonReviewed,
name: this.createdChannel.name,
});
this.payload.channel = {
color: {
hex: this.createdChannel.color.hex,
value: this.createdChannel.color.value,
},
nonReviewed: this.createdChannel.nonReviewed,
name: this.createdChannel.name,
};
},
publish() {
const requiredProps = [
{
@ -391,21 +448,20 @@ export default {
return;
}
}
const parentEl = $('#dependencies-accordion');
const parentEl = $('#deps-management');
const depCollapseEl = $('#dep-collapse');
for (const platform in this.payload.dependencies) {
for (const dep of this.payload.dependencies[platform]) {
if (!dep.project_id && !dep.external_url) {
this.scrollTo('#dependencies-accordion');
this.scrollTo('#deps-management');
depCollapseEl.collapse('show');
$(`#${platform}-${dep.name}-link-cell`).addClass('invalid-input');
console.error(`Missing link for ${dep.name} on ${platform}`);
return;
}
}
}
if (Object.keys(this.payload.platforms).length === 0) {
this.scrollTo('#dependencies-accordion');
this.scrollTo('#deps-management');
depCollapseEl.collapse('show');
parentEl.addClass('invalid-input');
return;
@ -413,7 +469,7 @@ export default {
for (const platform in this.payload.platforms) {
if (!this.payload.platforms[platform].length) {
depCollapseEl.collapse('show');
this.scrollTo('#dependencies-accordion');
this.scrollTo('#deps-management');
$(`#${platform}-row`).addClass('invalid-input');
return;
}
@ -446,10 +502,10 @@ export default {
}
)
.then(() => {
$('form')
.attr('action', window.ROUTES.parse('VERSIONS_PUBLISH', this.ownerName, this.projectSlug, this.payload.versionString))
.attr('method', 'post')
.submit();
const publishUrl = window.ROUTES.parse('VERSIONS_PUBLISH', this.ownerName, this.projectSlug, this.payload.versionString);
const form = $(`<form action="${publishUrl}" method="post" style="display: none"></form>`).appendTo('body');
form.append(`<input name="${window.csrfInfo.parameterName}" value="${window.csrfInfo.token}" />`);
form.submit();
});
},
},
@ -458,3 +514,33 @@ export default {
},
};
</script>
<style lang="scss" scoped>
.input-group {
margin-bottom: 1em;
&.meta-info {
width: unset;
input:disabled {
background-color: #00000017;
}
&.meta-info-checkbox {
justify-content: center;
.input-group-prepend > * {
border-right: none;
span {
position: relative;
top: 1px;
}
}
.input-group-append > * {
border-left: none;
}
}
}
}
</style>

View File

@ -2,13 +2,7 @@
<div class="row">
<div class="col-md-9">
<div class="project-search" :class="{ 'input-group': q.length > 0 }">
<input
type="text"
class="form-control"
v-model="q"
@keydown="resetPage"
:placeholder="queryPlaceholder"
/>
<input type="text" class="form-control" v-model="q" @keydown="resetPage" :placeholder="queryPlaceholder" />
<span class="input-group-btn" v-if="q.length > 0">
<button class="btn btn-default" type="button" @click="q = ''">
<i class="fas fa-times"></i>
@ -16,9 +10,7 @@
</span>
</div>
<div v-if="!isDefault" class="clearSelection">
<a @click="reset"
><i class="fa fa-window-close"></i> Clear current search query, categories, platform, and sort</a
>
<a @click="reset"><i class="fa fa-window-close"></i> Clear current search query, categories, platform, and sort</a>
</div>
<project-list
v-bind="listBinding"
@ -66,11 +58,7 @@
</div>
<div class="list-group platform-list">
<a
class="list-group-item list-group-item-action"
@click="tags = []"
v-bind:class="{ active: tags.length === 0 }"
>
<a class="list-group-item list-group-item-action" @click="tags = []" v-bind:class="{ active: tags.length === 0 }">
<span class="parent">Any</span>
</a>
<a

View File

@ -1,69 +0,0 @@
<template>
<div>
<div v-show="loading">
<i class="fas fa-spinner fa-spin"></i>
<span>Loading platforms for you...</span>
</div>
<div v-show="!loading && selectedPlatform == null">
<span v-for="platform in platforms" @click="select(platform)" style="cursor: pointer" :key="platform.name">
<Tag :name="platform.name" :color="platform.tag" />
</span>
</div>
<div v-if="!loading && selectedPlatform != null">
<span @click="unselect">
<i class="fas fa-times-circle"></i>
</span>
<input type="hidden" name="platform" :value="selectedPlatform.id" form="form-publish" />
<Tag :name="selectedPlatform.name" :color="selectedPlatform.tag" />
<div>
<template v-for="(v, index) in selectedPlatform.possibleVersions" :key="index">
<label :for="'version-' + v" style="margin-left: 10px">
{{ v }}
</label>
<input form="form-publish" :id="'version-' + v" type="checkbox" name="versions" :value="v" />
</template>
</div>
</div>
</div>
</template>
<script>
import $ from 'jquery';
import Tag from './components/Tag';
export default {
name: 'platform-choice',
components: {
Tag,
},
data() {
return {
platforms: [],
loading: true,
selectedPlatform: null,
};
},
methods: {
select: function (platform) {
this.selectedPlatform = platform;
},
unselect: function () {
this.selectedPlatform = null;
},
},
created() {
const self = this;
$.ajax({
url: '/api/v1/platforms',
dataType: 'json',
complete: function () {
self.loading = false;
},
success: function (platforms) {
self.platforms = platforms;
},
});
},
};
</script>

View File

@ -27,12 +27,7 @@
<label for="add-version-input" class="sr-only">Add Version</label>
<input type="text" id="add-version-input" class="form-control" v-model="inputs[platform]" />
<div class="input-group-append">
<button
type="button"
class="btn btn-primary"
@click="addVersion(platform)"
:disabled="!inputs[platform]"
>
<button type="button" class="btn btn-primary" @click="addVersion(platform)" :disabled="!inputs[platform]">
<i class="fas fa-plus"></i>
</button>
</div>

View File

@ -0,0 +1,151 @@
<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>
</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"
@channel-created="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,12 +1,5 @@
<template>
<ProjectList
:owner="user"
:offset="(page - 1) * limit"
:limit="limit"
@prevPage="page--"
@nextPage="page++"
@jumpToPage="page = $event"
></ProjectList>
<ProjectList :owner="user" :offset="(page - 1) * limit" :limit="limit" @prevPage="page--" @nextPage="page++" @jumpToPage="page = $event"></ProjectList>
</template>
<script>

View File

@ -2,9 +2,7 @@
<div class="version-list">
<div class="row text-center">
<div class="col-12">
<a v-if="canUpload" class="btn yellow" :href="ROUTES.parse('VERSIONS_SHOW_CREATOR', projectOwner, projectSlug)"
>Upload a New Version</a
>
<a v-if="canUpload" class="btn yellow" :href="ROUTES.parse('VERSIONS_SHOW_CREATOR', projectOwner, projectSlug)">Upload a New Version</a>
</div>
</div>
<div v-show="loading">
@ -22,18 +20,13 @@
>
<div class="container-fluid">
<div class="row">
<div
class="col-4 col-md-2 col-lg-2"
:set="(channel = version.tags.find((filterTag) => filterTag.name === 'Channel'))"
>
<div class="col-4 col-md-2 col-lg-2" :set="(channel = version.tags.find((filterTag) => filterTag.name === 'Channel'))">
<div class="row">
<div class="col-12">
<span class="text-bold">{{ version.name }}</span>
</div>
<div class="col-12">
<span v-if="channel" class="channel" v-bind:style="{ background: channel.color.background }">{{
channel.data
}}</span>
<span v-if="channel" class="channel" v-bind:style="{ background: channel.color.background }">{{ channel.data }}</span>
</div>
</div>
</div>

View File

@ -23,11 +23,7 @@ export class API {
resolve(data);
})
.fail((xhr) => {
if (
xhr.responseJSON &&
(xhr.responseJSON.error === 'Api session expired' ||
xhr.responseJSON.error === 'Invalid session')
) {
if (xhr.responseJSON && (xhr.responseJSON.error === 'Api session expired' || xhr.responseJSON.error === 'Invalid session')) {
// This should never happen but just in case we catch it and invalidate the session to definitely get a new one
API.invalidateSession();
API.request(url, method, data)
@ -53,10 +49,7 @@ export class API {
if (window.isLoggedIn) {
session = parseJsonOrNull(localStorage.getItem('api_session'));
if (
session === null ||
(!isNaN(new Date(session.expires).getTime()) && new Date(session.expires) < date)
) {
if (session === null || (!isNaN(new Date(session.expires).getTime()) && new Date(session.expires) < date)) {
return $.ajax({
url: '/api/v2/authenticate/user',
method: 'POST',
@ -79,10 +72,7 @@ export class API {
}
} else {
session = parseJsonOrNull(localStorage.getItem('public_api_session'));
if (
session === null ||
(!isNaN(new Date(session.expires).getTime()) && new Date(session.expires) < date)
) {
if (session === null || (!isNaN(new Date(session.expires).getTime()) && new Date(session.expires) < date)) {
$.ajax({
url: '/api/v2/authenticate',
method: 'POST',

View File

@ -1,230 +1,208 @@
<template>
<tr>
<td colspan="2">
<div id="dependencies-accordion">
<div class="card bg-light">
<div class="card-header" id="dep-heading">
<button
class="btn btn-lg btn-block btn-primary"
data-toggle="collapse"
data-target="#dep-collapse"
aria-expanded="true"
aria-controls="dep-collapse"
>
Manage Platforms/Dependencies
</button>
<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 id="dep-collapse" class="collapse" aria-labelledby="dep-heading" data-parent="#dependencies-accordion">
<div v-if="!loading" class="card-body">
<template v-for="(platform, platformKey) in platformInfo" :key="platformKey">
<div class="col-3">
<div class="clearfix"></div>
<div>
<div class="row no-gutters">
<!--is there a better way of making two columns?-->
<div
:id="`${platformKey}-row`"
class="row platform-row align-items-center"
:style="{
backgroundColor: !platforms[platformKey] ? '#00000022' : platform.tag.background + '45',
}"
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 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>
<div v-for="version in versionList" :key="version">
<label :for="`${platformKey}-version-${version}`">
{{ version }}
</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('project_id', 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>
<input
:id="`${platformKey}-version-${version}`"
type="checkbox"
:value="version"
v-model="platforms[platformKey]"
class="mr-2"
:disabled="!platforms[platformKey]"
/>
</div>
</div>
</template>
</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('project_id', 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>
</div>
</td>
</tr>
</template>
</div>
</div>
</template>
<script>
import { nextTick } from 'vue';
@ -305,7 +283,6 @@ export default {
const platformName = platformObj.name.toUpperCase();
this.platforms[platformName] = platformObj.versions;
}
console.log(this.prevVersion.dependencies);
for (const platform in this.prevVersion.dependencies) {
this.dependencies[platform.toUpperCase()] = this.prevVersion.dependencies[platform];
}
@ -318,7 +295,6 @@ export default {
},
methods: {
setDependencyLinks(deps, platformName) {
console.log(deps);
for (const dep of deps) {
if (dep.project_id) {
this.dependencyLinking[platformName][dep.name] = 'Hangar';

View File

@ -1,12 +1,7 @@
<template>
<template v-if="enabled">
<!-- Edit -->
<button
type="button"
class="btn btn-sm btn-edit btn-page btn-default"
title="Edit"
@click.stop="pageBtnClick($event.currentTarget)"
>
<button type="button" class="btn btn-sm btn-edit btn-page btn-default" title="Edit" @click.stop="pageBtnClick($event.currentTarget)">
<i class="fas fa-edit"></i> Edit
</button>
@ -18,12 +13,7 @@
</div>
<div v-if="saveable" class="btn-edit-container btn-save-container" title="Save">
<button
form="form-editor-save"
type="submit"
class="btn btn-sm btn-save btn-page btn-default"
@click.stop="pageBtnClick($event.currentTarget)"
>
<button form="form-editor-save" type="submit" class="btn btn-sm btn-save btn-page btn-default" @click.stop="pageBtnClick($event.currentTarget)">
<i class="fas fa-save"></i>
</button>
</div>
@ -63,9 +53,7 @@
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
Are you sure you want to delete this {{ subject.toLowerCase() }}? This cannot be undone.
</div>
<div class="modal-body">Are you sure you want to delete this {{ subject.toLowerCase() }}? This cannot be undone.</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<HangarForm method="post" :action="deleteCall" clazz="form-inline">

View File

@ -0,0 +1,22 @@
<template>
<slot name="activator" v-bind:targetId="targetId"></slot>
<teleport to="body">
<div class="modal fade" :id="targetId" tabindex="-1" role="dialog" :aria-labelledby="labelId" aria-hidden="true">
<div class="modal-dialog" role="document" :class="modalClass">
<div class="modal-content">
<slot name="modal-content"></slot>
</div>
</div>
</div>
</teleport>
</template>
<script>
export default {
name: 'HangarModal',
props: {
targetId: String,
labelId: String,
modalClass: String,
},
};
</script>

View File

@ -0,0 +1,186 @@
<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-inline">
<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: ['channel-created', '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('channel-created', 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,64 +0,0 @@
<template>
<div class="float-right" id="upload-platform-tags">
<template v-for="[platform, tag] in tagsArray" :key="platform">
<div class="tags">
<span
:style="{
color: tag.color.foreground,
backgroundColor: tag.color.background,
borderColor: tag.color.background,
}"
class="tag"
>
{{ tag.name }}
</span>
</div>
<div v-if="!empty(platforms)">
<template v-for="version in platforms[platform].possibleVersions" :key="version">
<label :for="`version-${version}`">{{ version }}</label>
<input
form="form-publish"
:id="`version-${version}`"
type="checkbox"
name="versions"
:value="version"
:checked="tag.data && tag.data.indexOf(version) > -1"
/>
<!-- <template v-if="(index + 1) % 5 === 0" v-html="</div><div>"> </template>-->
</template>
</div>
</template>
</div>
</template>
<script>
import { isEmpty } from 'lodash-es';
import axios from 'axios';
export default {
name: 'PlatformTags',
props: {
tags: Array,
},
data() {
return {
tagsArray: [],
platforms: {},
};
},
created() {
for (const obj of this.tags) {
this.tagsArray.push([Object.keys(obj)[0], obj[Object.keys(obj)[0]]]);
}
axios.get('/api/v1/platforms').then((res) => {
for (const platform of res.data) {
this.platforms[platform.name.toUpperCase()] = platform;
}
});
},
methods: {
empty(obj) {
return isEmpty(obj);
},
},
};
</script>

View File

@ -16,34 +16,18 @@
<div class="container-fluid">
<div class="row">
<div class="col-12 col-sm-1">
<Icon
:name="project.namespace.owner"
:src="project.icon_url"
extra-classes="user-avatar-sm"
></Icon>
<Icon :name="project.namespace.owner" :src="project.icon_url" extra-classes="user-avatar-sm"></Icon>
</div>
<div class="col-12 col-sm-11">
<div class="row">
<div class="col-sm-6">
<a
:href="
routes.Projects.show(
project.namespace.owner,
project.namespace.slug
).absoluteURL()
"
class="title"
>
<a :href="routes.Projects.show(project.namespace.owner, project.namespace.slug).absoluteURL()" class="title">
{{ project.name }}
</a>
</div>
<div class="col-sm-6 hidden-xs">
<div class="info minor">
<span
class="stat recommended-version"
title="Recommended version"
v-if="project.recommended_version"
>
<span class="stat recommended-version" title="Recommended version" v-if="project.recommended_version">
<i class="far fa-gem"></i>
<a
:href="
@ -58,24 +42,14 @@
</a>
</span>
<span class="stat" title="Views"
><i class="fas fa-eye"></i>
{{ formatStats(project.stats.views) }}</span
>
<span class="stat" title="Views"><i class="fas fa-eye"></i> {{ formatStats(project.stats.views) }}</span>
<span class="stat" title="Download"
><i class="fas fa-download"></i>
{{ formatStats(project.stats.downloads) }}</span
>
<span class="stat" title="Stars"
><i class="fas fa-star"></i>
{{ formatStats(project.stats.stars) }}</span
><i class="fas fa-download"></i> {{ formatStats(project.stats.downloads) }}</span
>
<span class="stat" title="Stars"><i class="fas fa-star"></i> {{ formatStats(project.stats.stars) }}</span>
<span :title="categoryFromId(project.category).name" class="stat">
<i
:class="'fa-' + categoryFromId(project.category).icon"
class="fas"
></i>
<i :class="'fa-' + categoryFromId(project.category).icon" class="fas"></i>
</span>
</div>
</div>
@ -174,17 +148,7 @@ export default {
this.update();
this.debouncedUpdateProps = debounce(this.update, 500);
this.$watch(
() =>
[
this.q,
this.categories,
this.tags,
this.owner,
this.sort,
this.relevance,
this.limit,
this.offset,
].join(),
() => [this.q, this.categories, this.tags, this.owner, this.sort, this.relevance, this.limit, this.offset].join(),
() => {
this.debouncedUpdateProps();
}

View File

@ -2,10 +2,8 @@ import { createApp } from 'vue';
import CreateVersion from '@/CreateVersion';
createApp(CreateVersion, {
defaultColor: window.DEFAULT_COLOR,
pendingVersion: window.PENDING_VERSION,
ownerName: window.OWNER_NAME,
projectSlug: window.PROJECT_SLUG,
channels: window.CHANNELS,
forumSync: window.FORUM_SYNC,
}).mount('#create-version');

View File

@ -39,6 +39,7 @@ import {
faKey,
faLink,
faList,
faListOl,
faLock,
faMagic,
faMoneyBillAlt,
@ -48,6 +49,7 @@ import {
faPlayCircle,
faPlus,
faPuzzlePiece,
faQuestion,
faQuestionCircle,
faReply,
faSave,
@ -57,6 +59,7 @@ import {
faSpinner,
faStar as fasStar,
faStopCircle,
faTag,
faTags,
faTerminal,
faThumbsUp as fasThumbsUp,
@ -120,6 +123,7 @@ library.add(
faChartArea,
faHeartbeat,
faList,
faListOl,
faSignOutAlt,
farThumbsUp,
faTrash,
@ -168,7 +172,9 @@ library.add(
faUserTag,
faTags,
faExclamationTriangle,
faCheckSquare
faCheckSquare,
faQuestion,
faTag
);
dom.watch();

View File

@ -1,4 +0,0 @@
import { createApp } from 'vue';
import PlatformChoice from '@/PlatformChoice';
createApp(PlatformChoice).mount('#platform-choice');

View File

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

View File

@ -9,10 +9,7 @@ $('body')
var idToDiff = $(this).attr('data-diff');
var diff = new diff_match_patch();
var textDiff = diff.diff_main(
$('textarea[data-oldstate="' + idToDiff + '"]').val(),
$('textarea[data-newstate="' + idToDiff + '"]').val()
);
var textDiff = diff.diff_main($('textarea[data-oldstate="' + idToDiff + '"]').val(), $('textarea[data-newstate="' + idToDiff + '"]').val());
diff.diff_cleanupSemantic(textDiff);
$('#modal-view-body').html(diff.diff_prettyHtml(textDiff).replace(/&para;/g, ''));
@ -22,9 +19,7 @@ $('body')
var idToShow = $(this).attr('data-view');
$('#modal-view-body').html(
'<textarea disabled style="width: 100%; height: 35vh">' +
$('textarea[data-oldstate="' + idToShow + '"]').val() +
'</textarea>'
'<textarea disabled style="width: 100%; height: 35vh">' + $('textarea[data-oldstate="' + idToShow + '"]').val() + '</textarea>'
);
$('#modal-view').modal('show');
})
@ -32,9 +27,7 @@ $('body')
var idToShow = $(this).attr('data-view');
$('#modal-view-body').html(
'<textarea disabled style="width: 100%; height: 35vh">' +
$('textarea[data-newstate="' + idToShow + '"]').val() +
'</textarea>'
'<textarea disabled style="width: 100%; height: 35vh">' + $('textarea[data-newstate="' + idToShow + '"]').val() + '</textarea>'
);
$('#modal-view').modal('show');
});

View File

@ -86,14 +86,7 @@ $(function () {
row.append($('<th>').text(token));
row.append($('<th>'));
row.append($('<th>').text(namedPerms));
row.append(
$('<th>').append(
$('<button>')
.addClass('btn btn-danger api-key-row-delete-button')
.text(DELETE_KEY)
.click(deleteKey(name, row))
)
);
row.append($('<th>').append($('<button>').addClass('btn btn-danger api-key-row-delete-button').text(DELETE_KEY).click(deleteKey(name, row))));
$('#api-key-rows:last-child').append(row);
});

View File

@ -21,11 +21,7 @@ export function apiV2Request(url, method = 'GET', data = {}) {
resolve(data);
})
.fail(function (xhr) {
if (
xhr.responseJSON &&
(xhr.responseJSON.error === 'Api session expired' ||
xhr.responseJSON.error === 'Invalid session')
) {
if (xhr.responseJSON && (xhr.responseJSON.error === 'Api session expired' || xhr.responseJSON.error === 'Invalid session')) {
// This should never happen but just in case we catch it and invalidate the session to definitely get a new one
invalidateApiSession();
apiV2Request(url, method, data)

View File

@ -90,14 +90,7 @@ export function initChannelManager(toggle, channelName, channelHex, title, call,
submitInput.submit(function (event) {
event.preventDefault();
var modal = getModal();
onCustomSubmit(
toggle,
modal.find('.channel-input').val(),
modal.find('.channel-color-input').val(),
title,
submit,
nonReviewed
);
onCustomSubmit(toggle, modal.find('.channel-input').val(), modal.find('.channel-color-input').val(), title, submit, nonReviewed);
});
} else {
// Set form action
@ -173,16 +166,7 @@ function initColorPicker() {
$(function () {
initModal();
if (DEFAULT_HEX && CHANNEL_CREATE_ROUTE) {
initChannelManager(
'#channel-new',
'',
DEFAULT_HEX,
'New channel',
CHANNEL_CREATE_ROUTE,
'post',
'Create channel',
false
);
initChannelManager('#channel-new', '', DEFAULT_HEX, 'New channel', CHANNEL_CREATE_ROUTE, 'post', 'Create channel', false);
}
if (window.loadDeleteManager) {

View File

@ -22,11 +22,7 @@ function bindKeyGen(e) {
success: function (key) {
console.log(key);
$('.input-key').val(key.value);
$this
.removeClass('btn-key-gen btn-info')
.addClass('btn-key-revoke btn-danger')
.data('key-id', key.id)
.off('click');
$this.removeClass('btn-key-gen btn-info').addClass('btn-key-revoke btn-danger').data('key-id', key.id).off('click');
$this.find('.text').text(keyRevokeText);
bindKeyRevoke($this);

View File

@ -10,9 +10,7 @@ import 'bootstrap/js/dist/tooltip';
const clipboardManager = new ClipboardJS('.copy-url');
clipboardManager.on('success', function () {
const element = $('.btn-download')
.tooltip({ title: 'Copied!', placement: 'bottom', trigger: 'manual' })
.tooltip('show');
const element = $('.btn-download').tooltip({ title: 'Copied!', placement: 'bottom', trigger: 'manual' }).tooltip('show');
setTimeout(function () {
element.tooltip('dispose');
}, 2200);

View File

@ -29,10 +29,7 @@ $(function () {
var user = result.user;
// Check if user is already defined
if (
$('input[value="' + user.id + '"]').length ||
$('.table-members').first('tr').find('strong').text() === user.username
) {
if ($('input[value="' + user.id + '"]').length || $('.table-members').first('tr').find('strong').text() === user.username) {
return;
}

View File

@ -27,12 +27,7 @@ function bindExpand(e) {
link.text(page.name); // this will sanitize the input
div.append(childPage);
}
$this
.removeClass('page-expand')
.addClass('page-collapse')
.find('[data-fa-i2svg]')
.removeClass('fa-plus-square')
.addClass('fa-minus-square');
$this.removeClass('page-expand').addClass('page-collapse').find('[data-fa-i2svg]').removeClass('fa-plus-square').addClass('fa-minus-square');
$this.off('click');
bindCollapse($this);
},
@ -44,12 +39,7 @@ function bindCollapse(e) {
e.click(function () {
var pageId = $(this).data('page-id');
$('.page-children[data-page-id="' + pageId + '"]').remove();
$(this)
.removeClass('page-collapse')
.addClass('page-expand')
.find('[data-fa-i2svg]')
.removeClass('fa-minus-square')
.addClass('fa-plus-square');
$(this).removeClass('page-collapse').addClass('page-expand').find('[data-fa-i2svg]').removeClass('fa-minus-square').addClass('fa-plus-square');
$(this).off('click');
bindExpand($(this));
});

View File

@ -26,17 +26,7 @@ $(function () {
if (parent.length) {
parentId = parent.val() === '-1' ? null : parent.val();
if (parentId !== null)
url =
'/' +
projectOwner +
'/' +
projectSlug +
'/pages/' +
parent.data('slug') +
'/' +
slugify(pageName) +
'/edit';
if (parentId !== null) url = '/' + projectOwner + '/' + projectSlug + '/pages/' + parent.data('slug') + '/' + slugify(pageName) + '/edit';
}
$.ajax({
method: 'post',

View File

@ -240,11 +240,7 @@ $(function () {
if (response.promoted_versions) {
let html = '';
response.promoted_versions.forEach((version) => {
const href = window.jsRoutes.controllers.project.Versions.show(
PROJECT_OWNER,
PROJECT_SLUG,
version.version
).absoluteURL();
const href = window.jsRoutes.controllers.project.Versions.show(PROJECT_OWNER, PROJECT_SLUG, version.version).absoluteURL();
html = html + "<li class='list-group-item'><a href='" + href + "'>" + version.version + '</a></li>';
});
$('.promoted-list').html(html);

View File

@ -17,13 +17,7 @@ $(function () {
$(this)
.text('pastdue ' + momentAgo.fromNow())
.css('color', 'darkred');
$(this)
.parent()
.parent()
.find('.status')
.removeClass()
.addClass('status far fa-fw fa-clock fa-2x')
.css('color', 'darkred');
$(this).parent().parent().find('.status').removeClass().addClass('status far fa-fw fa-clock fa-2x').css('color', 'darkred');
}
});

View File

@ -41,11 +41,7 @@ $(function () {
var newItem = $('<div class="list-group-item"></div>');
newItem.data({ role: selected.val() });
newItem.text(selected.text());
newItem.append(
$(
'<span class="float-right"><a href="#" class="global-role-delete"><i class="fa fa-trash"></i></a></span>'
)
);
newItem.append($('<span class="float-right"><a href="#" class="global-role-delete"><i class="fa fa-trash"></i></a></span>'));
newItem.insertBefore(globalRoleList.children().last());
// Remove from select
selected.remove();

View File

@ -29,59 +29,55 @@ function loadActions(increment, action) {
pages[action] += increment;
var offset = (pages[action] - 1) * CONTENT_PER_PAGE;
apiV2Request('users/' + USERNAME + '/' + action + '?offset=' + offset + '&limit=' + CONTENT_PER_PAGE).then(
function (result) {
//TODO: Use pagination info
var tbody = getStarsPanel(action).find('.card-body').find('tbody');
apiV2Request('users/' + USERNAME + '/' + action + '?offset=' + offset + '&limit=' + CONTENT_PER_PAGE).then(function (result) {
//TODO: Use pagination info
var tbody = getStarsPanel(action).find('.card-body').find('tbody');
var content = [];
var content = [];
if (result.pagination.count === 0) {
content.push(
$('<tr>').append($('<td>').append($("<i class='minor'>").text(NO_ACTION_MESSAGE[action])))
);
} else {
for (var project of result.result) {
var link = $('<a>')
.attr('href', '/' + project.namespace.owner + '/' + project.namespace.slug)
.text(project.namespace.owner + '/')
.append($('<strong>').text(project.namespace.slug));
var versionDiv = $("<div class='float-right'>");
if (project.recommended_version) {
versionDiv.append($("<span class='minor'>").text(project.recommended_version.version));
}
var versionIcon = $('<i>');
versionIcon.attr('title', CATEGORY_TITLE[project.category]);
versionIcon.addClass('fas fa-fw').addClass(CATEGORY_ICON[project.category]);
versionDiv.append(versionIcon);
content.push($('<tr>').append($('<td>').append(link, versionDiv)));
if (result.pagination.count === 0) {
content.push($('<tr>').append($('<td>').append($("<i class='minor'>").text(NO_ACTION_MESSAGE[action]))));
} else {
for (var project of result.result) {
var link = $('<a>')
.attr('href', '/' + project.namespace.owner + '/' + project.namespace.slug)
.text(project.namespace.owner + '/')
.append($('<strong>').text(project.namespace.slug));
var versionDiv = $("<div class='float-right'>");
if (project.recommended_version) {
versionDiv.append($("<span class='minor'>").text(project.recommended_version.version));
}
}
// Done loading, set the table to the result
tbody.empty();
tbody.append(content);
var footer = getStarsFooter(action);
var prev = footer.find('.prev');
var versionIcon = $('<i>');
versionIcon.attr('title', CATEGORY_TITLE[project.category]);
versionIcon.addClass('fas fa-fw').addClass(CATEGORY_ICON[project.category]);
versionDiv.append(versionIcon);
// Check if there is a last page
if (pages[action] > 1) {
prev.show();
} else {
prev.hide();
}
// Check if there is a next page
var next = footer.find('.next');
if (result.pagination.count > pages[action] * CONTENT_PER_PAGE) {
next.show();
} else {
next.hide();
content.push($('<tr>').append($('<td>').append(link, versionDiv)));
}
}
);
// Done loading, set the table to the result
tbody.empty();
tbody.append(content);
var footer = getStarsFooter(action);
var prev = footer.find('.prev');
// Check if there is a last page
if (pages[action] > 1) {
prev.show();
} else {
prev.hide();
}
// Check if there is a next page
var next = footer.find('.next');
if (result.pagination.count > pages[action] * CONTENT_PER_PAGE) {
next.show();
} else {
next.hide();
}
});
}
function formAsync(form, route, onSuccess) {

View File

@ -54,17 +54,7 @@ $(function () {
if (!exists) {
setColorInput(channelHex);
select.find(':selected').removeAttr('selected');
select.append(
'<option data-color="' +
channelHex +
'" ' +
'value="' +
channelName +
'" ' +
'selected>' +
channelName +
'</option>'
);
select.append('<option data-color="' + channelHex + '" ' + 'value="' + channelName + '" ' + 'selected>' + channelName + '</option>');
}
$('#channel-settings').modal('hide');

View File

@ -0,0 +1,32 @@
.channel {
align-items: center;
display: inline-flex;
border-radius: 3px;
font-size: 0.75em;
height: 2em;
padding-left: 0.75em;
padding-right: 0.75em;
white-space: nowrap;
}
.channel {
font-weight: bold;
padding: 2px 4px;
text-align: center;
color: white;
cursor: default;
}
.channel-sm {
font-size: 10px;
}
.channel-head {
@include towardsBottomRight(15px, 23px);
font-size: 20px;
display: inline-block;
}
.channel-flat {
border-radius: 0;
}

View File

@ -39,24 +39,23 @@
border-bottom: 1px solid $lighter;
}
.plugin-meta-table {
@include basic-border();
@include box-shadow4(0, 1px, 1px, rgba(0,0,0,0.05));
input[type="text"] { width: 100%; }
tr { width: 100%; }
td { padding: 10px; }
& > tr:nth-child(2n) { background-color: #f5f5f5; }
.rv { padding: 0; }
margin-top: 5px;
width: 100%;
& > tr > td:nth-child(2n) {
text-align: right;
float: right;
}
}
//.plugin-meta-table {
// @include basic-border();
// @include box-shadow4(0, 1px, 1px, rgba(0,0,0,0.05));
//
// tr { width: 100%; }
// td { padding: 10px; }
// & > tr:nth-child(2n) { background-color: #f5f5f5; }
// .rv { padding: 0; }
//
// margin-top: 5px;
// width: 100%;
//
// & > tr > td:nth-child(2n) {
// text-align: right;
// float: right;
// }
//}
.create-buttons > form {
margin-bottom: 0;

View File

@ -140,28 +140,6 @@
border-bottom: 1px solid black;
}
.channel {
font-weight: bold;
padding: 2px 4px;
text-align: center;
color: white;
cursor: default;
}
.channel-sm {
font-size: 10px;
}
.channel-head {
@include towardsBottomRight(15px, 23px);
font-size: 20px;
display: inline-block;
}
.channel-flat {
border-radius: 0;
}
.spongeapi {
background-color: $sponge_yellow;
color: black;

View File

@ -16,6 +16,8 @@
@import "dropdowns";
@import "buttons";
@import "channel";
html {
position: relative;
min-height: 100%;
@ -259,17 +261,6 @@ select.form-control, input.form-control {
}
}
.channel {
align-items: center;
display: inline-flex;
border-radius: 3px;
font-size: 0.75em;
height: 2em;
padding-left: 0.75em;
padding-right: 0.75em;
white-space: nowrap;
}
.table tbody > tr > td {
vertical-align: middle;
}

View File

@ -0,0 +1,8 @@
import { computed } from 'vue';
export function useModelWrapper(props, emit, name = 'modelValue') {
return computed({
get: () => props[name],
set: (val) => emit(`update:${name}`, val),
});
}

View File

@ -1,5 +1,8 @@
package io.papermc.hangar.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.papermc.hangar.db.model.ProjectsTable;
import io.papermc.hangar.model.Color;
import io.papermc.hangar.model.NamedPermission;
@ -23,13 +26,15 @@ import java.util.function.Supplier;
public class ChannelsController extends HangarController {
private final ChannelService channelService;
private final ObjectMapper mapper;
private final Supplier<ProjectsTable> projectsTable;
private final Supplier<ProjectData> projectData;
@Autowired
public ChannelsController(ChannelService channelService, Supplier<ProjectsTable> projectsTable, Supplier<ProjectData> projectData) {
public ChannelsController(ChannelService channelService, ObjectMapper mapper, Supplier<ProjectsTable> projectsTable, Supplier<ProjectData> projectData) {
this.channelService = channelService;
this.mapper = mapper;
this.projectsTable = projectsTable;
this.projectData = projectData;
}
@ -40,8 +45,16 @@ public class ChannelsController extends HangarController {
@GetMapping("/{author}/{slug}/channels")
public ModelAndView showList(@PathVariable String author, @PathVariable String slug) {
ModelAndView mv = new ModelAndView("projects/channels/list");
mv.addObject("p", projectData.get());
mv.addObject("channels", channelService.getChannelsWithVersionCount(projectData.get().getProject().getId()));
ProjectData projData = projectData.get();
ArrayNode channels = mapper.createArrayNode();
channelService.getChannelsWithVersionCount(projData.getProject().getId()).forEach((projectChannelsTable, count) -> {
ObjectNode channel = mapper.createObjectNode()
.put("versionCount", count)
.set("channel", mapper.valueToTree(projectChannelsTable));
channels.add(channel);
});
mv.addObject("p", projData);
mv.addObject("channels", channels);
return fillModel(mv);
}
@ -49,8 +62,8 @@ public class ChannelsController extends HangarController {
@UserLock(route = Routes.PROJECTS_SHOW, args = "{#author, #slug}")
@Secured("ROLE_USER")
@PostMapping("/{author}/{slug}/channels")
public ModelAndView create(@PathVariable String author, @PathVariable String slug, @RequestParam("channel-input") String channelId, @RequestParam("channel-color-input") Color channelColor) {
channelService.addProjectChannel(projectsTable.get().getId(), channelId, channelColor, false);
public ModelAndView create(@PathVariable String author, @PathVariable String slug, @RequestParam("name") String channelId, @RequestParam("color") Color channelColor, @RequestParam("nonReviewed") boolean nonReviewed) {
channelService.addProjectChannel(projectsTable.get().getId(), channelId, channelColor, nonReviewed);
return Routes.CHANNELS_SHOW_LIST.getRedirect(author, slug);
}
@ -58,9 +71,13 @@ public class ChannelsController extends HangarController {
@UserLock(route = Routes.PROJECTS_SHOW, args = "{#author, #slug}")
@Secured("ROLE_USER")
@PostMapping("/{author}/{slug}/channels/{channel}")
public ModelAndView save(@PathVariable String author, @PathVariable String slug, @PathVariable String channel,
@RequestParam("channel-input") String newChannelName, @RequestParam("channel-color-input") String newChannelHex) {
channelService.updateProjectChannel(projectsTable.get().getId(), channel, newChannelName, Color.getByHexStr(newChannelHex));
public ModelAndView save(@PathVariable String author,
@PathVariable String slug,
@PathVariable String channel,
@RequestParam("name") String name,
@RequestParam("color") Color color,
@RequestParam(value = "nonReviewed") boolean nonReviewed) {
channelService.updateProjectChannel(projectsTable.get().getId(), channel, name, color, nonReviewed);
return Routes.CHANNELS_SHOW_LIST.getRedirect(author, slug);
}

View File

@ -50,7 +50,6 @@ public abstract class HangarController {
mav.addObject("utils", templateHelper);
mav.addObject("mapper", mapper);
try {
mav.addObject("Routes", staticModels.get("io.papermc.hangar.util.Routes"));
} catch (TemplateModelException e) {

View File

@ -419,150 +419,6 @@ public class VersionsController extends HangarController {
return Routes.VERSIONS_SHOW.getRedirect(author, slug, versionName);
}
@ProjectPermission(NamedPermission.CREATE_VERSION)
@UserLock(route = Routes.PROJECTS_SHOW, args = "{#author, #slug}")
@Secured("ROLE_USER")
@PostMapping(value = "/{author}/{slug}/versions/publish", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public ModelAndView publish(@PathVariable String author,
@PathVariable String slug,
@RequestParam String versionString,
@RequestParam String externalUrl,
@RequestParam Platform platform,
@RequestParam(required = false) String versionDescription,
@RequestParam(defaultValue = "false") boolean unstable,
@RequestParam(defaultValue = "false") boolean recommended,
@RequestParam("channel-input") String channelInput,
@RequestParam(value = "channel-color-input", required = false) Color channelColorInput,
@RequestParam(value = "non-reviewed", defaultValue = "false") boolean nonReviewed,
@RequestParam(value = "forum-post", defaultValue = "false") boolean forumPost,
@RequestParam(required = false) String content,
@RequestParam List<String> versions,
RedirectAttributes attributes) {
ProjectsTable project = projectsTable.get();
PendingVersion pendingVersion = new PendingVersion(
versionString,
new VersionDependencies(),
List.of(new PlatformDependency(platform, versions)),
versionDescription,
project.getId(),
null,
null,
null,
project.getOwnerId(),
channelInput,
channelColorInput,
null,
externalUrl,
forumPost,
versionService.getMostRelevantVersion(project));
return _publish(author, slug, versionString, unstable, recommended, channelInput, channelColorInput, forumPost, nonReviewed,content, pendingVersion, attributes, pendingVersion.getPlatforms());
}
private ModelAndView _publish(String author, String slug, String versionName, boolean unstable, boolean recommended, String channelInput, Color channelColorInput, boolean forumPost, boolean isNonReviewed, String content, PendingVersion pendingVersion, RedirectAttributes attributes, List<PlatformDependency> platformDependencies) {
ProjectData projData = projectData.get();
Color channelColor = channelColorInput == null ? hangarConfig.channels.getColorDefault() : channelColorInput;
if (pendingVersion == null) {
AlertUtil.showAlert(attributes, AlertUtil.AlertType.ERROR, "error.plugin.timeout");
return Routes.VERSIONS_SHOW_CREATOR.getRedirect(author, slug);
}
if (platformDependencies.stream().anyMatch(s -> !s.getPlatform().getPossibleVersions().containsAll(s.getVersions()))) {
AlertUtil.showAlert(attributes, AlertType.ERROR, "error.plugin.invalidVersion");
return Routes.VERSIONS_SHOW_CREATOR.getRedirect(author, slug);
}
List<ProjectChannelsTable> projectChannels = channelService.getProjectChannels(projData.getProject().getId());
String alertMsg = null;
String[] alertArgs = new String[0];
Optional<ProjectChannelsTable> channelOptional = projectChannels.stream().filter(ch -> ch.getName().equals(channelInput.trim())).findAny();
ProjectChannelsTable channel;
if (channelOptional.isEmpty()) {
if (projectChannels.size() >= hangarConfig.projects.getMaxChannels()) {
alertMsg = "error.channel.maxChannels";
alertArgs = new String[]{String.valueOf(hangarConfig.projects.getMaxChannels())};
} else if (projectChannels.stream().anyMatch(ch -> ch.getColor() == channelColor)) {
alertMsg = "error.channel.duplicateColor";
}
if (alertMsg != null) {
AlertUtil.showAlert(attributes, AlertUtil.AlertType.ERROR, alertMsg, alertArgs);
return Routes.VERSIONS_SHOW_CREATOR.getRedirect(author, slug);
}
channel = channelService.addProjectChannel(projData.getProject().getId(), channelInput.trim(), channelColor, isNonReviewed);
} else {
channel = channelOptional.get();
}
PendingVersion newPendingVersion = pendingVersion.copy(
channel.getName(),
channel.getColor(),
forumPost,
content,
platformDependencies
);
if (versionService.exists(newPendingVersion)) {
AlertUtil.showAlert(attributes, AlertUtil.AlertType.ERROR, "error.plugin.versionExists");
return Routes.VERSIONS_SHOW_CREATOR.getRedirect(author, slug);
}
ProjectVersionsTable version;
try {
version = newPendingVersion.complete(request, projData, projectFactory);
} catch (HangarException e) {
AlertUtil.showAlert(attributes, AlertUtil.AlertType.ERROR, e.getMessage(), e.getArgs());
return Routes.VERSIONS_SHOW_CREATOR.getRedirect(author, slug);
}
if (recommended) {
projData.getProject().setRecommendedVersionId(version.getId());
projectDao.get().update(projData.getProject());
}
if (unstable) {
versionService.addUnstableTag(version.getId());
}
userActionLogService.version(request, LoggedActionType.VERSION_UPLOADED.with(VersionContext.of(projData.getProject().getId(), version.getId())), "published", "");
return Routes.VERSIONS_SHOW.getRedirect(author, slug, versionName);
}
// @ProjectPermission(NamedPermission.CREATE_VERSION)
// @UserLock(route = Routes.PROJECTS_SHOW, args = "{#author, #slug}")
// @Secured("ROLE_USER")
// @PostMapping(value = "/{author}/{slug}/versions/{version:.+}", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
// public ModelAndView publish(@PathVariable String author,
// @PathVariable String slug,
// @PathVariable("version") String versionName,
// @RequestParam(defaultValue = "false") boolean unstable,
// @RequestParam(defaultValue = "false") boolean recommended,
// @RequestParam("channel-input") String channelInput,
// @RequestParam(value = "channel-color-input", required = false) Color channelColorInput,
// @RequestParam(value = "non-reviewed", defaultValue = "false") boolean nonReviewed,
// @RequestParam(value = "forum-post", defaultValue = "false") boolean forumPost,
// @RequestParam(required = false) String content,
// @RequestParam List<String> versions,
// RedirectAttributes attributes) {
// ProjectsTable project = projectsTable.get();
// PendingVersion pendingVersion = cacheManager.getCache(CacheConfig.PENDING_VERSION_CACHE).get(project.getId() + "/" + versionName, PendingVersion.class);
// return _publish(author,
// slug,
// versionName,
// unstable,
// recommended,
// channelInput,
// channelColorInput,
// forumPost,
// nonReviewed,
// content,
// pendingVersion,
// attributes,
// Optional.ofNullable(pendingVersion).map(PendingVersion::getPlatforms).orElse(new ArrayList<>())
// );
// }
@GetMapping("/{author}/{slug}/versions/{version:.*}")
public ModelAndView show(@PathVariable String author, @PathVariable String slug, @PathVariable String version, ModelMap modelMap) {
ModelAndView mav = new ModelAndView("projects/versions/view");

View File

@ -27,8 +27,8 @@ public interface ProjectChannelDao {
@GetGeneratedKeys
ProjectChannelsTable insert(@BindBean ProjectChannelsTable projectChannel);
@SqlUpdate("UPDATE project_channels SET name = :name, color = :color WHERE project_id = :projectId AND name = :oldName")
void update(long projectId, String oldName, String name, @EnumByOrdinal Color color);
@SqlUpdate("UPDATE project_channels SET name = :name, color = :color, is_non_reviewed = :nonReviewed WHERE project_id = :projectId AND name = :oldName")
void update(long projectId, String oldName, String name, @EnumByOrdinal Color color, boolean nonReviewed);
@SqlQuery("SELECT * FROM project_channels WHERE project_id = :projectId LIMIT 1")
ProjectChannelsTable getFirstChannel(long projectId);
@ -50,7 +50,7 @@ public interface ProjectChannelDao {
"CASE WHEN '<channelName>' IN (SELECT \"name\" FROM project_channels WHERE project_id = :projectId AND name != :oldChannelName) THEN 'DUPLICATE_NAME' " +
" WHEN <colorValue> IN (SELECT color FROM project_channels WHERE project_id = :projectId AND name != :oldChannelName) THEN 'UNIQUE_COLOR' " +
"END")
ChannelService.InvalidChannelCreationReason validateChanneUpdate(long projectId, String oldChannelName, @Define String channelName, @Define long colorValue);
ChannelService.InvalidChannelCreationReason validateChannelUpdate(long projectId, String oldChannelName, @Define String channelName, @Define long colorValue);
@SqlQuery("SELECT * FROM project_channels WHERE project_id = :projectId AND (name = :channelName OR id = :channelId)")
ProjectChannelsTable getProjectChannel(long projectId, String channelName, Long channelId);

View File

@ -5,6 +5,11 @@ import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonFormat.Shape;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@JsonFormat(shape = Shape.OBJECT)
public enum Color {
@ -28,6 +33,7 @@ public enum Color {
TRANSPARENT(17, "transparent");
private static final Color[] VALUES = values();
private static List<Color> CHANNEL_COLORS = null;
private final int value;
private final String hex;
@ -44,6 +50,15 @@ public enum Color {
return hex;
}
@JsonCreator
public static Color getByIdAndHex(@JsonProperty("hex") String hex, @JsonProperty("value") int id) {
Color color = VALUES[id];
if (color.hex.equalsIgnoreCase(hex)) {
return color;
}
return null;
}
@JsonCreator
public static Color getById(@JsonProperty("value") int id) {
return VALUES[id];
@ -59,4 +74,11 @@ public enum Color {
public static Color[] getValues() {
return VALUES;
}
public static List<Color> getNonTransparentValues() {
if (CHANNEL_COLORS == null) {
CHANNEL_COLORS = Arrays.stream(VALUES).filter(c -> c.value <= 15).collect(Collectors.toList());
}
return CHANNEL_COLORS;
}
}

View File

@ -75,8 +75,6 @@ public class PaperPluginFileHandler extends FileTypeHandler {
result.add(new DataValue.DependencyDataValue(FileTypeHandler.DEPENDENCIES, getPlatform(), dependencies));
}
// System.out.println(dependencies);
List<String> versions = new ArrayList<>();
if (data.containsKey("api-version")) {
versions.add(data.get("api-version").toString());

View File

@ -13,6 +13,7 @@ import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@Service
public class ChannelService {
@ -58,14 +59,14 @@ public class ChannelService {
return channelFactory.createChannel(projectId, channelName, color, isNonReviewed);
}
public void updateProjectChannel(long projectId, String oldChannel, String channelName, Color color) {
public void updateProjectChannel(long projectId, String oldChannel, String channelName, Color color, boolean nonReviewed) {
if (!hangarConfig.channels.isValidChannelName(channelName)) {
throw new HangarException("error.channel.invalidName", channelName);
}
InvalidChannelCreationReason reason = channelDao.get().validateChanneUpdate(projectId, oldChannel, channelName, color.getValue());
InvalidChannelCreationReason reason = channelDao.get().validateChannelUpdate(projectId, oldChannel, channelName, color.getValue());
checkInvalidChannelCreationReason(reason);
channelDao.get().update(projectId, oldChannel, channelName, color);
channelDao.get().update(projectId, oldChannel, channelName, color, nonReviewed);
}
private void checkInvalidChannelCreationReason(@Nullable InvalidChannelCreationReason reason) {

View File

@ -93,7 +93,7 @@ showFooter: Boolean = true, noContainer: Boolean = false, additionalMeta: Html =
<#if scriptsEnabled>
<script>
window.ROUTES = ${mapper.valueToTree(Routes.getJsRoutes())}
window.ROUTES = ${mapper.valueToTree(Routes.getJsRoutes())};
window.ROUTES.parse = function (key, ...params) {
var route = window.ROUTES[key];
for (let param of params) {

View File

@ -7,16 +7,20 @@
<#assign scriptsVar>
<script <#--@CSPNonce.attr-->>
window.PROJECT_OWNER = '${p.project.ownerName}';
window.PROJECT_SLUG = '${p.project.slug}';
window.DEFAULT_HEX = '${config.channels.colorDefault.hex}';
window.CHANNEL_CREATE_ROUTE = '${Routes.CHANNELS_CREATE.getRouteUrl(p.project.ownerName, p.project.slug)}';
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)};
</script>
<script type="text/javascript" src="<@hangar.url "js/channelManage.js" />"></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>
<@base.base title=message additionalScripts=scriptsVar additionalStyling=styleVar>
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
@ -27,70 +31,7 @@
<p class="minor create-blurb">
<@spring.message "channel.list.description" />
</p>
<table class="table no-border centered">
<tbody>
<#list channels as channel, versions>
<tr>
<td>
<div class="channel"
style="background-color: ${channel.color.hex}">${channel.name}</div>
</td>
<td>
<div class="btn btn-sm yellow" data-toggle="modal" data-target="#channel-settings"
id="channel-edit-${channel.id}">Edit
</div>
</td>
<#if channels?size gt 1>
<td>
<div class="btn btn-sm yellow"
<#if versions gt 0>
id="channel-delete-${channel.id}" data-toggle="modal"
data-target="#modal-delete">
<#else>
id="channel-delete-${channel.id}" data-channel-delete="safe-delete"
data-channel-id="${channel.id}"
>
<@form.form method="POST" action=Routes.CHANNELS_DELETE.getRouteUrl(p.project.ownerName, p.project.slug, channel.name)
id="form-delete-${channel.id}"
class="form-channel-delete">
<@csrf.formField />
</@form.form>
</#if>
Delete
</div>
</td>
</#if>
<script <#--@CSPNonce.attr-->>
window.loadDeleteManager = function() {
initChannelDelete('#channel-delete-${channel.id}', '${channel.name}', ${versions});
initChannelManager(
"#channel-edit-${channel.id}", "${channel.name}", "${channel.color.hex}",
"Edit channel", "${Routes.CHANNELS_SAVE.getRouteUrl(
p.project.ownerName, p.project.slug, channel.name)}",
"post", "Save changes", ${channel.isNonReviewed?string('true', 'false')}
);
}
</script>
</tr>
</#list>
</tbody>
</table>
<a href="${Routes.VERSIONS_SHOW_LIST.getRouteUrl(p.project.ownerName, p.project.slug)}"
class="float-left btn btn-default">
<i class="fas fa-arrow-left"></i>
</a>
<a href="#" id="channel-new" class="float-right btn btn-primary"
<#if channels?size gte config.projects.maxChannels>
disabled data-toggle="tooltip" data-placement="left"
title="<@spring.message "channel.edit.maxReached" />"
<#else>
data-toggle="modal" data-target="#channel-settings"
</#if>
>
<i class="fas fa-plus"></i>
</a>
<@modalManage.modalManage />
<div id="release-channels"></div>
</div>
</div>
</div>
@ -109,14 +50,15 @@
<div class="modal-body">
<p><@spring.message "channel.delete.info" /></p>
<p class="minor">
<strong class="version-count"></strong> <i><@spring.message "channel.delete.info.versions" /></i>
<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 method="post" action="#" class="form-channel-delete">
<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>

View File

@ -11,21 +11,15 @@
<#assign mainWidth = "col-md-10">
<#assign scriptsVar>
<script type="text/javascript" src="<@hangar.url "js/channelManage.js" />"></script>
<#-- <script type="text/javascript" src="<@hangar.url "js/pluginUpload.js" />"></script>-->
<#-- <script type="text/javascript" src="<@hangar.url "js/projectDetail.js" />"></script>-->
<#if pending?? && !pending.dependencies??>
<script type="text/javascript" src="<@hangar.url "js/platform-choice.js" />"></script>
</#if>
<script>
window.DEFAULT_COLOR = '${config.channels.colorDefault.hex}';
window.OWNER_NAME = '${ownerName}';
window.PROJECT_SLUG = '${projectSlug}';
window.PENDING_VERSION = ${mapper.valueToTree(pending)};
window.CHANNELS = ${mapper.valueToTree(channels)};
window.CHANNELS = ${channels};
window.FORUM_SYNC = ${forumSync?c};
window.MAX_CHANNEL_NAME_LEN = ${config.channels.maxNameLen};
window.COLORS = ${mapper.valueToTree(@helper["io.papermc.hangar.model.Color"].getNonTransparentValues())};
</script>
<script type="text/javascript" src="<@hangar.url "js/versionCreateChannelNew.js" />"></script>
<script type="text/javascript" src="<@hangar.url "js/create-version.js" />"></script>
</#assign>
<#assign styleVar>
@ -63,6 +57,6 @@
</div>
<@modalManage.modalManage />
<#-- <@modalManage.modalManage />-->
</@base.base>