mirror of
https://github.com/aljazceru/CTFd.git
synced 2026-02-08 15:54:21 +01:00
Rewrite the flag creation modal to be in vuejs (#1715)
* Rewrite flag creation modal to VueJS * Rewrite flag edit modal to VueJS * Rewrite flag list tab in the Admin Panel challenge page to VueJS * Closes #1693
This commit is contained in:
@@ -1,137 +0,0 @@
|
||||
import $ from "jquery";
|
||||
import CTFd from "core/CTFd";
|
||||
import nunjucks from "nunjucks";
|
||||
import { ezQuery } from "core/ezq";
|
||||
|
||||
export function deleteFlag(event) {
|
||||
event.preventDefault();
|
||||
const flag_id = $(this).attr("flag-id");
|
||||
const row = $(this)
|
||||
.parent()
|
||||
.parent();
|
||||
|
||||
ezQuery({
|
||||
title: "Delete Flag",
|
||||
body: "Are you sure you want to delete this flag?",
|
||||
success: function() {
|
||||
CTFd.fetch("/api/v1/flags/" + flag_id, {
|
||||
method: "DELETE"
|
||||
})
|
||||
.then(function(response) {
|
||||
return response.json();
|
||||
})
|
||||
.then(function(response) {
|
||||
if (response.success) {
|
||||
row.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function addFlagModal(_event) {
|
||||
$.get(CTFd.config.urlRoot + "/api/v1/flags/types", function(response) {
|
||||
const data = response.data;
|
||||
const flag_type_select = $("#flags-create-select");
|
||||
flag_type_select.empty();
|
||||
|
||||
let option = $("<option> -- </option>");
|
||||
flag_type_select.append(option);
|
||||
|
||||
for (const key in data) {
|
||||
if (data.hasOwnProperty(key)) {
|
||||
option = $(
|
||||
"<option value='{0}'>{1}</option>".format(key, data[key].name)
|
||||
);
|
||||
flag_type_select.append(option);
|
||||
}
|
||||
}
|
||||
$("#flag-edit-modal").modal();
|
||||
});
|
||||
|
||||
$("#flag-edit-modal form").submit(function(event) {
|
||||
event.preventDefault();
|
||||
const params = $(this).serializeJSON(true);
|
||||
params["challenge"] = window.CHALLENGE_ID;
|
||||
CTFd.fetch("/api/v1/flags", {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(params)
|
||||
})
|
||||
.then(function(response) {
|
||||
return response.json();
|
||||
})
|
||||
.then(function(_response) {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
$("#flag-edit-modal").modal();
|
||||
}
|
||||
|
||||
export function editFlagModal(event) {
|
||||
event.preventDefault();
|
||||
const flag_id = $(this).attr("flag-id");
|
||||
const row = $(this)
|
||||
.parent()
|
||||
.parent();
|
||||
|
||||
$.get(CTFd.config.urlRoot + "/api/v1/flags/" + flag_id, function(response) {
|
||||
const data = response.data;
|
||||
$.get(CTFd.config.urlRoot + data.templates.update, function(template_data) {
|
||||
$("#edit-flags form").empty();
|
||||
$("#edit-flags form").off();
|
||||
|
||||
const template = nunjucks.compile(template_data);
|
||||
$("#edit-flags form").append(template.render(data));
|
||||
|
||||
$("#edit-flags form").submit(function(event) {
|
||||
event.preventDefault();
|
||||
const params = $("#edit-flags form").serializeJSON();
|
||||
|
||||
CTFd.fetch("/api/v1/flags/" + flag_id, {
|
||||
method: "PATCH",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(params)
|
||||
})
|
||||
.then(function(response) {
|
||||
return response.json();
|
||||
})
|
||||
.then(function(response) {
|
||||
if (response.success) {
|
||||
$(row)
|
||||
.find(".flag-content")
|
||||
.text(response.data.content);
|
||||
$("#edit-flags").modal("toggle");
|
||||
}
|
||||
});
|
||||
});
|
||||
$("#edit-flags").modal();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function flagTypeSelect(event) {
|
||||
event.preventDefault();
|
||||
const flag_type_name = $(this)
|
||||
.find("option:selected")
|
||||
.text();
|
||||
|
||||
$.get(CTFd.config.urlRoot + "/api/v1/flags/types/" + flag_type_name, function(
|
||||
response
|
||||
) {
|
||||
const data = response.data;
|
||||
$.get(CTFd.config.urlRoot + data.templates.create, function(template_data) {
|
||||
const template = nunjucks.compile(template_data);
|
||||
$("#create-keys-entry-div").html(template.render());
|
||||
$("#create-keys-button-div").show();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div id="media-modal" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="container">
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<div id="flag-create-modal" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header text-center">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>Create Flag</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="create-keys-select-div">
|
||||
<label for="create-keys-select" class="control-label">
|
||||
Choose Flag Type
|
||||
</label>
|
||||
<select
|
||||
class="form-control custom-select"
|
||||
@change="selectType($event)"
|
||||
>
|
||||
<option> -- </option>
|
||||
<option
|
||||
v-for="type in Object.keys(types)"
|
||||
:value="type"
|
||||
:key="type"
|
||||
>{{ type }}</option
|
||||
>
|
||||
</select>
|
||||
</div>
|
||||
<br />
|
||||
<form @submit.prevent="submitFlag">
|
||||
<div id="create-flag-form" v-html="createForm"></div>
|
||||
<button
|
||||
class="btn btn-success float-right"
|
||||
type="submit"
|
||||
v-if="createForm"
|
||||
>
|
||||
Create Flag
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import $ from "jquery";
|
||||
import CTFd from "core/CTFd";
|
||||
import nunjucks from "nunjucks";
|
||||
|
||||
export default {
|
||||
name: "FlagCreationForm",
|
||||
props: {
|
||||
challenge_id: Number
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
types: {},
|
||||
selectedType: null,
|
||||
createForm: ""
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
selectType: function(event) {
|
||||
let flagType = event.target.value;
|
||||
if (this.types[flagType] === undefined) {
|
||||
this.selectedType = null;
|
||||
this.createForm = "";
|
||||
return;
|
||||
}
|
||||
let createFormURL = this.types[flagType]["templates"]["create"];
|
||||
|
||||
$.get(CTFd.config.urlRoot + createFormURL, template_data => {
|
||||
const template = nunjucks.compile(template_data);
|
||||
this.selectedType = flagType;
|
||||
this.createForm = template.render();
|
||||
});
|
||||
},
|
||||
loadTypes: function() {
|
||||
CTFd.fetch("/api/v1/flags/types", {
|
||||
method: "GET"
|
||||
})
|
||||
.then(response => {
|
||||
return response.json();
|
||||
})
|
||||
.then(response => {
|
||||
this.types = response.data;
|
||||
});
|
||||
},
|
||||
submitFlag: function(event) {
|
||||
let form = $(event.target);
|
||||
let params = form.serializeJSON(true);
|
||||
params["challenge"] = this.$props.challenge_id;
|
||||
|
||||
CTFd.fetch("/api/v1/flags", {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(params)
|
||||
})
|
||||
.then(response => {
|
||||
return response.json();
|
||||
})
|
||||
.then(_response => {
|
||||
this.$emit("refreshFlags", this.$options.name);
|
||||
});
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadTypes();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
112
CTFd/themes/admin/assets/js/components/flags/FlagEditForm.vue
Normal file
112
CTFd/themes/admin/assets/js/components/flags/FlagEditForm.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div id="flag-edit-modal" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header text-center">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3 class="text-center">Edit Flag</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form
|
||||
method="POST"
|
||||
v-html="editForm"
|
||||
@submit.prevent="updateFlag"
|
||||
></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import $ from "jquery";
|
||||
import CTFd from "core/CTFd";
|
||||
import nunjucks from "nunjucks";
|
||||
|
||||
export default {
|
||||
name: "FlagEditForm",
|
||||
props: {
|
||||
flag_id: Number
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
flag: {},
|
||||
editForm: ""
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
flag_id: {
|
||||
immediate: true,
|
||||
handler(val, oldVal) {
|
||||
if (val !== null) {
|
||||
this.loadFlag();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadFlag: function() {
|
||||
CTFd.fetch(`/api/v1/flags/${this.$props.flag_id}`, {
|
||||
method: "GET"
|
||||
})
|
||||
.then(response => {
|
||||
return response.json();
|
||||
})
|
||||
.then(response => {
|
||||
this.flag = response.data;
|
||||
let editFormURL = this.flag["templates"]["update"];
|
||||
|
||||
$.get(CTFd.config.urlRoot + editFormURL, template_data => {
|
||||
const template = nunjucks.compile(template_data);
|
||||
this.editForm = template.render(this.flag);
|
||||
});
|
||||
});
|
||||
},
|
||||
updateFlag: function(event) {
|
||||
let form = $(event.target);
|
||||
let params = form.serializeJSON(true);
|
||||
|
||||
CTFd.fetch(`/api/v1/flags/${this.$props.flag_id}`, {
|
||||
method: "PATCH",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(params)
|
||||
})
|
||||
.then(response => {
|
||||
return response.json();
|
||||
})
|
||||
.then(response => {
|
||||
this.$emit("refreshFlags", this.$options.name);
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.flag_id) {
|
||||
this.loadFlag();
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.flag_id) {
|
||||
this.loadFlag();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
150
CTFd/themes/admin/assets/js/components/flags/FlagList.vue
Normal file
150
CTFd/themes/admin/assets/js/components/flags/FlagList.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<FlagCreationForm
|
||||
ref="FlagCreationForm"
|
||||
:challenge_id="challenge_id"
|
||||
@refreshFlags="refreshFlags"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FlagEditForm
|
||||
ref="FlagEditForm"
|
||||
:flag_id="editing_flag_id"
|
||||
@refreshFlags="refreshFlags"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<table id="flagsboard" class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="text-center"><b>Type</b></td>
|
||||
<td class="text-center"><b>Flag</b></td>
|
||||
<td class="text-center"><b>Settings</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr :name="flag.id" v-for="flag in flags" :key="flag.id">
|
||||
<td class="text-center">{{ flag.type }}</td>
|
||||
<td class="text-break">
|
||||
<pre class="flag-content">{{ flag.content }}</pre>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<i
|
||||
role="button"
|
||||
class="btn-fa fas fa-edit edit-flag"
|
||||
:flag-id="flag.id"
|
||||
:flag-type="flag.type"
|
||||
@click="editFlag(flag.id)"
|
||||
></i>
|
||||
<i
|
||||
role="button"
|
||||
class="btn-fa fas fa-times delete-flag"
|
||||
:flag-id="flag.id"
|
||||
@click="deleteFlag(flag.id)"
|
||||
></i>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="col-md-12">
|
||||
<button
|
||||
id="flag-add-button"
|
||||
class="btn btn-success d-inline-block float-right"
|
||||
@click="addFlag()"
|
||||
>
|
||||
Create Flag
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import $ from "jquery";
|
||||
import CTFd from "core/CTFd";
|
||||
import FlagCreationForm from "./FlagCreationForm.vue";
|
||||
import FlagEditForm from "./FlagEditForm.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FlagCreationForm,
|
||||
FlagEditForm
|
||||
},
|
||||
props: {
|
||||
challenge_id: Number
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
flags: [],
|
||||
editing_flag_id: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
loadFlags: function() {
|
||||
CTFd.fetch(`/api/v1/challenges/${this.$props.challenge_id}/flags`, {
|
||||
method: "GET",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
return response.json();
|
||||
})
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.flags = response.data;
|
||||
}
|
||||
});
|
||||
},
|
||||
refreshFlags(caller) {
|
||||
this.loadFlags();
|
||||
let modal;
|
||||
switch (caller) {
|
||||
case "FlagEditForm":
|
||||
modal = this.$refs.FlagEditForm.$el;
|
||||
$(modal).modal("hide");
|
||||
break;
|
||||
case "FlagCreationForm":
|
||||
modal = this.$refs.FlagCreationForm.$el;
|
||||
$(modal).modal("hide");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
addFlag: function() {
|
||||
let modal = this.$refs.FlagCreationForm.$el;
|
||||
$(modal).modal();
|
||||
},
|
||||
editFlag: function(flag_id) {
|
||||
this.editing_flag_id = flag_id;
|
||||
let modal = this.$refs.FlagEditForm.$el;
|
||||
$(modal).modal();
|
||||
},
|
||||
deleteFlag: function(flag_id) {
|
||||
if (confirm("Are you sure you'd like to delete this flag?")) {
|
||||
CTFd.fetch(`/api/v1/flags/${flag_id}`, {
|
||||
method: "DELETE"
|
||||
})
|
||||
.then(response => {
|
||||
return response.json();
|
||||
})
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.loadFlags();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadFlags();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -12,18 +12,13 @@ import { addRequirement, deleteRequirement } from "../challenges/requirements";
|
||||
import { bindMarkdownEditors } from "../styles";
|
||||
import Vue from "vue/dist/vue.esm.browser";
|
||||
import CommentBox from "../components/comments/CommentBox.vue";
|
||||
import FlagList from "../components/flags/FlagList.vue";
|
||||
import {
|
||||
showHintModal,
|
||||
editHint,
|
||||
deleteHint,
|
||||
showEditHintModal
|
||||
} from "../challenges/hints";
|
||||
import {
|
||||
addFlagModal,
|
||||
editFlagModal,
|
||||
deleteFlag,
|
||||
flagTypeSelect
|
||||
} from "../challenges/flags";
|
||||
|
||||
const displayHint = data => {
|
||||
ezAlert({
|
||||
@@ -420,10 +415,15 @@ $(() => {
|
||||
$(".edit-hint").click(showEditHintModal);
|
||||
$("#hint-edit-form").submit(editHint);
|
||||
|
||||
$("#flag-add-button").click(addFlagModal);
|
||||
$(".delete-flag").click(deleteFlag);
|
||||
$("#flags-create-select").change(flagTypeSelect);
|
||||
$(".edit-flag").click(editFlagModal);
|
||||
// Load FlagList component
|
||||
if (document.querySelector("#challenge-flags")) {
|
||||
const flagList = Vue.extend(FlagList);
|
||||
let vueContainer = document.createElement("div");
|
||||
document.querySelector("#challenge-flags").appendChild(vueContainer);
|
||||
new flagList({
|
||||
propsData: { challenge_id: window.CHALLENGE_ID }
|
||||
}).$mount(vueContainer);
|
||||
}
|
||||
|
||||
// Because this JS is shared by a few pages,
|
||||
// we should only insert the CommentBox if it's actually in use
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -6,8 +6,6 @@
|
||||
|
||||
{% block content %}
|
||||
{% include "admin/modals/hints/edit.html" %}
|
||||
{% include "admin/modals/flags/create.html" %}
|
||||
{% include "admin/modals/flags/edit.html" %}
|
||||
|
||||
<div class="modal fade" id="challenge-window" role="dialog">
|
||||
</div>
|
||||
|
||||
@@ -1,27 +1,2 @@
|
||||
<table id="flagsboard" class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="text-center"><b>Type</b></td>
|
||||
<td class="text-center"><b>Flag</b></td>
|
||||
<td class="text-center"><b>Settings</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for flag in flags %}
|
||||
<tr name="{{ flag.id }}">
|
||||
<td class="text-center">{{ flag.type }}</td>
|
||||
<td class="text-break">
|
||||
<pre class="flag-content">{{ flag.content }}</pre>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<i role="button" class="btn-fa fas fa-edit edit-flag" flag-id="{{ flag.id }}" flag-type="{{ flag.type }}"></i>
|
||||
<i role="button" class="btn-fa fas fa-times delete-flag" flag-id="{{ flag.id }}"></i>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="col-md-12">
|
||||
<button id="flag-add-button" class="btn btn-success d-inline-block float-right">Create Flag</button>
|
||||
<div id="challenge-flags">
|
||||
</div>
|
||||
@@ -1,35 +0,0 @@
|
||||
<div id="flag-edit-modal" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<input type="hidden" class="chal-id" name="chal-id">
|
||||
<div class="modal-header text-center">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>Create Flag</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="create-keys-select-div">
|
||||
<label for="create-keys-select" class="control-label">Choose Flag Type</label>
|
||||
<select class="form-control custom-select" id="flags-create-select">
|
||||
</select>
|
||||
</div>
|
||||
<br>
|
||||
<form>
|
||||
<div id="create-keys-entry-div">
|
||||
</div>
|
||||
<div style="display:none;" id="create-keys-button-div" class="text-center">
|
||||
<button id="create-keys-submit" class="btn btn-success">Create Flag</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,22 +0,0 @@
|
||||
<div id="edit-flags" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header text-center">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3 class="text-center">Edit Flag</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="POST">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user