diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a749ee7..e98f57b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,6 +56,7 @@ jobs: --exclude 'build' \ --exclude '*.zip' \ --exclude 'tests' \ + --exclude 'bin' \ --exclude 'phpunit.xml' \ --exclude '.phpunit.cache' (cd build && zip -rq datalumo.zip datalumo) diff --git a/bin/ensure-index-php b/bin/ensure-index-php new file mode 100755 index 0000000..cfd748b --- /dev/null +++ b/bin/ensure-index-php @@ -0,0 +1,49 @@ +#!/usr/bin/env php +isDir()) { + return true; + } + + $name = $current->getFilename(); + + if (in_array($name, $skip, true)) { + return false; + } + + return ! str_contains($current->getPathname(), DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.'bin'); + }, + ), + RecursiveIteratorIterator::SELF_FIRST, +); + +$dirs = [$root]; + +foreach ($iterator as $file) { + if ($file->isDir()) { + $dirs[] = $file->getPathname(); + } +} + +foreach ($dirs as $dir) { + $index = $dir.DIRECTORY_SEPARATOR.'index.php'; + + if (is_file($index)) { + continue; + } + + file_put_contents($index, $contents); +} diff --git a/bin/fresh-site b/bin/fresh-site new file mode 100755 index 0000000..9d0dc1d --- /dev/null +++ b/bin/fresh-site @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# Spin up a clean WordPress + WooCommerce site on SQLite, with this plugin +# linked in, dummy posts/products, and admin / password. +# +# bin/fresh-site +# bin/fresh-site --force +# SITE_DIR=~/Sites/datalumo-fresh bin/fresh-site +set -euo pipefail + +PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)" +SITE_DIR="${SITE_DIR:-$HOME/Sites/datalumo-fresh}" +SITE_SLUG="$(basename "$SITE_DIR")" +SITE_URL="${SITE_URL:-http://${SITE_SLUG}.test}" +FORCE=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --force) FORCE=1; shift ;; + --url) SITE_URL="$2"; shift 2 ;; + --dir) SITE_DIR="$2"; SITE_SLUG="$(basename "$SITE_DIR")"; shift 2 ;; + -h|--help) + echo "Usage: bin/fresh-site [--force] [--dir PATH] [--url URL]" + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac +done + +WP=(wp --path="$SITE_DIR" --allow-root) +export WP_CLI_PHP_ARGS="${WP_CLI_PHP_ARGS:--d memory_limit=512M}" + +need() { + command -v "$1" >/dev/null 2>&1 || { + echo "Missing $1." >&2 + exit 1 + } +} + +need wp +need php +need curl +need unzip + +if ! php -r 'exit(extension_loaded("pdo_sqlite") ? 0 : 1);'; then + echo "PHP is missing pdo_sqlite." >&2 + exit 1 +fi + +case "$SITE_DIR" in + /|"$HOME"|"$HOME/"|/) + echo "Refusing to use $SITE_DIR as the site path." >&2 + exit 1 + ;; +esac + +if [[ -e "$SITE_DIR" ]]; then + if [[ "$FORCE" -ne 1 ]]; then + echo "$SITE_DIR already exists. Re-run with --force to wipe it." >&2 + exit 1 + fi + echo "Wiping $SITE_DIR" + rm -rf "$SITE_DIR" +fi + +echo "Installing WordPress in $SITE_DIR" +mkdir -p "$SITE_DIR" +wp core download --path="$SITE_DIR" + +wp config create \ + --path="$SITE_DIR" \ + --dbname=wordpress \ + --dbuser=wordpress \ + --dbpass=wordpress \ + --dbhost=127.0.0.1 \ + --skip-check \ + --extra-php <<'PHP' +define( 'DB_ENGINE', 'sqlite' ); +define( 'WP_ENVIRONMENT_TYPE', 'local' ); +define( 'WP_DEBUG', true ); +define( 'WP_DEBUG_DISPLAY', false ); +define( 'WP_DEBUG_LOG', true ); +define( 'DATALUMO_SSL_VERIFY', false ); +define( 'DATALUMO_API_URL', 'https://dl.test' ); +PHP + +echo "Adding SQLite drop-in" +SQLITE_ZIP="$(mktemp -t sqlite-wp).zip" +curl -fsSL "https://downloads.wordpress.org/plugin/sqlite-database-integration.latest-stable.zip" -o "$SQLITE_ZIP" +unzip -qo "$SQLITE_ZIP" -d "$SITE_DIR/wp-content/plugins" +rm -f "$SQLITE_ZIP" +cp "$SITE_DIR/wp-content/plugins/sqlite-database-integration/db.copy" "$SITE_DIR/wp-content/db.php" + +"${WP[@]}" core install \ + --url="$SITE_URL" \ + --title="Datalumo Fresh" \ + --admin_user=admin \ + --admin_password=password \ + --admin_email=admin@example.com \ + --skip-email + +"${WP[@]}" plugin activate sqlite-database-integration +"${WP[@]}" theme install twentytwentyfive --activate + +echo "Linking Datalumo plugin" +if [[ ! -d "$PLUGIN_DIR/vendor" ]]; then + composer install --working-dir="$PLUGIN_DIR" --no-interaction +fi +ln -sfn "$PLUGIN_DIR" "$SITE_DIR/wp-content/plugins/datalumo" +"${WP[@]}" plugin activate datalumo + +echo "Installing WooCommerce" +"${WP[@]}" plugin install woocommerce --activate +"${WP[@]}" eval 'if (class_exists("WC_Install")) { WC_Install::create_pages(); }' +"${WP[@]}" option update woocommerce_currency EUR +"${WP[@]}" option update woocommerce_default_country NL +"${WP[@]}" option update woocommerce_coming_soon no || true +"${WP[@]}" option update woocommerce_store_pages_only no || true +"${WP[@]}" option update woocommerce_onboarding_profile '{"skipped":true,"completed":true}' --format=json || true +"${WP[@]}" option update woocommerce_task_list_hidden yes || true +"${WP[@]}" option update woocommerce_task_list_complete yes || true + +"${WP[@]}" rewrite structure '/%postname%/' --hard +"${WP[@]}" rewrite flush --hard + +echo "Seeding posts and products" +"${WP[@]}" eval-file "$PLUGIN_DIR/bin/fresh-site-seed.php" + +echo +echo "Ready." +echo " Site $SITE_URL" +echo " Admin $SITE_URL/wp-admin/" +echo " User admin" +echo " Password password" +echo " Settings $SITE_URL/wp-admin/options-general.php?page=datalumo" +echo " Datalumo https://dl.test (DATALUMO_API_URL)" +echo " Database $SITE_DIR/wp-content/database/.ht.sqlite" diff --git a/bin/fresh-site-seed.php b/bin/fresh-site-seed.php new file mode 100644 index 0000000..fab5f15 --- /dev/null +++ b/bin/fresh-site-seed.php @@ -0,0 +1,185 @@ + 'Visitor guide', + 'post_name' => 'visitor-guide', + 'post_status' => 'publish', + 'post_type' => 'page', + 'post_content' => <<<'HTML' +

This is a clean demo shop for trying the Datalumo WordPress plugin.

+

Sign in at /wp-admin/ with admin / password, then open Settings → Datalumo and press Connect with Datalumo.

+HTML, +], true); + +wp_insert_post([ + 'post_title' => 'About', + 'post_name' => 'about', + 'post_status' => 'publish', + 'post_type' => 'page', + 'post_content' => '

Datalumo Fresh is a disposable WordPress site. Wipe it with bin/fresh-site --force.

', +], true); + +$posts = [ + [ + 'Opening hours and pickup', + 'We are open Tuesday to Saturday, 10:00 to 18:00. Weekend pickup is at the side door on Kerkstraat. Closed on Mondays.', + ], + [ + 'Shipping and returns', + 'Orders placed before 15:00 ship the same day inside the Netherlands. You have 30 days to return unused items. Start a return from your account or reply to the order email.', + ], + [ + 'Care for wool', + 'Hand wash wool in cold water and lay it flat to dry. A wool beanie can go in a mesh bag on a gentle cycle if you skip the dryer.', + ], + [ + 'How we roast coffee', + 'Beans are roasted in small batches every Thursday. Light roasts land on Friday. Dark roasts are bagged the same afternoon.', + ], + [ + 'Gift wrapping', + 'Add a note at checkout and we wrap the order in recycled paper. Gift receipts hide prices. We do not print prices on the packing slip when you ask.', + ], +]; + +foreach ($posts as [$title, $content]) { + wp_insert_post([ + 'post_title' => $title, + 'post_status' => 'publish', + 'post_type' => 'post', + 'post_content' => '

'.$content.'

', + ], true); +} + +if ($guide && ! is_wp_error($guide)) { + update_option('show_on_front', 'page'); + update_option('page_on_front', $guide); +} + +if (! class_exists('WooCommerce')) { + WP_CLI::warning('WooCommerce is not active; skipped products.'); + + return; +} + +$simple = [ + ['Canvas Tote', '29.00', 'A heavy canvas tote that stands on its own. Natural colour, one size.', 'tote'], + ['Ceramic Mug', '14.00', 'A 300ml stoneware mug. Dishwasher safe. Speckled cream glaze.', 'mug'], + ['Wool Beanie', '22.00', 'Merino beanie, unisex. Hand wash or a gentle cycle in a mesh bag.', 'beanie'], + ['Drip Coffee 250g', '11.50', 'Thursday roast. Light and chocolatey. Ground for filter on request.', 'coffee'], +]; + +foreach ($simple as [$name, $price, $description, $sku]) { + $product = new WC_Product_Simple(); + $product->set_name($name); + $product->set_regular_price($price); + $product->set_short_description($description); + $product->set_description($description); + $product->set_sku($sku); + $product->set_manage_stock(true); + $product->set_stock_quantity(25); + $product->set_catalog_visibility('visible'); + $product->set_status('publish'); + $product->save(); +} + +foreach (['color' => 'Color', 'size' => 'Size'] as $slug => $label) { + if (wc_attribute_taxonomy_id_by_name($slug)) { + continue; + } + + wc_create_attribute([ + 'name' => $label, + 'slug' => $slug, + 'type' => 'select', + 'order_by' => 'menu_order', + 'has_archives' => false, + ]); +} + +delete_transient('wc_attribute_taxonomies'); +WC_Cache_Helper::invalidate_cache_group('woocommerce-attributes'); +WC()->attributes = null; + +if (! taxonomy_exists('pa_color') || ! taxonomy_exists('pa_size')) { + foreach (wc_get_attribute_taxonomies() as $taxonomy) { + $name = wc_attribute_taxonomy_name($taxonomy->attribute_name); + register_taxonomy($name, ['product'], []); + } +} + +foreach (['Blue', 'Red'] as $color) { + wp_insert_term($color, 'pa_color'); +} + +foreach (['Small', 'Large'] as $size) { + wp_insert_term($size, 'pa_size'); +} + +$colorAttribute = new WC_Product_Attribute(); +$colorAttribute->set_id(wc_attribute_taxonomy_id_by_name('color')); +$colorAttribute->set_name('pa_color'); +$colorAttribute->set_options(array_values(array_filter([ + get_term_by('name', 'Blue', 'pa_color')->term_id ?? null, + get_term_by('name', 'Red', 'pa_color')->term_id ?? null, +]))); +$colorAttribute->set_visible(true); +$colorAttribute->set_variation(true); + +$sizeAttribute = new WC_Product_Attribute(); +$sizeAttribute->set_id(wc_attribute_taxonomy_id_by_name('size')); +$sizeAttribute->set_name('pa_size'); +$sizeAttribute->set_options(array_values(array_filter([ + get_term_by('name', 'Small', 'pa_size')->term_id ?? null, + get_term_by('name', 'Large', 'pa_size')->term_id ?? null, +]))); +$sizeAttribute->set_visible(true); +$sizeAttribute->set_variation(true); + +$jacket = new WC_Product_Variable(); +$jacket->set_name('Trail Jacket'); +$jacket->set_sku('jacket'); +$jacket->set_short_description('A packable shell. Pick a colour, then a size.'); +$jacket->set_description('Two-way zip, stuffs into its own pocket. Colour and size are variations so chat add-to-cart can walk those steps.'); +$jacket->set_attributes([$colorAttribute, $sizeAttribute]); +$jacket->set_catalog_visibility('visible'); +$jacket->set_status('publish'); +$jacketId = $jacket->save(); + +$prices = [ + 'Blue' => ['Small' => '89.00', 'Large' => '89.00'], + 'Red' => ['Small' => '92.00', 'Large' => '92.00'], +]; + +foreach ($prices as $color => $sizes) { + foreach ($sizes as $size => $price) { + $variation = new WC_Product_Variation(); + $variation->set_parent_id($jacketId); + $variation->set_attributes([ + 'pa_color' => sanitize_title($color), + 'pa_size' => sanitize_title($size), + ]); + $variation->set_regular_price($price); + $variation->set_sku('jacket-'.sanitize_title($color).'-'.sanitize_title($size)); + $variation->set_manage_stock(true); + $variation->set_stock_quantity(8); + $variation->set_status('publish'); + $variation->save(); + } +} + +WC_Product_Variable::sync($jacketId); + +WP_CLI::success('Seeded pages, posts, simple products, and a variable Trail Jacket.'); diff --git a/bin/index.php b/bin/index.php new file mode 100644 index 0000000..6220032 --- /dev/null +++ b/bin/index.php @@ -0,0 +1,2 @@ + form, +.datalumo-settings-main .form-table { + margin-top: 0; +} + +.datalumo-settings-main .form-table tr:first-child th, +.datalumo-settings-main .form-table tr:first-child td { + padding-top: 0; +} + +.datalumo-settings-aside { + display: flex; + flex: 0 1 18rem; + flex-direction: column; + gap: 0.75rem; + width: 18rem; + position: sticky; + top: 2.75rem; +} + +.datalumo-help-card, +.datalumo-docs-card { + margin: 0; + padding: 1rem 1.1rem; + border: 1px solid #dcdcde; + border-radius: 8px; + background: #fff; +} + +.datalumo-help-card h2 { + margin: 0 0 0.65rem; + font-size: 13px; + font-weight: 600; +} + +.datalumo-help-fields { + display: flex; + flex-direction: column; + gap: 0.55rem; +} + +.datalumo-help-fields textarea { + display: block; + box-sizing: border-box; + width: 100%; + max-width: 100%; + min-height: 5.5rem; + margin: 0; + resize: vertical; +} + +.datalumo-settings-aside .datalumo-help-submit.button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + width: 100%; + margin: 0; + padding: 0 12px; + line-height: 1; +} + +.datalumo-help-icon { + display: block; + width: 16px; + height: 16px; + flex-shrink: 0; +} + +.datalumo-docs-card p { + margin: 0; +} + +.datalumo-docs-card p + p { + margin-top: 0.25rem; + color: #646970; +} diff --git a/resources/css/index.php b/resources/css/index.php new file mode 100644 index 0000000..6220032 --- /dev/null +++ b/resources/css/index.php @@ -0,0 +1,2 @@ + 0) { + return true; + } + } + + return Boolean(payload.sku && String(payload.sku).trim()); + } + + function finish(detail, result) { + if (typeof detail.respond === 'function') { + detail.respond(result); + + return; + } + + window.dispatchEvent(new CustomEvent('datalumo:action-result', { + detail: { + answer_id: detail.answer_id, + tool_id: detail.tool_id, + run: detail.run, + ok: result.ok !== false, + message: result.message || '', + }, + })); + } + + function refreshCart(fragments, cartHash) { + if (typeof window.jQuery === 'undefined') { + return; + } + + var $ = window.jQuery; + + $.each(fragments, function (selector, html) { + $(selector).replaceWith(html); + }); + + $(document.body).trigger('added_to_cart', [fragments, cartHash, null]); + $(document.body).trigger('wc_fragment_refresh'); + } +})(); diff --git a/resources/js/admin.js b/resources/js/admin.js index 98b6191..15b12dc 100644 --- a/resources/js/admin.js +++ b/resources/js/admin.js @@ -36,6 +36,24 @@ // --- Connection tab --------------------------------------------------- + var modeButtons = document.querySelectorAll('[data-datalumo-mode]'); + var grantPanel = document.getElementById('datalumo-mode-grant'); + var manualPanel = document.getElementById('datalumo-mode-manual'); + + modeButtons.forEach(function (button) { + button.addEventListener('click', function () { + var mode = button.getAttribute('data-datalumo-mode'); + + if (grantPanel) { + grantPanel.hidden = mode !== 'grant'; + } + + if (manualPanel) { + manualPanel.hidden = mode !== 'manual'; + } + }); + }); + var connectButton = document.getElementById('datalumo-connect'); if (connectButton) { @@ -46,7 +64,6 @@ result.textContent = '…'; var payload = { - organisation_id: document.getElementById('datalumo-organisation-id').value.trim(), token: document.getElementById('datalumo-token').value.trim(), }; @@ -59,20 +76,14 @@ } post('datalumo_connect', payload).then(function (response) { - connectButton.disabled = false; - if (response.success) { - var organisation = response.data.organisation || {}; - result.textContent = sprintf( - config.i18n.connected, - organisation.name || organisation.id || '', - (response.data.sources || []).length - ); - result.classList.add('is-success'); - } else { - result.textContent = config.i18n.connectionFailed + ' ' + ((response.data || {}).message || ''); - result.classList.add('is-error'); + window.location.reload(); + return; } + + connectButton.disabled = false; + result.textContent = config.i18n.connectionFailed + ' ' + ((response.data || {}).message || ''); + result.classList.add('is-error'); }); }); } @@ -183,4 +194,14 @@ document.querySelectorAll('.datalumo-sync-status[data-sync]').forEach(function (status) { pollStatus(status.getAttribute('data-sync')); }); + + var autoSync = new URLSearchParams(window.location.search).get('datalumo_sync'); + + if (autoSync) { + var autoStart = document.querySelector('.datalumo-sync-start[data-sync="' + autoSync + '"]'); + + if (autoStart && ! autoStart.disabled) { + autoStart.click(); + } + } })(); diff --git a/resources/js/host-navigation.js b/resources/js/host-navigation.js new file mode 100644 index 0000000..5fac3ec --- /dev/null +++ b/resources/js/host-navigation.js @@ -0,0 +1,144 @@ +/** + * Handles Datalumo host actions that open a page: cart, checkout, or a + * WordPress post. + */ +(function () { + 'use strict'; + + var config = window.datalumoHostNavigation; + + if (! config) { + return; + } + + var events = config.events || ['view_cart', 'open_checkout', 'open_page']; + + window.addEventListener('datalumo:action', function (event) { + var detail = event.detail || {}; + + if (events.indexOf(detail.name) === -1) { + return; + } + + event.preventDefault(); + + var payload = isPlainObject(detail.payload) ? detail.payload : {}; + + if (detail.name === 'view_cart') { + go(detail, config.cartUrl, (config.i18n && config.i18n.no_cart) || ''); + + return; + } + + if (detail.name === 'open_checkout') { + go(detail, config.checkoutUrl, (config.i18n && config.i18n.no_checkout) || ''); + + return; + } + + resolveThenGo(detail, payload); + }); + + function resolveThenGo(detail, payload) { + var body = new FormData(); + var request = {}; + var key; + + for (key in payload) { + if (Object.prototype.hasOwnProperty.call(payload, key)) { + request[key] = payload[key]; + } + } + + request.event = detail.name; + body.append('action', 'datalumo_host_navigation'); + body.append('_ajax_nonce', config.nonce); + body.append('payload', JSON.stringify(request)); + + fetch(config.ajaxUrl, { + method: 'POST', + credentials: 'same-origin', + body: body, + }) + .then(function (response) { + return response.json(); + }) + .then(function (json) { + var data = (json && json.data) || {}; + var url = typeof data.url === 'string' ? data.url : ''; + + if (json && json.success && isSameSite(url)) { + finish(detail, { ok: true, message: '' }); + window.location.assign(url); + + return; + } + + finish(detail, { + ok: false, + message: (typeof data.message === 'string' && data.message) + || (config.i18n && config.i18n.failed) + || '', + }); + }) + .catch(function () { + finish(detail, { + ok: false, + message: (config.i18n && config.i18n.failed) || '', + }); + }); + } + + function go(detail, url, missingMessage) { + if (! isSameSite(url)) { + finish(detail, { ok: false, message: missingMessage || (config.i18n && config.i18n.failed) || '' }); + + return; + } + + finish(detail, { ok: true, message: '' }); + window.location.assign(url); + } + + function isSameSite(url) { + if (! url || typeof url !== 'string') { + return false; + } + + try { + var parsed = new URL(url, window.location.href); + var home = String(config.homeHost || window.location.hostname).toLowerCase(); + var host = parsed.hostname.toLowerCase(); + + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + return false; + } + + return host === home || host === 'www.' + home || 'www.' + host === home; + } catch (error) { + return false; + } + } + + function isPlainObject(value) { + return Boolean(value) && typeof value === 'object' && ! Array.isArray(value); + } + + function finish(detail, result) { + if (typeof detail.respond === 'function') { + detail.respond(result); + + return; + } + + window.dispatchEvent(new CustomEvent('datalumo:action-result', { + detail: { + answer_id: detail.answer_id, + tool_id: detail.tool_id, + run: detail.run, + ok: result.ok !== false, + message: result.message || '', + }, + })); + } +})(); diff --git a/resources/js/index.php b/resources/js/index.php new file mode 100644 index 0000000..6220032 --- /dev/null +++ b/resources/js/index.php @@ -0,0 +1,2 @@ + true], 'objects'); -unset($postTypes['attachment']); +/** @var string $datalumo_tab (set by SettingsPage::render) */ +$datalumo_organisation = Options::get('organisation', []); +$datalumo_sources = (array) Options::get('sources', []); +$datalumo_syncs = (array) Options::get('syncs', []); +$datalumo_post_types = get_post_types(['public' => true], 'objects'); +unset($datalumo_post_types['attachment']); -$tabs = [ +$datalumo_tabs = [ 'connection' => __('Connection', 'datalumo'), 'content-sync' => __('Content sync', 'datalumo'), 'chatbot' => __('Chatbot', 'datalumo'), 'search-box' => __('Search box', 'datalumo'), 'enhanced-search' => __('Enhanced search', 'datalumo'), ]; + +$datalumo_nonce_ok = isset($_GET['_wpnonce']) + && wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'datalumo_settings_updated'); +$datalumo_notice = $datalumo_nonce_ok && isset($_GET['datalumo_notice']) + ? sanitize_key(wp_unslash($_GET['datalumo_notice'])) + : ''; +$datalumo_notices = [ + 'connected' => [__('Connected to Datalumo.', 'datalumo'), 'success'], + 'grant_failed' => [__('Connect did not finish. Start again from this page.', 'datalumo'), 'error'], + 'disconnected' => [__('Disconnected. The API key remains in Datalumo under API keys until you revoke it there.', 'datalumo'), 'success'], + 'setup_saved' => [__('Setup saved.', 'datalumo'), 'success'], + 'invalid_widget_key' => [__('Widget keys look like org-id/widget-id. An API token or secret will not work there.', 'datalumo'), 'error'], +]; +$datalumo_connected = Options::isConnected(); +$datalumo_setup_pending = $datalumo_connected && (bool) Options::get('setup_pending'); +$datalumo_chat_key = (string) Options::get('chatbot.widget_key', ''); +$datalumo_search_key = (string) Options::get('search_box.widget_key', '') ?: (string) Options::get('enhanced.widget_key', ''); +$datalumo_setup_source_id = (string) Options::get('setup_source_id', ''); +$datalumo_setup_types = ['post', 'page']; + +foreach ($datalumo_syncs as $datalumo_sync_row) { + if (($datalumo_sync_row['source_id'] ?? '') === $datalumo_setup_source_id) { + $datalumo_setup_types = $datalumo_sync_row['post_types'] ?? $datalumo_setup_types; + break; + } +} ?>

Datalumo

- -

+ +
+

+
- + + + -
- - - + 'for-developers', + 'chatbot' => 'chat-page-actions', + 'search-box' => 'add-a-search-box', + 'enhanced-search' => 'enhanced-search', + default => '', + }; + $datalumo_docs_hint = match ($datalumo_tab) { + 'content-sync' => __('Custom fields, filters, and sync details.', 'datalumo'), + 'chatbot' => __('Shortcodes, visitor identity, and chat page actions.', 'datalumo'), + 'search-box', 'enhanced-search' => __('Search box shortcode and enhanced search.', 'datalumo'), + default => __('Connect, sync, and widgets.', 'datalumo'), + }; + ?> - - - +
+
+ + + + + + + + + + + + +
+ + +
+

+

+
+ +
+ + + + +
+ + + +
+
+ + +
+ + + + +
+ +
+
+ + + +
+ + + + +
+ +
+
+ + +

+ + +

+ +
+ + + + + + +
+

+
+ + + + + +
> + +

+ + +

+
+ + +

+ +

+
+ + +
> +
+ + + +
+ + + + + + + + + + + + - + - - - - - - - - + + - - - - - - - - - - + + + + +
+ +
+ + + + +

-

+ 1; + ?>
- $sync) : ?> -
- + $datalumo_sync) : ?> +
+ - + - @@ -145,10 +327,9 @@ class="nav-tab "> -

- + @@ -180,14 +361,13 @@ class="nav-tab "> -

- + @@ -199,7 +379,7 @@ class="nav-tab "> - + @@ -221,11 +401,11 @@ class="nav-tab ">