إنشاء فئة أكثر سهولة

أشار أحدهم إلى أن إنشاء الفئات (categories) والوسوم (tags) يبدو مرهقًا، وأن هذا الاحتكاك قد يثبط المسؤولين الجدد عن تبني ديسكورس (Discourse).

طلبت من Gemini إنشاء مُخطط للفئات (category planner). تبدو النتيجة جميلة جدًا. لا أعتقد أنه ينبغي أو يمكن إضافتها إلى ديسكورس كما هي، لأسباب عديدة (جيدة)، ولكن لا يزال من المثير للاهتمام إلقاء نظرة عليها. قد تثير أفكارًا لمستقبل ديسكورس.

السياق: https://meta.discourse.org/t/how-to-add-multiple-tags-up-front/390796?u=canapin

نقل فئة خارج نطاق الأصل (parent) يكون غريبًا بعض الشيء. يجب عليك سحبها فوق الأصل، ثم إلى اليسار. ليس بديهيًا، ولكن مرة أخرى، هذا مفهوم واجهة أكثر من كونه حلاً حقيقيًا.

chrome_Zl3VkfeCTI

قد تبدو واجهة مسؤول ديسكورس (Discourse admin UI) قاسية في البداية. البرنامج معقد، ومليء بالإعدادات، وأصبح أكثر تعقيدًا على مر السنين. قد يكون إجراء تعديل على بعض أجزاء الإدارة بجعلها أكثر مرئية وتفاعلية ومرحة، خاصة المناطق التي يصل إليها الناس فورًا عندما يحاولون تكوين الخيارات الأساسية لمجتمعهم، أمرًا مجديًا.

أتساءل كم عدد الأشخاص الذين ينفرون من ديسكورس بعد أن تخيفهم الواجهة الحالية. :thinking:


قررت إنشاء هذا الموضوع بعد هذا الرد على إبداع Gemini:

12 إعجابًا

نحن نخطط حاليًا لتحسين إنشاء الفئات، بما في ذلك جميع الميزات المختلفة التي يمكن أن تشملها الفئات (التصويت، الحلول، إلخ) — جميع ملاحظاتكم صحيحة جدًا!

13 إعجابًا

واو، هذا مذهل، أنا أستخدمه الآن وهو حقًا النوع الذي كنت أفكر فيه. مجرد سؤال واحد في هذه المرحلة، بالنسبة للأذونات، هل تعني “المجموعة” (group) جميع الأعضاء؟ إذا أردت منح أذونات لمجموعة فرعية (مثل “الأطباء البيطريون” - vets)، هل أكتب فقط “vets” وهذا كل شيء؟

أوه، وأيضًا: هل يمكن استيراد ملف .csv إلى Discourse في هذه المرحلة أم لا؟ (ليس أمرًا جللًا إذا لم يكن كذلك، فمن الرائع بالفعل الحصول على واجهة يمكنني من خلالها العبث بهيكلي).

4 إعجابات

بشكل افتراضي، يكون الجميع ما لم تحدد مجموعات. عند النقر على إضافة مجموعة، يجب أن يكتشف مجموعاتك الحالية.

اعتقدت أن هناك طريقة لإضافة كميات كبيرة عبر ملف csv ولكني لا أراها. قد تتفاجأ من عدد الملفات التي يمكنك نسخها/لصقها مباشرة في الحقل من ملف csv.

إعجاب واحد (1)

أعتقد أن هناك التباسًا، أنا أسأل عن واجهة المستخدم التي بناها @Canapin للتو… في الوقت الحالي ليس لدي أي مجموعات أو أي شيء مُعد في ديسكورس، أنا فقط أحاول تصميم الهيكل!

إعجاب واحد (1)

نشأ هذا الأداة من سؤالي لـ Gemini، “كيف تتخيل واجهة أكثر ودية لإدارة الفئات في Discourse؟”

إنها في الواقع مجرد نموذج أولي تفاعلي للعب به :slightly_smiling_face:.
لم يتم اختبار أزرار الاستيراد/التصدير، وبالتأكيد لا يمكن استيراد التصديرات إلى Discourse.

حول المجموعات: في Discourse الفعلي، هناك ثلاثة أذونات (عرض، والرد، والإنشاء) يمكن تعيينها للمجموعات المضمنة أو المخصصة. في هذه الأداة، حقل الأذونات هو مثل البقية، مجرد عنصر نائب اعتباطي.

فكر في هذا على أنه لوح أبيض مفصل لتخطيط الفئات أكثر من كونه أي شيء متصل بـ Discourse نفسه :slightly_smiling_face:

إعجابَين (2)

رائع حقًا - إنه قابل للاستخدام بالفعل! لقد قمت بتصدير مجموعة من إصدارات ما قمت به، ولكن لم أحاول الاستيراد بعد (لم أجرؤ على تحديث علامة التبويب، هيهيهي).

شكرًا على التوضيح بخصوص حقل الأذونات في “النموذج التفاعلي المفيد بالفعل” :slight_smile:

إنه جيد بالنسبة لي كما هو بالفعل!

سؤال مع ذلك: هل هناك أي طريقة لاستيراد قائمة فئات إلى Discourse، كما هو الحال عند استيراد العلامات من ملف CSV؟

إعجاب واحد (1)

هناك العديد من نصوص الهجرة الكاملة للمنتديات، ولكني لم أسمع قط عن أداة مخصصة فقط لإعداد فئات فارغة.

إذا كان إنشاء الفئات الخاصة بك عبر الواجهة يبدو وكأنه عمل كثير، فربما يكون لديك عدد كبير جدًا من الفئات… :sweat_smile:

بعد إنشاء بضع فئات، ستعتاد عليها. لم أقم بذلك منذ فترة، ولقد أضفت للتو فئة جديدة بإعدادات أساسية للون والموضع والأذونات مع موضوع ترحيب محدث في حوالي 90 ثانية: (فيديو)

(سأشير إلى أن العديد من الفئات في القائمة المرئية هناك مقتصرة على مجموعة واحدة أو أخرى. بصفتي مسؤولاً، أرى كل شيء، لكن معظم الأعضاء يرون 6 أو 7 فئات فقط في قائمتهم.)

إعجاب واحد (1)

@Canapin حسنًا، بعد اللعب به قليلاً، قمت بتحديث نافذة المتصفح عن طريق الخطأ، مما أتاح لي فرصة تجربة زر “استيراد”. يمكنني إفادتك بأن التصدير يعمل (انظر الملف المرفق)، ولكن الاستيراد يفشل بصمت :joy:

discourse_categories-7.csv (3.4 كيلوبايت)

ربما :sweat_smile: ولكن إذا كنت سأضيقها، فإن الواجهة ليست المكان الذي سيساعدني في تضييقها :wink:

أتعلم، أعتقد أن المشكلة ليست في الوقت الفعلي اللازم لإنشاء فئة، بل في العبء المعرفي الناتج عن الاضطرار إلى التنقل ذهابًا وإيابًا بين الشاشات حيث لا يظهر سوى جزء صغير (فئة واحدة… ومجموعة فرعية من الإعدادات الخاصة بها) من الهيكل العام. ما يفعله النموذج المرئي هو السماح لي بإبقاء كل شيء أمامي أثناء عبثي بالفئات والإعدادات، حتى أتمكن من التكرار ورؤية ما ينجح وما لا ينجح.

إعجاب واحد (1)

أجل، أنا أتفق تمامًا بخصوص التخطيط وتصميم نموذج لهيكلك أولاً. كنت أعلق على فكرة استيراد قائمة فئات إلى Discourse، وهو ما قد يكون منطقيًا إذا كنت Proctor & Gamble، ولكن بالنسبة لمعظمنا سيكون عصرًا كبيرًا لعصير قليل. :wink:

إعجاب واحد (1)

إذا أردت محاولة تعديل الأداة لجعل الاستيراد يعمل على الأقل، فكيف يمكنني فعل ذلك؟ هل هناك طريقة لعمل “نسخة” من الأداة التي أنشأتها للعمل عليها؟ هل كان سؤالك حقًا مجرد تلك الكلمات القليلة؟

متابعةً هنا. أنا الآن في ديسكورس (Discourse) أحاول تنظيف الفئات الخاصة بي. إنها عملية مرهقة حقًا.

  1. الذهاب إلى صفحة “جميع الفئات”
  2. النقر على الفئة التي أريد تعديلها
  3. النقر على المفتاح (wrench)، وإجراء التعديلات
  4. النقر على “حفظ” (save) في الأسفل ولا يوجد أي مؤشر مرئي يخبرني بأن ذلك قد أنجز شيئًا
  5. حسنًا، أفترض أنني انتهيت، إلى أين أذهب الآن؟ آه، “العودة إلى الفئة”
  6. هممم، أنا الآن عدت إلى صفحة الفئة التي لم أعد مهتمًا بها، كيف أعود إلى صفحة الفئات الرئيسية؟ آه، الشريط الجانبي > جميع الفئات… أفترض أنني كان يجب أن أنقر على ذلك أولاً

بعض توقعاتي خلال العملية:

  • عندما أنقر على “حفظ”، أود أن يظهر شيء ما في الصفحة لتأكيد أنني حفظت بالفعل
  • بالنسبة لحالة الاستخدام الخاصة بي، حيث أتنقل عبر جميع فئاتي لإعادة هيكلتها، أو تغيير الأذونات، أو الألوان، أو الأوصاف، أو ما إلى ذلك، أريد (على الأقل) العودة إلى صفحة الفئات الرئيسية، ونعم، يوجد هذا الرابط في الشريط الجانبي، لكنني استمررت في البحث عن شيء ما في منطقة المحتوى الرئيسية “ليأخذني إلى حيث أحتاج”
  • حتى لو لم نحصل على شيء جميل مثل النموذج المرئي المقدم في هذا الموضوع، سيكون من الأسهل بكثير الحصول على جدول كبير يحتوي على جميع الفئات والإعدادات الهامة فيه حتى أتمكن من تعديلها، حتى أتمكن من إجراء هذا الإعداد الأولي للفئات/التنظيف دون الحاجة إلى النقر عبر (حرفيًا) عشرات الشاشات مرارًا وتكرارًا مع تطور استراتيجية هيكل المجتمع، دون نظرة عامة جيدة.

قد يكون من المفيد استخدام القائمة المنسدلة للانتقال إلى الفئة التالية إذا كنت ترغب في تعديلها.

أنا أيضًا غير متأكد مما تتوقعه عند النقر على حفظ. في منتدى الخاص بي، يتغير الزر إلى “جاري الحفظ” وبمجرد الانتهاء، يعود إلى “حفظ”.

أوه أنت على حق، ذكر شخص ما (أنت؟) ذلك بالفعل ولكني نسيت.

إذا كان يفعل ذلك (قد يكون كذلك!) فإنه يحدث بسرعة كبيرة لدرجة أنني لا أرى شيئًا. بالإضافة إلى ذلك، يجب أن أقول إنه بمجرد أن يعطي عقلي الأمر لإصابعي للنقر، تكون عيناي بالفعل في مكان آخر. أتوقع شيئًا صغيرًا في الجزء العلوي من الصفحة يقول “تم الحفظ” - مثل ووردبريس، أعتقد :wink:

I gave Gemini the main attributes of a category (name, color, tags, etc) and asked: “Can you output a js/html5 tool to create and organize categories with these attributes? We should also be able to quickly edit and rearrange”.

The produced tool was already great, but I fine-tuned it by asking to remove/add attributes and features as well as tweaking the design.

Probably. You can send the code to Gemini (use the Canvas mode) and ask it to tweak it like you want:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Discourse Category Planner</title>
    <style>
        /* Base Styling */
        body { font-family: 'Inter', sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; background-color: #f8f9fa; }
        h1 { border-bottom: 2px solid #e9ecef; padding-bottom: 10px; margin-bottom: 20px; color: #343a40; }
        
        /* Layout */
        .main-container { display: flex; gap: 30px; }
        @media (max-width: 900px) {
            .main-container { flex-direction: column; }
        }
        .form-section { flex: 1.2; padding: 15px; border-radius: 8px; background: #fff; box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
        .list-section { flex: 1.8; padding: 15px; border-radius: 8px; background: #fff; box-shadow: 0 4px 6px rgba(0,0,0,0.05); }

        /* Form Controls */
        .form-row { display: flex; gap: 15px; margin-bottom: 10px; }
        .form-group { flex: 1; display: flex; flex-direction: column; }
        label { font-weight: 600; margin-bottom: 3px; font-size: 0.9em; color: #495057; }
        input[type="text"], select, textarea { padding: 8px; border: 1px solid #ced4da; border-radius: 4px; }
        
        /* Buttons */
        .action-button { padding: 10px 15px; border: none; border-radius: 44px; cursor: pointer; background: #007bff; color: white; margin-top: 10px; transition: background 0.2s; }
        .action-button:hover { background: #0056b3; }
        .utility-buttons { display: flex; gap: 10px; margin-top: 20px; }
        .utility-buttons .action-button { background: #6c757d; }
        .utility-buttons .action-button:hover { background: #5a6268; }


        /* --- Color Input Styling --- */
        input[type="color"] {
            -webkit-appearance: none;
            -moz-appearance: none;
            appearance: none;
            border: 1px solid #ced4da;
            padding: 0;
            height: 38px;
            cursor: pointer;
            border-radius: 4px;
            overflow: hidden;
        }
        input[type="color"]::-webkit-color-swatch-wrapper {
            padding: 0;
        }
        input[type="color"]::-webkit-color-swatch {
            border: none;
            border-radius: 4px;
            height: 100%; 
            display: block;
        }
        /* --- End Color Input Styling --- */
        
        /* Management List Styles (Draggable) */
        #category-list { list-style: none; padding: 0; margin-top: 15px; }
        .category-item { 
            display: flex; 
            align-items: center; 
            padding: 12px; 
            border: 1px solid #dee2e6; 
            border-radius: 6px; 
            margin-bottom: 8px; 
            cursor: move; 
            background: #ffffff;
            transition: box-shadow 0.2s;
        }
        .category-item:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
        .category-item.dragging { opacity: 0.6; border: 2px dashed #007bff; background: #eaf3ff; }
        
        /* Subcategory Indentation in Management List (Middle Column) */
        .category-item.is-sub {
            margin-left: 30px;
            border-left: 3px solid #ced4da; 
            background-color: #f7f7f7;
        }

        /* --- DRAG HINT STYLES --- */
        .category-item.drag-over { border-bottom: 2px solid #007bff; }
        .category-item.indent-hint {
            border-bottom: 2px solid #28a745;
            box-shadow: 5px 0 0 0 #28a745 inset;
            margin-left: 30px !important;
            background-color: #eaf3ff;
        }
        .category-item.outdent-hint {
            border-bottom: 2px solid #dc3545;
            box-shadow: -5px 0 0 0 #dc3545 inset;
            margin-left: 0 !important;
            background-color: #ffeaea;
        }
        /* --- END DRAG HINT STYLES --- */


        .category-actions { flex-shrink: 0; display: flex; gap: 5px; }

        /* --- Category Display Container (Applies to both Management List and Preview) --- */
        .category-info, .preview-category { 
            flex-grow: 1; 
            display: flex;
            align-items: flex-start; 
            padding: 0; 
        }
        
        /* Wrapper for the colored elements (Vertical Bar + Square + Text) */
        .category-display-block {
            display: flex;
            flex-direction: column;
            padding-left: 8px; 
            border-left: 4px solid transparent; 
            flex-grow: 1;
        }

        /* Wrapper for the Square and Name/Text */
        .square-and-text-wrapper {
            display: flex;
            align-items: flex-start;
        }

        /* Colored Square - Removed border-radius */
        .color-square {
            width: 12px;
            height: 12px;
            border-radius: 0; 
            margin-right: 8px;
            flex-shrink: 0;
            border: 1px solid rgba(0,0,0,0.1);
            margin-top: 3px;
        }

        /* Category titles use default color (dark gray/black) and are bold */
        .category-name { 
            font-size: 1em; 
            font-weight: 700;
            color: #343a40; 
        } 
        .category-desc { font-size: 0.8em; color: #6c757d; }

        /* --- Pill Styling --- */
        .pill-container, .preview-pill-wrapper { 
            margin-top: 5px; 
            display: flex; 
            flex-wrap: wrap; 
            gap: 6px; 
            align-items: center; 
        }

        /* Permissions - Subtler styling */
        .management-permission-pill, .preview-permission-pill { 
            font-size: 0.7em; 
            font-weight: 600; 
            background: #e9ecef; /* Lighter background */
            color: #495057; /* Darker text */
            padding: 3px 6px; 
            border-radius: 4px;
            text-transform: uppercase;
            border: 1px solid #ced4da; /* Add a subtle border for definition */
        }

        .attr-pill, .preview-tag-pill { /* Tags */
            font-size: 0.7em; 
            background: #f8f9fa; /* Very light background for less emphasis */
            color: #495057;
            padding: 3px 6px; 
            border-radius: 4px;
            display: inline-block;
            border: 1px solid #e9ecef; /* Subtle border */
        }
        
        /* Edit/Delete Button Styles */
        .edit-btn, .delete-btn { 
            background: #6c757d; 
            color: white; 
            padding: 5px 8px; 
            font-size: 0.75em; 
            border-radius: 4px; 
            border: none; 
        }
        .delete-btn { 
            background: #dc3545; 
        }
        
    </style>
</head>
<body>
    <h1>📋 Discourse Category & Hierarchy Planner</h1>
    
    <div class="main-container">
        
        <div class="form-section">
            <h2>Add/Edit Category Attributes</h2>
            <form id="category-form">
                <input type="hidden" id="category-id">
                <div class="form-row">
                    <div class="form-group">
                        <label for="name">1. Name</label> 
                        <input type="text" id="name" required oninput="updateLiveFormPreview()">
                    </div>
                    <div class="form-group">
                        <label for="color">2. Color</label>
                        <!-- Custom styled color input -->
                        <input type="color" id="color" value="#007bff" oninput="updateLiveFormPreview()">
                    </div>
                </div>
                <div class="form-row">
                    <div class="form-group">
                        <label for="description">3. Description</label>
                        <textarea id="description" rows="2" oninput="updateLiveFormPreview()"></textarea>
                    </div>
                    <div class="form-group">
                        <label for="permissions">4. Permissions (e.g., public, staff, group)</label> 
                        <input type="text" id="permissions" value="Public" oninput="updateLiveFormPreview()">
                    </div>
                </div>
                <div class="form-row">
                    <div class="form-group" style="flex: 2;">
                        <label for="tags">5. Tags (Comma separated, e.g., "bug, feature")</label>
                        <input type="text" id="tags" placeholder="Optional tags for posts in this category" oninput="updateLiveFormPreview()">
                    </div>
                </div>
                <input type="hidden" id="parentId" value=""> 

                <div class="form-row">
                    <div class="form-group" style="flex: 2;">
                        <button type="submit" id="submit-btn" class="action-button">Add New Category</button>
                    </div>
                </div>
            </form>

            <div id="live-form-preview" style="padding: 10px; border: 1px dashed #ced4da; border-radius: 6px; background: #fefefe;">
                <h3 style="margin-top: 0; font-size: 1em;">Preview</h3>
                <div class="preview-category" style="padding: 10px; border: none; align-items: flex-start;">
                    <div class="category-display-block" id="preview-display-block" style="border-left-color: #007bff; padding-left: 8px;">
                        <div class="square-and-text-wrapper">
                            <div class="color-square" id="preview-color-bar-field" style="background-color: #007bff;"></div>
                            <div class="category-name" id="preview-name-field">[Category Name]</div>
                        </div>
                        <div class="category-desc" id="preview-desc-field" style="color: #6c757d;">[Description goes here]</div>
                        <div class="preview-pill-wrapper" id="preview-pills-field">
                            <!-- Pills will be inserted here by JS -->
                        </div>
                    </div>
                </div>
            </div>
            
            <div class="utility-buttons">
                <button id="export-btn" class="action-button">Export (CSV)</button>
                <button id="import-btn" class="action-button">Import (CSV)</button>
                <input type="file" id="csv-file-input" accept=".csv" style="display: none;">
            </div>
        </div>

        <div class="list-section">
            <h2>Category Management (Drag to Reorder)</h2>
            <p style="font-size: 0.85em; color: #6c757d;">
                Drag an element to reorder. Drag to the **right** to create a subcategory (Green hint), drag to the **left** to promote (Red hint).
            </p>
            <ul id="category-list">
                <!-- Categories will be rendered here by JavaScript -->
            </ul>

            <ul id="live-preview-list" style="display: none;"></ul>
        </div>
        
    </div>

    <script>
        let categories = []; 
        let dragStartX = 0; 
        
        const form = document.getElementById('category-form');
        const list = document.getElementById('category-list');
        const previewList = document.getElementById('live-preview-list'); 
        const submitBtn = document.getElementById('submit-btn');
        const exportBtn = document.getElementById('export-btn');
        const importBtn = document.getElementById('import-btn');
        const csvFileInput = document.getElementById('csv-file-input');
        
        // --- CSV Export/Import Handlers ---

        function arrayToCsv(data) {
            const headers = ['id', 'name', 'description', 'color', 'permissions', 'tags', 'parentId'];
            let csv = headers.join(',') + '\n';
            data.forEach(row => {
                const values = headers.map(header => {
                    let value = row[header] || '';
                    // Escape commas and quotes for CSV format
                    if (typeof value === 'string') {
                        value = value.replace(/"/g, '""');
                        if (value.includes(',') || value.includes('\n')) {
                            value = `"${value}"`;
                        }
                    }
                    return value;
                });
                csv += values.join(',') + '\n';
            });
            return csv;
        }

        function csvToArray(csv) {
            const lines = csv.split('\n').filter(line => line.trim() !== '');
            if (lines.length < 2) return [];

            const headers = lines[0].split(',');
            const data = [];

            for (let i = 1; i < lines.length; i++) {
                const values = lines[i].match(/(".*?"|[^",\s]+)(?=\s*,|\s*$)/g) || lines[i].split(',');
                if (values.length === headers.length) {
                    let row = {};
                    for (let j = 0; j < headers.length; j++) {
                        let value = values[j].trim();
                        if (value.startsWith('"') && value.endsWith('"')) {
                            value = value.substring(1, value.length - 1).replace(/""/g, '"');
                        }
                        row[headers[j]] = value;
                    }
                    data.push(row);
                }
            }
            return data;
        }


        exportBtn.addEventListener('click', () => {
            const csvData = arrayToCsv(categories);
            const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8;' });
            const link = document.createElement('a');
            
            if (link.download !== undefined) {
                const url = URL.createObjectURL(blob);
                link.setAttribute('href', url);
                link.setAttribute('download', 'discourse_categories.csv');
                link.style.visibility = 'hidden';
                document.body.appendChild(link);
                link.click();
                document.body.removeChild(link);
            } else {
                alert('Export not supported in this browser.');
            }
        });

        importBtn.addEventListener('click', () => {
            csvFileInput.click();
        });

        csvFileInput.addEventListener('change', (event) => {
            const file = event.target.files[0];
            if (!file) return;

            const reader = new FileReader();
            reader.onload = (e) => {
                const csvContent = e.target.result;
                const importedData = csvToArray(csvContent);
                
                if (importedData.length > 0) {
                    categories = importedData;
                    renderAll();
                    alert(`Successfully imported ${importedData.length} categories.`);
                } else {
                    alert('Error importing CSV: Data is empty or incorrectly formatted.');
                }
            };
            reader.readAsText(file);
        });

        // --- Core Data Management & Rendering ---

        const findParent = (parentId) => categories.find(c => c.id === parentId);

        // Recursive function to find all descendants of a category ID
        function getDescendantIds(parentId) {
            let descendants = [];
            const children = categories.filter(c => c.parentId === parentId);
            
            children.forEach(child => {
                descendants.push(child.id);
                descendants = descendants.concat(getDescendantIds(child.id)); 
            });
            return descendants;
        }

        // Function to update the simple preview box next to the form
        function updateLiveFormPreview() {
            const name = document.getElementById('name').value || '[Category Name]';
            const description = document.getElementById('description').value || '[Description goes here]';
            const color = document.getElementById('color').value;
            const permissions = document.getElementById('permissions').value;
            const tagsInput = document.getElementById('tags').value;

            // Apply color to the square and left border
            document.getElementById('preview-color-bar-field').style.backgroundColor = color;
            document.getElementById('preview-display-block').style.borderLeftColor = color;
            
            document.getElementById('preview-name-field').textContent = name;
            document.getElementById('preview-desc-field').textContent = description;
            
            // Generate Pills HTML
            let pillsHtml = `<span class="preview-permission-pill">${permissions}</span>`;

            if (tagsInput) {
                const tagsArray = tagsInput.split(',').map(tag => tag.trim()).filter(tag => tag.length > 0);
                tagsArray.forEach(tag => {
                    pillsHtml += `<span class="preview-tag-pill">${tag}</span>`;
                });
            }

            document.getElementById('preview-pills-field').innerHTML = pillsHtml;
        }


        // Function to render the management list
        function renderAll() {
            list.innerHTML = ''; 
            previewList.innerHTML = '';
            
            let finalOrder = [];
            const parentCategories = categories.filter(c => !c.parentId);
            const subCategories = categories.filter(c => c.parentId);

            parentCategories.sort((a, b) => categories.indexOf(a) - categories.indexOf(b)).forEach(parent => {
                finalOrder.push(parent);
                const children = subCategories.filter(c => c.parentId === parent.id);
                children.sort((a, b) => categories.indexOf(a) - categories.indexOf(b)).forEach(child => {
                    finalOrder.push(child);
                });
            });

            subCategories.filter(c => !findParent(c.parentId)).forEach(orphan => {
                finalOrder.push(orphan);
            });


            finalOrder.forEach(cat => {
                const isSub = !!cat.parentId;
                
                // --- 1. Render Management List Item (Draggable) ---
                const managementItem = document.createElement('li');
                managementItem.className = 'category-item';
                if (isSub) {
                    managementItem.classList.add('is-sub');
                }
                managementItem.setAttribute('data-id', cat.id);
                managementItem.setAttribute('draggable', true);

                // Build tags HTML
                const tagsHtml = cat.tags ? cat.tags.split(',').map(tag => `<span class="attr-pill">${tag.trim()}</span>`).join('') : '';

                managementItem.innerHTML = `
                    <div class="category-info">
                        <div class="category-display-block" style="border-left-color: ${cat.color};">
                            <div class="square-and-text-wrapper">
                                <span class="color-square" style="background-color: ${cat.color};"></span>
                                <div class="category-name">${cat.name} ${isSub ? '— Subcategory' : ''}</div>
                            </div>
                            <div class="category-desc">${cat.description || ''}</div>
                            <div class="pill-container">
                                <span class="management-permission-pill">${cat.permissions}</span>
                                ${tagsHtml}
                            </div>
                        </div>
                    </div>
                    <div class="category-actions">
                        <button class="edit-btn" onclick="editCategory('${cat.id}')">Edit</button>
                        <button class="delete-btn" onclick="deleteCategory('${cat.id}')">Delete</button>
                    </div>
                `;
                list.appendChild(managementItem);
            });
            
            addDragDropListeners();
        }

        // Function to handle form submission (Add or Update)
        form.addEventListener('submit', (e) => {
            e.preventDefault();
            
            const idField = document.getElementById('category-id');
            const id = idField.value;
            
            const newCategory = {
                id: id || 'cat_' + Date.now(), 
                name: document.getElementById('name').value,
                description: document.getElementById('description').value,
                color: document.getElementById('color').value,
                permissions: document.getElementById('permissions').value,
                tags: document.getElementById('tags').value.trim(),
                parentId: '',
            };
            
            if (id) {
                const index = categories.findIndex(c => c.id === id);
                if (index !== -1) {
                    newCategory.parentId = categories[index].parentId; 
                    categories[index] = newCategory;
                }
            } else {
                categories.push(newCategory);
            }

            form.reset();
            idField.value = '';
            submitBtn.textContent = 'Add New Category';
            updateLiveFormPreview();
            renderAll();
        });

        // Function to populate the form for editing
        function editCategory(id) {
            const cat = categories.find(c => c.id === id);
            if (!cat) return;

            document.getElementById('category-id').value = cat.id;
            document.getElementById('name').value = cat.name;
            document.getElementById('description').value = cat.description;
            document.getElementById('color').value = cat.color;
            document.getElementById('permissions').value = cat.permissions;
            document.getElementById('tags').value = cat.tags || '';
            
            submitBtn.textContent = 'Update Category';
            updateLiveFormPreview();
            window.scrollTo(0, 0); 
        }

        // Function to delete a category
        function deleteCategory(id) {
            // Removed confirm() call to allow execution in sandboxed environment
            categories = categories.filter(c => c.id !== id && c.parentId !== id); 
            renderAll();
        }


        // --- Drag and Drop Logic for Rearrangement and Nesting ---

        let draggedItem = null;

        function removeAllDragHints() {
            document.querySelectorAll('.category-item').forEach(li => {
                li.classList.remove('indent-hint', 'outdent-hint', 'drag-over');
                const cat = categories.find(c => c.id === li.dataset.id);
                if (cat) {
                    li.style.marginLeft = cat.parentId ? '30px' : '0';
                }
            });
        }

        function addDragDropListeners() {
            document.querySelectorAll('.category-item').forEach(item => {
                
                item.addEventListener('dragstart', (e) => {
                    draggedItem = item;
                    e.dataTransfer.effectAllowed = 'move';
                    e.dataTransfer.setData('text/plain', item.dataset.id);
                    dragStartX = e.clientX; // Capture starting X position
                    setTimeout(() => item.classList.add('dragging'), 0);
                });

                item.addEventListener('dragend', () => {
                    draggedItem.classList.remove('dragging');
                    draggedItem = null;
                    removeAllDragHints(); 
                });

                item.addEventListener('dragleave', (e) => {
                    e.currentTarget.classList.remove('indent-hint', 'outdent-hint', 'drag-over');
                });

                item.addEventListener('dragover', (e) => {
                    e.preventDefault(); 
                    e.dataTransfer.dropEffect = 'move';
                    
                    const dropTargetElement = e.currentTarget;
                    const dropTargetId = dropTargetElement.dataset.id;
                    const dropTargetCat = categories.find(c => c.id === dropTargetId);
                    const draggedCat = categories.find(c => c.id === draggedItem.dataset.id);
                    
                    if (draggedItem === dropTargetElement) return;

                    removeAllDragHints();
                    dropTargetElement.classList.add('drag-over');

                    const INDENT_THRESHOLD_PX = 40; 
                    const OUTDENT_SHIFT_PX = 50; 
                    const currentX = e.clientX;
                    const horizontalShift = currentX - dragStartX; 
                    
                    // --- LOGIC FOR HINT APPLICATION ---

                    // 1. Check for INDENT (Drag Right)
                    if (horizontalShift > INDENT_THRESHOLD_PX) {
                        // Check if the target can accept children (i.e., is not already a child)
                        if (!dropTargetCat.parentId) {
                            dropTargetElement.classList.add('indent-hint');
                            dropTargetElement.classList.remove('drag-over');
                        }
                    } 
                    
                    // 2. Check for OUTDENT (Drag Left)
                    else if (draggedCat.parentId && horizontalShift < -OUTDENT_SHIFT_PX) {
                        dropTargetElement.classList.add('outdent-hint');
                        dropTargetElement.classList.remove('drag-over');
                    }
                    
                    // 3. Middle Ground: No hint means only vertical reordering occurs on drop.
                });

                item.addEventListener('drop', (e) => {
                    e.preventDefault();
                    if (draggedItem !== e.currentTarget) {
                        
                        const dropTargetElement = e.currentTarget;
                        const dropTargetId = dropTargetElement.dataset.id;
                        const draggedId = draggedItem.dataset.id;
                        
                        let draggedIndex = categories.findIndex(c => c.id === draggedId);
                        let dropIndex = categories.findIndex(c => c.id === dropTargetId);

                        // 1. Re-sequence the categories array (Linear Order)
                        if (draggedIndex !== -1 && dropIndex !== -1) {
                            const [temp] = categories.splice(draggedIndex, 1);
                            categories.splice(dropIndex, 0, temp);
                        }

                        // Re-find indices after resequencing
                        const newDraggedIndex = categories.findIndex(c => c.id === draggedId);
                        const draggedCat = categories[newDraggedIndex];
                        
                        // 2. Determine New Parent ID based on the active hint
                        
                        let newParentId = draggedCat.parentId; // Default: Keep current hierarchy status
                        let updateDescendants = false;

                        if (dropTargetElement.classList.contains('indent-hint')) {
                            // Promoting to child: The dragged category's new parent is the drop target.
                            newParentId = dropTargetId;
                            updateDescendants = true; 
                        } else if (dropTargetElement.classList.contains('outdent-hint')) {
                            // Promoting to parent: The dragged category's new parent is null/empty.
                            newParentId = '';
                            updateDescendants = true; 
                        }
                        
                        draggedCat.parentId = newParentId;

                        // 3. Update Descendants if hierarchy changed for the dragged parent
                        if (updateDescendants) {
                            const descendantIds = getDescendantIds(draggedId);
                            descendantIds.forEach(descendantId => {
                                const descendantCat = categories.find(c => c.id === descendantId);
                                if (descendantCat) {
                                    // Descendants follow the dragged parent's new status
                                    descendantCat.parentId = newParentId;
                                }
                            });
                        }

                        removeAllDragHints(); 
                        renderAll(); 
                    }
                });
            });
        }

        // --- Initial Load (Updated Categories) ---

        categories.push({
            id: 'cat_001', name: 'General', description: 'General community discussion and help.', color: '#007bff', permissions: 'Public', parentId: '', tags: 'tag 1, tag 2'
        });
        categories.push({
            id: 'cat_004', name: 'Subcategory Example', description: '', color: '#007bff', permissions: 'Public', parentId: 'cat_001', tags: 'tag 3'
        });
        categories.push({
            id: 'cat_002', name: 'Staff', description: 'Private area for moderation and administration.', color: '#ffc107', permissions: 'Staff Only', parentId: '', tags: ''
        });
        categories.push({
            id: 'cat_003', name: 'Site Feedback', description: 'Discuss the forum itself and suggest improvements.', color: '#28a745', permissions: 'Public', parentId: '', tags: 'site, feedback'
        });

        renderAll();
        updateLiveFormPreview();
    </script>
</body>
</html>
إعجاب واحد (1)