Plugin Directory

Changeset 3411238


Ignore:
Timestamp:
12/04/2025 05:16:54 PM (2 months ago)
Author:
neonic
Message:

Add product bulk organize action and some improvements

Location:
lens-media-library-folders/trunk
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • lens-media-library-folders/trunk/assets/js/modules/features/Settings.js

    r3408664 r3411238  
    5858        // Settings storage key
    5959        this.storageKey = 'lens_settings';
     60        this.localStorageMigrationFlag = 'lens_settings_migrated_v1';
     61        this.localStorageIgnoredKeys = ['telemetry_enabled'];
     62        this.telemetryCacheKey = 'lens_telemetry_cache';
    6063       
    6164        // Current settings cache
     
    7578        this.$resetButton = null;
    7679        this.$resetConfirmInput = null;
    77         this.$resetStatusMessage = null;
     80        this.$resetStatusMessage = null;
     81
     82        // One-time cleanup: remove telemetry flag from old localStorage so fresh installs default to opt-out
     83        this.purgeLegacyTelemetryCache();
    7884
    7985        this.apiAccess = {
     
    189195            const saved = localStorage.getItem(this.storageKey);
    190196            const settings = saved ? JSON.parse(saved) : {};
     197
     198            // Never hydrate telemetry from local cache; must come from server
     199            this.localStorageIgnoredKeys.forEach((key) => {
     200                if (settings[key] !== undefined) {
     201                    delete settings[key];
     202                }
     203            });
    191204
    192205            if (settings['sidebar-width'] === undefined) {
     
    299312    writeSettingsToLocal(settings) {
    300313        try {
    301             localStorage.setItem(this.storageKey, JSON.stringify(settings));
     314            const filtered = { ...settings };
     315            this.localStorageIgnoredKeys.forEach((key) => {
     316                if (filtered[key] !== undefined) {
     317                    delete filtered[key];
     318                }
     319            });
     320
     321            localStorage.setItem(this.storageKey, JSON.stringify(filtered));
    302322            return true;
    303323        } catch (error) {
    304324            console.error('[Settings] Error saving settings to localStorage:', error);
    305325            return false;
     326        }
     327    }
     328
     329    /**
     330     * Persist telemetry state with activation stamp so reinstalls (new activation) ignore stale cache.
     331     */
     332    writeTelemetryCache(value) {
     333        try {
     334            const activation = this.getTelemetryActivation();
     335            const payload = {
     336                value: Boolean(value),
     337                activation
     338            };
     339            localStorage.setItem(this.telemetryCacheKey, JSON.stringify(payload));
     340        } catch (error) {
     341            console.warn('[Settings] Unable to write telemetry cache', error);
     342        }
     343    }
     344
     345    /**
     346     * Read telemetry cache if activation matches current install.
     347     */
     348    readTelemetryCache() {
     349        try {
     350            const raw = localStorage.getItem(this.telemetryCacheKey);
     351            if (!raw) return null;
     352            const parsed = JSON.parse(raw);
     353            if (!parsed || typeof parsed.activation === 'undefined') {
     354                return null;
     355            }
     356            if (parsed.activation !== this.getTelemetryActivation()) {
     357                return null;
     358            }
     359            return typeof parsed.value === 'undefined' ? null : Boolean(parsed.value);
     360        } catch (error) {
     361            console.warn('[Settings] Unable to read telemetry cache', error);
     362            return null;
     363        }
     364    }
     365
     366    /**
     367     * Get current activation stamp passed from server (resets on uninstall/reinstall).
     368     */
     369    getTelemetryActivation() {
     370        const config = window.lens_config || {};
     371        const telemetry = config.telemetry || {};
     372        const activation = telemetry.first_activated || 0;
     373        return Number(activation) || 0;
     374    }
     375
     376    /**
     377     * One-time cleanup to ensure telemetry opt-in isn't carried across uninstall/reinstall
     378     */
     379    purgeLegacyTelemetryCache() {
     380        try {
     381            if (localStorage.getItem(this.localStorageMigrationFlag)) {
     382                return;
     383            }
     384
     385            const saved = localStorage.getItem(this.storageKey);
     386            if (saved) {
     387                const settings = JSON.parse(saved);
     388                if (settings && settings.telemetry_enabled !== undefined) {
     389                    delete settings.telemetry_enabled;
     390                    localStorage.setItem(this.storageKey, JSON.stringify(settings));
     391                }
     392            }
     393
     394            localStorage.setItem(this.localStorageMigrationFlag, '1');
     395        } catch (error) {
     396            console.warn('[Settings] Unable to purge legacy telemetry cache', error);
    306397        }
    307398    }
     
    16161707        };
    16171708
     1709        // Telemetry: trust server, but fall back to cached state if activation matches and server didn't return true
     1710        const cachedTelemetry = this.readTelemetryCache();
     1711        if (typeof cachedTelemetry === 'boolean' && this.currentSettings['telemetry_enabled'] !== true) {
     1712            this.currentSettings['telemetry_enabled'] = cachedTelemetry;
     1713        }
     1714
    16181715        this.currentSettings = this.normalizeSettings(this.currentSettings);
    16191716
     
    17191816            ...collectedSettings
    17201817        };
     1818
     1819        // Persist telemetry state locally with activation stamp (used only as fallback, not for reinstalls)
     1820        if (Object.prototype.hasOwnProperty.call(collectedSettings, 'telemetry_enabled')) {
     1821            this.writeTelemetryCache(collectedSettings['telemetry_enabled']);
     1822        }
    17211823
    17221824        if (!this.writeSettingsToLocal(this.currentSettings)) {
  • lens-media-library-folders/trunk/assets/js/modules/posts/PostMediaOrganizer.js

    r3407095 r3411238  
    851851         */
    852852        initBulkActions() {
     853            this.bindBulkFormSubmission();
     854
    853855            // Check if bulk organize was triggered
    854856            const urlParams = new URLSearchParams(window.location.search);
     
    866868            // See initBulkActions()
    867869        }
     870
     871        /**
     872         * Bind bulk form submission to prevent page refresh for Lens action.
     873         */
     874        bindBulkFormSubmission() {
     875            const $form = $('#posts-filter');
     876            if (!$form.length) {
     877                return;
     878            }
     879
     880            const getSelectedAction = () => {
     881                const top = $('#bulk-action-selector-top').val();
     882                const bottom = $('#bulk-action-selector-bottom').val();
     883                return top !== '-1' ? top : bottom;
     884            };
     885
     886            $form.on('submit.lensBulkOrganize', this.handleBulkFormSubmit.bind(this, $form, getSelectedAction));
     887        }
     888
     889        /**
     890         * Handle Lens bulk organize submit without reloading.
     891         */
     892        handleBulkFormSubmit($form, getSelectedAction, e) {
     893            const action = getSelectedAction();
     894            if (action !== 'lens_bulk_organize_media') {
     895                return;
     896            }
     897
     898            e.preventDefault();
     899            e.stopPropagation();
     900
     901            const postIds = this.getSelectedPostIds($form);
     902
     903            if (!postIds.length) {
     904                this.showNotification(__('Please select posts and try again','lens-media-library-folders'), 'info');
     905                return;
     906            }
     907
     908            this.showBulkOrganizeModal(postIds.length, postIds);
     909        }
     910
     911        /**
     912         * Collect selected post IDs from the list table.
     913         */
     914        getSelectedPostIds($form) {
     915            return $form.find('input[name="post[]"]:checked').map((index, el) => parseInt(el.value, 10)).get().filter((id) => !isNaN(id));
     916        }
    868917       
    869918        /**
    870919         * Show bulk organize modal
    871920         */
    872         async showBulkOrganizeModal(postCount) {
    873             // Get the transient data from server
    874             const transientKey = 'lens_bulk_organize_' + this.config.userId;
    875            
     921        async showBulkOrganizeModal(postCount, selectedPostIds = null) {
     922            const postIdsFromSelection = Array.isArray(selectedPostIds) ? selectedPostIds.filter((id) => !isNaN(parseInt(id, 10))) : null;
     923
    876924            try {
    877                 // First get the post IDs from transient
     925                if (postIdsFromSelection && postIdsFromSelection.length) {
     926                    const statsResponse = await this.apiClient.post('post-media/stats', {
     927                        post_ids: postIdsFromSelection
     928                    });
     929
     930                    if (!statsResponse.success) {
     931                        this.showNotification(__('Failed to get media statistics','lens-media-library-folders'), 'error');
     932                        return;
     933                    }
     934
     935                    this.showBulkModal(postIdsFromSelection, statsResponse.data);
     936                    return;
     937                }
     938
     939                // Fallback to server-provided transient (compatibility with existing flow)
    878940                const response = await $.ajax({
    879941                    url: this.config.ajaxUrl,
     
    893955                   
    894956                    // Show a helpful message instead of error
    895                 this.showNotification(__('Please select posts and try again','lens-media-library-folders'), 'info');
     957                    this.showNotification(__('Please select posts and try again','lens-media-library-folders'), 'info');
    896958                    return;
    897959                }
     
    10261088            });
    10271089           
     1090            // Preload media so organize works without expanding the list
     1091            this.loadBulkMedia(postIds);
     1092
    10281093            // Show modal with fade-in effect after initialization
    10291094            setTimeout(() => {
     
    10571122            this.bulkModal.find('.lens-modal-organize').on('click', async () => {
    10581123                const folderData = bulkFolderSelector.getValue();
    1059                
    1060                 // Confirm action
    1061                 if (!confirm(this.config.strings.confirmBulk)) {
    1062                     return;
    1063                 }
    10641124               
    10651125                await this.organizeBulkMedia(postIds, folderData.id, folderData.name);
  • lens-media-library-folders/trunk/readme.txt

    r3409943 r3411238  
    320320
    321321== Changelog ==
     322
     323= 1.0.1 - 2025-12-04 =
     324* Added: "Organize Media" to WooCommerce product bulk actions, allowing product media to be moved in a single pass.
     325* Minor bug fixes and improvements.
    322326
    323327= 1.0.0 - 2025-12-01 =
  • lens-media-library-folders/trunk/src/Admin/AssetLoader.php

    r3407095 r3411238  
    12311231                    'max_file_size_readable' => $import_max_file_size_label,
    12321232                    'supported_formats'      => array( 'csv', 'json', 'xml', 'zip' ),
     1233                ),
     1234                'telemetry'             => array(
     1235                    'first_activated' => (int) get_option( \Lens\Admin\Telemetry::OPTION_FIRST_ACTIVATED, 0 ),
    12331236                ),
    12341237            );
  • lens-media-library-folders/trunk/src/Admin/PostMediaManager.php

    r3407095 r3411238  
    2222use Lens\Models\Folder;
    2323use Lens\Models\Attachment;
     24use Lens\Models\Permission;
    2425use WP_Post;
    2526use WP_Error;
     
    3334
    3435    /**
     36     * Post types that should expose Lens bulk actions.
     37     *
     38     * @var array
     39     */
     40    private $bulk_action_post_types = array( 'post', 'page', 'product' );
     41
     42    /**
    3543     * Constructor.
    3644     */
     
    5462
    5563        // Add bulk actions
    56         add_filter( 'bulk_actions-edit-post', array( $this, 'add_bulk_actions' ) );
    57         add_filter( 'bulk_actions-edit-page', array( $this, 'add_bulk_actions' ) );
    58         add_filter( 'handle_bulk_actions-edit-post', array( $this, 'handle_bulk_actions' ), 10, 3 );
    59         add_filter( 'handle_bulk_actions-edit-page', array( $this, 'handle_bulk_actions' ), 10, 3 );
     64        foreach ( $this->get_bulk_action_post_types() as $post_type ) {
     65            add_filter( "bulk_actions-edit-{$post_type}", array( $this, 'add_bulk_actions' ) );
     66            add_filter( "handle_bulk_actions-edit-{$post_type}", array( $this, 'handle_bulk_actions' ), 10, 3 );
     67        }
    6068
    6169        // AJAX handlers
     
    20852093        $trashed_media_ids     = $trashed_media_handler->get_trashed_attachment_ids();
    20862094
     2095        // Provide lightweight permission map for list/editor contexts
     2096        $user_permissions = array(
     2097            'lens_manage_media'   => Permission::current_user_can_capability( 'lens_manage_media' ),
     2098            'lens_bulk_operations' => Permission::current_user_can_capability( 'lens_bulk_operations' ),
     2099        );
     2100
     2101        $permissions_available = (bool) apply_filters( 'lens/permissions/is_available', false, get_current_user_id() );
     2102
    20872103        // IMPORTANT: Localize ApiClient BEFORE enqueueing it
    20882104        wp_localize_script(
     
    20962112                'debug'           => defined( 'WP_DEBUG' ) && WP_DEBUG,
    20972113                'trashedMediaIds' => $trashed_media_ids, // Pre-loaded trashed IDs for Classic Editor
     2114                'user_permissions' => $user_permissions,
     2115                'permissions_available' => $permissions_available,
    20982116            )
    20992117        );
     
    28402858
    28412859    /**
     2860     * Get post types that should expose Lens bulk actions.
     2861     *
     2862     * @return array Post types.
     2863     */
     2864    private function get_bulk_action_post_types() {
     2865        /**
     2866         * Filters the post types that display Lens bulk actions.
     2867         *
     2868         * @since 1.0.0
     2869         *
     2870         * @param array $post_types Post types that should expose Lens bulk actions.
     2871         */
     2872        return apply_filters( 'lens_bulk_action_post_types', $this->bulk_action_post_types );
     2873    }
     2874
     2875    /**
    28422876     * Add bulk actions to post list.
    28432877     *
     
    32103244        }
    32113245
    3212         $valid_screens = array( 'post', 'page', 'edit-post', 'edit-page' );
     3246        $valid_screens = array( 'post', 'page', 'product', 'edit-post', 'edit-page', 'edit-product' );
    32133247        $screen_id     = $screen->id;
    32143248        $base          = $screen->base;
  • lens-media-library-folders/trunk/src/Rest/Controllers/SettingsController.php

    r3408648 r3411238  
    350350
    351351                // Handle special telemetry setting
    352                 if ( isset( $config['handler'] ) && 'telemetry' === $config['handler'] ) {
    353                     if ( ! current_user_can( 'manage_options' ) ) {
    354                         return $this->error(
    355                             Constants::REST_ERROR_FORBIDDEN,
    356                             __( 'You do not have permission to change telemetry settings.', 'lens-media-library-folders' ),
    357                             rest_authorization_required_code()
    358                         );
    359                     }
    360 
    361                     Telemetry::update_consent( $value );
    362                     $updated[ $key ] = $value;
    363                     continue;
    364                 }
     352            if ( isset( $config['handler'] ) && 'telemetry' === $config['handler'] ) {
     353                // Same capability already enforced by update_settings_permissions_check (upload_files).
     354                Telemetry::update_consent( $value );
     355                $updated[ $key ] = $value;
     356                continue;
     357            }
    365358
    366359            $meta_key = isset( $config['user_meta_key'] ) ? $config['user_meta_key'] : Constants::user_meta( $key );
Note: See TracChangeset for help on using the changeset viewer.