add delete button & fix it error handel with toast
This commit is contained in:
parent
afb97f51bb
commit
095e0a05e7
|
|
@ -9,7 +9,6 @@
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-diary-admin-page #recording-button {
|
.audio-diary-admin-page #recording-button {
|
||||||
background-color: red;
|
background-color: red;
|
||||||
color: white;
|
color: white;
|
||||||
|
|
@ -56,25 +55,26 @@
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* سایه برای دادن عمق */
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* سایه برای دادن عمق */
|
||||||
}
|
}
|
||||||
|
|
||||||
#visualizer {
|
.audio-diary-admin-list-page #visualizer {
|
||||||
background-color: #f9f9f9; /* پسزمینه ملایم */
|
background-color: #f9f9f9; /* پسزمینه ملایم */
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.audio-diary-admin-list-page table {
|
||||||
.wrap table {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrap th, .wrap td {
|
.audio-diary-admin-list-page th,
|
||||||
|
.audio-diary-admin-list-page td {
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrap th {
|
.audio-diary-admin-list-page th {
|
||||||
background-color: #f2f2f2;
|
background-color: #f2f2f2;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,24 +16,22 @@ jQuery(document).ready(function($) {
|
||||||
const canvas = document.getElementById('visualizer');
|
const canvas = document.getElementById('visualizer');
|
||||||
const canvasCtx = canvas.getContext('2d');
|
const canvasCtx = canvas.getContext('2d');
|
||||||
|
|
||||||
function draw() {
|
function draw() {
|
||||||
requestAnimationFrame(draw);
|
requestAnimationFrame(draw);
|
||||||
analyser.getByteTimeDomainData(dataArray);
|
analyser.getByteTimeDomainData(dataArray);
|
||||||
|
|
||||||
canvasCtx.clearRect(0, 0, canvas.width, canvas.height); // حذف پسزمینه سیاه
|
canvasCtx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
// تنظیمات خط وسط
|
|
||||||
const centerY = canvas.height / 2;
|
const centerY = canvas.height / 2;
|
||||||
canvasCtx.beginPath();
|
canvasCtx.beginPath();
|
||||||
canvasCtx.moveTo(0, centerY);
|
canvasCtx.moveTo(0, centerY);
|
||||||
canvasCtx.lineTo(canvas.width, centerY);
|
canvasCtx.lineTo(canvas.width, centerY);
|
||||||
canvasCtx.strokeStyle = 'rgba(0, 0, 0, 0.2)'; // خط وسط نیمه شفاف
|
canvasCtx.strokeStyle = 'rgba(0, 0, 0, 0.2)';
|
||||||
canvasCtx.lineWidth = 1;
|
canvasCtx.lineWidth = 1;
|
||||||
canvasCtx.stroke();
|
canvasCtx.stroke();
|
||||||
|
|
||||||
// تنظیمات خط موج صدا
|
|
||||||
canvasCtx.lineWidth = 2;
|
canvasCtx.lineWidth = 2;
|
||||||
canvasCtx.strokeStyle = 'rgb(0, 0, 0)'; // خط موج صدا مشکی
|
canvasCtx.strokeStyle = 'rgb(0, 0, 0)';
|
||||||
|
|
||||||
canvasCtx.beginPath();
|
canvasCtx.beginPath();
|
||||||
let sliceWidth = canvas.width * 1.0 / bufferLength;
|
let sliceWidth = canvas.width * 1.0 / bufferLength;
|
||||||
|
|
@ -90,31 +88,31 @@ jQuery(document).ready(function($) {
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
$.toast({
|
$.toast({
|
||||||
text: "Audio saved successfully", // Text that is to be shown in the toast
|
text: "Audio saved successfully",
|
||||||
heading: 'Note', // Optional heading to be shown on the toast
|
heading: 'Note',
|
||||||
icon: 'success', // Type of toast icon
|
icon: 'success',
|
||||||
showHideTransition: 'fade', // fade, slide or plain
|
showHideTransition: 'fade',
|
||||||
allowToastClose: true, // Boolean value true or false
|
allowToastClose: true,
|
||||||
hideAfter: 3000, // false to make it sticky or number representing the miliseconds as time after which toast needs to be hidden
|
hideAfter: 3000,
|
||||||
stack: 3, // false if there should be only one toast at a time or a number representing the maximum number of toasts to be shown at a time
|
stack: 3,
|
||||||
position: 'bottom-center', // bottom-left or bottom-right or bottom-center or top-left or top-right or top-center or mid-center or an object representing the left, right, top, bottom values
|
position: 'bottom-center',
|
||||||
textAlign: 'left', // Text alignment i.e. left, right or center
|
textAlign: 'left',
|
||||||
loader: true, // Whether to show loader or not. True by default
|
loader: true,
|
||||||
loaderBg: '#9EC600', // Background color of the toast loader
|
loaderBg: '#9EC600',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
$.toast({
|
$.toast({
|
||||||
text: "Failed to save audio", // Text that is to be shown in the toast
|
text: "Failed to save audio",
|
||||||
heading: 'Note', // Optional heading to be shown on the toast
|
heading: 'Note',
|
||||||
icon: 'error', // Type of toast icon
|
icon: 'error',
|
||||||
showHideTransition: 'fade', // fade, slide or plain
|
showHideTransition: 'fade',
|
||||||
allowToastClose: true, // Boolean value true or false
|
allowToastClose: true,
|
||||||
hideAfter: 3000, // false to make it sticky or number representing the miliseconds as time after which toast needs to be hidden
|
hideAfter: 3000,
|
||||||
stack: 3, // false if there should be only one toast at a time or a number representing the maximum number of toasts to be shown at a time
|
stack: 3,
|
||||||
position: 'top-center', // bottom-left or bottom-right or bottom-center or top-left or top-right or top-center or mid-center or an object representing the left, right, top, bottom values
|
position: 'top-center',
|
||||||
textAlign: 'left', // Text alignment i.e. left, right or center
|
textAlign: 'left',
|
||||||
loader: true, // Whether to show loader or not. True by default
|
loader: true,
|
||||||
loaderBg: '#9EC600', // Background color of the toast loader
|
loaderBg: '#9EC600',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -123,13 +121,154 @@ jQuery(document).ready(function($) {
|
||||||
|
|
||||||
$('#visualizer').show();
|
$('#visualizer').show();
|
||||||
isRecording = true;
|
isRecording = true;
|
||||||
$('#recording-button').css('background-color', 'green'); // تغییر رنگ دکمه به سبز برای نشان دادن حالت ضبط
|
$('#recording-button').css('background-color', 'green');
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
mediaRecorder.stop();
|
mediaRecorder.stop();
|
||||||
$('#visualizer').hide();
|
$('#visualizer').hide();
|
||||||
isRecording = false;
|
isRecording = false;
|
||||||
$('#recording-button').css('background-color', 'red'); // بازگشت به رنگ قرمز
|
$('#recording-button').css('background-color', 'red');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.delete-audio').on('click', function() {
|
||||||
|
let fileName = $(this).data('file');
|
||||||
|
let $audioRow = $(this).closest('tr'); // یافتن ردیف حاوی فایل صوتی برای حذف آن
|
||||||
|
|
||||||
|
if (confirm("Are you sure you want to delete this audio file?")) {
|
||||||
|
$.ajax({
|
||||||
|
url: ajaxurl,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'delete_audio',
|
||||||
|
file_name: fileName
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
$.toast({
|
||||||
|
text: "Audio file deleted successfully",
|
||||||
|
heading: 'Note',
|
||||||
|
icon: 'success',
|
||||||
|
showHideTransition: 'fade',
|
||||||
|
allowToastClose: true,
|
||||||
|
hideAfter: 3000,
|
||||||
|
stack: 3,
|
||||||
|
position: 'bottom-center',
|
||||||
|
textAlign: 'left',
|
||||||
|
loader: true,
|
||||||
|
loaderBg: '#9EC600',
|
||||||
|
});
|
||||||
|
|
||||||
|
// پس از حذف موفق فایل، ردیف مربوطه را از لیست حذف کنید
|
||||||
|
$audioRow.fadeOut(400, function() {
|
||||||
|
$(this).remove();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$.toast({
|
||||||
|
text: "Failed to delete audio file: " + response.data,
|
||||||
|
heading: 'Error',
|
||||||
|
icon: 'error',
|
||||||
|
showHideTransition: 'fade',
|
||||||
|
allowToastClose: true,
|
||||||
|
hideAfter: 3000,
|
||||||
|
stack: 3,
|
||||||
|
position: 'top-center',
|
||||||
|
textAlign: 'left',
|
||||||
|
loader: true,
|
||||||
|
loaderBg: '#FF0000',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(jqXHR, textStatus, errorThrown) {
|
||||||
|
console.log('AJAX Error: ' + textStatus + ': ' + errorThrown);
|
||||||
|
$.toast({
|
||||||
|
text: "AJAX Error: " + textStatus + ": " + errorThrown,
|
||||||
|
heading: 'Error',
|
||||||
|
icon: 'error',
|
||||||
|
showHideTransition: 'fade',
|
||||||
|
allowToastClose: true,
|
||||||
|
hideAfter: 3000,
|
||||||
|
stack: 3,
|
||||||
|
position: 'top-center',
|
||||||
|
textAlign: 'left',
|
||||||
|
loader: true,
|
||||||
|
loaderBg: '#FF0000',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// حذف فایلهای انتخابشده
|
||||||
|
$('#delete-selected').on('click', function() {
|
||||||
|
let selectedFiles = [];
|
||||||
|
$('.select-audio:checked').each(function() {
|
||||||
|
selectedFiles.push($(this).val());
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selectedFiles.length > 0 && confirm("Are you sure you want to delete selected audio files?")) {
|
||||||
|
$.ajax({
|
||||||
|
url: ajaxurl,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'delete_selected_audios',
|
||||||
|
files: selectedFiles
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
$.toast({
|
||||||
|
text: "Selected audio files deleted successfully",
|
||||||
|
heading: 'Note',
|
||||||
|
icon: 'success',
|
||||||
|
showHideTransition: 'fade',
|
||||||
|
allowToastClose: true,
|
||||||
|
hideAfter: 3000,
|
||||||
|
stack: 3,
|
||||||
|
position: 'bottom-center',
|
||||||
|
textAlign: 'left',
|
||||||
|
loader: true,
|
||||||
|
loaderBg: '#9EC600',
|
||||||
|
});
|
||||||
|
|
||||||
|
// پس از حذف موفق فایلها، لیست را بهروزرسانی کنید
|
||||||
|
selectedFiles.forEach(function(fileName) {
|
||||||
|
$('.delete-audio[data-file="' + fileName + '"]').closest('tr').fadeOut(400, function() {
|
||||||
|
$(this).remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$.toast({
|
||||||
|
text: "Failed to delete selected audio files: " + response.data,
|
||||||
|
heading: 'Error',
|
||||||
|
icon: 'error',
|
||||||
|
showHideTransition: 'fade',
|
||||||
|
allowToastClose: true,
|
||||||
|
hideAfter: 3000,
|
||||||
|
stack: 3,
|
||||||
|
position: 'top-center',
|
||||||
|
textAlign: 'left',
|
||||||
|
loader: true,
|
||||||
|
loaderBg: '#FF0000',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(jqXHR, textStatus, errorThrown) {
|
||||||
|
console.log('AJAX Error: ' + textStatus + ': ' + errorThrown);
|
||||||
|
$.toast({
|
||||||
|
text: "AJAX Error: " + textStatus + ": " + errorThrown,
|
||||||
|
heading: 'Error',
|
||||||
|
icon: 'error',
|
||||||
|
showHideTransition: 'fade',
|
||||||
|
allowToastClose: true,
|
||||||
|
hideAfter: 3000,
|
||||||
|
stack: 3,
|
||||||
|
position: 'top-center',
|
||||||
|
textAlign: 'left',
|
||||||
|
loader: true,
|
||||||
|
loaderBg: '#FF0000',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -30,33 +30,70 @@ class Audio_Diary_Admin_Page {
|
||||||
add_action('admin_menu', array($this, 'add_menu_item'));
|
add_action('admin_menu', array($this, 'add_menu_item'));
|
||||||
add_action('admin_enqueue_scripts', array($this, 'enqueue_scripts'));
|
add_action('admin_enqueue_scripts', array($this, 'enqueue_scripts'));
|
||||||
add_action('wp_ajax_save_audio', array($this, 'save_audio'));
|
add_action('wp_ajax_save_audio', array($this, 'save_audio'));
|
||||||
add_action('wp_ajax_delete_audio_files', 'delete_audio_files');
|
add_action('wp_ajax_delete_audio', array($this, 'delete_audio'));
|
||||||
|
add_action('wp_ajax_delete_selected_audios', array($this, 'delete_selected_audios'));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
$this->create_audio_folder();
|
$this->create_audio_folder();
|
||||||
}
|
}
|
||||||
|
|
||||||
function delete_audio_files() {
|
function delete_audio() {
|
||||||
check_ajax_referer('audio-diary-nonce', '_ajax_nonce');
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_send_json_error('Unauthorized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isset($_POST['files']) || !is_array($_POST['files'])) {
|
if (empty($_POST['file_name'])) {
|
||||||
wp_send_json_error('Invalid request');
|
wp_send_json_error('No file specified');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$file_name = sanitize_file_name($_POST['file_name']);
|
||||||
|
$uploads = wp_upload_dir();
|
||||||
|
$file_path = $uploads['basedir'] . '/audio-diary/' . $file_name;
|
||||||
|
|
||||||
|
if (file_exists($file_path) && unlink($file_path)) {
|
||||||
|
wp_send_json_success('File deleted');
|
||||||
|
} else {
|
||||||
|
$error = file_exists($file_path) ? 'Failed to delete file' : 'File does not exist';
|
||||||
|
wp_send_json_error($error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function delete_selected_audios() {
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_send_json_error('Unauthorized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($_POST['files']) || !is_array($_POST['files'])) {
|
||||||
|
wp_send_json_error('No files specified');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$uploads = wp_upload_dir();
|
$uploads = wp_upload_dir();
|
||||||
$audio_dir = $uploads['basedir'] . '/audio-diary/';
|
$base_path = $uploads['basedir'] . '/audio-diary/';
|
||||||
$deleted = [];
|
$errors = [];
|
||||||
|
|
||||||
foreach ($_POST['files'] as $file) {
|
foreach ($_POST['files'] as $file_name) {
|
||||||
$file_path = $audio_dir . basename($file);
|
$file_name = sanitize_file_name($file_name);
|
||||||
if (file_exists($file_path) && unlink($file_path)) {
|
$file_path = $base_path . $file_name;
|
||||||
$deleted[] = $file;
|
|
||||||
|
if (file_exists($file_path)) {
|
||||||
|
if (!unlink($file_path)) {
|
||||||
|
$errors[] = $file_name;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$errors[] = $file_name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($deleted)) {
|
if (empty($errors)) {
|
||||||
wp_send_json_error('No files deleted');
|
wp_send_json_success('All files deleted');
|
||||||
|
} else {
|
||||||
|
wp_send_json_error('Failed to delete files: ' . implode(', ', $errors));
|
||||||
}
|
}
|
||||||
|
|
||||||
wp_send_json_success($deleted);
|
|
||||||
}
|
}
|
||||||
public function add_menu_item() {
|
public function add_menu_item() {
|
||||||
add_menu_page(
|
add_menu_page(
|
||||||
|
|
@ -103,7 +140,7 @@ class Audio_Diary_Admin_Page {
|
||||||
wp_mkdir_p($audio_dir);
|
wp_mkdir_p($audio_dir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public function save_audio() {
|
public function save_audio() {
|
||||||
if (!current_user_can('manage_options')) {
|
if (!current_user_can('manage_options')) {
|
||||||
wp_send_json_error('Unauthorized');
|
wp_send_json_error('Unauthorized');
|
||||||
return;
|
return;
|
||||||
|
|
@ -118,9 +155,15 @@ class Audio_Diary_Admin_Page {
|
||||||
$uploads = wp_upload_dir();
|
$uploads = wp_upload_dir();
|
||||||
$upload_path = $uploads['basedir'] . '/audio-diary/';
|
$upload_path = $uploads['basedir'] . '/audio-diary/';
|
||||||
|
|
||||||
|
// Check if the directory exists, if not create it
|
||||||
|
if (!file_exists($upload_path)) {
|
||||||
|
wp_mkdir_p($upload_path);
|
||||||
|
}
|
||||||
|
|
||||||
$file_name = 'audio-' . time() . '.wav';
|
$file_name = 'audio-' . time() . '.wav';
|
||||||
$file_path = $upload_path . $file_name;
|
$file_path = $upload_path . $file_name;
|
||||||
|
|
||||||
|
// Move the uploaded file to the audio-diary directory
|
||||||
if (move_uploaded_file($file['tmp_name'], $file_path)) {
|
if (move_uploaded_file($file['tmp_name'], $file_path)) {
|
||||||
wp_send_json_success('File uploaded');
|
wp_send_json_success('File uploaded');
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -128,5 +171,6 @@ class Audio_Diary_Admin_Page {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
Audio_Diary_Admin_Page::get_instance();
|
Audio_Diary_Admin_Page::get_instance();
|
||||||
|
|
@ -2,19 +2,25 @@
|
||||||
$uploads = wp_upload_dir();
|
$uploads = wp_upload_dir();
|
||||||
$audio_files = glob($uploads['basedir'] . '/audio-diary/*.wav');
|
$audio_files = glob($uploads['basedir'] . '/audio-diary/*.wav');
|
||||||
|
|
||||||
|
if (!is_array($audio_files)) {
|
||||||
|
$audio_files = [];
|
||||||
|
}
|
||||||
|
|
||||||
usort($audio_files, function($a, $b) {
|
usort($audio_files, function($a, $b) {
|
||||||
return filemtime($b) - filemtime($a);
|
return filemtime($b) - filemtime($a);
|
||||||
});
|
});
|
||||||
|
|
||||||
?>
|
?>
|
||||||
<div class="wrap">
|
<div class="wrap audio-diary-admin-list-page">
|
||||||
<h1><?php _e('Recorded Audios', 'audio-diary'); ?></h1>
|
<h1><?php _e('Recorded Audios', 'audio-diary'); ?></h1>
|
||||||
|
<button id="delete-selected"><?php _e('Delete Selected', 'audio-diary'); ?></button>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th><?php _e('Select', 'audio-diary'); ?></th>
|
||||||
<th><?php _e('Date', 'audio-diary'); ?></th>
|
<th><?php _e('Date', 'audio-diary'); ?></th>
|
||||||
<th><?php _e('Time', 'audio-diary'); ?></th>
|
<th><?php _e('Time', 'audio-diary'); ?></th>
|
||||||
<th><?php _e('Audio', 'audio-diary'); ?></th>
|
<th><?php _e('Audio', 'audio-diary'); ?></th>
|
||||||
|
<th><?php _e('Delete', 'audio-diary'); ?></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
@ -22,13 +28,19 @@ usort($audio_files, function($a, $b) {
|
||||||
$file_date = date("Y-m-d", filemtime($file));
|
$file_date = date("Y-m-d", filemtime($file));
|
||||||
$file_time = date("H:i:s", filemtime($file));
|
$file_time = date("H:i:s", filemtime($file));
|
||||||
$file_url = $uploads['baseurl'] . '/audio-diary/' . basename($file);
|
$file_url = $uploads['baseurl'] . '/audio-diary/' . basename($file);
|
||||||
|
$file_name = basename($file);
|
||||||
?>
|
?>
|
||||||
<tr>
|
<tr>
|
||||||
|
<td><input type="checkbox" class="select-audio" value="<?php echo $file_name; ?>"></td>
|
||||||
<td><?php echo $file_date; ?></td>
|
<td><?php echo $file_date; ?></td>
|
||||||
<td><?php echo $file_time; ?></td>
|
<td><?php echo $file_time; ?></td>
|
||||||
<td>
|
<td>
|
||||||
<audio controls src="<?php echo $file_url; ?>"></audio>
|
<audio controls src="<?php echo $file_url; ?>"></audio>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="delete-audio"
|
||||||
|
data-file="<?php echo $file_name; ?>"><?php _e('Delete', 'audio-diary'); ?></button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue