mirror of
https://gitlab.com/crafty-controller/crafty-4.git
synced 2025-09-04 14:21:50 +00:00
Compare commits
27 Commits
7b83f8622e
...
1b0bf1def3
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1b0bf1def3 | ||
![]() |
cfe845686a | ||
![]() |
f5a772c562 | ||
![]() |
c33a14e426 | ||
![]() |
b24eef3b80 | ||
![]() |
47756ca410 | ||
![]() |
98578708bc | ||
![]() |
3c02df35f6 | ||
![]() |
5ca51045a8 | ||
![]() |
49c1e6800f | ||
![]() |
5c720d5210 | ||
![]() |
8f8aacf4a9 | ||
![]() |
0e21d50716 | ||
![]() |
2eee82172c | ||
![]() |
2fe4320db6 | ||
![]() |
d231c128de | ||
![]() |
f6e578315b | ||
![]() |
256dd2713e | ||
![]() |
6f262b39ef | ||
![]() |
4a9d81bab7 | ||
![]() |
9a2774a181 | ||
![]() |
738d148c2a | ||
![]() |
99d6d374b4 | ||
![]() |
c597164a1a | ||
![]() |
61b2be2e8e | ||
![]() |
9d7f511775 | ||
![]() |
0d371a0bb5 |
@@ -1,5 +1,5 @@
|
||||
# Changelog
|
||||
## --- [4.5.2] - 2025/TBD
|
||||
## --- [4.5.3] - 2025/TBD
|
||||
### New features
|
||||
TBD
|
||||
### Bug fixes
|
||||
@@ -10,6 +10,13 @@ TBD
|
||||
TBD
|
||||
<br><br>
|
||||
|
||||
## --- [4.5.2] - 2025/08/31
|
||||
### Bug fixes
|
||||
- Bedrock Builder | Utilize self call instead of static call for unzipping bedrock archives ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/884))
|
||||
- Fix Human Readable Sizes on Dashboard ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/885))
|
||||
- Correct Builder to support new Neoforge versioning scheme ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/886))
|
||||
<br><br>
|
||||
|
||||
## --- [4.5.1] - 2025/08/25
|
||||
### Bug fixes
|
||||
- Fix bug where all file methods that were not `GET` methods, were returning "method not allowed" ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/882))
|
||||
|
@@ -1,5 +1,5 @@
|
||||
[](https://craftycontrol.com)
|
||||
# Crafty Controller 4.5.2
|
||||
# Crafty Controller 4.5.3
|
||||
> Python based Control Panel for your Minecraft Server
|
||||
|
||||
## What is Crafty Controller?
|
||||
|
@@ -1059,21 +1059,25 @@ class FileHelpers:
|
||||
return zlib.decompress(bytes_to_decompress)
|
||||
|
||||
@staticmethod
|
||||
def get_dir_size(server_path, raw_bytes=True):
|
||||
def get_dir_size(server_path):
|
||||
"""Recursively calculates dir size. Returns size in bytes. Must calculate human
|
||||
readable based on returned data
|
||||
|
||||
Args:
|
||||
server_path (str): Path to calculate size
|
||||
|
||||
Returns:
|
||||
_type_: Integer
|
||||
"""
|
||||
# because this is a recursive function, we will return bytes,
|
||||
# and set human readable later
|
||||
total = 0
|
||||
for entry in os.scandir(server_path):
|
||||
if entry.is_dir(follow_symlinks=False):
|
||||
total += FileHelpers.get_dir_size(entry.path, raw_bytes=raw_bytes)
|
||||
total += FileHelpers.get_dir_size(entry.path)
|
||||
else:
|
||||
total += entry.stat(follow_symlinks=False).st_size
|
||||
if raw_bytes:
|
||||
return total
|
||||
|
||||
level_total_size = Helpers.human_readable_file_size(total)
|
||||
|
||||
return level_total_size
|
||||
return total
|
||||
|
||||
@staticmethod
|
||||
def get_drive_free_space(file_location: Path):
|
||||
|
@@ -244,7 +244,7 @@ class ImportHelpers:
|
||||
|
||||
unzip_path = self.helper.wtol_path(file_path)
|
||||
# unzips archive that was downloaded.
|
||||
FileHelpers.unzip_file(unzip_path)
|
||||
self.file_helper.unzip_file(unzip_path)
|
||||
# adjusts permissions for execution if os is not windows
|
||||
|
||||
if not self.helper.is_os_windows():
|
||||
|
@@ -577,7 +577,7 @@ class Controller:
|
||||
if data["create_type"] == "minecraft_java":
|
||||
if root_create_data["create_type"] == "download_jar":
|
||||
# modded update urls from server jars will only update the installer
|
||||
if create_data["type"] != "forge-installer":
|
||||
if create_data["type"] not in MODDED_TYPES:
|
||||
server_obj = self.servers.get_server_obj(new_server_id)
|
||||
url = self.big_bucket.get_fetch_url(
|
||||
create_data["category"],
|
||||
|
@@ -775,14 +775,15 @@ class ServerInstance:
|
||||
# We get the server command parameters from forge script
|
||||
server_command = re.findall(
|
||||
r"java @([a-zA-Z0-9_\.]+)"
|
||||
r" @([a-z.\/\-]+)([0-9.\-]+)"
|
||||
r"\/\b([a-z_0-9]+\.txt)\b( .{2,4})?",
|
||||
r" @([a-z./\-]+)"
|
||||
r"([0-9.\-]+(?:-[a-zA-Z0-9]+)?)"
|
||||
r"\/\b([a-z_0-9]+\.txt)\b"
|
||||
r"( .{2,4})?",
|
||||
run_file_text,
|
||||
)[0]
|
||||
|
||||
version = server_command[2]
|
||||
executable_path = f"{server_command[1]}{server_command[2]}/"
|
||||
|
||||
# Let's set the proper server executable
|
||||
server_obj.executable = os.path.join(
|
||||
f"{executable_path}{version_info[0][0]}-{version}-server.jar"
|
||||
@@ -1476,7 +1477,9 @@ class ServerInstance:
|
||||
|
||||
def start_dir_calc_task(self):
|
||||
server_dt = HelperServers.get_server_data_by_id(self.server_id)
|
||||
self.server_size = self.file_helper.get_dir_size(server_dt["path"])
|
||||
self.server_size = Helpers.human_readable_file_size(
|
||||
self.file_helper.get_dir_size(server_dt["path"])
|
||||
)
|
||||
self.dir_scheduler.add_job(
|
||||
self.calc_dir_size,
|
||||
"interval",
|
||||
@@ -1492,7 +1495,9 @@ class ServerInstance:
|
||||
|
||||
def calc_dir_size(self):
|
||||
server_dt = HelperServers.get_server_data_by_id(self.server_id)
|
||||
self.server_size = self.file_helper.get_dir_size(server_dt["path"])
|
||||
self.server_size = Helpers.human_readable_file_size(
|
||||
self.file_helper.get_dir_size(server_dt["path"])
|
||||
)
|
||||
|
||||
# **********************************************************************************
|
||||
# Minecraft Servers Statistics
|
||||
|
@@ -107,15 +107,10 @@ files_rename_schema = {
|
||||
file_delete_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filename": {
|
||||
"type": "string",
|
||||
"minLength": 5,
|
||||
"error": "typeString",
|
||||
"fill": True,
|
||||
},
|
||||
"filenames": {"type": "array", "items": {"type": "string", "minLength": 5}}
|
||||
},
|
||||
"required": ["filenames"],
|
||||
"additionalProperties": False,
|
||||
"minProperties": 1,
|
||||
}
|
||||
|
||||
|
||||
@@ -330,30 +325,49 @@ class ApiServersServerFilesIndexHandler(BaseApiHandler):
|
||||
"error_data": f"{str(err)}",
|
||||
},
|
||||
)
|
||||
if not Helpers.validate_traversal(
|
||||
self.controller.servers.get_server_data_by_id(server_id)["path"],
|
||||
data["filename"],
|
||||
):
|
||||
return self.finish_json(
|
||||
400,
|
||||
{
|
||||
"status": "error",
|
||||
"error": "TRAVERSAL DETECTED",
|
||||
"error_data": str(e),
|
||||
},
|
||||
)
|
||||
|
||||
if os.path.isdir(data["filename"]):
|
||||
proc = FileHelpers.del_dirs(data["filename"])
|
||||
else:
|
||||
proc = FileHelpers.del_file(data["filename"])
|
||||
# disabling pylint because return value could be truthy
|
||||
# but not a true boolean value
|
||||
if proc == True: # pylint: disable=singleton-comparison
|
||||
return self.finish_json(200, {"status": "ok"})
|
||||
return self.finish_json(
|
||||
500, {"status": "error", "error": "SERVER RUNNING", "error_data": str(proc)}
|
||||
)
|
||||
# Sort paths in assending ASCII order
|
||||
normalized_paths = sorted(data["filenames"])
|
||||
# extract only unique paths so at to not delete files "implicitly"
|
||||
unique_paths = []
|
||||
last_path = None
|
||||
for path in normalized_paths:
|
||||
if not Helpers.validate_traversal(
|
||||
self.controller.servers.get_server_data_by_id(server_id)["path"],
|
||||
path,
|
||||
):
|
||||
return self.finish_json(
|
||||
400,
|
||||
{
|
||||
"status": "error",
|
||||
"error": "TRAVERSAL DETECTED",
|
||||
"error_data": str(e),
|
||||
},
|
||||
)
|
||||
|
||||
if last_path and path.startswith(last_path + os.sep):
|
||||
continue
|
||||
unique_paths.append(path)
|
||||
last_path = path
|
||||
|
||||
for path in unique_paths:
|
||||
if os.path.isdir(path):
|
||||
proc = FileHelpers.del_dirs(path)
|
||||
else:
|
||||
proc = FileHelpers.del_file(path)
|
||||
# disabling pylint because return value could be truthy
|
||||
# but not a true boolean value
|
||||
if proc != True: # pylint: disable=singleton-comparison
|
||||
return self.finish_json(
|
||||
500,
|
||||
{
|
||||
"status": "error",
|
||||
"error": "SERVER RUNNING",
|
||||
"error_data": str(proc),
|
||||
},
|
||||
)
|
||||
|
||||
return self.finish_json(200, {"status": "ok"})
|
||||
|
||||
def patch(self, server_id: str, _backup_id):
|
||||
auth_data = self.authenticate_user()
|
||||
@@ -902,9 +916,7 @@ class ApiServersServerFileDownload(BaseApiHandler):
|
||||
)
|
||||
archive_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
target_total_size = self.file_helper.get_dir_size(
|
||||
Path(download_path), raw_bytes=True
|
||||
)
|
||||
target_total_size = self.file_helper.get_dir_size(Path(download_path))
|
||||
free_drive_storage = self.file_helper.get_drive_free_space(
|
||||
Path(download_path)
|
||||
)
|
||||
|
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"major": 4,
|
||||
"minor": 5,
|
||||
"sub": 2
|
||||
"sub": 3
|
||||
}
|
||||
|
@@ -87,6 +87,14 @@ end %} {% block content %}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<br/>
|
||||
<div class="d-none" id="massActions">
|
||||
<span>Mass actions: </span>
|
||||
<br/>
|
||||
<button class="btn btn-danger mr-2" id="massActionButtonDelete" onclick="deleteMultiFileE(event)"> Delete </button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="editor_container" class="col-md-6 col-sm-12">
|
||||
@@ -95,8 +103,7 @@ end %} {% block content %}
|
||||
<div class="editorManager">
|
||||
<h2 id="fileError"></h2>
|
||||
<div id="editorParent">
|
||||
{{ translate('serverFiles', 'editingFile', data['lang']) }}
|
||||
<span id="editingFile"></span>
|
||||
{{ translate('serverFiles', 'editingFile', data['lang']) }} <span id="editingFile"></span>
|
||||
<div id="editor" onresize="editor.resize()" style="resize: both; width: 100%">
|
||||
file_contents
|
||||
</div>
|
||||
@@ -293,7 +300,37 @@ end %} {% block content %}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateFileCheckBox(event) {
|
||||
if (!event.target.checked){
|
||||
updateTreeUnchecked(event.target, event.target.getAttribute('pdir'));
|
||||
} else {
|
||||
updateTreeChecked(event.target.getAttribute("value"))
|
||||
}
|
||||
const checkedBoxes = document.querySelectorAll('input.file-check-deletion:checked');
|
||||
const deleteBtn = document.getElementById('massActions');
|
||||
if (checkedBoxes.length > 0) {
|
||||
deleteBtn.classList.remove('d-none');
|
||||
} else {
|
||||
deleteBtn.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
function updateTreeUnchecked(checkBoxElem, parentDir) {
|
||||
if (parentDir == "root"){
|
||||
return;
|
||||
}
|
||||
if (!checkBoxElem.checked) {
|
||||
const pElem = document.getElementById(parentDir+"delBox");
|
||||
pElem.checked = false;
|
||||
updateTreeUnchecked(pElem, pElem.getAttribute("pdir"))
|
||||
}
|
||||
}
|
||||
function updateTreeChecked(pdir) {
|
||||
const children = document.querySelectorAll(`[pdir="${pdir}"]`);
|
||||
children.forEach(function(elem) {
|
||||
elem.checked = true;
|
||||
updateTreeChecked(elem.getAttribute("value"))
|
||||
});
|
||||
}
|
||||
function setFileName(name) {
|
||||
let fileName = name || "default.txt";
|
||||
document.getElementById("editingFile").innerText = fileName;
|
||||
@@ -527,26 +564,29 @@ end %} {% block content %}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteItem(path, el, callback) {
|
||||
async function deleteItems(files, el, callback) {
|
||||
const token = getCookie("_xsrf");
|
||||
let res = await fetch(`/api/v2/servers/${serverId}/files`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"X-XSRFToken": token,
|
||||
},
|
||||
body: JSON.stringify({ filename: path }),
|
||||
body: JSON.stringify({ filenames: files }),
|
||||
});
|
||||
let responseData = await res.json();
|
||||
if (responseData.status === "ok") {
|
||||
el = document.getElementById(path + "li");
|
||||
$(el).remove();
|
||||
document.getElementById("files-tree-nav").style.display = "none";
|
||||
files.forEach(function(path) {
|
||||
el = document.getElementById(path + "li");
|
||||
$(el).remove();
|
||||
document.getElementById("files-tree-nav").style.display = "none";
|
||||
})
|
||||
} else {
|
||||
bootbox.alert({
|
||||
title: responseData.error,
|
||||
message: responseData.error_data
|
||||
});
|
||||
}
|
||||
updateFileCheckBox();
|
||||
}
|
||||
|
||||
async function unZip(path, callback) {
|
||||
@@ -714,38 +754,57 @@ end %} {% block content %}
|
||||
|
||||
function process_tree_response(response) {
|
||||
let path = response.data.root_path.path;
|
||||
|
||||
let text = ``;
|
||||
let pdir = 'root'
|
||||
let checked = "";
|
||||
if (!response.data.root_path.top) {
|
||||
text = `<ul class="tree-nested d-block" id="${path}ul">`;
|
||||
pdir = path
|
||||
if (document.getElementById(`${path}delBox`).checked) {
|
||||
checked = "checked";
|
||||
}
|
||||
}
|
||||
Object.entries(response.data).forEach(([key, value]) => {
|
||||
if (key === "root_path" || key === "db_stats") {
|
||||
//continue is not valid in for each. Return acts as a continue.
|
||||
return;
|
||||
}
|
||||
let checked = "";
|
||||
let excluded = "";
|
||||
let dpath = value.path;
|
||||
let filename = key;
|
||||
if (value.dir) {
|
||||
if (value.excluded) {
|
||||
checked = "checked";
|
||||
excluded = "checked";
|
||||
}
|
||||
text += `<li class="tree-item" id="${dpath}li" data-path="${dpath}">
|
||||
\n<div id="${dpath}" data-path="${dpath}" data-name="${filename}" class="tree-caret tree-ctx-item tree-folder">
|
||||
<input type="checkbox" class="checkBoxClass d-none file-check" name="root_path" value="${dpath}" ${checked}>
|
||||
<span id="${dpath}span" class="files-tree-title" data-path="${dpath}" data-name="${filename}" onclick="getDirView(event)">
|
||||
<i class="far fa-folder text-info"></i>
|
||||
<i class="far fa-folder-open text-info"></i>
|
||||
${filename}
|
||||
</span>
|
||||
</input></div></li>`;
|
||||
text += `
|
||||
<li class="tree-item" id="${dpath}li" data-path="${dpath}">
|
||||
<div id="${dpath}" data-path="${dpath}" data-name="${filename}" class="tree-caret tree-ctx-item tree-folder">
|
||||
<input type="checkbox" class="checkBoxClass d-none file-check" name="root_path" value="${dpath}" ${excluded}>
|
||||
<input type="checkbox" id="${dpath}delBox" class="checkBoxClass file-check-deletion" name="root_path" value="${dpath}" onclick="updateFileCheckBox(event)" pdir="${pdir}" ${checked}>
|
||||
<span id="${dpath}span" class="files-tree-title" data-path="${dpath}" data-name="${filename}" onclick="getDirView(event)" data-path="${dpath}" data-name="${filename}">
|
||||
<i class="far fa-folder text-info"></i>
|
||||
<i class="far fa-folder-open text-info"></i>
|
||||
${filename}
|
||||
</span>
|
||||
</input>
|
||||
</input>
|
||||
</div>
|
||||
</li>
|
||||
`;
|
||||
} else {
|
||||
text += `<li
|
||||
class="d-block tree-ctx-item tree-file"
|
||||
data-path="${dpath}"
|
||||
data-name="${filename}"
|
||||
onclick="clickOnFile(event)" id="${dpath}li"><input type='checkbox' class="checkBoxClass d-none file-check" name='root_path' value="${dpath}" ${checked}><span style="margin-right: 6px;">
|
||||
<i class="far fa-file"></i></span></input>${filename}</li>`;
|
||||
text += `
|
||||
<li class="d-block tree-ctx-item tree-file" data-path="${dpath}" data-name="${filename}" id="${dpath}li">
|
||||
<input type='checkbox' class="checkBoxClass d-none file-check" name='root_path' value="${dpath}" ${excluded}>
|
||||
<input type='checkbox' id="${dpath}delBox" class="checkBoxClass file-check-deletion" name='root_path' value="${dpath}" onclick="updateFileCheckBox(event)" pdir="${pdir}" ${checked}>
|
||||
<span style="margin-right: 6px;" onclick="clickOnFile(event)" data-path="${dpath}" data-name="${filename}" >
|
||||
<i class="far fa-file"></i>
|
||||
${filename}
|
||||
</span>
|
||||
</input>
|
||||
</input>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
});
|
||||
if (!response.data.root_path.top) {
|
||||
@@ -762,9 +821,10 @@ end %} {% block content %}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
document.getElementById(path + "span").classList.add("tree-caret-down");
|
||||
document.getElementById(path).innerHTML += text;
|
||||
document.getElementById(path).classList.add("clicked");
|
||||
const parentElement = document.getElementById(path);
|
||||
parentElement.querySelector('.files-tree-title').classList.add("tree-caret-down");
|
||||
parentElement.insertAdjacentHTML('beforeend', text);
|
||||
parentElement.classList.add("clicked");
|
||||
} catch {
|
||||
console.log("Bad");
|
||||
}
|
||||
@@ -786,6 +846,9 @@ end %} {% block content %}
|
||||
setTreeViewContext();
|
||||
}, 1000);
|
||||
}
|
||||
function getNumberFilesChecked(){
|
||||
const checkedBoxes = document.querySelectorAll('input.file-check:checked');
|
||||
}
|
||||
function getToggleMain(event) {
|
||||
path = event.target.parentElement.getAttribute("data-path");
|
||||
document.getElementById("files-tree").classList.toggle("d-block");
|
||||
@@ -957,28 +1020,56 @@ end %} {% block content %}
|
||||
function deleteFileE(event) {
|
||||
path = event.target.parentElement.getAttribute("data-path");
|
||||
name = event.target.parentElement.getAttribute("data-name");
|
||||
bootbox.confirm({
|
||||
size: "",
|
||||
title:
|
||||
"{% raw translate('serverFiles', 'deleteItemQuestion', data['lang']) %}",
|
||||
closeButton: false,
|
||||
message:
|
||||
"{% raw translate('serverFiles', 'deleteItemQuestionMessage', data['lang']) %}",
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: "{{ translate('serverFiles', 'yesDelete', data['lang']) }}",
|
||||
className: "btn-danger",
|
||||
},
|
||||
cancel: {
|
||||
label: "{{ translate('serverFiles', 'noDelete', data['lang']) }}",
|
||||
className: "btn-link",
|
||||
},
|
||||
},
|
||||
callback: function (result) {
|
||||
if (!result) return;
|
||||
deleteItem(path);
|
||||
},
|
||||
files = {path}
|
||||
renderDeleteConfimationModel(files)
|
||||
}
|
||||
|
||||
function deleteMultiFileE(event) {
|
||||
const filesToDelete = document.querySelectorAll('input.file-check-deletion:checked');
|
||||
const files = [];
|
||||
filesToDelete.forEach(fileInput => {
|
||||
const parentElement = fileInput.parentElement;
|
||||
const path = parentElement.getAttribute("data-path");
|
||||
|
||||
files.push(path);
|
||||
});
|
||||
|
||||
renderDeleteConfimationModel(files)
|
||||
}
|
||||
|
||||
function renderDeleteConfimationModel(files) {
|
||||
path = renderDeleteList(files);
|
||||
bootbox.confirm({
|
||||
size: "",
|
||||
title:
|
||||
"{% raw translate('serverFiles', 'deleteItemQuestion', data['lang']) %}",
|
||||
closeButton: false,
|
||||
message:
|
||||
"{% raw translate('serverFiles', 'deleteItemQuestionMessage', data['lang']) %}",
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: "{{ translate('serverFiles', 'yesDelete', data['lang']) }}",
|
||||
className: "btn-danger",
|
||||
},
|
||||
cancel: {
|
||||
label: "{{ translate('serverFiles', 'noDelete', data['lang']) }}",
|
||||
className: "btn-link",
|
||||
},
|
||||
},
|
||||
callback: function (result) {
|
||||
if (!result) return;
|
||||
deleteItems(files);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function renderDeleteList(files) {
|
||||
list = "<ul>";
|
||||
files.forEach(file => {
|
||||
list += `<li> ${file} </li>`;
|
||||
});
|
||||
list += `</ul>`;
|
||||
return list;
|
||||
}
|
||||
|
||||
getTreeView($("#root_dir").data("path"));
|
||||
@@ -1006,4 +1097,4 @@ end %} {% block content %}
|
||||
}
|
||||
</script>
|
||||
<script src="../../static/assets/js/shared/upload.js"></script>
|
||||
{% end %}
|
||||
{% end %}
|
||||
|
@@ -521,7 +521,7 @@
|
||||
"default": "Default",
|
||||
"delete": "Delete",
|
||||
"deleteItemQuestion": "Are you sure you want to delete \" + name + \"?",
|
||||
"deleteItemQuestionMessage": "You are deleting \\\"\" + path + \"\\\"!<br/><br/>This action will be irreversible and it'll be lost forever!",
|
||||
"deleteItemQuestionMessage": "You are deleting \" + path + \"<br/><br/>This action will be irreversible and it'll be lost forever!",
|
||||
"download": "Download",
|
||||
"editingFile": "Editing file",
|
||||
"error": "Error while getting files",
|
||||
@@ -807,4 +807,4 @@
|
||||
"webhook_body": "Webhook Body",
|
||||
"webhooks": "Webhooks"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -463,7 +463,7 @@
|
||||
"default": "За замовчуванням",
|
||||
"delete": "Видалити",
|
||||
"deleteItemQuestion": "Ви впевнені що бажаєте видалити \" + name + \"?",
|
||||
"deleteItemQuestionMessage": "Ви видаляєте \\\"\" + path + \"\\\"!<br/><br/>Це незворотня дія!",
|
||||
"deleteItemQuestionMessage": "Ви видаляєте \" + path + \"<br/><br/>Це незворотня дія!",
|
||||
"download": "Завантажити",
|
||||
"editingFile": "Редагувати файл",
|
||||
"error": "Помилка отримання файлів",
|
||||
|
@@ -3,7 +3,7 @@ sonar.organization=crafty-controller
|
||||
|
||||
# This is the name and version displayed in the SonarCloud UI.
|
||||
sonar.projectName=Crafty 4
|
||||
sonar.projectVersion=4.5.2
|
||||
sonar.projectVersion=4.5.3
|
||||
sonar.python.version=3.9, 3.10, 3.11
|
||||
sonar.exclusions=app/migrations/**, app/frontend/static/assets/vendors/**
|
||||
|
||||
|
Reference in New Issue
Block a user