| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315 |
- "use client";
- import { useQuery } from "@tanstack/react-query";
- import { useMemo, useState } from "react";
- import { get } from "../../lib/api";
- interface PathConfigEditorProps {
- value: any;
- onChange: (value: any) => void;
- }
- export default function PathConfigEditor({
- value,
- onChange
- }: PathConfigEditorProps) {
- const [useJsonMode, setUseJsonMode] = useState(false);
- const [jsonValue, setJsonValue] = useState("");
- const [formData, setFormData] = useState<Record<string, any>>({});
- // Fetch handbrake presets on mount
- const { data: handbrakePresets = [] } = useQuery({
- queryKey: ["handbrake", "presets"],
- queryFn: () => get("/handbrake/presets"),
- select: (data) => (Array.isArray(data) ? data : [])
- });
- // Fetch extensions from settings
- const { data: extensions = [] } = useQuery({
- queryKey: ["config", "settings", "extensions"],
- queryFn: () => get("/config/settings/extensions"),
- select: (data) => (Array.isArray(data) ? data : [])
- });
- // Initialize form data and json value - default to form mode
- const derivedFormData = useMemo(() => {
- if (value && typeof value === "object" && !Array.isArray(value)) {
- return { ...value };
- }
- return {};
- }, [value]);
- const derivedJsonValue = useMemo(() => {
- return JSON.stringify(value || {}, null, 2);
- }, [value]);
- const handleJsonChange = (newJson: string) => {
- try {
- const parsed = JSON.parse(newJson);
- onChange(parsed);
- } catch {
- // Invalid JSON, don't update parent yet
- }
- };
- const handleFormFieldChange = (key: string, fieldValue: any) => {
- const newFormData = { ...derivedFormData, [key]: fieldValue };
- onChange(newFormData);
- };
- const addFormField = () => {
- const newKey = `field_${Object.keys(derivedFormData).length + 1}`;
- handleFormFieldChange(newKey, "");
- };
- const removeFormField = (key: string) => {
- const newFormData = { ...derivedFormData };
- delete newFormData[key];
- onChange(newFormData);
- };
- const switchToJsonMode = () => {
- setUseJsonMode(true);
- onChange(derivedFormData);
- };
- const switchToFormMode = () => {
- try {
- // When switching to form mode, we use the current value (which comes from props)
- if (typeof value === "object" && !Array.isArray(value)) {
- setUseJsonMode(false);
- return;
- }
- // If we can't convert to form mode, stay in JSON mode
- } catch {
- // Invalid JSON, stay in JSON mode
- }
- };
- // Render specific field types
- const renderFieldInput = (key: string, val: any) => {
- switch (key) {
- case "exts":
- return (
- <div className="flex-1">
- <select
- multiple
- value={Array.isArray(val) ? val : []}
- onChange={(e) => {
- const selected = Array.from(
- e.target.selectedOptions,
- (option) => option.value
- );
- handleFormFieldChange(key, selected);
- }}
- className="w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 text-xs h-24"
- >
- {extensions.map((ext) => (
- <option key={ext} value={ext}>
- {ext}
- </option>
- ))}
- </select>
- <p className="text-xs text-gray-500 mt-1">
- Hold Ctrl/Cmd to select multiple
- </p>
- </div>
- );
- case "ext":
- return (
- <select
- value={val || ""}
- onChange={(e) => handleFormFieldChange(key, e.target.value)}
- className="flex-1 rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 text-xs"
- >
- <option value="">Select extension...</option>
- {extensions.map((ext) => (
- <option key={ext} value={ext}>
- {ext}
- </option>
- ))}
- </select>
- );
- case "preset":
- return (
- <select
- value={val || ""}
- onChange={(e) => handleFormFieldChange(key, e.target.value)}
- className="flex-1 rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 text-xs"
- >
- <option value="">Select preset...</option>
- {handbrakePresets.map((preset) => (
- <option key={preset} value={preset}>
- {preset}
- </option>
- ))}
- </select>
- );
- case "clean":
- return (
- <div className="flex-1">
- <textarea
- value={
- typeof val === "object"
- ? JSON.stringify(val, null, 2)
- : val || ""
- }
- onChange={(e) => {
- try {
- const parsed = JSON.parse(e.target.value);
- handleFormFieldChange(key, parsed);
- } catch {
- handleFormFieldChange(key, e.target.value);
- }
- }}
- placeholder='{"regex": "replacement"}'
- rows={6}
- className="w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 font-mono text-xs"
- />
- <p className="text-xs text-gray-500 mt-1">
- JSON object with regex patterns
- </p>
- </div>
- );
- default:
- if (typeof val === "boolean") {
- return (
- <div className="flex items-center gap-2 flex-1">
- <input
- type="checkbox"
- checked={val}
- onChange={(e) => handleFormFieldChange(key, e.target.checked)}
- className="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
- />
- <span className="text-xs text-gray-600 dark:text-gray-400">
- Boolean
- </span>
- </div>
- );
- } else if (typeof val === "number") {
- return (
- <input
- type="number"
- value={val}
- onChange={(e) =>
- handleFormFieldChange(key, parseFloat(e.target.value) || 0)
- }
- placeholder="Number value"
- className="flex-1 rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 text-xs"
- />
- );
- } else {
- return (
- <input
- type="text"
- value={val || ""}
- onChange={(e) => handleFormFieldChange(key, e.target.value)}
- placeholder="String value"
- className="flex-1 rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 text-xs"
- />
- );
- }
- }
- };
- if (useJsonMode) {
- return (
- <div className="space-y-3">
- <div className="flex items-center justify-between">
- <label className="block text-sm font-medium text-gray-700 dark:text-gray-200">
- Configuration (JSON)
- </label>
- <button
- type="button"
- onClick={switchToFormMode}
- className="text-xs text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-200"
- >
- Switch to Form
- </button>
- </div>
- <textarea
- value={derivedJsonValue}
- onChange={(e) => handleJsonChange(e.target.value)}
- placeholder='{"key": "value"}'
- rows={6}
- className="w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 font-mono text-xs"
- />
- {(() => {
- try {
- JSON.parse(derivedJsonValue);
- return null;
- } catch {
- return (
- <p className="text-xs text-red-600 dark:text-red-400">
- Invalid JSON format
- </p>
- );
- }
- })()}
- </div>
- );
- }
- return (
- <div className="space-y-3">
- <div className="flex items-center justify-between">
- <label className="block text-sm font-medium text-gray-700 dark:text-gray-200">
- Configuration (Form)
- </label>
- <button
- type="button"
- onClick={switchToJsonMode}
- className="text-xs text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-200"
- >
- Switch to JSON
- </button>
- </div>
- {Object.keys(derivedFormData).length === 0 ? (
- <p className="text-sm text-gray-500 italic text-center py-4">
- No configuration fields. Add a field or switch to JSON mode.
- </p>
- ) : (
- <div className="space-y-3">
- {Object.entries(derivedFormData).map(([key, val]) => (
- <div key={key} className="flex gap-2 items-start">
- <input
- type="text"
- value={key}
- onChange={(e) => {
- const newFormData = { ...derivedFormData };
- delete newFormData[key];
- newFormData[e.target.value] = val;
- onChange(newFormData);
- }}
- placeholder="Field name"
- className="w-1/3 rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 text-xs"
- />
- {renderFieldInput(key, val)}
- <button
- type="button"
- onClick={() => removeFormField(key)}
- className="inline-flex items-center rounded-md bg-red-600 px-2 py-1 text-xs font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
- >
- ×
- </button>
- </div>
- ))}
- </div>
- )}
- <button
- type="button"
- onClick={addFormField}
- className="text-xs text-green-600 hover:text-green-800 dark:text-green-400 dark:hover:text-green-200"
- >
- + Add Field
- </button>
- </div>
- );
- }
|