Skip to content

Commit af56d99

Browse files
committed
Add inputable commands feature
1 parent de85aa4 commit af56d99

File tree

4 files changed

+315
-52
lines changed

4 files changed

+315
-52
lines changed

config/artisan-command-palette-ui.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@
2424
// Enable/disable command palette in production
2525
'enabled_in_production' => false,
2626

27+
// Commands that require input parameters
28+
'commands_with_input' => [
29+
'cache:forget' => [
30+
'label' => 'Key',
31+
'placeholder' => 'Enter cache key',
32+
'required' => true,
33+
],
34+
// Add more commands that require input here
35+
// Example:
36+
// 'make:model' => [
37+
// 'label' => 'Name',
38+
// 'placeholder' => 'Enter model name',
39+
// 'required' => true,
40+
// ],
41+
],
42+
2743
// Predefined command groups
2844
'command_groups' => [
2945
'Optimize' => [

resources/views/index.blade.php

Lines changed: 180 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,27 @@
9999
</div>
100100

101101
<div id="output-global" class="fixed top-4 right-4 z-50 max-w-sm hidden"></div>
102+
103+
<!-- Command Input Modal -->
104+
<div id="command-input-modal" class="fixed inset-0 z-50 items-center justify-center hidden">
105+
<div class="absolute inset-0 bg-black bg-opacity-50"></div>
106+
<div class="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md p-6 mx-4">
107+
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4" id="modal-title">Command Input</h3>
108+
<div class="mb-4">
109+
<label id="input-label" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Input</label>
110+
<input type="text" id="command-input-field" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" placeholder="">
111+
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400" id="current-command-display"></p>
112+
</div>
113+
<div class="flex justify-end space-x-3">
114+
<button id="cancel-input" class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
115+
Cancel
116+
</button>
117+
<button id="submit-input" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
118+
Run Command
119+
</button>
120+
</div>
121+
</div>
122+
</div>
102123

103124
<div class="flex-none h-[45vh] mb-4">
104125
<div class="w-full overflow-x-auto whitespace-nowrap">
@@ -113,11 +134,19 @@
113134
@foreach($groupCommands as $command)
114135
<div>
115136
<button
116-
class="w-full px-2 py-1 text-sm text-left border border-blue-500 text-blue-500 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/30 rounded truncate transition-colors command-btn"
137+
class="w-full px-2 py-1 text-sm text-left border border-blue-500 text-blue-500 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/30 rounded truncate transition-colors command-btn flex items-center justify-between"
117138
data-command="{{ $command['command'] }}"
118139
data-tooltip="{{ $command['description'] }}"
119140
>
120-
{{ $command['command'] }}
141+
<span>{{ $command['command'] }}</span>
142+
<span class="input-icon hidden ml-1 flex-shrink-0">
143+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" class="text-blue-500 dark:text-blue-400" stroke="currentColor">
144+
<path d="M19 7H5C3.34315 7 2 8.34315 2 10V19C2 20.6569 3.34315 22 5 22H19C20.6569 22 22 20.6569 22 19V10C22 8.34315 20.6569 7 19 7Z" stroke-width="1" stroke-linejoin="round"></path>
145+
<path d="M12 7V5.53078C12 4.92498 12.4123 4.39693 13 4.25V4.25C13.5877 4.10307 14 3.57502 14 2.96922V2" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"></path>
146+
<path d="M7 12L8 12M11.5 12L12.5 12M16 12L17 12" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"></path>
147+
<path d="M7 17L17 17" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"></path>
148+
</svg>
149+
</span>
121150
</button>
122151
</div>
123152
@endforeach
@@ -147,6 +176,41 @@ class="w-full px-2 py-1 text-sm text-left border border-blue-500 text-blue-500 d
147176

148177
<script>
149178
document.addEventListener('DOMContentLoaded', function() {
179+
// Store commands that require input
180+
let commandsWithInput = {};
181+
182+
// Fetch commands with input requirements
183+
axios.get('{{ route("artisan-command-palette.commands") }}')
184+
.then(response => {
185+
if (response.data && response.data.commands_with_input) {
186+
commandsWithInput = response.data.commands_with_input;
187+
188+
// Mark commands that require input with an icon
189+
Object.keys(commandsWithInput).forEach(commandName => {
190+
const commandButtons = document.querySelectorAll(`.command-btn[data-command="${commandName}"]`);
191+
commandButtons.forEach(button => {
192+
const iconElement = button.querySelector('.input-icon');
193+
if (iconElement) {
194+
iconElement.classList.remove('hidden');
195+
iconElement.classList.add('inline-block');
196+
}
197+
});
198+
});
199+
}
200+
})
201+
.catch(error => {
202+
console.error('Error fetching commands with input:', error);
203+
});
204+
205+
// Command input modal elements
206+
const commandInputModal = document.getElementById('command-input-modal');
207+
const commandInputField = document.getElementById('command-input-field');
208+
const inputLabel = document.getElementById('input-label');
209+
const modalTitle = document.getElementById('modal-title');
210+
const currentCommandDisplay = document.getElementById('current-command-display');
211+
const submitInputBtn = document.getElementById('submit-input');
212+
const cancelInputBtn = document.getElementById('cancel-input');
213+
150214
// Custom tooltip implementation
151215
const tooltip = document.getElementById('tooltip');
152216
document.querySelectorAll('[data-tooltip]').forEach(element => {
@@ -168,55 +232,124 @@ class="w-full px-2 py-1 text-sm text-left border border-blue-500 text-blue-500 d
168232
document.querySelectorAll('.command-btn').forEach(button => {
169233
button.addEventListener('click', async function() {
170234
const command = this.dataset.command;
171-
const originalText = this.textContent;
172-
173-
// Disable button and show loading
174-
this.disabled = true;
175-
this.innerHTML = `
176-
<svg class="animate-spin h-4 w-4 inline mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
177-
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
178-
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
179-
</svg>
180-
Running...
181-
`;
235+
const originalHTML = this.innerHTML;
236+
237+
// Check if command requires input
238+
if (commandsWithInput[command]) {
239+
// Show input modal
240+
const inputConfig = commandsWithInput[command];
241+
modalTitle.textContent = `Input for ${command}`;
242+
inputLabel.textContent = inputConfig.label || 'Input';
243+
commandInputField.placeholder = inputConfig.placeholder || 'Enter input value';
244+
currentCommandDisplay.textContent = `Command: ${command}`;
245+
246+
// Show modal with flex display
247+
commandInputModal.classList.remove('hidden');
248+
commandInputModal.classList.add('flex');
249+
commandInputField.focus();
250+
251+
// Store the command button reference for later use
252+
submitInputBtn.dataset.commandButton = button.dataset.command;
253+
return;
254+
}
255+
256+
// For commands without input, proceed as usual
257+
await executeCommand(command, this);
258+
});
259+
});
260+
261+
// Handle input submission
262+
submitInputBtn.addEventListener('click', async function() {
263+
const command = this.dataset.commandButton;
264+
const inputValue = commandInputField.value.trim();
265+
const buttonElement = document.querySelector(`.command-btn[data-command="${command}"]`);
266+
267+
// Validate input if required
268+
if (commandsWithInput[command]?.required && !inputValue) {
269+
showAlert('error', 'Input is required for this command');
270+
return;
271+
}
272+
273+
// Hide modal
274+
commandInputModal.classList.add('hidden');
275+
commandInputModal.classList.remove('flex');
276+
277+
// Execute command with input
278+
await executeCommand(command, buttonElement, inputValue);
279+
280+
// Reset input field
281+
commandInputField.value = '';
282+
});
283+
284+
// Handle cancel button
285+
cancelInputBtn.addEventListener('click', function() {
286+
commandInputModal.classList.add('hidden');
287+
commandInputModal.classList.remove('flex');
288+
commandInputField.value = '';
289+
});
290+
291+
// Handle Enter key in input field
292+
commandInputField.addEventListener('keydown', function(e) {
293+
if (e.key === 'Enter') {
294+
submitInputBtn.click();
295+
} else if (e.key === 'Escape') {
296+
cancelInputBtn.click();
297+
}
298+
});
299+
300+
// Execute command function
301+
async function executeCommand(command, buttonElement, inputValue = null) {
302+
const originalHTML = buttonElement.innerHTML;
182303
183-
// Update command status
184-
document.getElementById('current-command').innerHTML = `
185-
<span class="inline-block w-2 h-2 rounded-full bg-yellow-400 mr-2"></span>
186-
Running: ${command}
187-
`;
304+
// Disable button and show loading
305+
buttonElement.disabled = true;
306+
buttonElement.innerHTML = `
307+
<svg class="animate-spin h-4 w-4 inline mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
308+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
309+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
310+
</svg>
311+
Running...
312+
`;
188313
189-
try {
190-
const response = await axios.post('{{ route("artisan-command-palette.execute") }}', {
191-
command: command
192-
});
314+
// Update command status
315+
document.getElementById('current-command').innerHTML = `
316+
<span class="inline-block w-2 h-2 rounded-full bg-yellow-400 mr-2"></span>
317+
Running: ${command}${inputValue ? ' ' + inputValue : ''}
318+
`;
193319
194-
document.getElementById('logs-output').innerHTML = response.data.output || 'Command executed successfully with no output.';
195-
showAlert('success', 'Command executed successfully');
196-
document.getElementById('current-command').innerHTML = `
197-
<span class="inline-block w-2 h-2 rounded-full bg-green-500 mr-2"></span>
198-
${command}
199-
`;
200-
// Scroll to bottom of output div
201-
const logsOutput = document.getElementById('logs-output');
202-
logsOutput.scrollTop = logsOutput.scrollHeight;
203-
} catch (error) {
204-
const response = error.response?.data;
205-
document.getElementById('logs-output').innerHTML = response?.error || 'Command execution failed.';
206-
showAlert('error', `${response?.message}: ${response?.error}`);
207-
document.getElementById('current-command').innerHTML = `
208-
<span class="inline-block w-2 h-2 rounded-full bg-red-500 mr-2"></span>
209-
${command}
210-
`;
211-
// Scroll to bottom of output div
212-
const logsOutput = document.getElementById('logs-output');
213-
logsOutput.scrollTop = logsOutput.scrollHeight;
214-
} finally {
215-
this.disabled = false;
216-
this.textContent = originalText;
320+
try {
321+
const payload = { command: command };
322+
if (inputValue) {
323+
payload.input_value = inputValue;
217324
}
218-
});
219-
});
325+
326+
const response = await axios.post('{{ route("artisan-command-palette.execute") }}', payload);
327+
328+
document.getElementById('logs-output').innerHTML = response.data.output || 'Command executed successfully with no output.';
329+
showAlert('success', 'Command executed successfully');
330+
document.getElementById('current-command').innerHTML = `
331+
<span class="inline-block w-2 h-2 rounded-full bg-green-500 mr-2"></span>
332+
${command}${inputValue ? ' ' + inputValue : ''}
333+
`;
334+
// Scroll to bottom of output div
335+
const logsOutput = document.getElementById('logs-output');
336+
logsOutput.scrollTop = logsOutput.scrollHeight;
337+
} catch (error) {
338+
const response = error.response?.data;
339+
document.getElementById('logs-output').innerHTML = response?.error || 'Command execution failed.';
340+
showAlert('error', `${response?.message}: ${response?.error}`);
341+
document.getElementById('current-command').innerHTML = `
342+
<span class="inline-block w-2 h-2 rounded-full bg-red-500 mr-2"></span>
343+
${command}
344+
`;
345+
// Scroll to bottom of output div
346+
const logsOutput = document.getElementById('logs-output');
347+
logsOutput.scrollTop = logsOutput.scrollHeight;
348+
} finally {
349+
buttonElement.disabled = false;
350+
buttonElement.innerHTML = originalHTML;
351+
}
352+
}
220353
221354
// Clear logs
222355
document.getElementById('clear-logs').addEventListener('click', function() {

src/Http/Controllers/ArtisanCommandController.php

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,6 @@ protected function getCommandGroups()
4444
return $commandGroups;
4545
}
4646

47-
/**
48-
* List all available Artisan commands.
49-
*
50-
* @return \Illuminate\Http\JsonResponse
51-
*/
5247
/**
5348
* List all available Artisan commands.
5449
*
@@ -59,10 +54,12 @@ public function listCommands()
5954
// Return both predefined command groups and all available commands
6055
$commandGroups = $this->getCommandGroups();
6156
$allCommands = $this->getAllCommands();
57+
$commandsWithInput = Config::get('artisan-command-palette-ui.commands_with_input', []);
6258

6359
return Response::json([
6460
'groups' => $commandGroups,
6561
'all' => $allCommands,
62+
'commands_with_input' => $commandsWithInput,
6663
]);
6764
}
6865

@@ -100,6 +97,7 @@ protected function getAllCommands()
10097
public function executeCommand(Request $request)
10198
{
10299
$command = $request->input('command');
100+
$inputValue = $request->input('input_value');
103101

104102
if (empty($command)) {
105103
return Response::json([
@@ -134,6 +132,12 @@ public function executeCommand(Request $request)
134132
], 403);
135133
}
136134

135+
// Check if command requires input and append it if provided
136+
$commandsWithInput = Config::get('artisan-command-palette-ui.commands_with_input', []);
137+
if (array_key_exists($commandName, $commandsWithInput) && !empty($inputValue)) {
138+
$command = $command . ' ' . $inputValue;
139+
}
140+
137141
// Execute the command
138142
try {
139143
Artisan::call($command);

0 commit comments

Comments
 (0)