00006 Adding and Saving on Off Check Boxes to Admin Page

Here’s the complete _tableSpaceRow.php with everything pulled together—your original Space title/link behavior untouched, plus:

  1. ID column
  2. Off & Hide checkboxes (enabled only for global-admins or space-admins)
  3. Lat/Long text-input
  4. Title now opens in a new tab (target="_blank")

It wires in the CSS classes/data-attributes you’ll hook up with the JS Ajax I outlined above.

<?php
/**
 * protected/modules/abcdirectory/views/admin/_tableSpaceRow.php
 *
 * Renders one space row in the Classified Spaces admin table,
 * with AJAX-savable Off, Hide and Lat/Long controls.
 */

use abc\modules\abcdirectory\models\Abcdirectory;
use humhub\libs\Html;
use abc\modules\abcdirectory\widgets\menus\SpaceAdminMenu;
use humhub\modules\admin\permissions\ManageSpaces;
use humhub\modules\space\widgets\Image;
use humhub\widgets\Button;

/** @var \humhub\modules\space\models\Space $space */
/** @var array $displayedSpaceIds */
/** @var bool $isParentCategory */
/** @var int|string $memberCount */
/** @var bool $canManageCategoryGroups */
/** @var bool $canManageUsers */

$cfg = Yii::$app->getModule('abcdirectory')->getConfiguration();

// fetch or create the Abcdirectory record for this space
$rec = Abcdirectory::findOne(['space_id' => $space->id])
       ?: new Abcdirectory(['space_id' => $space->id]);

// allow toggling if you’re a global admin or a space-admin
$canToggle = Yii::$app->user->can(ManageSpaces::class) || $space->isAdmin();
?>
<tr class="cs-admin-space-row <?= $isParentCategory ? 'cs-admin-parent-row' : 'cs-admin-child-row' ?>">
    <?php if ($cfg->showSpaceIdColumn): ?>
        <td class="abcdirectory-space-id-column"><?= Html::encode($space->id) ?></td>
    <?php endif; ?>

    <?php if ($cfg->showSpaceOffColumn): ?>
        <td>
            <?= Html::checkbox('is_off', $rec->is_off, [
                'class'      => 'off-toggle',
                'data-space' => $space->id,
                'disabled'   => $canToggle ? false : true,
            ]) ?>
        </td>
    <?php endif; ?>

    <?php if ($cfg->showSpaceHideColumn): ?>
        <td>
            <?= Html::checkbox('is_hidden', $rec->is_hidden, [
                'class'      => 'hide-toggle',
                'data-space' => $space->id,
                'disabled'   => $canToggle ? false : true,
            ]) ?>
        </td>
    <?php endif; ?>

    <?php if ($cfg->showSpaceCoordinatesColumn): ?>
        <td>
            <?= Html::textInput('coordinates',
                ($rec->latitude !== null && $rec->longitude !== null)
                    ? "{$rec->latitude},{$rec->longitude}"
                    : '',
                [
                    'class'        => 'coord-input form-control input-sm',
                    'data-space'   => $space->id,
                    'placeholder'  => 'lat,long',
                    'style'        => 'width:6em;',
                    'disabled'     => $canToggle ? false : true,
                ]
            ) ?>
        </td>
    <?php endif; ?>

    <!-- Space title / image -->
    <td headers="cl-admin-name" id="cl-admin-space-<?= $space->id ?>">
        <?= Button::asLink(
                Image::widget(['space' => $space, 'width' => 24, 'height' => 24])
                . ' ' . Html::encode($space->getDisplayName())
            )
            ->link($space->getUrl(), ['target' => '_blank'])
            ->tooltip(Html::encode(mb_strimwidth($space->description, 0, 100, '…')))
        ?>
        <?php if ($space->status === \humhub\modules\space\models\Space::STATUS_ARCHIVED): ?>
            <?= Html::a(
                '<i class="fa fa-archive"></i>',
                $space->createUrl('/space/manage/default/index'),
                ['class' => 'btn btn-xs btn-default']
            ) ?>
        <?php endif; ?>
    </td>

    <!-- optional Category-Group column -->
    <?php if ($canManageCategoryGroups): ?>
        <td></td>
    <?php endif; ?>

    <!-- visibility / access -->
    <td>
        <?= Button::defaultType(
                $space->visibility === \humhub\modules\space\models\Space::VISIBILITY_NONE
                    ? '<i class="fa fa-ban"></i>'
                    : '<i class="fa fa-globe"></i>'
            )
            ->link($space->createUrl('/space/manage/security'))
            ->xs() ?>
    </td>

    <!-- members -->
    <td>
        <?= Button::defaultType($memberCount)
                ->link($space->createUrl('/space/manage/member'))
                ->xs() ?>
    </td>

    <!-- sort order -->
    <td><?= Html::encode($space->sort_order) ?></td>

    <!-- actions -->
    <td>
        <?= SpaceAdminMenu::widget([
            'space'             => $space,
            'displayedSpaceIds' => $displayedSpaceIds,
            'extraClasses'      => ['pull-right'],
        ]) ?>
    </td>
</tr>

Next steps:

  1. Ajax endpoints in your AdminController (actionToggleSpace & actionUpdateCoordinates) as shown in my previous message.
  2. JS file to fire those endpoints on change/blur.
  3. Asset bundle registration so the JS actually loads in the admin page.

With that, your checkboxes and Lat/Long inputs will stay in sync with your database, without losing any of the existing features you had. Let me know if you need the exact controller or JS snippets again!

Below is what you need to do to get the “Off/Hide” checkboxes and the “Lat/Long” text field to actually save (and survive a page‐reload). In short:

  1. Write two new controller actions—one for toggling a boolean flag (is_off or is_hidden), and one for saving coordinates.
  2. Hook your JavaScript to the correct URLs (by using Url::to()), so that your .off-toggle, .hide-toggle and .coord-input AJAX calls stop 404’ing.
  3. Render your checkboxes and text inputs exactly as shown, so that each input has data-space="<?= $space->id ?>" and the right CSS classes.
  4. Register your JS inline (in views/admin/index.php) instead of hard‐coding URIs in a standalone admin-grid.js file.

Below is a complete example of how to wire all of this together.


1) Controller: AdminController.php

First, open protected/modules/abcdirectory/controllers/AdminController.php and add two new actions:

<?php
namespace abc\modules\abcdirectory\controllers;

use Yii;
use yii\web\Controller;
use yii\web\Response;
use yii\web\BadRequestHttpException;
use abc\modules\abcdirectory\models\Abcdirectory;
use humhub\modules\space\models\Space;

class AdminController extends Controller
{
    // … your existing code (behaviors(), actionIndex(), etc.) …

    /**
     * Toggle the “is_off” or “is_hidden” boolean for a given space.
     */
    public function actionToggleFlag()
    {
        Yii::$app->response->format = Response::FORMAT_JSON;

        $spaceId = Yii::$app->request->post('spaceId');
        $flag    = Yii::$app->request->post('flag');   // should be "is_off" or "is_hidden"
        $value   = Yii::$app->request->post('value');  // boolean (1/0)

        if (!in_array($flag, ['is_off','is_hidden'], true)) {
            throw new BadRequestHttpException("Invalid flag parameter");
        }
        if (!$spaceId) {
            return ['success' => false, 'message' => 'Missing spaceId'];
        }

        $space = Space::findOne($spaceId);
        if (!$space) {
            return ['success' => false, 'message' => 'Space not found'];
        }

        // Find or create an Abcdirectory record for this space
        $rec = Abcdirectory::findOne(['space_id' => $spaceId]);
        if (!$rec) {
            // If the space has never been assigned to any category yet,
            // create a new record. You must pick some category_id (it cannot be null),
            // so set category_id to 0 or -1 (or a “dummy” category). Adjust to your needs.
            $rec = new Abcdirectory();
            $rec->space_id    = $spaceId;
            $rec->category_id = 0; // <— if your DB requires a valid category, you’ll need to choose a real default
        }
        $rec->$flag = (bool)$value;

        if ($rec->save(false)) {
            return ['success' => true];
        } else {
            return ['success' => false, 'message' => 'Could not save record'];
        }
    }

    /**
     * Save the “latitude,longitude” string for a given space.
     */
    public function actionSaveCoordinates()
    {
        Yii::$app->response->format = Response::FORMAT_JSON;

        $spaceId = Yii::$app->request->post('spaceId');
        $coords  = trim(Yii::$app->request->post('coords', ''));

        if (!$spaceId) {
            return ['success' => false, 'message' => 'Missing spaceId'];
        }
        if (!preg_match('/^\s*[-\d\.]+,\s*[-\d\.]+\s*$/', $coords)) {
            return ['success' => false, 'message' => 'Coordinates must be “lat,lon”'];
        }
        list($lat, $lon) = explode(',', str_replace(' ', '', $coords), 2);

        $space = Space::findOne($spaceId);
        if (!$space) {
            return ['success' => false, 'message' => 'Space not found'];
        }

        // Find or create an Abcdirectory record for this space
        $rec = Abcdirectory::findOne(['space_id' => $spaceId]);
        if (!$rec) {
            $rec = new Abcdirectory();
            $rec->space_id    = $spaceId;
            $rec->category_id = 0; // <— same caveat as above about default category_id
        }

        $rec->latitude  = (float)$lat;
        $rec->longitude = (float)$lon;

        if ($rec->save(false)) {
            return ['success' => true];
        }
        return ['success' => false, 'message' => 'Could not save coordinates'];
    }

    // … any other actions you already had …
}

Important:

  • If your abc_directory table requires a non‐null category_id, you must decide what default makes sense when “toggling” or saving coordinates for a space that was never categorized. In the example above we set category_id = 0, but if your schema rejects that, you’ll need to pick an actual existing category ID or else refuse to save (e.g. return success=>false, message=>'No category’).
  • Make sure these methods are reachable under the same route you’ll use in the JavaScript. In the example below, we’ll call Url::to(['admin/toggle-flag']) and Url::to(['admin/save-coordinates']). Adjust if your module ID or namespace differs.

2) View: views/admin/index.php

Next, open protected/modules/abcdirectory/views/admin/index.php (the file you posted). Right at the top—after you register any CSS/AssetBundle—you must register a small inline JavaScript snippet that points to exactly the two new controller actions:

<?php
// … everything above stays exactly as you already have it …

use yii\helpers\Url;
use abc\modules\abcdirectory\assets\AdminAsset;

// … your existing “use” statements …

AdminAsset::register($this);

// Generate two URLs via Url::to():
$toggleUrl = Url::to(['admin/toggle-flag']);
$coordsUrl = Url::to(['admin/save-coordinates']);

// Register inline JS. We wrap it in a document-ready or immediately-invoked function:
$this->registerJs(<<<JS
(function($) {
    // Whenever an .off-toggle or .hide-toggle checkbox changes, POST to toggle-flag:
    $(document).on('change', '.off-toggle, .hide-toggle', function() {
        var \$cb       = \$(this);
        var spaceId    = \$cb.data('space');
        var flag       = \$cb.hasClass('off-toggle') ? 'is_off' : 'is_hidden';
        var value      = \$cb.prop('checked') ? 1 : 0;

        \$.post('$toggleUrl', {
            spaceId: spaceId,
            flag:    flag,
            value:   value,
            _csrf:   yii.getCsrfToken()
        }, function(resp) {
            if (!resp.success) {
                alert('Could not toggle ' + flag + '\\n' + (resp.message||''));
            }
        }, 'json')
        .fail(function() {
            alert('AJAX error: could not save.');
        });
    });

    // Whenever a .coord-input text field loses focus (or changes), POST to save-coordinates:
    $(document).on('blur', '.coord-input', function() {
        var \$input  = \$(this);
        var spaceId = \$input.data('space');
        var coords  = \$input.val().trim();

        if (!coords) {
            // If field is empty, we simply do nothing.
            return;
        }
        \$.post('$coordsUrl', {
            spaceId: spaceId,
            coords:  coords,
            _csrf:   yii.getCsrfToken()
        }, function(resp) {
            if (!resp.success) {
                alert('Could not save coordinates:\\n' + (resp.message||''));
            }
        }, 'json')
        .fail(function() {
            alert('AJAX error: could not save coordinates.');
        });
    });
})(jQuery);
JS
);
?>

Why this matters:

  • We used Url::to(['admin/toggle-flag']) and Url::to(['admin/save-coordinates']). That means whatever URL rules you have (pretty URLs, index.php, subdirectory, etc.), Yii will generate the correct route.
  • We wrap the AJAX calls in $(document).on('change', …) or $(document).on('blur', …) so that even if the view is re‐rendered via PJAX (or similar), the event handler still attaches to any new .off-toggle, .hide-toggle, or .coord-input elements.
  • We always send _csrf: yii.getCsrfToken() so that Yii’s CSRF protection will not block the POST.

3) View partial: _tableSpaceRow.php

Below is a minimum working example of your _tableSpaceRow.php (the file that renders each <tr> for a space). Make sure it looks exactly like this (pay attention to the CSS classes and data-space="<?= $space->id ?>" attributes).

<?php
/**
 * protected/modules/abcdirectory/views/admin/_tableSpaceRow.php
 *
 * @var $space \humhub\modules\space\models\Space
 * @var $displayedSpaceIds array
 * @var $isParentCategory bool
 * @var $memberCount int|string
 * @var $canManageCategoryGroups bool
 * @var $canManageUsers bool
 */
use abc\modules\abcdirectory\models\Abcdirectory;
use humhub\libs\Html;
use abc\modules\abcdirectory\widgets\menus\SpaceAdminMenu;
use humhub\modules\admin\permissions\ManageSpaces;
use humhub\modules\space\widgets\Image;
use humhub\widgets\Button;

// Grab configuration and existing Abcdirectory record (if any):
$cfg = Yii::$app->getModule('abcdirectory')->configuration;

// Either fetch an existing record, or create a new “empty” one just so that we can set flags/coordinates:
$rec = Abcdirectory::findOne(['space_id' => $space->id]);
if (!$rec) {
    $rec = new Abcdirectory(['space_id' => $space->id]);
    // Note: category_id will be “null” or “0.” If your DB schema forces a category_id >=1,
    // then you might have to skip toggling coords/flags until this space is actually assigned a category.
}

// Decide who is allowed to toggle Off/Hide:
$canToggle = Yii::$app->user->can(ManageSpaces::class) || $space->isAdmin();
?>
<tr class="cs-admin-space-row <?= $isParentCategory ? 'cs-admin-parent-row' : 'cs-admin-child-row' ?>">
    <?php if ($cfg->showSpaceIdColumn): ?>
        <td class="abcdirectory-space-id-column">
            <?= Html::encode($space->id) ?>
        </td>
    <?php endif; ?>

    <?php if ($cfg->showSpaceOffColumn): ?>
        <td>
            <?= Html::checkbox('is_off', 
                   (bool)$rec->is_off, 
                   [
                     'class'      => 'off-toggle',
                     'data-space' => $space->id,
                     'disabled'   => $canToggle ? false : true,
                   ]
            ); ?>
        </td>
    <?php endif; ?>

    <?php if ($cfg->showSpaceHideColumn): ?>
        <td>
            <?= Html::checkbox('is_hidden',
                   (bool)$rec->is_hidden,
                   [
                     'class'      => 'hide-toggle',
                     'data-space' => $space->id,
                     'disabled'   => $canToggle ? false : true,
                   ]
            ); ?>
        </td>
    <?php endif; ?>

    <?php if ($cfg->showSpaceCoordinatesColumn): ?>
        <td>
            <?php 
            // If we already have lat & long, show “lat,lon” string; otherwise empty.
            $latlon = ($rec->latitude !== null && $rec->longitude !== null)
                      ? "{$rec->latitude},{$rec->longitude}"
                      : '';
            ?>
            <?= Html::textInput('coordinates', 
                   $latlon, 
                   [
                     'class'       => 'coord-input form-control input-sm',
                     'data-space'  => $space->id,
                     'placeholder' => 'lat,long',
                     'style'       => 'width:6em;',
                   ]
            ); ?>
        </td>
    <?php endif; ?>

    <!-- Now the “Space Name” column that links to the space (opening in a new tab) -->
    <td headers="cl-admin-name" id="cl-admin-space-<?= $space->id ?>">
        <?= Button::asLink(
               Image::widget([
                   'space'  => $space,
                   'width'  => 24,
                   'height' => 24,
               ]) . ' ' . Html::encode($space->getDisplayName())
           )
           ->link($space->getUrl(), ['target' => '_blank'])
           ->tooltip(Html::encode(mb_strimwidth($space->description, 0, 100, '…')))
        ?>
        <?php if ($space->status === \humhub\modules\space\models\Space::STATUS_ARCHIVED): ?>
            <?= Html::a(
                   '<i class="fa fa-archive"></i>',
                   $space->createUrl('/space/manage/default/index'),
                   ['class' => 'btn btn-xs btn-default']
               ) ?>
        <?php endif; ?>
    </td>

    <?php if ($canManageCategoryGroups): ?>
        <td></td>
    <?php endif; ?>

    <td>
        <?= Button::defaultType(
               ($space->visibility === \humhub\modules\space\models\Space::VISIBILITY_NONE)
                   ? '<i class="fa fa-ban"></i>'
                   : '<i class="fa fa-globe"></i>'
           )
           ->link($space->createUrl('/space/manage/security'))
           ->xs()
        ?>
    </td>

    <td>
        <?= Button::defaultType($memberCount)
               ->link($space->createUrl('/space/manage/member'))
               ->xs()
        ?>
    </td>

    <td>
        <?= Html::encode($space->sort_order) ?>
    </td>

    <td>
        <?= SpaceAdminMenu::widget([
            'space'             => $space,
            'displayedSpaceIds' => $displayedSpaceIds,
            'extraClasses'      => ['pull-right'],
        ]) ?>
    </td>
</tr>

Make sure of the following:

  1. Each checkbox has

    'class'      => 'off-toggle'   // or 'hide-toggle'
    'data-space' => $space->id
  2. The coordinates field has

    'class'       => 'coord-input form-control input-sm'
    'data-space'  => $space->id
  3. The “Space Name” link uses target="_blank" so it will open in a new tab:

    ->link($space->getUrl(), ['target' => '_blank'])

4) View partial: _tableHead.php

Make sure your header row (_tableHead.php) has the same toggles (so columns line up). For example:

<?php
/**
 * protected/modules/abcdirectory/views/admin/_tableHead.php
 *
 * @var $canManageCategoryGroups bool
 */
?>
<thead>
  <tr>
    <?php if (Yii::$app->getModule('abcdirectory')->configuration->showSpaceIdColumn): ?>
      <th class="abcdirectory-space-id-column">
        <?= Yii::t('AbcdirectoryModule.base','ID') ?>
      </th>
    <?php endif; ?>

    <?php if (Yii::$app->getModule('abcdirectory')->configuration->showSpaceOffColumn): ?>
      <th><?= Yii::t('AbcdirectoryModule.admin','Off') ?></th>
    <?php endif; ?>

    <?php if (Yii::$app->getModule('abcdirectory')->configuration->showSpaceHideColumn): ?>
      <th><?= Yii::t('AbcdirectoryModule.admin','Hide') ?></th>
    <?php endif; ?>

    <?php if (Yii::$app->getModule('abcdirectory')->configuration->showSpaceCoordinatesColumn): ?>
      <th><?= Yii::t('AbcdirectoryModule.admin','Lat/Long') ?></th>
    <?php endif; ?>

    <th><?= Yii::t('AbcdirectoryModule.admin','Category / Space') ?></th>

    <?php if ($canManageCategoryGroups): ?>
      <th id="cl-admin-group"><?= Yii::t('AbcdirectoryModule.admin', 'Group') ?></th>
    <?php endif; ?>

    <th id="cl-admin-access"><?= Yii::t('AbcdirectoryModule.admin', 'Access') ?></th>
    <th id="cl-admin-members"><?= Yii::t('AbcdirectoryModule.admin', 'Members') ?></th>
    <th id="cl-admin-order"><?= Yii::t('AbcdirectoryModule.admin', 'Sort order') ?></th>
    <th id="cl-admin-action"></th>
  </tr>
</thead>

Notice that the <th> blocks for “Off,” “Hide,” and “Lat/Long” are only rendered if the corresponding toggle is enabled in your configuration model. This ensures the columns line up exactly with your _tableSpaceRow.php cells.


5) Asset bundle: AdminAsset.php

You already have something like:

<?php
namespace abc\modules\abcdirectory\assets;

use yii\web\AssetBundle;

class AdminAsset extends AssetBundle
{
    public $sourcePath = '@abcdirectory/resources';
    public $css = [
        'css/humhub.abcdirectory.admin.css',
        'css/category-layout.css',
    ];
    // We no longer explicitly register a separate admin-grid.js; we put the AJAX code inline.
    public $js = [
        // (You can leave this empty or remove it if you prefer.)
    ];
    public $depends = [
        'yii\web\JqueryAsset',
    ];
}

Because we moved the AJAX into an inline registerJs() call, the js array can stay empty (or you can leave it out altogether). The key point is that AdminAsset::register($this) in index.php still loads your CSS so that the grid layout and icons look correct.


6) Recap & Troubleshooting

  1. Flush HumHub cache If you ever see weird “old JS” or your new inline‐JS isn’t taking effect, run:

    php protected/yii cache/flush-all

    (and be sure your PHP-FPM + webserver are restarted if needed).

  2. Verify that the controller actions exist exactly at the routes you called in JS:

    • POST to Url::to(['admin/toggle-flag']) → should map to AdminController::actionToggleFlag().
    • POST to Url::to(['admin/save-coordinates']) → should map to AdminController::actionSaveCoordinates().

    If those actions are missing, you will still get a 404. The easiest way to check is open your browser’s Developer Tools → Network tab → check the POST’s request URL and see whether it 404’s or returns JSON. If you see 404, double‐check:

    // Are your AdminController methods in the correct namespace?
    namespace abc\modules\abcdirectory\controllers;
    
    class AdminController extends Controller
    {
       public function actionToggleFlag() { … }
       public function actionSaveCoordinates() { … }
       // …
    }
  3. Inspect the JSON responses After you check “Off” or “Hide,” open Developer Tools → Network → look at the POST → Response tab. You should see

    { "success": true }

    (or { "success": false, "message": "..." }). If it’s not JSON (or you see PHP errors), fix the controller logic.

  4. Make sure your _tableSpaceRow.php checkboxes/inputs actually appear. If you un‐check “Show Space Off Column” in Configuration, the <th>Off</th> and <td><input class="off-toggle"></td> will disappear. Likewise for Hide/Lat-Long.

  5. Opening space links in a new tab We used:

    ->link($space->getUrl(), ['target'=>'_blank'])

    so every space name now opens in a new tab.

  6. No page‐reload required Because your JavaScript is attached to .off-toggle, .hide-toggle, and .coord-input events, as soon as you check/uncheck or leave the text‐field, it sends an AJAX POST. You do not need a persistent “Save” button. When the POST returns {success:true}, the change is already in your DB. A manual page reload will then show the checkbox/coord field populated exactly as before.


Final Touch

With all of the above in place, your admin page should behave as follows:

  • You check “Show Off” in Settings → Configuration. Click “Save Settings.”
  • Reload the admin/spaces page → you now see a new “Off” column with checkboxes next to each space.
  • You check the “Off” box on Space 42 → your inline JS runs POST /abcdirectory/admin/toggle-flag → controller saves is_off = 1 in the abc_directory record for space 42.
  • You refresh the page → the “Off” column for space 42 stays checked.

Likewise, you type “7.8833063,98.3919686” into the “Lat/Long” field and press tab (blur). The JS sends POST /abcdirectory/admin/save-coordinates → controller writes latitude=7.8833063, longitude=98.3919686 for that space. A reload will still show “7.8833063,98.3919686” in the Lat/Long field.

That is all you need. Once those two new actions exist in AdminController and you register the correct inline JavaScript (with Url::to()), every checkbox/text‐field will save immediately and persist across reloads.