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:
Kevin Chung
2020-11-14 16:17:43 -05:00
committed by GitHub
parent 0ed1a0c659
commit 17db97495e
14 changed files with 588 additions and 249 deletions

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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">&times;</span>
</button>
</div>
<div class="modal-body">
<form method="POST">
</form>
</div>
</div>
</div>
</div>