import { createPinia } from 'pinia' import { __, _x } from '@wordpress/i18n' import { ref } from 'vue' import { applyQuantityStep, buildTierTableRows, clampMin, formatPrice as formatPriceHelper, getI18n, getTieredUnitPrice as getTieredUnitPriceHelper, hasNoPrice as hasNoPriceHelper, isMinQuantity as isQuantityAtMin, l10nObj, normalizeQuantity, safeUrl, sanitizeSubtitleHtml, type TierTableRow } from '@/helper' import { useCollectionsStore } from '@/api' import $ from 'jquery' import { notification, } from 'ant-design-vue' import { parse } from 'path' import { generatePaginationItems } from '@/utils/pagination' import { filterEligibleCartItems, filterSelectedCartItems, addCartItemsSequentially, finalizeAddToCart, getAddAllRedirectUrl, showAddToCartFailure } from '@/utils/cartUtils' import type { CartItem } from '@/utils/cartUtils' import { debounce, SEARCH_DEBOUNCE_MS } from '@/utils/debounce' import { attachEscHandler, detachEscHandler } from '../shared/escHandler' import { refreshWishlistData } from '../shared/wishlistDataMethods' import { fetchUsers } from '../shared/fetchUsers' import { isAllSelected, toggleAllSelection, toggleItemSelection } from '../shared/wishlistSelection' const pinia = createPinia() const { getWishlistItems, deleteCollections, deleteCollectionItems, updateWishlistItem, updateWishlistItemPosition, saveCollection, } = useCollectionsStore(pinia) const loadedObject = l10nObj() const selectedWishlist = loadedObject.selectedWishlist const wcCurrency = loadedObject.wcCurrency const isLoggedIn = loadedObject.isLoggedIn const wishlistsAsOptions = loadedObject.wishlistsAsOptions || [] const proDependencies = loadedObject.proDependencies || {} const activePro = proDependencies?.activePro || false const wpRoles = proDependencies?.wpRoles || {} // Pro-supplied wishlist table options. Each stays inert (off / empty) unless // Pro is installed with an active license and the matching setting is on. const showSkuColumn = !!proDependencies?.showSkuColumn const showVariationPrice = !!proDependencies?.showVariationPrice const noPriceText = proDependencies?.noPriceText || '' // Logged-in wishlist items always carry a collection_item `id` — use it as the // variation/selection key. Non-null assertion is safe because items are loaded // from the collection_items table, which has `id` as its primary key. const getItemKey = (item: CartItem): number => item.id! // Per-item tier-table memo, keyed by the item object so entries are released // when the list is re-fetched (new objects) and cannot leak. Keyed weakly to // avoid rebuilding the rows on every template access (the v-if and v-for each // call getTierTable) and on every unrelated reactive re-render. const tierTableCache = new WeakMap() // Per-item selected-variation memo, keyed weakly by the item object for the // same reasons as the tier-table memo above. Resolving the variation scans the // product's variation list, and the template asks for it several times per row // per render (the price cell's v-if chain, the SKU cell, the tier price // source), so the scan is done once per selection instead of once per access. const selectedVariationCache = new WeakMap() // Group a whole-number quantity with the store's configured thousand separator // (e.g. 1000 -> "1,000") so tier range labels read like the rest of the store. // The separator is applied via a replacement function so `$` sequences in it // (e.g. a `$`-based separator) are treated literally, not as `String.replace` // special patterns. const groupQuantity = (value: number): string => { const separator = wcCurrency.thousand_separator ?? ',' return String(value).replace(/\B(?=(\d{3})+(?!\d))/g, () => separator) } // Translatable strings. const wishlistTitle = getI18n('wishlist_title', selectedWishlist?.name) const wishlistSubtitle = getI18n('wishlist_subtitle', selectedWishlist?.description) const searchPlaceholder = getI18n('search_placeholder', __('Search for anything...', 'saveto-wishlist-lite-for-woocommerce')) const searchLabel = getI18n('search_label', __('Search', 'saveto-wishlist-lite-for-woocommerce')) const clearSearch = getI18n('clear_search', __('Clear search', 'saveto-wishlist-lite-for-woocommerce')) const tableInfoString = getI18n('table_info', __('Showing %1$s-%2$s of %3$s items', 'saveto-wishlist-lite-for-woocommerce')) const emptyTitle = getI18n('empty_product_title', __('No products found', 'saveto-wishlist-lite-for-woocommerce')) const emptyDescription = getI18n('empty_product_description', __('Add products to your wishlist to get started.', 'saveto-wishlist-lite-for-woocommerce')) const loginToView = getI18n('login_to_view', __('Login to view your wishlists.', 'saveto-wishlist-lite-for-woocommerce')) const allWishlists = getI18n('all_wishlists', __('All Wishlists', 'saveto-wishlist-lite-for-woocommerce')) const continueShopping = getI18n('continue_shopping', __('Continue Shopping', 'saveto-wishlist-lite-for-woocommerce')) const shareOnFacebook = getI18n('share_facebook', __('Share on Facebook', 'saveto-wishlist-lite-for-woocommerce')) const shareOnTwitter = getI18n('share_twitter', __('Share on Twitter', 'saveto-wishlist-lite-for-woocommerce')) const shareOnPinterest = getI18n('share_pinterest', __('Share on Pinterest', 'saveto-wishlist-lite-for-woocommerce')) const shareByEmailOld = getI18n('share_email', __('Share by Email', 'saveto-wishlist-lite-for-woocommerce')) const shareOnWhatsapp = getI18n('share_whatsapp', __('Share on WhatsApp', 'saveto-wishlist-lite-for-woocommerce')) const copyLinkToClipboard = getI18n('copy_link', __('Copy link to clipboard', 'saveto-wishlist-lite-for-woocommerce')) const linkCopied = getI18n('link_copied', __('Link copied to clipboard', 'saveto-wishlist-lite-for-woocommerce')) const loading = getI18n('loading', __('Loading...', 'saveto-wishlist-lite-for-woocommerce')) const transferWishlist = getI18n('transfer_wishlist', __('Move to Wishlist', 'saveto-wishlist-lite-for-woocommerce')) const moving = getI18n('moving', __('Moving...', 'saveto-wishlist-lite-for-woocommerce')) const move = getI18n('move', __('Move', 'saveto-wishlist-lite-for-woocommerce')) const cancel = getI18n('cancel', __('Cancel', 'saveto-wishlist-lite-for-woocommerce')) const draftText = getI18n('draft', __('Draft', 'saveto-wishlist-lite-for-woocommerce')) const viewProduct = getI18n('view_product', __('View Product', 'saveto-wishlist-lite-for-woocommerce')) const editWishlistText = getI18n('edit_wishlist', __('Edit Wishlist', 'saveto-wishlist-lite-for-woocommerce')) const welcome = getI18n('welcome', __('Welcome', 'saveto-wishlist-lite-for-woocommerce')) const enterEmailToAccess = getI18n('enter_email_to_access', __('Enter your email address to access the wishlist', 'saveto-wishlist-lite-for-woocommerce')) const emailAddress = getI18n('email_address', __('Email Address', 'saveto-wishlist-lite-for-woocommerce')) const emailPlaceholder = getI18n('email_placeholder', __('you@example.com', 'saveto-wishlist-lite-for-woocommerce')) const continueToWishlist = getI18n('continue_to_wishlist', __('Continue to Wishlist', 'saveto-wishlist-lite-for-woocommerce')) const verifying = getI18n('verifying', __('Verifying...', 'saveto-wishlist-lite-for-woocommerce')) const emailAccessDenied = getI18n('email_access_denied', __('Access denied. Please enter a valid email address that has been shared with this wishlist.', 'saveto-wishlist-lite-for-woocommerce')) const wishlistName = getI18n('wishlist_name', __('Wishlist Name', 'saveto-wishlist-lite-for-woocommerce')) const wishlistDescription = getI18n('wishlist_description', __('Wishlist Description', 'saveto-wishlist-lite-for-woocommerce')) const publish = getI18n('publish', __('Publish', 'saveto-wishlist-lite-for-woocommerce')) const isDefault = getI18n('is_default', __('Is Default', 'saveto-wishlist-lite-for-woocommerce')) const isPublic = getI18n('is_public', __('Is Public', 'saveto-wishlist-lite-for-woocommerce')) const publicTooltip = getI18n('public_tooltip', __('Anyone with the link can view this wishlist.', 'saveto-wishlist-lite-for-woocommerce')) const update = getI18n('update', __('Update', 'saveto-wishlist-lite-for-woocommerce')) const updating = getI18n('updating', __('Updating...', 'saveto-wishlist-lite-for-woocommerce')) const shareText = getI18n('share', __('Share', 'saveto-wishlist-lite-for-woocommerce')) const shareByEmail = getI18n('share_by_email', __('Share by Email', 'saveto-wishlist-lite-for-woocommerce')) const enterEmail = getI18n('enter_email', __('Enter email address', 'saveto-wishlist-lite-for-woocommerce')) const addText = getI18n('add', __('Add', 'saveto-wishlist-lite-for-woocommerce')) const shareWithUsers = getI18n('share_with_users', __('Share with Users', 'saveto-wishlist-lite-for-woocommerce')) const restrictionsText = getI18n('restrictions', __('Restrictions', 'saveto-wishlist-lite-for-woocommerce')) const restrictByRoles = getI18n('restrict_by_roles', __('Restrict by Roles', 'saveto-wishlist-lite-for-woocommerce')) const copiedLabel = getI18n('copied', __('Copied', 'saveto-wishlist-lite-for-woocommerce')) const copyLabel = getI18n('copy', __('Copy', 'saveto-wishlist-lite-for-woocommerce')) const shareLink = getI18n('share_link', __('Share Link', 'saveto-wishlist-lite-for-woocommerce')) const clearSelect = getI18n('clear_select', __('Clear', 'saveto-wishlist-lite-for-woocommerce')) const searchRoles = getI18n('search_roles', __('Search roles...', 'saveto-wishlist-lite-for-woocommerce')) const searchUsers = getI18n('search_users', __('Search users...', 'saveto-wishlist-lite-for-woocommerce')) const whoCanEdit = getI18n('who_can_edit', __('Who Can Edit', 'saveto-wishlist-lite-for-woocommerce')) const confirmDeleteTitle = getI18n('confirm_delete_title', __('Delete Item', 'saveto-wishlist-lite-for-woocommerce')) const confirmRemoveItemMessage = getI18n('confirm_remove_item_message', __('Are you sure you want to remove this item from the wishlist?', 'saveto-wishlist-lite-for-woocommerce')) const tableViewLabel = getI18n('table_view', __('Table view', 'saveto-wishlist-lite-for-woocommerce')) const gridViewLabel = getI18n('grid_view', __('Grid view', 'saveto-wishlist-lite-for-woocommerce')) const viewModeLabel = getI18n('view_mode', __('View mode', 'saveto-wishlist-lite-for-woocommerce')) // User's preferred view mode for the wishlist items page. Persisted in // localStorage so the choice survives navigation and reloads. Falls back to // 'table' (existing layout) when no preference has been saved or the stored // value is unrecognised (e.g. older or corrupt entries). const VIEW_MODE_STORAGE_KEY = 'stwlite_wishlist_view_mode' const readStoredViewMode = (): 'table' | 'grid' => { try { const stored = localStorage.getItem(VIEW_MODE_STORAGE_KEY) return stored === 'grid' ? 'grid' : 'table' } catch { return 'table' } } const table = { show: getI18n('show', __('Show', 'saveto-wishlist-lite-for-woocommerce')), previous: getI18n('previous', __('Previous', 'saveto-wishlist-lite-for-woocommerce')), next: getI18n('next', __('Next', 'saveto-wishlist-lite-for-woocommerce')), add_to_cart: getI18n('add_to_cart', __('Add to Cart', 'saveto-wishlist-lite-for-woocommerce')), delete: getI18n('delete', __('Delete', 'saveto-wishlist-lite-for-woocommerce')), manage: getI18n('manage', __('Manage', 'saveto-wishlist-lite-for-woocommerce')), view_product: viewProduct, view: getI18n('view', __('View', 'saveto-wishlist-lite-for-woocommerce')), transfer: getI18n('transfer', __('Transfer', 'saveto-wishlist-lite-for-woocommerce')), more_actions: getI18n('more_actions', __('More Actions', 'saveto-wishlist-lite-for-woocommerce')), product_table: { image: getI18n('product_table.image', __('Image', 'saveto-wishlist-lite-for-woocommerce')), name: getI18n('product_table.name', __('Product Name', 'saveto-wishlist-lite-for-woocommerce')), sku: getI18n('product_table.sku', __('SKU', 'saveto-wishlist-lite-for-woocommerce')), // translators: Shown in the wishlist SKU column when a product has no SKU. Abbreviation of "not available". not_available: getI18n('product_table.not_available', _x('N/A', 'wishlist SKU column placeholder', 'saveto-wishlist-lite-for-woocommerce')), price: getI18n('product_table.price', __('Price', 'saveto-wishlist-lite-for-woocommerce')), date_added: getI18n('product_table.date_added', __('Date Added', 'saveto-wishlist-lite-for-woocommerce')), stock_status: getI18n('product_table.stock_status', __('Stock Status', 'saveto-wishlist-lite-for-woocommerce')), quantity: getI18n('product_table.quantity', __('Quantity', 'saveto-wishlist-lite-for-woocommerce')), priority: getI18n('product_table.priority', __('Priority', 'saveto-wishlist-lite-for-woocommerce')), subtotal: getI18n('product_table.subtotal', __('Subtotal', 'saveto-wishlist-lite-for-woocommerce')), actions: getI18n('product_table.actions', __('Actions', 'saveto-wishlist-lite-for-woocommerce')), tiered_pricing: getI18n('product_table.tiered_pricing', __('Tiered pricing', 'saveto-wishlist-lite-for-woocommerce')), }, in_stock: getI18n('in_stock', __('In stock', 'saveto-wishlist-lite-for-woocommerce')), allow_backorder: getI18n('allow_backorder', __('Allow backorder', 'saveto-wishlist-lite-for-woocommerce')), out_of_stock: getI18n('out_of_stock', __('Out of stock', 'saveto-wishlist-lite-for-woocommerce')), adding_item: getI18n('adding_item', __('Adding to cart...', 'saveto-wishlist-lite-for-woocommerce')), select_options: getI18n('select_options', __('Select options', 'saveto-wishlist-lite-for-woocommerce')), add_all_to_cart: getI18n('add_all_to_cart', __('Add All to Cart', 'saveto-wishlist-lite-for-woocommerce')), adding_all_to_cart: getI18n('adding_all_to_cart', __('Adding all to cart...', 'saveto-wishlist-lite-for-woocommerce')), add_selected_to_cart: getI18n('add_selected_to_cart', __('Add Selected to Cart', 'saveto-wishlist-lite-for-woocommerce')), adding_selected_to_cart: getI18n('adding_selected_to_cart', __('Adding selected to cart...', 'saveto-wishlist-lite-for-woocommerce')), decrease_quantity: getI18n('decrease_quantity', __('Decrease quantity', 'saveto-wishlist-lite-for-woocommerce')), increase_quantity: getI18n('increase_quantity', __('Increase quantity', 'saveto-wishlist-lite-for-woocommerce')), } const wishlist = ref({ title: wishlistTitle, subtitle: wishlistSubtitle, allWishlists: allWishlists, continueShopping: continueShopping, isLoggedIn: isLoggedIn, searchPlaceholder: searchPlaceholder, searchLabel: searchLabel, clearSearch: clearSearch, userRole: loadedObject.user_role, table: table, empty: { title: emptyTitle, description: emptyDescription, }, loginToView: loginToView, pageUrls: loadedObject.pageUrls, loading: loading, listOptions: wishlistsAsOptions, transferWishlist: transferWishlist, moving: moving, move: move, cancel: cancel, draft: draftText, sanitizeSubtitleHtml: sanitizeSubtitleHtml, editWishlist: editWishlistText, welcome: welcome, enterEmailToAccess: enterEmailToAccess, emailAddress: emailAddress, emailPlaceholder: emailPlaceholder, continueToWishlist: continueToWishlist, verifying: verifying, wishlistName: wishlistName, wishlistDescription: wishlistDescription, publish: publish, isDefault: isDefault, isPublic: isPublic, publicTooltip: publicTooltip, update: update, updating: updating, activePro: activePro, share: shareText, shareByEmail: shareByEmail, enterEmail: enterEmail, add: addText, shareWithUsers: shareWithUsers, restrictions: restrictionsText, restrictByRoles: restrictByRoles, copiedLabel: copiedLabel, copyLabel: copyLabel, shareLink: shareLink, clearSelect: clearSelect, searchRoles: searchRoles, searchUsers: searchUsers, whoCanEdit: whoCanEdit, confirmDeleteTitle: confirmDeleteTitle, confirmRemoveItemMessage: confirmRemoveItemMessage, tableViewLabel: tableViewLabel, gridViewLabel: gridViewLabel, viewModeLabel: viewModeLabel, pageStyles: loadedObject.pageStyles || {}, hideDisabledCartButton: loadedObject.hideDisabledCartButton || false, showSkuColumn: showSkuColumn, noPriceText: noPriceText, }) const getWishlistProducts = async (params: any) => { let lists: any[] = [] let pagination = { current: 1, pageSize: 10, total: 0, totalPages: 0, } await getWishlistItems({ page: params.page, posts_per_page: params.posts_per_page, search: params.search, collection_id: params.collection_id, sort_column: params.sort_column, sort_order: params.sort_order, email_allowed: params.email_allowed, }).then((response: any) => { // Pagination pagination = { ...pagination, total: response.total, totalPages: Math.ceil(response.total / params.posts_per_page), } // Table data lists = response.data.map((item: any) => ({ ...item, status: item.status, })) }).catch((e: any) => { if (e.error?.message) { notification.error({ message: getI18n('error', __('Error', 'saveto-wishlist-lite-for-woocommerce')), description: e.error?.message, class: 'wishlist-lists-error-notification', }) } }) return { lists, pagination, } } const wishlistItemMounted = () => { // Held in the factory closure rather than read back off the scope: petite-vue // re-binds every scope method on mount, and `Function.prototype.bind` drops // the `cancel` property this needs. One timer per mounted element. const debouncedSearch = debounce(function (this: any) { this.pagination.current = 1 this.wishlistData }, SEARCH_DEBOUNCE_MS) return { wishlist, lists: [] as any[], fetchingItems: false, deletingItem: false, addingItem: false, addingAllItems: false, addingSelectedItems: false, deletingItemId: 0, addingItemId: 0, selectedItems: [] as number[], deleteConfirmPopup: false, deleteConfirmId: 0 as number, deleteConfirmVariantId: 0 as number, _escHandler: null as null | ((e: KeyboardEvent) => void), transferringItemId: 0, transferWishlistPopup: false, transferringWishlist: false, activeActionsMenu: null as number | null, transferWishlist: { collection_id: selectedWishlist?.id || 0, }, search: '', user_id: parseInt(loadedObject.user_id as unknown as string) || loadedObject.user_id, collection_id: selectedWishlist?.id, collection_user_id: parseInt(selectedWishlist?.user_id as unknown as string) || selectedWishlist?.user_id, isPublic: selectedWishlist?.is_public, collectionStatus: selectedWishlist?.status, collectionEmails: selectedWishlist?.emails || [], collectionUserIds: selectedWishlist?.user_ids || [], collectionUserRoles: selectedWishlist?.allowed_roles || [], collectionAllowedEditEmails: selectedWishlist?.allowed_edit_emails || [], collectionAllowedRoles: selectedWishlist?.allowed_roles || [], emailAccessPopup: false, emailAccessInput: '', verifyingEmail: false, editWishlistPopup: false, editingWishlist: false, editWishlist: { id: '', name: '', description: '', status: 'publish', is_default: false, is_public: false, url_code: '', emails: [] as string[], user_ids: [] as number[], allowed_roles: [] as string[], allowed_edit_emails: [] as string[], }, editWishlistEmailInput: '', editWishlistSelectedUsers: [] as any[], editWishlistUserOptions: [] as any[], editWishlistUserSearch: '', editWishlistFilteredUserOptions: [] as any[], editWishlistSelectedRoles: [] as any[], editWishlistRoleOptions: [] as any[], editWishlistRoleSearch: '', editWishlistFilteredRoleOptions: [] as any[], editWishlistEditEmailInput: '', users: [] as any[], copied: false, emailAccessChecked: false, verifyEmailAccess: true, checkEmailAccessOnLoad() { let showEmailAccessPopup = false; // Check if user logged in id is in the collection user ids if (this.collection_user_id === this.user_id) { this.verifyEmailAccess = true; return; } // Show email access popup on load if emails data is not empty but user_ids and allowed_edit_emails are empty if (this.collectionEmails.length > 0 && parseInt(this.isPublic) !== 1) { showEmailAccessPopup = true; } else if (this.collectionAllowedEditEmails.length > 0 && parseInt(this.isPublic) !== 1) { showEmailAccessPopup = true; } if (!this.emailAccessChecked && showEmailAccessPopup) { // Check if email is stored in session/localStorage const storedEmail = localStorage.getItem('stwlite_wishlist_email_' + this.collection_id) if (!storedEmail || (!this.collectionEmails.some((email: string) => email.toLowerCase() === storedEmail.toLowerCase()) && !this.collectionAllowedEditEmails.some((email: string) => email.toLowerCase() === storedEmail.toLowerCase()))) { this.emailAccessPopup = true this.verifyEmailAccess = false const latestEmail = localStorage.getItem('stwlite_wishlist_latest_email') if (latestEmail) { this.emailAccessInput = latestEmail } } this.emailAccessChecked = true } }, get canPersistQuantity() { return this.collection_user_id === this.user_id || wishlist.value.userRole === 'admin' || this.canEditWishlist }, get canEditWishlist() { // make sure pro is active and wishlist owner is the current user if (!activePro) { return false } // Admin can always edit if (wishlist.value.userRole === 'admin') { return true } // Owner can always edit if (this.collection_user_id === this.user_id) { return true } // Check if email has permission via allowed_edit_emails const localStorageEmail = localStorage.getItem('stwlite_wishlist_email_' + this.collection_id) if (localStorageEmail && this.collectionAllowedEditEmails.length > 0) { return this.collectionAllowedEditEmails.some((email: string) => email.toLowerCase() === (localStorageEmail as string).toLowerCase()) } // Check if user is logged in and has permission via allowed_roles if (wishlist.value.isLoggedIn && this.collectionAllowedRoles.length > 0) { // Validate if user's role is in the allowed roles array return this.collectionAllowedRoles.includes(wishlist.value.userRole) } return false }, social_urls: { facebook: 'https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent(wishlist.value.pageUrls.wishlistUrl + selectedWishlist?.url_code) + '&t=' + encodeURIComponent(wishlist.value.title), twitter: 'https://twitter.com/intent/tweet?url=' + encodeURIComponent(wishlist.value.pageUrls.wishlistUrl + selectedWishlist?.url_code) + '&text=' + encodeURIComponent(wishlist.value.title), pinterest: 'https://pinterest.com/pin/create/button/?url=' + encodeURIComponent(wishlist.value.pageUrls.wishlistUrl + selectedWishlist?.url_code) + '&description=' + encodeURIComponent(wishlist.value.title), email: 'mailto:?subject=' + encodeURIComponent(wishlist.value.title) + '&body=' + encodeURIComponent(wishlist.value.pageUrls.wishlistUrl + selectedWishlist?.url_code), whatsapp: 'https://wa.me/?text=' + encodeURIComponent(wishlist.value.title + ' ' + wishlist.value.pageUrls.wishlistUrl + selectedWishlist?.url_code), }, social_share_text: { facebook: shareOnFacebook, twitter: shareOnTwitter, pinterest: shareOnPinterest, email: shareByEmailOld, whatsapp: shareOnWhatsapp, copy_link: copyLinkToClipboard, }, selectedDefaultVariation: [] as any[], selectedVariation: [] as any[], viewMode: readStoredViewMode(), setViewMode(mode: 'table' | 'grid') { this.viewMode = mode try { localStorage.setItem(VIEW_MODE_STORAGE_KEY, mode) } catch { // Storage may be unavailable (private mode, quota); the in-memory // toggle still works for this session. } }, sortColumn: 'sort_order', sortOrder: 'ASC', pagination: { current: 1, pageSize: 10, total: 0, totalPages: 0, }, tableInfo: '', draggingIndex: null, dropTarget: null, dropPosition: null as any, sharePermission: false, get wishlistData() { // Check email access on load this.checkEmailAccessOnLoad() const storedEmail = localStorage.getItem('stwlite_wishlist_email_' + this.collection_id) let notAllowed = false if (parseInt(this.isPublic) !== 1 && !wishlist.value.isLoggedIn) { const userAllowedById = this.collectionUserIds.includes(this.user_id) const emailAllowedForEdit = loadedObject.user_email && this.collectionAllowedEditEmails.some((email: string) => email.toLowerCase() === (loadedObject.user_email as string).toLowerCase()) const userEmailAllowedForEdit = storedEmail && this.collectionAllowedEditEmails.some((email: string) => email.toLowerCase() === storedEmail.toLowerCase()) const emailAllowed = storedEmail && this.collectionEmails.some((email: string) => email.toLowerCase() === storedEmail.toLowerCase()) const userEmailAllowed = (loadedObject.user_email && this.collectionEmails.some((email: string) => email.toLowerCase() === (loadedObject.user_email as string).toLowerCase())) const userRoleAllowed = this.collectionUserRoles.includes(wishlist.value.userRole) // If any of the 3 conditions is true, allow access to the wishlist if (userAllowedById || emailAllowed || userEmailAllowed || emailAllowedForEdit || userEmailAllowedForEdit || userRoleAllowed) { notAllowed = false this.sharePermission = true } else { notAllowed = true this.sharePermission = false } } if (notAllowed || !this.verifyEmailAccess) { return { lists: [], } } const params = { page: this.pagination.current, posts_per_page: this.pagination.pageSize, search: this.search, collection_id: this.collection_id, sort_column: this.sortColumn, sort_order: this.sortOrder, email_allowed: storedEmail, } this.fetchingItems = true getWishlistProducts(params).then((res: any) => { this.lists = res.lists this.pagination.total = res.pagination.total this.pagination.totalPages = res.pagination.totalPages this.fetchingItems = false this.updatePagination() this.assignDefaultVariation() }) return { lists: this.lists, pagination: this.pagination, } }, get allSelected(): boolean { return isAllSelected(this, getItemKey) }, toggleSelectAll() { toggleAllSelection(this, getItemKey) }, toggleSelectItem(itemId: number) { toggleItemSelection(this, itemId) }, onSearch: debouncedSearch, clearSearch() { debouncedSearch.cancel() this.search = '' this.pagination.current = 1 this.wishlistData }, // Drop a pending debounced search when the host element disconnects, so it // cannot fire against a scope that is no longer on the page. destroy() { debouncedSearch.cancel() }, handleSort(column: string) { this.sortColumn = column this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc' this.wishlistData }, updateTableInfo() { const startIndex = (this.pagination.current - 1) * this.pagination.pageSize + 1 const endIndex = Math.min(this.pagination.current * this.pagination.pageSize, this.pagination.total) this.tableInfo = tableInfoString.replace('%1$s', startIndex.toString()).replace('%2$s', endIndex.toString()).replace('%3$s', this.pagination.total.toString()) }, handleItemsPerPageChange(event: any) { this.pagination.pageSize = parseInt(event.target.value) this.pagination.current = 1 this.selectedItems = [] this.wishlistData }, handlePagination(direction: string) { if (direction === 'prev' && this.pagination.current > 1) { this.pagination.current-- } else if (direction === 'next' && this.pagination.current < this.pagination.totalPages) { this.pagination.current++ } this.selectedItems = [] this.wishlistData }, handlePaginationNumber(page: number) { this.pagination.current = page this.selectedItems = [] this.wishlistData }, updatePagination() { this.updateTableInfo() }, get paginationItems() { const current = this.pagination.current const totalPages = this.pagination.totalPages return generatePaginationItems(current, totalPages) }, handleDeleteItem(id: number, variantId: number) { this.deleteConfirmId = id this.deleteConfirmVariantId = variantId this.deleteConfirmPopup = true this._escHandler = attachEscHandler(() => this.handleDeleteItemCancel()) }, handleDeleteItemConfirm() { const id = this.deleteConfirmId const variantId = this.deleteConfirmVariantId this.deleteConfirmPopup = false this.deleteConfirmId = 0 this.deleteConfirmVariantId = 0 this._escHandler = detachEscHandler(this._escHandler) this.deletingItemId = id this.deletingItem = true deleteCollectionItems(this.collection_id, id, variantId).then((res: any) => { this.deletingItem = false this.deletingItemId = 0 this.wishlistData }).catch((e: any) => { this.deletingItem = false this.deletingItemId = 0 }) }, handleDeleteItemCancel() { this.deleteConfirmPopup = false this.deleteConfirmId = 0 this.deleteConfirmVariantId = 0 this._escHandler = detachEscHandler(this._escHandler) }, getMinQuantity(item: any): number { return clampMin(Number(item?.product?.min_quantity)) }, handleQuantityChange(id: number, event: any) { const item = this.lists.find((entry: any) => entry.id === id) const quantity = normalizeQuantity(event.target.value, this.getMinQuantity(item)) event.target.value = quantity if (this.canPersistQuantity) { updateWishlistItem({ action: 'update_wishlist_item', id, quantity: quantity }) } }, handleQuantityStep(item: { id: number; quantity: number | string }, delta: number) { const next = applyQuantityStep(item, delta, this.getMinQuantity(item)) if (next === null) return item.quantity = next if (this.canPersistQuantity) { updateWishlistItem({ action: 'update_wishlist_item', id: item.id, quantity: next }) } }, isMinQuantity(item: { quantity: number | string }): boolean { return isQuantityAtMin(item, this.getMinQuantity(item)) }, hasNoPrice(item: any): boolean { return hasNoPriceHelper(item) }, // When the "Hide Disabled Add to Cart Buttons" setting is on, suppress the // Add to Cart button for products it would otherwise render disabled for // (out of stock, no price, or not purchasable — e.g. catalog-mode plugins). // Mirrors the button's `:disabled` binding minus the transient states // (adding, variation not yet selected). shouldHideCartButton(item: any): boolean { return wishlist.value.hideDisabledCartButton && (item.product.is_purchasable === false || item.product.stock === 'outofstock' || this.hasNoPrice(item)) }, formatPrice(price: number): string { return formatPriceHelper(price, wcCurrency.currency_symbol, wcCurrency.decimals, wcCurrency.decimal_separator) }, // Resolve the variation currently chosen for a line item — seeded from the // variation saved with the item and updated as the shopper changes the // attribute selects. Undefined for simple products and for combinations // that match no variation (e.g. a variation deleted since it was saved). getSelectedVariation(item: any): any { const product = item.product if (product.type !== 'variable') { return undefined } // Read the reactive selection up front so petite-vue tracks it and // re-renders on change even when the memo answers the call. const variationId = this.selectedVariation[item.id] const cached = selectedVariationCache.get(item) if (cached && cached.variationId === variationId) { return cached.variation } const variation = product.variations?.variations?.find((candidate: any) => candidate.variation_id === variationId) selectedVariationCache.set(item, { variationId, variation }) return variation }, // Resolve the price source used for tier pricing: the selected variation for // variable products (or undefined until a valid combination is chosen), the // product itself for simple ones. getTierPriceSource(item: any): any { if (item.product.type === 'variable') { return this.getSelectedVariation(item) } return item.product }, // Price html shown in the Price column, or an empty string when there is // no price to show. With Pro active, a resolved variation owns the price // outright — including when it has none, because falling back to the // parent's range there would show a price the selection does not have. // Everything else uses the parent's price html. getPriceHtml(item: any): string { if (showVariationPrice) { const variation = this.getSelectedVariation(item) if (variation) { return variation.price_formatted || '' } } return hasNoPriceHelper(item) ? '' : item.product.price_formatted }, // SKU shown in the SKU column: the selected variation's SKU when it has // one, else the parent product's, else a placeholder. Compared against // empty rather than tested for truthiness, because "0" is a valid SKU. getSku(item: any): string { const variationSku = this.getSelectedVariation(item)?.sku if (variationSku !== undefined && variationSku !== null && variationSku !== '') { return String(variationSku) } const productSku = item.product.sku if (productSku !== undefined && productSku !== null && productSku !== '') { return String(productSku) } return table.product_table.not_available }, // Build the tier table for the current quantity and (for variable products) // the selected variation, so it highlights the active tier and swaps with // the variation just like a simple product's table. getTierTable(item: any): TierTableRow[] { if (this.hasNoPrice(item)) { return [] } // Read the reactive inputs (quantity, selected variation) up front so // petite-vue tracks them and re-renders on change even on a cache hit. const min = this.getMinQuantity(item) const qty = normalizeQuantity(item.quantity, min) const priceSource = this.getTierPriceSource(item) // The rows depend only on quantity and the selected variation for a given // loaded item, so memoize on that signature and rebuild only when it changes. const signature = qty + '|' + (priceSource?.variation_id ?? 'none') const cached = tierTableCache.get(item) if (cached && cached.signature === signature) { return cached.rows } const rows = buildTierTableRows(priceSource, qty, min, groupQuantity) tierTableCache.set(item, { signature, rows }) return rows }, calculateSubtotal(item: any) { if (this.hasNoPrice(item)) { return '—' } const qty = normalizeQuantity(item.quantity, this.getMinQuantity(item)) const priceSource = this.getTierPriceSource(item) // Apply the price source's tier price for the chosen quantity, falling // back to its regular price when no tier applies. A variable product with // no matching variation has no price source, so the subtotal is 0. const price = priceSource ? getTieredUnitPriceHelper(priceSource, qty) * qty : 0 return this.formatPrice(price) }, handleAddToCart(productType: string, collectionId: string, productId: number, quantity: number, itemId: number) { const self = this // Use itemId to get the selected variation for this specific line item const variationId = this.selectedVariation[itemId] || 0 const data = { action: 'stwlite_add_to_cart', nonce: loadedObject.ajax.nonce, collection_id: collectionId, product_id: productId, quantity: quantity, product_type: productType, variation_id: variationId, } this.addingItemId = productId this.addingItem = true $.ajax({ type: 'POST', url: loadedObject.ajax.url, data: data, success: function (res) { // Only an explicit success is a success. The server answers a // rejection with `{status:'error'}` under HTTP 200, and a PHP fatal // prints a non-JSON body under HTTP 200 as well — treating anything // that is not `'success'` as a failure keeps the second case from // reporting a cart add that never happened. if (res?.status !== 'success') { // Configurable products (e.g. PPOM add-ons) cannot be added straight // from the wishlist; the server routes the customer to the product // page to reconfigure instead of adding an incomplete item. That // navigation leaves the page, so it needs no busy-state reset. const redirect = safeUrl(res?.redirect) if (redirect) { window.location.assign(redirect) return } showAddToCartFailure(res?.message) // Release the busy state so the button leaves "Adding to cart..." // and the customer can correct the problem and retry. self.addingItem = false self.addingItemId = 0 } else { notification.success({ message: res.message, class: 'wishlist-lists-success-notification', }) self.addingItem = false self.addingItemId = 0 // Refresh WooCommerce cart fragments. document.body.dispatchEvent(new CustomEvent('wc_fragment_refresh')); // Refresh wishlist items. self.wishlistData } }, // A network failure never reaches `success`, so the busy state has to be // released here too or the button stays on "Adding to cart..." forever. error: function () { showAddToCartFailure() self.addingItem = false self.addingItemId = 0 }, }) }, async handleAddAllToCart() { if (this.addingAllItems) return const itemsToAdd = filterEligibleCartItems(this.lists, this.selectedVariation, getItemKey) if (itemsToAdd.length === 0) { notification.warning({ message: getI18n('no_items_to_add', __('No eligible items to add to cart.', 'saveto-wishlist-lite-for-woocommerce')), class: 'wishlist-lists-warning-notification', }) return } this.addingAllItems = true const result = await addCartItemsSequentially(itemsToAdd, this.selectedVariation, getItemKey) this.addingAllItems = false finalizeAddToCart(result, () => refreshWishlistData(this), getAddAllRedirectUrl()) }, async handleAddSelectedToCart() { if (this.addingSelectedItems || this.selectedItems.length === 0) return const itemsToAdd = filterSelectedCartItems(this.lists, this.selectedItems, this.selectedVariation, getItemKey) if (itemsToAdd.length === 0) { notification.warning({ message: getI18n('no_items_to_add', __('No eligible items to add to cart.', 'saveto-wishlist-lite-for-woocommerce')), class: 'wishlist-lists-warning-notification', }) return } this.addingSelectedItems = true const result = await addCartItemsSequentially(itemsToAdd, this.selectedVariation, getItemKey) this.addingSelectedItems = false this.selectedItems = [] finalizeAddToCart(result, () => refreshWishlistData(this), getAddAllRedirectUrl()) }, handleCopyLink() { const textToCopy = wishlist.value.pageUrls.wishlistUrl + selectedWishlist?.url_code if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(textToCopy).then(() => { notification.success({ message: linkCopied, class: 'wishlist-lists-success-notification', }) }) } else { // Fallback for older browsers const textArea = document.createElement('textarea') textArea.value = textToCopy textArea.style.position = 'fixed' textArea.style.left = '-999999px' textArea.style.top = '-999999px' document.body.appendChild(textArea) textArea.focus() textArea.select() document.execCommand('copy') document.body.removeChild(textArea) notification.success({ message: linkCopied, class: 'wishlist-lists-success-notification', }) } }, assignDefaultVariation() { this.lists.forEach((item: any) => { if ( item.product.type === 'variable' && item.product.variations?.attributes && Object.keys(item.product.variations?.attributes).length > 0 ) { const attributes = item.product.variations.attributes // Use item.id instead of item.product.id to ensure each line item has its own variation state if (!this.selectedDefaultVariation[item.id]) { this.selectedDefaultVariation[item.id] = {} } // Check if item has a variant_id and set default variation based on it if (item.variant_id && item.variant_id > 0 && item.product.variations?.variations) { // Find the variation that matches the variant_id const matchingVariation = item.product.variations.variations.find( (variation: any) => variation.variation_id === parseInt(item.variant_id) ) if (matchingVariation && matchingVariation.attributes) { // Set the default variation attributes from the matching variation Object.keys(matchingVariation.attributes).forEach((attributeKey) => { // Normalize attribute key - remove 'attribute_' prefix if present // Variation attributes may have keys like 'attribute_pa_color' but template uses 'pa_color' const normalizedKey = attributeKey.replace(/^attribute_/, '') // Only set if this attribute exists in the product's variation attributes if (attributes.hasOwnProperty(normalizedKey)) { this.selectedDefaultVariation[item.id][normalizedKey] = matchingVariation.attributes[attributeKey] || '' } }) } else { // Fallback to first attribute option if variation not found Object.keys(attributes).forEach((attribute) => { this.selectedDefaultVariation[item.id][attribute] = attributes[attribute][0]?.name || '' }) } } else { // No variant_id, use first attribute option as default Object.keys(attributes).forEach((attribute) => { this.selectedDefaultVariation[item.id][attribute] = attributes[attribute][0]?.name || '' }) } this.handleVariationChange(item) this.calculateSubtotal(item) } }) }, handleVariationChange(item: any) { const itemId = item.id const product = item.product const variations = product.variations.variations // Get the selected attributes for this specific item (using item.id for uniqueness) const selectedAttributes = this.selectedDefaultVariation[itemId] || {} as any; // Find the variation that matches all selected attributes let variation = variations.find((variationItem: any) => { return Object.keys(selectedAttributes).every(attr => { // Normalize variation attribute keys for comparison const variationAttrKey = 'attribute_' + attr return variationItem.attributes[variationAttrKey] === selectedAttributes[attr] || variationItem.attributes[attr] === selectedAttributes[attr] }) }) const variationId = variation?.variation_id || '' // Use item.id instead of product.id to track variation per line item this.selectedVariation[itemId] = variationId // Update the stock status for this specific item if (variation) { item.product.stock = variation.stock if (variation.image_url) { item.product.image_url = variation.image_url } } else { // Set to out of stock if no variation matches the selected combination. item.product.stock = 'outofstock' } }, getVariationLink(item: any): string { const baseLink = item.product.link const selectedAttrs = this.selectedDefaultVariation[item.id] || {} if (Object.keys(selectedAttrs).length === 0) { return baseLink } const params = new URLSearchParams( Object.fromEntries( Object.entries(selectedAttrs).map(([key, value]) => [`attribute_${key}`, value as string]) ) ).toString() return baseLink.includes('?') ? `${baseLink}&${params}` : `${baseLink}?${params}` }, handleTransferItem(id: number) { this.transferWishlistPopup = true this.transferringItemId = id }, toggleActionsMenu(itemId: number) { this.activeActionsMenu = this.activeActionsMenu === itemId ? null : itemId }, closeActionsMenu() { this.activeActionsMenu = null }, handleDocumentClick(event: Event) { const target = event.target as HTMLElement // Close actions menu if clicking outside of it if (!target.closest('.stwlite-tw-dropdown-menu-container')) { this.closeActionsMenu() } }, handleTransferWishlist() { if (this.transferWishlist.collection_id !== this.collection_id) { this.transferringWishlist = true updateWishlistItem({ action: 'move_wishlist_item', id: this.transferringItemId, collection_id: this.transferWishlist.collection_id }).then((res: any) => { if (res.status === 'success') { notification.success({ message: res.message, class: 'wishlist-lists-success-notification', }) this.wishlistData this.transferWishlistPopup = false this.transferringWishlist = false this.transferringItemId = 0 this.transferWishlist.collection_id = this.collection_id } else { notification.error({ message: getI18n('error', __('Error', 'saveto-wishlist-lite-for-woocommerce')), description: getI18n('failed_to_move_item', __('Failed to move item to wishlist.', 'saveto-wishlist-lite-for-woocommerce')), class: 'wishlist-lists-error-notification', }) } }).catch((e: any) => { this.transferringWishlist = false this.transferringItemId = 0 notification.error({ message: getI18n('error', __('Error', 'saveto-wishlist-lite-for-woocommerce')), description: getI18n('failed_to_move_item', __('Failed to move item to wishlist.', 'saveto-wishlist-lite-for-woocommerce')), class: 'wishlist-lists-error-notification', }) }) } else { notification.error({ message: getI18n('error', __('Error', 'saveto-wishlist-lite-for-woocommerce')), description: getI18n('failed_to_move_item_to_same_wishlist', __('Failed to move item to wishlist. Please select a different wishlist.', 'saveto-wishlist-lite-for-woocommerce')), class: 'wishlist-lists-error-notification', }) } }, handleCloseTransferWishlistPopup() { this.transferWishlistPopup = false }, handleEditWishlist() { if (this.canEditWishlist && this.collection_user_id !== this.user_id) { // Check if email is stored in session/localStorage const storedEmail = localStorage.getItem('stwlite_wishlist_email_' + this.collection_id) if (!storedEmail || (!this.collectionEmails.some((email: string) => email.toLowerCase() === storedEmail.toLowerCase()) && !this.collectionAllowedEditEmails.some((email: string) => email.toLowerCase() === storedEmail.toLowerCase()))) { this.emailAccessPopup = true return } } // Open edit popup this.handleEditWishlistPopup() }, handleEditWishlistPopup() { this.editWishlistPopup = true this.editWishlist = { id: this.collection_id, name: wishlist.value.title || '', description: wishlist.value.subtitle || '', status: this.collectionStatus || 'publish', is_default: selectedWishlist?.is_default === '1' ? true : false, is_public: parseInt(this.isPublic) === 1 ? true : false, url_code: selectedWishlist?.url_code || '', emails: this.collectionEmails || [], user_ids: this.collectionUserIds || [], allowed_roles: this.collectionUserRoles || [], allowed_edit_emails: this.collectionAllowedEditEmails || [], } // Initialize role options from wpRoles this.editWishlistRoleOptions = Object.entries(wpRoles).map(([key, value]) => ({ label: value, value: key, })) // Initialize selected roles - map role values to role objects this.editWishlistSelectedRoles = (this.editWishlist.allowed_roles || []).map((roleValue: string) => this.editWishlistRoleOptions.find((role: any) => role.value === roleValue) ).filter((role: any) => role !== undefined) this.editWishlistRoleSearch = '' this.editWishlistFilteredRoleOptions = [] // Initialize user options and selected users if (this.users.length === 0) { fetchUsers().then((users: any[]) => { this.users = users this.editWishlistUserOptions = users.map((user: any) => ({ label: user.name, value: user.id, })) this.editWishlistSelectedUsers = (this.editWishlist.user_ids || []).map((userId: number) => this.editWishlistUserOptions.find((user: any) => user.value === userId) ).filter((user: any) => user !== undefined) this.editWishlistUserSearch = '' this.editWishlistFilteredUserOptions = [] this.editWishlistEditEmailInput = '' }) } else { this.editWishlistUserOptions = this.users.map((user: any) => ({ label: user.name, value: user.id, })) this.editWishlistSelectedUsers = (this.editWishlist.user_ids || []).map((userId: number) => this.editWishlistUserOptions.find((user: any) => user.value === userId) ).filter((user: any) => user !== undefined) this.editWishlistUserSearch = '' this.editWishlistFilteredUserOptions = [] this.editWishlistEditEmailInput = '' } this.editWishlistEmailInput = '' }, handleCloseEditWishlistPopup() { this.editWishlistPopup = false this.editWishlist = { id: '', name: '', description: '', status: 'publish', is_default: false, is_public: false, url_code: '', emails: [], user_ids: [], allowed_roles: [], allowed_edit_emails: [], } this.editWishlistEmailInput = '' this.editWishlistSelectedUsers = [] this.editWishlistSelectedRoles = [] this.editWishlistEditEmailInput = '' this.editWishlistUserSearch = '' this.editWishlistFilteredUserOptions = [] this.editWishlistRoleSearch = '' this.editWishlistFilteredRoleOptions = [] }, handleSaveEditWishlist() { const isDefault = this.editWishlist.status === 'publish' ? this.editWishlist.is_default : false const data = { action: 'update_wishlist', id: this.editWishlist.id, name: this.editWishlist.name, description: this.editWishlist.description, status: this.editWishlist.status, is_default: isDefault === true ? '1' : '0', is_public: this.editWishlist.is_public === true ? '1' : '0', url_code: this.editWishlist.url_code, emails: this.editWishlist.emails, user_ids: this.editWishlist.user_ids, allowed_roles: this.editWishlist.allowed_roles, allowed_edit_emails: this.editWishlist.allowed_edit_emails, from_edit: this.collection_user_id === this.user_id ? 'owner' : 'guest', } this.editingWishlist = true saveCollection(data as any).then((res: any) => { notification.success({ message: getI18n('success', __('Success', 'saveto-wishlist-lite-for-woocommerce')), description: res.message, class: 'wishlist-lists-success-notification', }) this.editWishlistPopup = false this.editingWishlist = false // Refresh the page to show updated data window.location.reload() }) .catch((e: any) => { notification.error({ message: getI18n('error', __('Error', 'saveto-wishlist-lite-for-woocommerce')), description: getI18n('error_message', __('Failed to update wishlist.', 'saveto-wishlist-lite-for-woocommerce')), class: 'wishlist-lists-error-notification', }) }) .finally(() => { this.editingWishlist = false }) }, handleAddEmail(context: 'edit') { const emailInput = this.editWishlistEmailInput const emailsArray = this.editWishlist.emails const email = emailInput.trim() if (email && email.includes('@') && !emailsArray.includes(email)) { emailsArray.push(email) this.editWishlistEmailInput = '' } }, handleRemoveEmail(email: string, context: 'edit') { this.editWishlist.emails = this.editWishlist.emails.filter((e: string) => e !== email) }, handleUserSelectionChange(userIds: number[], context: 'edit') { this.editWishlist.user_ids = userIds }, handleRoleSelectionChange(roles: string[], context: 'edit') { this.editWishlist.allowed_roles = roles }, handleClearUsers(context: 'edit') { this.editWishlist.user_ids = [] this.editWishlistSelectedUsers = [] this.editWishlistUserSearch = '' this.editWishlistFilteredUserOptions = [] }, handleClearRoles(context: 'edit') { this.editWishlist.allowed_roles = [] this.editWishlistSelectedRoles = [] this.editWishlistRoleSearch = '' this.editWishlistFilteredRoleOptions = [] }, filterUsers(query: string) { this.editWishlistUserSearch = query if (!query || query.trim() === '') { this.editWishlistFilteredUserOptions = [] return } const lowerQuery = query.toLowerCase().trim() this.editWishlistFilteredUserOptions = this.editWishlistUserOptions.filter((user: any) => user.label.toLowerCase().includes(lowerQuery) && !this.editWishlistSelectedUsers.some((selected: any) => selected.value === user.value) ) }, selectUser(user: any) { if (!this.editWishlistSelectedUsers.some((selected: any) => selected.value === user.value)) { this.editWishlistSelectedUsers.push(user) this.editWishlist.user_ids = this.editWishlistSelectedUsers.map((u: any) => u.value) this.editWishlistUserSearch = '' this.editWishlistFilteredUserOptions = [] } }, removeUser(userId: number) { this.editWishlistSelectedUsers = this.editWishlistSelectedUsers.filter((user: any) => user.value !== userId) this.editWishlist.user_ids = this.editWishlistSelectedUsers.map((u: any) => u.value) }, filterRoles(query: string) { this.editWishlistRoleSearch = query if (!query || query.trim() === '') { this.editWishlistFilteredRoleOptions = [] return } const lowerQuery = query.toLowerCase().trim() this.editWishlistFilteredRoleOptions = this.editWishlistRoleOptions.filter((role: any) => role.label.toLowerCase().includes(lowerQuery) && !this.editWishlistSelectedRoles.some((selected: any) => selected.value === role.value) ) }, selectRole(role: any) { if (!this.editWishlistSelectedRoles.some((selected: any) => selected.value === role.value)) { this.editWishlistSelectedRoles.push(role) this.editWishlist.allowed_roles = this.editWishlistSelectedRoles.map((r: any) => r.value) this.editWishlistRoleSearch = '' this.editWishlistFilteredRoleOptions = [] } }, removeRole(roleValue: string) { this.editWishlistSelectedRoles = this.editWishlistSelectedRoles.filter((role: any) => role.value !== roleValue) this.editWishlist.allowed_roles = this.editWishlistSelectedRoles.map((r: any) => r.value) }, handleAddEditEmail() { const emailInput = this.editWishlistEditEmailInput const emailsArray = this.editWishlist.allowed_edit_emails const email = emailInput.trim() if (email && email.includes('@') && !emailsArray.includes(email)) { emailsArray.push(email) this.editWishlistEditEmailInput = '' } }, handleRemoveEditEmail(email: string) { this.editWishlist.allowed_edit_emails = this.editWishlist.allowed_edit_emails.filter((e: string) => e !== email) }, handleClearEditUsers() { this.editWishlist.allowed_edit_emails = [] this.editWishlistEditEmailInput = '' }, async handleCopy(text: string) { try { // Copy text to clipboard using JavaScript if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text) } else { // fallback for older browsers const textarea = document.createElement('textarea') textarea.value = text textarea.style.position = 'fixed' // avoid scrolling to bottom textarea.style.opacity = '0' document.body.appendChild(textarea) textarea.focus() textarea.select() try { document.execCommand('copy') } finally { document.body.removeChild(textarea) } } this.copied = true setTimeout(() => { this.copied = false }, 2000) } catch (error) { this.copied = false } }, handleEmailAccess() { const email = this.emailAccessInput.trim().toLowerCase() if (!email || !email.includes('@')) { notification.error({ message: getI18n('error', __('Error', 'saveto-wishlist-lite-for-woocommerce')), description: getI18n('invalid_email', __('Please enter a valid email address.', 'saveto-wishlist-lite-for-woocommerce')), class: 'wishlist-lists-error-notification', }) return } // Check if email is in the shared emails list const emailMatches = this.collectionEmails.some((sharedEmail: string) => sharedEmail.toLowerCase() === email ) // Check if email is in the allowed edit emails list const emailAllowedForEdit = this.collectionAllowedEditEmails.some((allowedEmail: string) => allowedEmail.toLowerCase() === email ) if (!emailMatches && !emailAllowedForEdit) { notification.error({ message: getI18n('error', __('Error', 'saveto-wishlist-lite-for-woocommerce')), description: getI18n('email_not_shared', __('This email address has not been shared with this wishlist.', 'saveto-wishlist-lite-for-woocommerce')), class: 'wishlist-lists-error-notification', }) return } // Store email in localStorage for this session localStorage.setItem('stwlite_wishlist_email_' + this.collection_id, email) // Save latest email to local storage for this session localStorage.setItem('stwlite_wishlist_latest_email', email) // Close email popup and open edit popup this.emailAccessPopup = false this.verifyingEmail = false this.verifyEmailAccess = true // Refresh the page to show updated data window.location.reload() }, handleCloseEmailAccessPopup() { this.emailAccessPopup = false this.emailAccessInput = '' // Redirect to wishlist page if (wishlist.value.pageUrls?.wishlistUrl && !this.canEditWishlist) { window.location.href = wishlist.value.pageUrls.wishlistUrl } }, onDragStart(index: any, event: any) { this.draggingIndex = index event.dataTransfer.effectAllowed = "move" event.dataTransfer.setData("text/plain", index) }, onDragEnd() { this.draggingIndex = null this.dropTarget = null this.dropPosition = null }, onDragOver(index: any, event: any) { this.dropTarget = index const target = (event.target as HTMLElement).closest('[draggable="true"]') as HTMLElement | null if (!target) return const rect = target.getBoundingClientRect() const isAbove = event.clientY < rect.top + rect.height / 2 this.dropPosition = isAbove ? 'above' : 'below' }, onDrop(index: any) { const dragged = this.lists.splice(this.draggingIndex as any, 1)[0] const insertIndex = this.draggingIndex as any < index && this.dropPosition === "below" ? index : index + (this.dropPosition === "below" ? 1 : 0) this.lists.splice(insertIndex, 0, dragged) updateWishlistItemPosition({ action: 'update_wishlist_item_position', lists: this.lists, }) this.onDragEnd() }, } } export default wishlistItemMounted