diff --git a/docs/auto-generate-skus/README.md b/docs/auto-generate-skus/README.md index aef1290a..f488ab8b 100644 --- a/docs/auto-generate-skus/README.md +++ b/docs/auto-generate-skus/README.md @@ -49,7 +49,7 @@ To illustrate, a shirt available at `myshop.com/products/stylish-shirt-503`, wit * XL, Heather gray: `503-XL-H` * Small, Red: `503-S-R` -To update your product handle, so as to control the first portion of generated SKUs, open the product in the Shopify admin, scroll to the bottom of the page, click "Edit website SEO", and update the "URL and handle" field to taste. :) +To update your product handle, so as to control the first portion of generated SKUs, open the product in the Shopify admin, scroll to the bottom of the page, click the edit icon next to "Search engine listing", and update the "URL handle" field to taste. :) ## Installing this task diff --git a/docs/auto-generate-skus/script.liquid b/docs/auto-generate-skus/script.liquid index 310a4ea4..cafc41e0 100644 --- a/docs/auto-generate-skus/script.liquid +++ b/docs/auto-generate-skus/script.liquid @@ -1,71 +1,109 @@ +{% assign skip_variants_that_already_have_skus = options.skip_variants_that_already_have_skus__boolean %} +{% assign product_options_to_keep_unabbreviated = options.product_options_to_keep_unabbreviated__array %} + +{% assign allowed_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" %} + +{% assign sku_base = product.handle | split: "-" | last | upcase %} + {% if event.preview %} - {% capture product_json %} - { - "handle": "stylish-shirt-503", - "variants": [ - { - "sku": "503-L-B", - "option1": "L", - "option2": "Black", - "admin_graphql_api_id": "gid://shopify/ProductVariant/1234567890" - }, - { - "sku": "503-XL-G", - "option1": "XL", - "option2": "Heather Grey", - "admin_graphql_api_id": "gid://shopify/ProductVariant/2345678901" - {% if options.skip_variants_that_already_have_skus__boolean %} - }, - { - "option1": "XXL", - "option2": "Red", - "admin_graphql_api_id": "gid://shopify/ProductVariant/3456789012" - {% endif %} - } - ], - "options": [ - { - "name": "Size" - }, - { - "name": "Color" + {% assign sku_base = "503" %} +{% endif %} + +{% assign cursor = nil %} +{% assign variants = array %} + +{% for n in (1..8) %} + {% capture query %} + query { + product(id: {{ product.admin_graphql_api_id | json }}) { + id + handle + variants( + first: 250 + after: {{ cursor | json }} + ) { + pageInfo { + hasNextPage + endCursor + } + nodes { + inventoryItem { + id + } + sku + selectedOptions { + name + value + } + } } - ] + } } {% endcapture %} - {% assign product = product_json | parse_json %} -{% endif %} + {% assign result = query | shopify %} -{% assign allowed_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" %} + {% if event.preview %} + {% capture result_json %} + { + "data": { + "product": { + "variants": { + "nodes": [ + { + "inventoryItem": { + "id": "gid://shopify/InventoryItem/1234567890" + }, + {% unless skip_variants_that_already_have_skus %}"sku": "503-XL-G",{% endunless %} + "selectedOptions": [ + { + "name": "Size", + "value": "XL" + }, + { + "name": "Color", + "value": "Heather Grey" + } + ] + } + ] + } + } + } + } + {% endcapture %} -{% assign sku_base = product.handle | split: "-" | last | upcase %} + {% assign result = result_json | parse_json %} + {% endif %} -{% for variant in product.variants %} - {% if options.skip_variants_that_already_have_skus__boolean and variant.sku != blank %} - {% log message: "variant already has a sku; skipping", original_sku: variant.sku %} + {% assign variants = variants | concat: result.data.product.variants.nodes %} + + {% if result.data.product.variants.pageInfo.hasNextPage %} + {% assign cursor = result.data.product.variants.pageInfo.endCursor %} + {% else %} + {% break %} + {% endif %} +{% endfor %} + +{% for variant in variants %} + {% if skip_variants_that_already_have_skus and variant.sku != blank %} + {% log + message: "variant already has a sku; skipping", + original_sku: variant.sku + %} {% continue %} {% endif %} {% assign variant_sku_generated = sku_base %} - {% assign variant_options = array %} - {% assign variant_options[0] = variant.option1 %} - {% assign variant_options[1] = variant.option2 %} - {% assign variant_options[2] = variant.option3 %} + {% for selected_option in variant.selectedOptions %} + {% if product_options_to_keep_unabbreviated contains selected_option.name %} + {% assign variant_sku_generated = variant_sku_generated | append: "-" | append: selected_option.value %} - {% assign variant_option_names = product.options | map: "name" %} - - {% for variant_option in variant_options %} - {% if variant_option == blank %} - {% continue %} - {% endif %} - - {% if options.product_options_to_keep_unabbreviated__array contains variant_option_names[forloop.index0] %} - {% assign variant_sku_generated = variant_sku_generated | append: "-" | append: variant_option %} {% else %} {% assign variant_option_abbreviation = "" %} - {% assign variant_option_characters = variant_option | split: "" %} + {% assign variant_option_characters = selected_option.value | split: "" %} + {% for variant_option_character in variant_option_characters %} {% if allowed_characters contains variant_option_character %} {% assign variant_option_abbreviation = variant_option_abbreviation | append: variant_option_character %} @@ -81,16 +119,20 @@ {% endfor %} {% if variant_sku_generated != variant.sku %} - {% log message: "updating sku", original_sku: variant.sku, generated_sku: variant_sku_generated %} + {% log + message: "updating sku", + original_sku: variant.sku, + generated_sku: variant_sku_generated + %} {% comment %} - -- update skus on inventory items, as this is no longer supported on variants as of 2024-07 API + -- as of the 2024-07 API, SKUs are updated on inventory items instead of variants {% endcomment %} {% action "shopify" %} mutation { inventoryItemUpdate( - id: {{ variant.inventory_item.admin_graphql_api_id | json }} + id: {{ variant.inventoryItem.id | json }} input: { sku: {{ variant_sku_generated | json }} } diff --git a/docs/auto-tag-orders-by-product-collections/script.liquid b/docs/auto-tag-orders-by-product-collections/script.liquid index 1eaf0803..5adc3588 100644 --- a/docs/auto-tag-orders-by-product-collections/script.liquid +++ b/docs/auto-tag-orders-by-product-collections/script.liquid @@ -1,40 +1,144 @@ -{% if event.topic contains "shopify/orders/" %} +{% assign collections_and_tags = options.collections_and_tags__keyval_required %} + +{% if event.topic == "shopify/orders/create" %} {% assign existing_tags = order.tags | split: ", " %} - {% assign tags_to_add = array %} + + {% comment %} + -- get product IDs from order line items to build the collections search query + {% endcomment %} + + {% assign order_product_ids = order.line_items | map: "product_id" | compact | uniq %} + + {% if order_product_ids == blank %} + {% unless event.preview %} + {% log "This order does not contain any products from the shop catalog." %} + {% break %} + {% endunless %} + {% endif %} + + {% assign search_query_parts = array %} + + {% for order_product_id in order_product_ids %} + {% assign search_query_parts[search_query_parts.size] = order_product_id | prepend: "product_id:" %} + {% endfor %} + + {% assign search_query = search_query_parts | join: " OR " %} + + {% unless event.preview %} + {% log search_query: search_query %} + {% endunless %} + + {% comment %} + -- get collections that contain any of this order's products + {% endcomment %} + + {% assign cursor = nil %} {% assign order_collections = array %} - {% for line_item in order.line_items %} - {% if line_item.product != blank %} - {% assign order_collections = order_collections | concat: line_item.product.collections %} + {% for n in (1..10) %} + {% capture query %} + query { + collections( + first: 250 + after: {{ cursor | json }} + query: {{ search_query | json }} + ) { + pageInfo { + hasNextPage + endCursor + } + nodes { + id + legacyResourceId + title + handle + } + } + } + {% endcapture %} + + {% assign result = query | shopify %} + + {% if event.preview %} + {% capture result_json %} + { + "data": { + "collections": { + "nodes": [ + { + "id": "gid://shopify/Collection/1234567890", + "legacyResourceId": "1234567890", + "title": "Best Widgets", + "handle": "best-widgets" + } + ] + } + } + } + {% endcapture %} + + {% assign result = result_json | parse_json %} + {% endif %} + + {% assign order_collections = order_collections | concat: result.data.collections.nodes %} + + {% if result.data.collections.pageInfo.hasNextPage %} + {% assign cursor = result.data.collections.pageInfo.endCursor %} + {% else %} + {% break %} {% endif %} {% endfor %} - {% assign order_collection_ids = order_collections | map: "id" | join: "," | split: "," %} + {% if order_collections == blank %} + {% unless event.preview %} + {% log "This order does not contain any products that belong to any collections." %} + {% break %} + {% endunless %} + {% endif %} + + {% comment %} + -- save the collection IDs, titles, and handles for comparison against the configured collections + {% endcomment %} + + {% assign order_collection_ids = order_collections | map: "legacyResourceId" %} {% assign order_collection_titles = order_collections | map: "title" %} {% assign order_collection_handles = order_collections | map: "handle" %} - {% for pair in options.collections_and_tags__keyval_required %} + {% log + order_collection_ids: order_collection_ids, + order_collection_titles: order_collection_titles, + order_collection_handles: order_collection_handles + %} + + {% comment %} + -- add order tags for any matched collections + {% endcomment %} + + {% assign tags_to_add = array %} + + {% for pair in collections_and_tags %} {% assign required_collection = pair[0] %} {% assign tags = pair[1] | split: "," %} {% if order_collection_ids contains required_collection - or order_collection_titles contains required_collection - or order_collection_handles contains required_collection + or order_collection_titles contains required_collection + or order_collection_handles contains required_collection %} {% for tag in tags %} {% assign stripped_tag = tag | strip %} + {% unless existing_tags contains stripped_tag %} - {% assign tags_to_add[tags_to_add.size] = stripped_tag %} + {% assign tags_to_add = tags_to_add | push: stripped_tag %} {% endunless %} {% endfor %} {% endif %} {% endfor %} {% if event.preview %} - {% assign tags_to_add[0] = options.collections_and_tags__keyval_required.first.last %} + {% assign tags_to_add[0] = collections_and_tags.first.last %} {% endif %} - {% if tags_to_add != empty %} + {% if tags_to_add != blank %} {% action "shopify" %} mutation { tagsAdd( @@ -134,16 +238,17 @@ {% assign order_collection_titles = order_collections | map: "title" %} {% assign order_collection_handles = order_collections | map: "handle" %} - {% for pair in options.collections_and_tags__keyval_required %} + {% for pair in collections_and_tags %} {% assign required_collection = pair[0] %} {% assign tags = pair[1] | split: "," %} {% if order_collection_ids contains required_collection - or order_collection_titles contains required_collection - or order_collection_handles contains required_collection + or order_collection_titles contains required_collection + or order_collection_handles contains required_collection %} {% for tag in tags %} {% assign stripped_tag = tag | strip %} + {% unless order.tags contains stripped_tag %} {% assign tags_to_add[tags_to_add.size] = stripped_tag %} {% endunless %} @@ -152,10 +257,10 @@ {% endfor %} {% if event.preview %} - {% assign tags_to_add[0] = options.collections_and_tags__keyval_required.first.last %} + {% assign tags_to_add[0] = collections_and_tags.first.last %} {% endif %} - {% if tags_to_add != empty %} + {% if tags_to_add != blank %} {% action "shopify" %} mutation { tagsAdd( diff --git a/docs/auto-tag-products-by-their-options/README.md b/docs/auto-tag-products-by-their-options/README.md index 511c8aba..b5d23ad1 100644 --- a/docs/auto-tag-products-by-their-options/README.md +++ b/docs/auto-tag-products-by-their-options/README.md @@ -54,10 +54,6 @@ mechanic/shopify/bulk_operation Use this task to add tags to your products, based on their options, for easy filtering. A shirt might be tagged with "Color-Blue" and "Size-XL", for example. Optionally, choose to ignore options that only appear on out-of-stock variants, or choose to apply all tags in lowercase. -Use this task to add tags to your products, based on their options, for easy filtering. A shirt might be tagged with "Color-Blue" and "Size-XL", for example. - -Optionally, choose to ignore options that only appear on out-of-stock variants, or choose to apply all tags in lowercase. - Change the separator to change the way tags are built. Using a dash results in "Color-Blue", an underscore results in "Color_Blue", and a colon with a space yields "Color: Blue". This task will remove option tags that are no longer applicable, by scanning for tag prefixes using the list of product options to consider. diff --git a/docs/auto-tag-products-by-their-options/script.liquid b/docs/auto-tag-products-by-their-options/script.liquid index dae79d23..fb1772b7 100644 --- a/docs/auto-tag-products-by-their-options/script.liquid +++ b/docs/auto-tag-products-by-their-options/script.liquid @@ -1,39 +1,50 @@ +{% assign product_options_to_consider = options.product_options_to_consider__array_required %} +{% assign option_name_and_value_separator = options.option_name_and_value_separator__required %} +{% assign use_lowercase_tags = options.use_lowercase_tags__boolean %} +{% assign ignore_variants_that_are_out_of_stock = options.ignore_variants_that_are_out_of_stock__boolean %} +{% assign run_for_all_products_daily = options.run_for_all_products_daily__boolean %} + {% comment %} - Preferred option order: - - {{ options.product_options_to_consider__array_required }} - {{ options.option_name_and_value_separator__required }} - {{ options.use_lowercase_tags__boolean }} - {{ options.ignore_variants_that_are_out_of_stock__boolean }} - {{ options.run_for_all_products_daily__boolean }} - {{ options.run_for_individual_products_when_they_are_created_or_updated__boolean }} - {{ options.run_for_individual_products_when_their_inventory_changes__boolean }} + -- use lowercase for option comparisons to capture variations in usage {% endcomment %} +{% assign lowercase_product_options_to_consider + = product_options_to_consider + | json + | downcase + | parse_json +%} + {% assign jobs = array %} -{% assign separator = options.option_name_and_value_separator__required %} +{% if event.topic contains "shopify/products/" or event.topic contains "shopify/inventory_levels/" %} + {% comment %} + -- for any of these events, query GraphQL to get single product object before paginating variants + {% endcomment %} -{% assign lowercase_product_options_to_consider = array %} -{% for option in options.product_options_to_consider__array_required %} - {% assign lowercase_product_options_to_consider[lowercase_product_options_to_consider.size] = option | downcase %} -{% endfor %} + {% if event.topic contains "shopify/products/" %} + {% capture query %} + query { + product(id: {{ product.admin_graphql_api_id | json }}) { + id + tags + } + } + {% endcapture %} -{% if options.use_lowercase_tags__boolean %} - {% assign product_options_to_consider = lowercase_product_options_to_consider %} -{% else %} - {% assign product_options_to_consider = options.product_options_to_consider__array_required %} -{% endif %} + {% assign result = query | shopify %} -{% if event.topic contains "shopify/products/" or event.topic contains "shopify/inventory_levels/" %} - {% if event.topic contains "shopify/inventory_levels/" %} + {% assign product = result.data.product %} + + {% elsif event.topic contains "shopify/inventory_levels/" %} {% capture query %} query { inventoryLevel(id: {{ inventory_level.admin_graphql_api_id | json }}) { item { variant { product { - legacyResourceId + id + tags } } } @@ -43,41 +54,39 @@ {% assign result = query | shopify %} - {% assign product = shop.products[result.data.inventoryLevel.item.variant.product.legacyResourceId] %} - {% endif %} - - {% if event.preview %} - {% assign product = hash %} - {% assign product["admin_graphql_api_id"] = "gid://shopify/Product/1234567890" %} - {% assign product["tags"] = "foo, bar, baz, " | append: options.product_options_to_consider__array_required.first | append: separator | append: "Red, " | append: options.product_options_to_consider__array_required.first | append: separator | append: "Green" %} + {% assign product = result.data.inventoryLevel.item.variant.product %} {% endif %} {% assign jobs[0] = hash %} - {% assign jobs[0]["product_id"] = product.admin_graphql_api_id %} - {% assign jobs[0]["existing_tags"] = product.tags | split: ", " %} + {% assign jobs[0]["product_id"] = product.id %} + {% assign jobs[0]["existing_tags"] = product.tags %} {% assign jobs[0]["selected_options_found"] = array %} + {% comment %} + -- get option names and values at variant level in case "ignore_variants_that_are_out_of_stock" is enabled + -- paginate to support up to 2K variants + {% endcomment %} + {% assign cursor = nil %} + {% assign selected_options_found = array %} - {% for n in (0..100) %} + {% for n in (1..8) %} {% capture query %} query { - product(id: {{ product.admin_graphql_api_id | json }}) { + product(id: {{ product.id | json }}) { variants( first: 250 after: {{ cursor | json }} ) { pageInfo { hasNextPage + endCursor } - edges { - cursor - node { - inventoryQuantity - selectedOptions { - name - value - } + nodes { + inventoryQuantity + selectedOptions { + name + value } } } @@ -87,65 +96,27 @@ {% assign result = query | shopify %} - {% if event.preview %} - {% capture result_json %} - { - "data": { - "product": { - "variants": { - "edges": [ - { - "node": { - "inventoryQuantity": 0, - "selectedOptions": [ - { - "name": {{ options.product_options_to_consider__array_required.first | json }}, - "value": "Green" - } - ] - } - }, - { - "node": { - "inventoryQuantity": 10, - "selectedOptions": [ - { - "name": "Title", - "value": "Default Title" - }, - { - "name": {{ options.product_options_to_consider__array_required.first | json }}, - "value": "Blue" - } - ] - } - } - ] - } - } - } - } - {% endcapture %} - - {% assign result = result_json | parse_json %} - {% endif %} - - {% for variant_edge in result.data.product.variants.edges %} - {% assign variant = variant_edge.node %} - - {% if options.ignore_variants_that_are_out_of_stock__boolean and variant.inventoryQuantity <= 0 %} + {% for variant in result.data.product.variants.nodes %} + {% if ignore_variants_that_are_out_of_stock and variant.inventoryQuantity <= 0 %} {% continue %} {% endif %} - {% assign jobs[0]["selected_options_found"] = jobs[0]["selected_options_found"] | concat: variant.selectedOptions %} + {% assign selected_options_found + = variant.selectedOptions + | default: array + | concat: selected_options_found + %} {% endfor %} {% if result.data.product.variants.pageInfo.hasNextPage %} - {% assign cursor = result.data.product.variants.edges.last.cursor %} + {% assign cursor = result.data.product.variants.pageInfo.endCursor %} {% else %} {% break %} {% endif %} {% endfor %} + + {% assign jobs[0]["selected_options_found"] = selected_options_found | uniq %} + {% elsif event.topic == "mechanic/user/trigger" or event.topic contains "mechanic/scheduler/" %} {% capture bulk_operation_query %} query { @@ -189,55 +160,10 @@ } } {% endaction %} -{% elsif event.topic == "mechanic/shopify/bulk_operation" %} - {% if event.preview %} - {% capture bulkOperation_json %} - { - "objects": [ - { - "__typename": "Product", - "id": "gid://shopify/Product/1234567890", - "tags": [ - "foo", - "bar", - "baz", - {{ options.product_options_to_consider__array_required.first | append: separator | append: "Red" | json }}, - {{ options.product_options_to_consider__array_required.first | append: separator | append: "Green" | json }} - ] - }, - { - "__typename": "ProductVariant", - "inventoryQuantity": 0, - "selectedOptions": [ - { - "name": {{ options.product_options_to_consider__array_required.first | json }}, - "value": "Green" - } - ], - "__parentId": "gid://shopify/Product/1234567890" - }, - { - "__typename": "ProductVariant", - "inventoryQuantity": 10, - "selectedOptions": [ - { - "name": "Title", - "value": "Default Title" - }, - { - "name": {{ options.product_options_to_consider__array_required.first | json }}, - "value": "Blue" - } - ], - "__parentId": "gid://shopify/Product/1234567890" - } - ] - } - {% endcapture %} - {% assign bulkOperation = bulkOperation_json | parse_json %} - {% endif %} + {% break %} +{% elsif event.topic == "mechanic/shopify/bulk_operation" %} {% assign products = bulkOperation.objects | where: "__typename", "Product" %} {% assign variants = bulkOperation.objects | where: "__typename", "ProductVariant" %} @@ -248,34 +174,81 @@ {% assign job["selected_options_found"] = array %} {% assign product_variants = variants | where: "__parentId", product.id %} + {% for product_variant in product_variants %} - {% if options.ignore_variants_that_are_out_of_stock__boolean and product_variant.inventoryQuantity <= 0 %} + {% if ignore_variants_that_are_out_of_stock and product_variant.inventoryQuantity <= 0 %} {% continue %} {% endif %} - {% assign job["selected_options_found"] = job["selected_options_found"] | concat: product_variant.selectedOptions %} + {% assign job["selected_options_found"] + = product_variant.selectedOptions + | default: array + | concat: job["selected_options_found"] + | uniq + %} {% endfor %} {% assign jobs[jobs.size] = job %} {% endfor %} {% endif %} -{% log jobs: jobs %} +{% comment %} + -- single preview data instance to handle culmination of each event type above +{% endcomment %} + +{% if event.preview %} + {% capture jobs_json %} + [ + { + "product_id": "gid://shopify/Product/1234567890", + "existing_tags": [ + "foo", + "bar", + "baz", + {{ product_options_to_consider.first | append: option_name_and_value_separator | append: "Red" | json }}, + {{ product_options_to_consider.first | append: option_name_and_value_separator | append: "Green" | json }} + ], + "selected_options_found": [ + { + "name": {{ product_options_to_consider.first | json }}, + "value": "Green" + }, + { + "name": "Title", + "value": "Default Title" + }, + { + "name": {{ product_options_to_consider.first | json }}, + "value": "Blue" + } + ] + } + ] + {% endcapture %} + + {% assign jobs = jobs_json | parse_json %} +{% endif %} + +{% comment %} + -- check tags and selected options of each product to make tagging decisions +{% endcomment %} {% for job in jobs %} {% assign tags_to_add = array %} {% assign tags_to_remove = array %} {% assign applicable_tags = array %} + {% for selected_option in job.selected_options_found %} {% assign lowercase_selected_option_name = selected_option.name | downcase %} + {% unless lowercase_product_options_to_consider contains lowercase_selected_option_name %} {% continue %} {% endunless %} - {% assign tag = selected_option.name | append: separator | append: selected_option.value %} + {% assign tag = selected_option.name | append: option_name_and_value_separator | append: selected_option.value %} - {% if options.use_lowercase_tags__boolean %} + {% if use_lowercase_tags %} {% assign tag = tag | downcase %} {% endif %} @@ -283,7 +256,7 @@ {% endfor %} {% for tag in job.existing_tags %} - {% assign lowercase_tag_prefix = tag | split: separator | first | downcase %} + {% assign lowercase_tag_prefix = tag | split: option_name_and_value_separator | first | downcase %} {% if lowercase_product_options_to_consider contains lowercase_tag_prefix %} {% unless applicable_tags contains tag %} @@ -312,7 +285,7 @@ } node { ... on Product { - onlineStoreUrl + id tags } } @@ -329,7 +302,7 @@ } node { ... on Product { - onlineStoreUrl + id tags } } diff --git a/docs/maintain-a-collection-of-new-products/README.md b/docs/maintain-a-collection-of-new-products/README.md index 33e6bfe1..52b821fd 100644 --- a/docs/maintain-a-collection-of-new-products/README.md +++ b/docs/maintain-a-collection-of-new-products/README.md @@ -33,8 +33,8 @@ mechanic/scheduler/daily Use this task to maintain a "New Products" collection, specifying either a number of products to include or the number of days to keep each product around. Easy! :) -To use this task, create a manual collection, and add the collection ID to the task options. (Find the ID by opening the collection in the Shopify admin, then looking at the URL in your browser's address bar. If the URL is https://example.myshopify.com/admin/collections/12345, the collection ID is 12345.) - +To use this task, create a manual collection, and add the collection ID to the task options. (See [finding a resource ID](https://learn.mechanic.dev/techniques/finding-a-resource-id) for help locating the collection ID.) + Use the "Run task" button to populate your collection for the first time. After that, this task will run daily, at midnight in your local timezone. During each run, the task will update the collection, adding new products and removing old ones as appropriate. ## Installing this task diff --git a/docs/maintain-a-collection-of-new-products/script.liquid b/docs/maintain-a-collection-of-new-products/script.liquid index d96017f9..d64ece8e 100644 --- a/docs/maintain-a-collection-of-new-products/script.liquid +++ b/docs/maintain-a-collection-of-new-products/script.liquid @@ -1,39 +1,87 @@ -{% assign collection = shop.collections[options.collection_id__number_required] %} +{% assign collection_id = options.collection_id__number_required | prepend: "gid://shopify/Collection/" %} {% assign days_to_keep_products = options.number_of_days_to_keep_a_product_in_this_collection__number %} {% assign product_count = options.number_of_products_to_keep_in_this_collection__number %} -{% if event.preview %} - {% capture collection_json %} - { - "admin_graphql_api_id": "gid://shopify/Collection/1234567890", - "products": [ - { - "admin_graphql_api_id": "gid://shopify/Product/9876543210" - } - ] - } - {% endcapture %} - - {% assign collection = collection_json | parse_json %} -{% endif %} - -{% if collection == blank %} - {% error "Configured collection ID does not exist in this shop." %} -{% endif %} - {% if days_to_keep_products == blank and product_count == blank %} {% error %} "Please fill in either \"Number of days to keep a product in this collection\" or \"Number of products to keep in this collection\" (but not both!)." {% enderror %} + {% elsif days_to_keep_products != blank and product_count != blank %} {% error "Please choose one of the options, but not both! :)" %} {% endif %} +{% comment %} + -- get IDs of all products currently in the collection +{% endcomment %} + +{% assign cursor = nil %} +{% assign current_product_ids = array %} + +{% for n in (1..100) %} + {% capture query %} + query { + collection(id: {{ collection_id | json }}) { + products( + first: 250 + after: {{ cursor | json }} + ) { + pageInfo { + hasNextPage + endCursor + } + nodes { + id + } + } + } + } + {% endcapture %} + + {% assign result = query | shopify %} + + {% if event.preview %} + {% capture result_json %} + { + "data": { + "collection": { + "products": { + "nodes": [ + { + "id": "gid://shopify/Product/1234567890" + } + ] + } + } + } + } + {% endcapture %} + + {% assign result = result_json | parse_json %} + {% endif %} + + {% assign current_product_ids + = result.data.collection.products.nodes + | map: "id" + | concat: current_product_ids + %} + + {% if result.data.collection.products.pageInfo.hasNextPage %} + {% assign cursor = result.data.collection.products.pageInfo.endCursor %} + {% else %} + {% break %} + {% endif %} +{% endfor %} + {% assign loop_count = 100 %} {% assign first_elements = 250 %} {% assign products_query = "status:active" %} {% if days_to_keep_products != blank %} + {% comment %} + -- calculate the "created at" threshold for products to keep in the collection + {% endcomment %} + {% if days_to_keep_products < 1 %} {% error %} "\"Number of days to keep a product in this collection\" should be > 0" @@ -44,11 +92,17 @@ {% assign days_to_keep_products_s = days_to_keep_products | times: 86400 %} {% assign created_at_threshold_s = now_s | minus: days_to_keep_products_s %} {% assign created_at_threshold = created_at_threshold_s | date: "%Y-%m-%d" %} + {% assign products_query = products_query | append: " created_at:>=" | append: created_at_threshold %} + {% elsif product_count != blank %} + {% comment %} + -- calculate the product pagination loops based on number of products to keep in the collection + {% endcomment %} + {% if product_count < 1 %} {% error %} "\"Number of products to keep in this collection\" should be > 0" @@ -59,6 +113,10 @@ {% assign remaining = product_count | modulo: 250 %} {% endif %} +{% comment %} + -- get qualifying product IDs based on chosen criteria +{% endcomment %} + {% assign cursor = nil %} {% assign qualifying_product_ids = array %} @@ -84,12 +142,10 @@ ) { pageInfo { hasNextPage + endCursor } - edges { - cursor - node { - id - } + nodes { + id } } } @@ -102,11 +158,9 @@ { "data": { "products": { - "edges": [ + "nodes": [ { - "node": { - "id": "gid://shopify/Product/1234567890" - } + "id": "gid://shopify/Product/2345678901" } ] } @@ -117,17 +171,19 @@ {% assign result = result_json | parse_json %} {% endif %} - {% assign result_product_ids = result.data.products.edges | map: "node" | map: "id" %} + {% assign result_product_ids = result.data.products.nodes | map: "id" %} {% assign qualifying_product_ids = qualifying_product_ids | concat: result_product_ids %} {% if result.data.products.pageInfo.hasNextPage %} - {% assign cursor = result.data.products.edges.last.cursor %} + {% assign cursor = result.data.products.pageInfo.endCursor %} {% else %} {% break %} {% endif %} {% endfor %} -{% assign current_product_ids = collection.products | map: "admin_graphql_api_id" %} +{% comment %} + -- create arrays for the products to add and to remove, and split them into groups of 250 +{% endcomment %} {% assign product_ids_to_add = array %} {% assign product_ids_to_remove = array %} @@ -152,7 +208,7 @@ {% for group_of_product_ids_to_add in groups_of_product_ids_to_add %} {% capture mutation %} collectionAddProducts{{ forloop.index0 }}: collectionAddProducts( - id: {{ collection.admin_graphql_api_id | json }} + id: {{ collection_id | json }} productIds: {{ group_of_product_ids_to_add | json }} ) { userErrors { @@ -172,7 +228,7 @@ {% for group_of_product_ids_to_remove in groups_of_product_ids_to_remove %} {% capture mutation %} collectionRemoveProducts{{ forloop.index0 }}: collectionRemoveProducts( - id: {{ collection.admin_graphql_api_id | json }} + id: {{ collection_id | json }} productIds: {{ group_of_product_ids_to_remove | json }} ) { userErrors { @@ -192,6 +248,7 @@ {{ mutations | join: newline }} } {% endaction %} + {% else %} {% log "This collection already has the correct products." %} {% endif %} diff --git a/tasks/auto-generate-skus.json b/tasks/auto-generate-skus.json index 7de861b6..c0b39661 100644 --- a/tasks/auto-generate-skus.json +++ b/tasks/auto-generate-skus.json @@ -1,5 +1,5 @@ { - "docs": "Save time by letting this task keep your SKUs up to date, generating them based on your product handle, and the initials of each variant option. Optionally, choose to have the task skip updating variants that already have SKUs. Useful for large catalogs, or for anyone who has a consistent SKU format.\n\nPlease note: This task updates SKUs for all products, whether or not they're configured with options and variants.\n\nThis task automatically maintains SKUs by combining these elements and joining them with a dash:\n\n1. The last portion of the product handle (e.g. `503`, if your product is available at `myshop.com/products/stylish-shirt-503`)\n2. The capital letters of the variant's first option, if there is one (e.g. `H` if the option is `Heather gray`, or `HG` if the option is `Heather Gray`)\n3. The capital letters of the variant's second option, if there is one\n4. The capital letters of the variant's third option, if there is one\n\n(To use the product option's full value, instead of abbreviating it, add the option name to the \"Product options to keep unabbreviated\" list.)\n\nTo illustrate, a shirt available at `myshop.com/products/stylish-shirt-503`, with options for size and color, might have these SKUs auto-generated:\n\n* Medium, Black: `503-M-B`\n* XL, Heather gray: `503-XL-H`\n* Small, Red: `503-S-R`\n\nTo update your product handle, so as to control the first portion of generated SKUs, open the product in the Shopify admin, scroll to the bottom of the page, click \"Edit website SEO\", and update the \"URL and handle\" field to taste. :)", + "docs": "Save time by letting this task keep your SKUs up to date, generating them based on your product handle, and the initials of each variant option. Optionally, choose to have the task skip updating variants that already have SKUs. Useful for large catalogs, or for anyone who has a consistent SKU format.\n\nPlease note: This task updates SKUs for all products, whether or not they're configured with options and variants.\n\nThis task automatically maintains SKUs by combining these elements and joining them with a dash:\n\n1. The last portion of the product handle (e.g. `503`, if your product is available at `myshop.com/products/stylish-shirt-503`)\n2. The capital letters of the variant's first option, if there is one (e.g. `H` if the option is `Heather gray`, or `HG` if the option is `Heather Gray`)\n3. The capital letters of the variant's second option, if there is one\n4. The capital letters of the variant's third option, if there is one\n\n(To use the product option's full value, instead of abbreviating it, add the option name to the \"Product options to keep unabbreviated\" list.)\n\nTo illustrate, a shirt available at `myshop.com/products/stylish-shirt-503`, with options for size and color, might have these SKUs auto-generated:\n\n* Medium, Black: `503-M-B`\n* XL, Heather gray: `503-XL-H`\n* Small, Red: `503-S-R`\n\nTo update your product handle, so as to control the first portion of generated SKUs, open the product in the Shopify admin, scroll to the bottom of the page, click the edit icon next to \"Search engine listing\", and update the \"URL handle\" field to taste. :)", "halt_action_run_sequence_on_error": false, "name": "Auto-generate SKUs", "online_store_javascript": null, @@ -9,7 +9,7 @@ }, "order_status_javascript": null, "perform_action_runs_in_sequence": false, - "script": "{% if event.preview %}\n {% capture product_json %}\n {\n \"handle\": \"stylish-shirt-503\",\n \"variants\": [\n {\n \"sku\": \"503-L-B\",\n \"option1\": \"L\",\n \"option2\": \"Black\",\n \"admin_graphql_api_id\": \"gid://shopify/ProductVariant/1234567890\"\n },\n {\n \"sku\": \"503-XL-G\",\n \"option1\": \"XL\",\n \"option2\": \"Heather Grey\",\n \"admin_graphql_api_id\": \"gid://shopify/ProductVariant/2345678901\"\n {% if options.skip_variants_that_already_have_skus__boolean %}\n },\n {\n \"option1\": \"XXL\",\n \"option2\": \"Red\",\n \"admin_graphql_api_id\": \"gid://shopify/ProductVariant/3456789012\"\n {% endif %}\n }\n ],\n \"options\": [\n {\n \"name\": \"Size\"\n },\n {\n \"name\": \"Color\"\n }\n ]\n }\n {% endcapture %}\n\n {% assign product = product_json | parse_json %}\n{% endif %}\n\n{% assign allowed_characters = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\" %}\n\n{% assign sku_base = product.handle | split: \"-\" | last | upcase %}\n\n{% for variant in product.variants %}\n {% if options.skip_variants_that_already_have_skus__boolean and variant.sku != blank %}\n {% log message: \"variant already has a sku; skipping\", original_sku: variant.sku %}\n {% continue %}\n {% endif %}\n\n {% assign variant_sku_generated = sku_base %}\n\n {% assign variant_options = array %}\n {% assign variant_options[0] = variant.option1 %}\n {% assign variant_options[1] = variant.option2 %}\n {% assign variant_options[2] = variant.option3 %}\n\n {% assign variant_option_names = product.options | map: \"name\" %}\n\n {% for variant_option in variant_options %}\n {% if variant_option == blank %}\n {% continue %}\n {% endif %}\n\n {% if options.product_options_to_keep_unabbreviated__array contains variant_option_names[forloop.index0] %}\n {% assign variant_sku_generated = variant_sku_generated | append: \"-\" | append: variant_option %}\n {% else %}\n {% assign variant_option_abbreviation = \"\" %}\n {% assign variant_option_characters = variant_option | split: \"\" %}\n {% for variant_option_character in variant_option_characters %}\n {% if allowed_characters contains variant_option_character %}\n {% assign variant_option_abbreviation = variant_option_abbreviation | append: variant_option_character %}\n {% endif %}\n {% endfor %}\n\n {% if variant_option_abbreviation == blank %}\n {% assign variant_option_abbreviation = variant_option_characters[0] | upcase %}\n {% endif %}\n\n {% assign variant_sku_generated = variant_sku_generated | append: \"-\" | append: variant_option_abbreviation %}\n {% endif %}\n {% endfor %}\n\n {% if variant_sku_generated != variant.sku %}\n {% log message: \"updating sku\", original_sku: variant.sku, generated_sku: variant_sku_generated %}\n\n {% comment %}\n -- update skus on inventory items, as this is no longer supported on variants as of 2024-07 API\n {% endcomment %}\n\n {% action \"shopify\" %}\n mutation {\n inventoryItemUpdate(\n id: {{ variant.inventory_item.admin_graphql_api_id | json }}\n input: {\n sku: {{ variant_sku_generated | json }}\n }\n ) {\n inventoryItem {\n variant {\n id\n displayName\n sku\n }\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n{% endfor %}\n", + "script": "{% assign skip_variants_that_already_have_skus = options.skip_variants_that_already_have_skus__boolean %}\n{% assign product_options_to_keep_unabbreviated = options.product_options_to_keep_unabbreviated__array %}\n\n{% assign allowed_characters = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\" %}\n\n{% assign sku_base = product.handle | split: \"-\" | last | upcase %}\n\n{% if event.preview %}\n {% assign sku_base = \"503\" %}\n{% endif %}\n\n{% assign cursor = nil %}\n{% assign variants = array %}\n\n{% for n in (1..8) %}\n {% capture query %}\n query {\n product(id: {{ product.admin_graphql_api_id | json }}) {\n id\n handle\n variants(\n first: 250\n after: {{ cursor | json }}\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n inventoryItem {\n id\n }\n sku\n selectedOptions {\n name\n value\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = query | shopify %}\n\n {% if event.preview %}\n {% capture result_json %}\n {\n \"data\": {\n \"product\": {\n \"variants\": {\n \"nodes\": [\n {\n \"inventoryItem\": {\n \"id\": \"gid://shopify/InventoryItem/1234567890\"\n },\n {% unless skip_variants_that_already_have_skus %}\"sku\": \"503-XL-G\",{% endunless %}\n \"selectedOptions\": [\n {\n \"name\": \"Size\",\n \"value\": \"XL\"\n },\n {\n \"name\": \"Color\",\n \"value\": \"Heather Grey\"\n }\n ]\n }\n ]\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n {% endif %}\n\n {% assign variants = variants | concat: result.data.product.variants.nodes %}\n\n {% if result.data.product.variants.pageInfo.hasNextPage %}\n {% assign cursor = result.data.product.variants.pageInfo.endCursor %}\n {% else %}\n {% break %}\n {% endif %}\n{% endfor %}\n\n{% for variant in variants %}\n {% if skip_variants_that_already_have_skus and variant.sku != blank %}\n {% log\n message: \"variant already has a sku; skipping\",\n original_sku: variant.sku\n %}\n {% continue %}\n {% endif %}\n\n {% assign variant_sku_generated = sku_base %}\n\n {% for selected_option in variant.selectedOptions %}\n {% if product_options_to_keep_unabbreviated contains selected_option.name %}\n {% assign variant_sku_generated = variant_sku_generated | append: \"-\" | append: selected_option.value %}\n\n {% else %}\n {% assign variant_option_abbreviation = \"\" %}\n {% assign variant_option_characters = selected_option.value | split: \"\" %}\n\n {% for variant_option_character in variant_option_characters %}\n {% if allowed_characters contains variant_option_character %}\n {% assign variant_option_abbreviation = variant_option_abbreviation | append: variant_option_character %}\n {% endif %}\n {% endfor %}\n\n {% if variant_option_abbreviation == blank %}\n {% assign variant_option_abbreviation = variant_option_characters[0] | upcase %}\n {% endif %}\n\n {% assign variant_sku_generated = variant_sku_generated | append: \"-\" | append: variant_option_abbreviation %}\n {% endif %}\n {% endfor %}\n\n {% if variant_sku_generated != variant.sku %}\n {% log\n message: \"updating sku\",\n original_sku: variant.sku,\n generated_sku: variant_sku_generated\n %}\n\n {% comment %}\n -- as of the 2024-07 API, SKUs are updated on inventory items instead of variants\n {% endcomment %}\n\n {% action \"shopify\" %}\n mutation {\n inventoryItemUpdate(\n id: {{ variant.inventoryItem.id | json }}\n input: {\n sku: {{ variant_sku_generated | json }}\n }\n ) {\n inventoryItem {\n variant {\n id\n displayName\n sku\n }\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n{% endfor %}\n", "subscriptions": [ "shopify/products/create", "shopify/products/update" diff --git a/tasks/auto-tag-orders-by-product-collections.json b/tasks/auto-tag-orders-by-product-collections.json index 4fa327ea..e7c6f9fe 100644 --- a/tasks/auto-tag-orders-by-product-collections.json +++ b/tasks/auto-tag-orders-by-product-collections.json @@ -11,7 +11,7 @@ }, "order_status_javascript": null, "perform_action_runs_in_sequence": false, - "script": "{% if event.topic contains \"shopify/orders/\" %}\n {% assign existing_tags = order.tags | split: \", \" %}\n {% assign tags_to_add = array %}\n {% assign order_collections = array %}\n\n {% for line_item in order.line_items %}\n {% if line_item.product != blank %}\n {% assign order_collections = order_collections | concat: line_item.product.collections %}\n {% endif %}\n {% endfor %}\n\n {% assign order_collection_ids = order_collections | map: \"id\" | join: \",\" | split: \",\" %}\n {% assign order_collection_titles = order_collections | map: \"title\" %}\n {% assign order_collection_handles = order_collections | map: \"handle\" %}\n\n {% for pair in options.collections_and_tags__keyval_required %}\n {% assign required_collection = pair[0] %}\n {% assign tags = pair[1] | split: \",\" %}\n\n {% if order_collection_ids contains required_collection\n or order_collection_titles contains required_collection\n or order_collection_handles contains required_collection\n %}\n {% for tag in tags %}\n {% assign stripped_tag = tag | strip %}\n {% unless existing_tags contains stripped_tag %}\n {% assign tags_to_add[tags_to_add.size] = stripped_tag %}\n {% endunless %}\n {% endfor %}\n {% endif %}\n {% endfor %}\n\n {% if event.preview %}\n {% assign tags_to_add[0] = options.collections_and_tags__keyval_required.first.last %}\n {% endif %}\n\n {% if tags_to_add != empty %}\n {% action \"shopify\" %}\n mutation {\n tagsAdd(\n id: {{ order.admin_graphql_api_id | json }}\n tags: {{ tags_to_add | uniq | json }}\n ) {\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n\n{% elsif event.topic == \"mechanic/user/trigger\" %}\n {% capture bulk_operation_query %}\n query {\n orders {\n edges {\n node {\n __typename\n id\n tags\n lineItems {\n edges {\n node {\n __typename\n id\n product {\n id\n collections {\n edges {\n node {\n __typename\n id\n legacyResourceId\n title\n handle\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% action \"shopify\" %}\n mutation {\n bulkOperationRunQuery(\n query: {{ bulk_operation_query | json }}\n ) {\n bulkOperation {\n id\n status\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n\n{% elsif event.topic == \"mechanic/shopify/bulk_operation\" %}\n {% if event.preview %}\n {% capture bulkOperation_objects_jsonl %}\n {\"__typename\":\"Order\",\"id\":\"gid:\\/\\/shopify\\/Order\\/1234567890\",\"tags\":[]}\n {% endcapture %}\n\n {% assign bulkOperation = hash %}\n {% assign bulkOperation[\"objects\"] = bulkOperation_objects_jsonl | parse_jsonl %}\n {% endif %}\n\n {% assign orders = bulkOperation.objects | where: \"__typename\", \"Order\" %}\n {% assign line_items = bulkOperation.objects | where: \"__typename\", \"LineItem\" %}\n {% assign collections = bulkOperation.objects | where: \"__typename\", \"Collection\" %}\n\n {% for order in orders %}\n {% assign tags_to_add = array %}\n {% assign order_collections = nil %}\n\n {% assign order_line_items = line_items | where: \"__parentId\", order.id %}\n\n {% for line_item in order_line_items %}\n {% assign line_item_collections = collections | where: \"__parentId\", line_item.id %}\n {% assign order_collections = order_collections | concat: line_item_collections %}\n {% endfor %}\n\n {% assign order_collections = order_collections | compact %}\n {% assign order_collection_ids = order_collections | map: \"legacyResourceId\" %}\n {% assign order_collection_titles = order_collections | map: \"title\" %}\n {% assign order_collection_handles = order_collections | map: \"handle\" %}\n\n {% for pair in options.collections_and_tags__keyval_required %}\n {% assign required_collection = pair[0] %}\n {% assign tags = pair[1] | split: \",\" %}\n\n {% if order_collection_ids contains required_collection\n or order_collection_titles contains required_collection\n or order_collection_handles contains required_collection\n %}\n {% for tag in tags %}\n {% assign stripped_tag = tag | strip %}\n {% unless order.tags contains stripped_tag %}\n {% assign tags_to_add[tags_to_add.size] = stripped_tag %}\n {% endunless %}\n {% endfor %}\n {% endif %}\n {% endfor %}\n\n {% if event.preview %}\n {% assign tags_to_add[0] = options.collections_and_tags__keyval_required.first.last %}\n {% endif %}\n\n {% if tags_to_add != empty %}\n {% action \"shopify\" %}\n mutation {\n tagsAdd(\n id: {{ order.id | json }}\n tags: {{ tags_to_add | uniq | json }}\n ) {\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n {% endfor %}\n{% endif %}", + "script": "{% assign collections_and_tags = options.collections_and_tags__keyval_required %}\n\n{% if event.topic == \"shopify/orders/create\" %}\n {% assign existing_tags = order.tags | split: \", \" %}\n\n {% comment %}\n -- get product IDs from order line items to build the collections search query\n {% endcomment %}\n\n {% assign order_product_ids = order.line_items | map: \"product_id\" | compact | uniq %}\n\n {% if order_product_ids == blank %}\n {% unless event.preview %}\n {% log \"This order does not contain any products from the shop catalog.\" %}\n {% break %}\n {% endunless %}\n {% endif %}\n\n {% assign search_query_parts = array %}\n\n {% for order_product_id in order_product_ids %}\n {% assign search_query_parts[search_query_parts.size] = order_product_id | prepend: \"product_id:\" %}\n {% endfor %}\n\n {% assign search_query = search_query_parts | join: \" OR \" %}\n\n {% unless event.preview %}\n {% log search_query: search_query %}\n {% endunless %}\n\n {% comment %}\n -- get collections that contain any of this order's products\n {% endcomment %}\n\n {% assign cursor = nil %}\n {% assign order_collections = array %}\n\n {% for n in (1..10) %}\n {% capture query %}\n query {\n collections(\n first: 250\n after: {{ cursor | json }}\n query: {{ search_query | json }}\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n id\n legacyResourceId\n title\n handle\n }\n }\n }\n {% endcapture %}\n\n {% assign result = query | shopify %}\n\n {% if event.preview %}\n {% capture result_json %}\n {\n \"data\": {\n \"collections\": {\n \"nodes\": [\n {\n \"id\": \"gid://shopify/Collection/1234567890\",\n \"legacyResourceId\": \"1234567890\",\n \"title\": \"Best Widgets\",\n \"handle\": \"best-widgets\"\n }\n ]\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n {% endif %}\n\n {% assign order_collections = order_collections | concat: result.data.collections.nodes %}\n\n {% if result.data.collections.pageInfo.hasNextPage %}\n {% assign cursor = result.data.collections.pageInfo.endCursor %}\n {% else %}\n {% break %}\n {% endif %}\n {% endfor %}\n\n {% if order_collections == blank %}\n {% unless event.preview %}\n {% log \"This order does not contain any products that belong to any collections.\" %}\n {% break %}\n {% endunless %}\n {% endif %}\n\n {% comment %}\n -- save the collection IDs, titles, and handles for comparison against the configured collections\n {% endcomment %}\n\n {% assign order_collection_ids = order_collections | map: \"legacyResourceId\" %}\n {% assign order_collection_titles = order_collections | map: \"title\" %}\n {% assign order_collection_handles = order_collections | map: \"handle\" %}\n\n {% log\n order_collection_ids: order_collection_ids,\n order_collection_titles: order_collection_titles,\n order_collection_handles: order_collection_handles\n %}\n\n {% comment %}\n -- add order tags for any matched collections\n {% endcomment %}\n\n {% assign tags_to_add = array %}\n\n {% for pair in collections_and_tags %}\n {% assign required_collection = pair[0] %}\n {% assign tags = pair[1] | split: \",\" %}\n\n {% if order_collection_ids contains required_collection\n or order_collection_titles contains required_collection\n or order_collection_handles contains required_collection\n %}\n {% for tag in tags %}\n {% assign stripped_tag = tag | strip %}\n\n {% unless existing_tags contains stripped_tag %}\n {% assign tags_to_add = tags_to_add | push: stripped_tag %}\n {% endunless %}\n {% endfor %}\n {% endif %}\n {% endfor %}\n\n {% if event.preview %}\n {% assign tags_to_add[0] = collections_and_tags.first.last %}\n {% endif %}\n\n {% if tags_to_add != blank %}\n {% action \"shopify\" %}\n mutation {\n tagsAdd(\n id: {{ order.admin_graphql_api_id | json }}\n tags: {{ tags_to_add | uniq | json }}\n ) {\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n\n{% elsif event.topic == \"mechanic/user/trigger\" %}\n {% capture bulk_operation_query %}\n query {\n orders {\n edges {\n node {\n __typename\n id\n tags\n lineItems {\n edges {\n node {\n __typename\n id\n product {\n id\n collections {\n edges {\n node {\n __typename\n id\n legacyResourceId\n title\n handle\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% action \"shopify\" %}\n mutation {\n bulkOperationRunQuery(\n query: {{ bulk_operation_query | json }}\n ) {\n bulkOperation {\n id\n status\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n\n{% elsif event.topic == \"mechanic/shopify/bulk_operation\" %}\n {% if event.preview %}\n {% capture bulkOperation_objects_jsonl %}\n {\"__typename\":\"Order\",\"id\":\"gid:\\/\\/shopify\\/Order\\/1234567890\",\"tags\":[]}\n {% endcapture %}\n\n {% assign bulkOperation = hash %}\n {% assign bulkOperation[\"objects\"] = bulkOperation_objects_jsonl | parse_jsonl %}\n {% endif %}\n\n {% assign orders = bulkOperation.objects | where: \"__typename\", \"Order\" %}\n {% assign line_items = bulkOperation.objects | where: \"__typename\", \"LineItem\" %}\n {% assign collections = bulkOperation.objects | where: \"__typename\", \"Collection\" %}\n\n {% for order in orders %}\n {% assign tags_to_add = array %}\n {% assign order_collections = nil %}\n\n {% assign order_line_items = line_items | where: \"__parentId\", order.id %}\n\n {% for line_item in order_line_items %}\n {% assign line_item_collections = collections | where: \"__parentId\", line_item.id %}\n {% assign order_collections = order_collections | concat: line_item_collections %}\n {% endfor %}\n\n {% assign order_collections = order_collections | compact %}\n {% assign order_collection_ids = order_collections | map: \"legacyResourceId\" %}\n {% assign order_collection_titles = order_collections | map: \"title\" %}\n {% assign order_collection_handles = order_collections | map: \"handle\" %}\n\n {% for pair in collections_and_tags %}\n {% assign required_collection = pair[0] %}\n {% assign tags = pair[1] | split: \",\" %}\n\n {% if order_collection_ids contains required_collection\n or order_collection_titles contains required_collection\n or order_collection_handles contains required_collection\n %}\n {% for tag in tags %}\n {% assign stripped_tag = tag | strip %}\n\n {% unless order.tags contains stripped_tag %}\n {% assign tags_to_add[tags_to_add.size] = stripped_tag %}\n {% endunless %}\n {% endfor %}\n {% endif %}\n {% endfor %}\n\n {% if event.preview %}\n {% assign tags_to_add[0] = collections_and_tags.first.last %}\n {% endif %}\n\n {% if tags_to_add != blank %}\n {% action \"shopify\" %}\n mutation {\n tagsAdd(\n id: {{ order.id | json }}\n tags: {{ tags_to_add | uniq | json }}\n ) {\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n {% endfor %}\n{% endif %}\n", "subscriptions": [ "shopify/orders/create", "mechanic/user/trigger", diff --git a/tasks/auto-tag-products-by-their-options.json b/tasks/auto-tag-products-by-their-options.json index 410c043a..42ce87a3 100644 --- a/tasks/auto-tag-products-by-their-options.json +++ b/tasks/auto-tag-products-by-their-options.json @@ -1,5 +1,5 @@ { - "docs": "Use this task to add tags to your products, based on their options, for easy filtering. A shirt might be tagged with \"Color-Blue\" and \"Size-XL\", for example. Optionally, choose to ignore options that only appear on out-of-stock variants, or choose to apply all tags in lowercase.\n\nUse this task to add tags to your products, based on their options, for easy filtering. A shirt might be tagged with \"Color-Blue\" and \"Size-XL\", for example.\n\nOptionally, choose to ignore options that only appear on out-of-stock variants, or choose to apply all tags in lowercase.\n\nChange the separator to change the way tags are built. Using a dash results in \"Color-Blue\", an underscore results in \"Color_Blue\", and a colon with a space yields \"Color: Blue\".\n\nThis task will remove option tags that are no longer applicable, by scanning for tag prefixes using the list of product options to consider.", + "docs": "Use this task to add tags to your products, based on their options, for easy filtering. A shirt might be tagged with \"Color-Blue\" and \"Size-XL\", for example. Optionally, choose to ignore options that only appear on out-of-stock variants, or choose to apply all tags in lowercase.\n\nChange the separator to change the way tags are built. Using a dash results in \"Color-Blue\", an underscore results in \"Color_Blue\", and a colon with a space yields \"Color: Blue\".\n\nThis task will remove option tags that are no longer applicable, by scanning for tag prefixes using the list of product options to consider.", "halt_action_run_sequence_on_error": false, "name": "Auto-tag products by their options", "online_store_javascript": null, @@ -17,7 +17,7 @@ }, "order_status_javascript": null, "perform_action_runs_in_sequence": false, - "script": "{% comment %}\n Preferred option order:\n\n {{ options.product_options_to_consider__array_required }}\n {{ options.option_name_and_value_separator__required }}\n {{ options.use_lowercase_tags__boolean }}\n {{ options.ignore_variants_that_are_out_of_stock__boolean }}\n {{ options.run_for_all_products_daily__boolean }}\n {{ options.run_for_individual_products_when_they_are_created_or_updated__boolean }}\n {{ options.run_for_individual_products_when_their_inventory_changes__boolean }}\n{% endcomment %}\n\n{% assign jobs = array %}\n\n{% assign separator = options.option_name_and_value_separator__required %}\n\n{% assign lowercase_product_options_to_consider = array %}\n{% for option in options.product_options_to_consider__array_required %}\n {% assign lowercase_product_options_to_consider[lowercase_product_options_to_consider.size] = option | downcase %}\n{% endfor %}\n\n{% if options.use_lowercase_tags__boolean %}\n {% assign product_options_to_consider = lowercase_product_options_to_consider %}\n{% else %}\n {% assign product_options_to_consider = options.product_options_to_consider__array_required %}\n{% endif %}\n\n{% if event.topic contains \"shopify/products/\" or event.topic contains \"shopify/inventory_levels/\" %}\n {% if event.topic contains \"shopify/inventory_levels/\" %}\n {% capture query %}\n query {\n inventoryLevel(id: {{ inventory_level.admin_graphql_api_id | json }}) {\n item {\n variant {\n product {\n legacyResourceId\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = query | shopify %}\n\n {% assign product = shop.products[result.data.inventoryLevel.item.variant.product.legacyResourceId] %}\n {% endif %}\n\n {% if event.preview %}\n {% assign product = hash %}\n {% assign product[\"admin_graphql_api_id\"] = \"gid://shopify/Product/1234567890\" %}\n {% assign product[\"tags\"] = \"foo, bar, baz, \" | append: options.product_options_to_consider__array_required.first | append: separator | append: \"Red, \" | append: options.product_options_to_consider__array_required.first | append: separator | append: \"Green\" %}\n {% endif %}\n\n {% assign jobs[0] = hash %}\n {% assign jobs[0][\"product_id\"] = product.admin_graphql_api_id %}\n {% assign jobs[0][\"existing_tags\"] = product.tags | split: \", \" %}\n {% assign jobs[0][\"selected_options_found\"] = array %}\n\n {% assign cursor = nil %}\n\n {% for n in (0..100) %}\n {% capture query %}\n query {\n product(id: {{ product.admin_graphql_api_id | json }}) {\n variants(\n first: 250\n after: {{ cursor | json }}\n ) {\n pageInfo {\n hasNextPage\n }\n edges {\n cursor\n node {\n inventoryQuantity\n selectedOptions {\n name\n value\n }\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = query | shopify %}\n\n {% if event.preview %}\n {% capture result_json %}\n {\n \"data\": {\n \"product\": {\n \"variants\": {\n \"edges\": [\n {\n \"node\": {\n \"inventoryQuantity\": 0,\n \"selectedOptions\": [\n {\n \"name\": {{ options.product_options_to_consider__array_required.first | json }},\n \"value\": \"Green\"\n }\n ]\n }\n },\n {\n \"node\": {\n \"inventoryQuantity\": 10,\n \"selectedOptions\": [\n {\n \"name\": \"Title\",\n \"value\": \"Default Title\"\n },\n {\n \"name\": {{ options.product_options_to_consider__array_required.first | json }},\n \"value\": \"Blue\"\n }\n ]\n }\n }\n ]\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n {% endif %}\n\n {% for variant_edge in result.data.product.variants.edges %}\n {% assign variant = variant_edge.node %}\n\n {% if options.ignore_variants_that_are_out_of_stock__boolean and variant.inventoryQuantity <= 0 %}\n {% continue %}\n {% endif %}\n\n {% assign jobs[0][\"selected_options_found\"] = jobs[0][\"selected_options_found\"] | concat: variant.selectedOptions %}\n {% endfor %}\n\n {% if result.data.product.variants.pageInfo.hasNextPage %}\n {% assign cursor = result.data.product.variants.edges.last.cursor %}\n {% else %}\n {% break %}\n {% endif %}\n {% endfor %}\n{% elsif event.topic == \"mechanic/user/trigger\" or event.topic contains \"mechanic/scheduler/\" %}\n {% capture bulk_operation_query %}\n query {\n products {\n edges {\n node {\n __typename\n id\n tags\n variants {\n edges {\n node {\n __typename\n inventoryQuantity\n selectedOptions {\n name\n value\n }\n }\n }\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% action \"shopify\" %}\n mutation {\n bulkOperationRunQuery(\n query: {{ bulk_operation_query | json }}\n ) {\n bulkOperation {\n id\n status\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n{% elsif event.topic == \"mechanic/shopify/bulk_operation\" %}\n {% if event.preview %}\n {% capture bulkOperation_json %}\n {\n \"objects\": [\n {\n \"__typename\": \"Product\",\n \"id\": \"gid://shopify/Product/1234567890\",\n \"tags\": [\n \"foo\",\n \"bar\",\n \"baz\",\n {{ options.product_options_to_consider__array_required.first | append: separator | append: \"Red\" | json }},\n {{ options.product_options_to_consider__array_required.first | append: separator | append: \"Green\" | json }}\n ]\n },\n {\n \"__typename\": \"ProductVariant\",\n \"inventoryQuantity\": 0,\n \"selectedOptions\": [\n {\n \"name\": {{ options.product_options_to_consider__array_required.first | json }},\n \"value\": \"Green\"\n }\n ],\n \"__parentId\": \"gid://shopify/Product/1234567890\"\n },\n {\n \"__typename\": \"ProductVariant\",\n \"inventoryQuantity\": 10,\n \"selectedOptions\": [\n {\n \"name\": \"Title\",\n \"value\": \"Default Title\"\n },\n {\n \"name\": {{ options.product_options_to_consider__array_required.first | json }},\n \"value\": \"Blue\"\n }\n ],\n \"__parentId\": \"gid://shopify/Product/1234567890\"\n }\n ]\n }\n {% endcapture %}\n\n {% assign bulkOperation = bulkOperation_json | parse_json %}\n {% endif %}\n\n {% assign products = bulkOperation.objects | where: \"__typename\", \"Product\" %}\n {% assign variants = bulkOperation.objects | where: \"__typename\", \"ProductVariant\" %}\n\n {% for product in products %}\n {% assign job = hash %}\n {% assign job[\"product_id\"] = product.id %}\n {% assign job[\"existing_tags\"] = product.tags %}\n {% assign job[\"selected_options_found\"] = array %}\n\n {% assign product_variants = variants | where: \"__parentId\", product.id %}\n {% for product_variant in product_variants %}\n {% if options.ignore_variants_that_are_out_of_stock__boolean and product_variant.inventoryQuantity <= 0 %}\n {% continue %}\n {% endif %}\n\n {% assign job[\"selected_options_found\"] = job[\"selected_options_found\"] | concat: product_variant.selectedOptions %}\n {% endfor %}\n\n {% assign jobs[jobs.size] = job %}\n {% endfor %}\n{% endif %}\n\n{% log jobs: jobs %}\n\n{% for job in jobs %}\n {% assign tags_to_add = array %}\n {% assign tags_to_remove = array %}\n\n {% assign applicable_tags = array %}\n {% for selected_option in job.selected_options_found %}\n {% assign lowercase_selected_option_name = selected_option.name | downcase %}\n {% unless lowercase_product_options_to_consider contains lowercase_selected_option_name %}\n {% continue %}\n {% endunless %}\n\n {% assign tag = selected_option.name | append: separator | append: selected_option.value %}\n\n {% if options.use_lowercase_tags__boolean %}\n {% assign tag = tag | downcase %}\n {% endif %}\n\n {% assign applicable_tags[applicable_tags.size] = tag %}\n {% endfor %}\n\n {% for tag in job.existing_tags %}\n {% assign lowercase_tag_prefix = tag | split: separator | first | downcase %}\n\n {% if lowercase_product_options_to_consider contains lowercase_tag_prefix %}\n {% unless applicable_tags contains tag %}\n {% assign tags_to_remove[tags_to_remove.size] = tag %}\n {% endunless %}\n {% endif %}\n {% endfor %}\n\n {% for tag in applicable_tags %}\n {% unless job.existing_tags contains tag %}\n {% assign tags_to_add[tags_to_add.size] = tag %}\n {% endunless %}\n {% endfor %}\n\n {% if tags_to_add != blank or tags_to_remove != blank %}\n {% action \"shopify\" %}\n mutation {\n {% if tags_to_remove != blank %}\n tagsRemove(\n id: {{ job.product_id | json }}\n tags: {{ tags_to_remove | json }}\n ) {\n userErrors {\n field\n message\n }\n node {\n ... on Product {\n onlineStoreUrl\n tags\n }\n }\n }\n {% endif %}\n {% if tags_to_add != blank %}\n tagsAdd(\n id: {{ job.product_id | json }}\n tags: {{ tags_to_add | json }}\n ) {\n userErrors {\n field\n message\n }\n node {\n ... on Product {\n onlineStoreUrl\n tags\n }\n }\n }\n {% endif %}\n }\n {% endaction %}\n {% endif %}\n{% endfor %}", + "script": "{% assign product_options_to_consider = options.product_options_to_consider__array_required %}\n{% assign option_name_and_value_separator = options.option_name_and_value_separator__required %}\n{% assign use_lowercase_tags = options.use_lowercase_tags__boolean %}\n{% assign ignore_variants_that_are_out_of_stock = options.ignore_variants_that_are_out_of_stock__boolean %}\n{% assign run_for_all_products_daily = options.run_for_all_products_daily__boolean %}\n\n{% comment %}\n -- use lowercase for option comparisons to capture variations in usage\n{% endcomment %}\n\n{% assign lowercase_product_options_to_consider\n = product_options_to_consider\n | json\n | downcase\n | parse_json\n%}\n\n{% assign jobs = array %}\n\n{% if event.topic contains \"shopify/products/\" or event.topic contains \"shopify/inventory_levels/\" %}\n {% comment %}\n -- for any of these events, query GraphQL to get single product object before paginating variants\n {% endcomment %}\n\n {% if event.topic contains \"shopify/products/\" %}\n {% capture query %}\n query {\n product(id: {{ product.admin_graphql_api_id | json }}) {\n id\n tags\n }\n }\n {% endcapture %}\n\n {% assign result = query | shopify %}\n\n {% assign product = result.data.product %}\n\n {% elsif event.topic contains \"shopify/inventory_levels/\" %}\n {% capture query %}\n query {\n inventoryLevel(id: {{ inventory_level.admin_graphql_api_id | json }}) {\n item {\n variant {\n product {\n id\n tags\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = query | shopify %}\n\n {% assign product = result.data.inventoryLevel.item.variant.product %}\n {% endif %}\n\n {% assign jobs[0] = hash %}\n {% assign jobs[0][\"product_id\"] = product.id %}\n {% assign jobs[0][\"existing_tags\"] = product.tags %}\n {% assign jobs[0][\"selected_options_found\"] = array %}\n\n {% comment %}\n -- get option names and values at variant level in case \"ignore_variants_that_are_out_of_stock\" is enabled\n -- paginate to support up to 2K variants\n {% endcomment %}\n\n {% assign cursor = nil %}\n {% assign selected_options_found = array %}\n\n {% for n in (1..8) %}\n {% capture query %}\n query {\n product(id: {{ product.id | json }}) {\n variants(\n first: 250\n after: {{ cursor | json }}\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n inventoryQuantity\n selectedOptions {\n name\n value\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = query | shopify %}\n\n {% for variant in result.data.product.variants.nodes %}\n {% if ignore_variants_that_are_out_of_stock and variant.inventoryQuantity <= 0 %}\n {% continue %}\n {% endif %}\n\n {% assign selected_options_found\n = variant.selectedOptions\n | default: array\n | concat: selected_options_found\n %}\n {% endfor %}\n\n {% if result.data.product.variants.pageInfo.hasNextPage %}\n {% assign cursor = result.data.product.variants.pageInfo.endCursor %}\n {% else %}\n {% break %}\n {% endif %}\n {% endfor %}\n\n {% assign jobs[0][\"selected_options_found\"] = selected_options_found | uniq %}\n\n{% elsif event.topic == \"mechanic/user/trigger\" or event.topic contains \"mechanic/scheduler/\" %}\n {% capture bulk_operation_query %}\n query {\n products {\n edges {\n node {\n __typename\n id\n tags\n variants {\n edges {\n node {\n __typename\n inventoryQuantity\n selectedOptions {\n name\n value\n }\n }\n }\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% action \"shopify\" %}\n mutation {\n bulkOperationRunQuery(\n query: {{ bulk_operation_query | json }}\n ) {\n bulkOperation {\n id\n status\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n\n {% break %}\n\n{% elsif event.topic == \"mechanic/shopify/bulk_operation\" %}\n {% assign products = bulkOperation.objects | where: \"__typename\", \"Product\" %}\n {% assign variants = bulkOperation.objects | where: \"__typename\", \"ProductVariant\" %}\n\n {% for product in products %}\n {% assign job = hash %}\n {% assign job[\"product_id\"] = product.id %}\n {% assign job[\"existing_tags\"] = product.tags %}\n {% assign job[\"selected_options_found\"] = array %}\n\n {% assign product_variants = variants | where: \"__parentId\", product.id %}\n\n {% for product_variant in product_variants %}\n {% if ignore_variants_that_are_out_of_stock and product_variant.inventoryQuantity <= 0 %}\n {% continue %}\n {% endif %}\n\n {% assign job[\"selected_options_found\"]\n = product_variant.selectedOptions\n | default: array\n | concat: job[\"selected_options_found\"]\n | uniq\n %}\n {% endfor %}\n\n {% assign jobs[jobs.size] = job %}\n {% endfor %}\n{% endif %}\n\n{% comment %}\n -- single preview data instance to handle culmination of each event type above\n{% endcomment %}\n\n{% if event.preview %}\n {% capture jobs_json %}\n [\n {\n \"product_id\": \"gid://shopify/Product/1234567890\",\n \"existing_tags\": [\n \"foo\",\n \"bar\",\n \"baz\",\n {{ product_options_to_consider.first | append: option_name_and_value_separator | append: \"Red\" | json }},\n {{ product_options_to_consider.first | append: option_name_and_value_separator | append: \"Green\" | json }}\n ],\n \"selected_options_found\": [\n {\n \"name\": {{ product_options_to_consider.first | json }},\n \"value\": \"Green\"\n },\n {\n \"name\": \"Title\",\n \"value\": \"Default Title\"\n },\n {\n \"name\": {{ product_options_to_consider.first | json }},\n \"value\": \"Blue\"\n }\n ]\n }\n ]\n {% endcapture %}\n\n {% assign jobs = jobs_json | parse_json %}\n{% endif %}\n\n{% comment %}\n -- check tags and selected options of each product to make tagging decisions\n{% endcomment %}\n\n{% for job in jobs %}\n {% assign tags_to_add = array %}\n {% assign tags_to_remove = array %}\n\n {% assign applicable_tags = array %}\n\n {% for selected_option in job.selected_options_found %}\n {% assign lowercase_selected_option_name = selected_option.name | downcase %}\n\n {% unless lowercase_product_options_to_consider contains lowercase_selected_option_name %}\n {% continue %}\n {% endunless %}\n\n {% assign tag = selected_option.name | append: option_name_and_value_separator | append: selected_option.value %}\n\n {% if use_lowercase_tags %}\n {% assign tag = tag | downcase %}\n {% endif %}\n\n {% assign applicable_tags[applicable_tags.size] = tag %}\n {% endfor %}\n\n {% for tag in job.existing_tags %}\n {% assign lowercase_tag_prefix = tag | split: option_name_and_value_separator | first | downcase %}\n\n {% if lowercase_product_options_to_consider contains lowercase_tag_prefix %}\n {% unless applicable_tags contains tag %}\n {% assign tags_to_remove[tags_to_remove.size] = tag %}\n {% endunless %}\n {% endif %}\n {% endfor %}\n\n {% for tag in applicable_tags %}\n {% unless job.existing_tags contains tag %}\n {% assign tags_to_add[tags_to_add.size] = tag %}\n {% endunless %}\n {% endfor %}\n\n {% if tags_to_add != blank or tags_to_remove != blank %}\n {% action \"shopify\" %}\n mutation {\n {% if tags_to_remove != blank %}\n tagsRemove(\n id: {{ job.product_id | json }}\n tags: {{ tags_to_remove | json }}\n ) {\n userErrors {\n field\n message\n }\n node {\n ... on Product {\n id\n tags\n }\n }\n }\n {% endif %}\n {% if tags_to_add != blank %}\n tagsAdd(\n id: {{ job.product_id | json }}\n tags: {{ tags_to_add | json }}\n ) {\n userErrors {\n field\n message\n }\n node {\n ... on Product {\n id\n tags\n }\n }\n }\n {% endif %}\n }\n {% endaction %}\n {% endif %}\n{% endfor %}\n", "subscriptions": [ "mechanic/user/trigger", "mechanic/shopify/bulk_operation", diff --git a/tasks/maintain-a-collection-of-new-products.json b/tasks/maintain-a-collection-of-new-products.json index 0f3b3f4f..4fcb97ab 100644 --- a/tasks/maintain-a-collection-of-new-products.json +++ b/tasks/maintain-a-collection-of-new-products.json @@ -1,5 +1,5 @@ { - "docs": "Use this task to maintain a \"New Products\" collection, specifying either a number of products to include or the number of days to keep each product around. Easy! :)\n\nTo use this task, create a manual collection, and add the collection ID to the task options. (Find the ID by opening the collection in the Shopify admin, then looking at the URL in your browser's address bar. If the URL is https://example.myshopify.com/admin/collections/12345, the collection ID is 12345.)\r\n\r\nUse the \"Run task\" button to populate your collection for the first time. After that, this task will run daily, at midnight in your local timezone. During each run, the task will update the collection, adding new products and removing old ones as appropriate.", + "docs": "Use this task to maintain a \"New Products\" collection, specifying either a number of products to include or the number of days to keep each product around. Easy! :)\n\nTo use this task, create a manual collection, and add the collection ID to the task options. (See [finding a resource ID](https://learn.mechanic.dev/techniques/finding-a-resource-id) for help locating the collection ID.)\n\nUse the \"Run task\" button to populate your collection for the first time. After that, this task will run daily, at midnight in your local timezone. During each run, the task will update the collection, adding new products and removing old ones as appropriate.", "halt_action_run_sequence_on_error": false, "name": "Maintain a collection of new products", "online_store_javascript": null, @@ -10,7 +10,7 @@ }, "order_status_javascript": null, "perform_action_runs_in_sequence": false, - "script": "{% assign collection = shop.collections[options.collection_id__number_required] %}\n{% assign days_to_keep_products = options.number_of_days_to_keep_a_product_in_this_collection__number %}\n{% assign product_count = options.number_of_products_to_keep_in_this_collection__number %}\n\n{% if event.preview %}\n {% capture collection_json %}\n {\n \"admin_graphql_api_id\": \"gid://shopify/Collection/1234567890\",\n \"products\": [\n {\n \"admin_graphql_api_id\": \"gid://shopify/Product/9876543210\"\n }\n ]\n }\n {% endcapture %}\n\n {% assign collection = collection_json | parse_json %}\n{% endif %}\n\n{% if collection == blank %}\n {% error \"Configured collection ID does not exist in this shop.\" %}\n{% endif %}\n\n{% if days_to_keep_products == blank and product_count == blank %}\n {% error %}\n \"Please fill in either \\\"Number of days to keep a product in this collection\\\" or \\\"Number of products to keep in this collection\\\" (but not both!).\"\n {% enderror %}\n{% elsif days_to_keep_products != blank and product_count != blank %}\n {% error \"Please choose one of the options, but not both! :)\" %}\n{% endif %}\n\n{% assign loop_count = 100 %}\n{% assign first_elements = 250 %}\n{% assign products_query = \"status:active\" %}\n\n{% if days_to_keep_products != blank %}\n {% if days_to_keep_products < 1 %}\n {% error %}\n \"\\\"Number of days to keep a product in this collection\\\" should be > 0\"\n {% enderror %}\n {% endif %}\n\n {% assign now_s = \"now\" | date: \"%s\" | times: 1 %}\n {% assign days_to_keep_products_s = days_to_keep_products | times: 86400 %}\n {% assign created_at_threshold_s = now_s | minus: days_to_keep_products_s %}\n {% assign created_at_threshold = created_at_threshold_s | date: \"%Y-%m-%d\" %}\n {% assign products_query = products_query\n | append: \" created_at:>=\"\n | append: created_at_threshold\n %}\n{% elsif product_count != blank %}\n {% if product_count < 1 %}\n {% error %}\n \"\\\"Number of products to keep in this collection\\\" should be > 0\"\n {% enderror %}\n {% endif %}\n\n {% assign loop_count = product_count | divided_by: 250 %}\n {% assign remaining = product_count | modulo: 250 %}\n{% endif %}\n\n{% assign cursor = nil %}\n{% assign qualifying_product_ids = array %}\n\n{% for n in (0..loop_count) %}\n {% if product_count != blank %}\n {% if forloop.last %}\n {% if remaining == 0 %}\n {% break %}\n {% endif %}\n\n {% assign first_elements = remaining %}\n {% endif %}\n {% endif %}\n\n {% capture query %}\n query {\n products(\n first: {{ first_elements }}\n after: {{ cursor | json }}\n query: {{ products_query | json }}\n reverse: true\n sortKey: CREATED_AT\n ) {\n pageInfo {\n hasNextPage\n }\n edges {\n cursor\n node {\n id\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = query | shopify %}\n\n {% if event.preview %}\n {% capture result_json %}\n {\n \"data\": {\n \"products\": {\n \"edges\": [\n {\n \"node\": {\n \"id\": \"gid://shopify/Product/1234567890\"\n }\n }\n ]\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n {% endif %}\n\n {% assign result_product_ids = result.data.products.edges | map: \"node\" | map: \"id\" %}\n {% assign qualifying_product_ids = qualifying_product_ids | concat: result_product_ids %}\n\n {% if result.data.products.pageInfo.hasNextPage %}\n {% assign cursor = result.data.products.edges.last.cursor %}\n {% else %}\n {% break %}\n {% endif %}\n{% endfor %}\n\n{% assign current_product_ids = collection.products | map: \"admin_graphql_api_id\" %}\n\n{% assign product_ids_to_add = array %}\n{% assign product_ids_to_remove = array %}\n\n{% for qualifying_product_id in qualifying_product_ids %}\n {% unless current_product_ids contains qualifying_product_id %}\n {% assign product_ids_to_add = product_ids_to_add | push: qualifying_product_id %}\n {% endunless %}\n{% endfor %}\n\n{% for current_product_id in current_product_ids %}\n {% unless qualifying_product_ids contains current_product_id %}\n {% assign product_ids_to_remove = product_ids_to_remove | push: current_product_id %}\n {% endunless %}\n{% endfor %}\n\n{% assign mutations = array %}\n\n{% if product_ids_to_add != blank %}\n {% assign groups_of_product_ids_to_add = product_ids_to_add | in_groups_of: 250, fill_with: false %}\n\n {% for group_of_product_ids_to_add in groups_of_product_ids_to_add %}\n {% capture mutation %}\n collectionAddProducts{{ forloop.index0 }}: collectionAddProducts(\n id: {{ collection.admin_graphql_api_id | json }}\n productIds: {{ group_of_product_ids_to_add | json }}\n ) {\n userErrors {\n field\n message\n }\n }\n {% endcapture %}\n\n {% assign mutations = mutations | push: mutation %}\n {% endfor %}\n{% endif %}\n\n{% if product_ids_to_remove != blank %}\n {% assign groups_of_product_ids_to_remove = product_ids_to_remove | in_groups_of: 250, fill_with: false %}\n\n {% for group_of_product_ids_to_remove in groups_of_product_ids_to_remove %}\n {% capture mutation %}\n collectionRemoveProducts{{ forloop.index0 }}: collectionRemoveProducts(\n id: {{ collection.admin_graphql_api_id | json }}\n productIds: {{ group_of_product_ids_to_remove | json }}\n ) {\n userErrors {\n field\n message\n }\n }\n {% endcapture %}\n\n {% assign mutations = mutations | push: mutation %}\n {% endfor %}\n{% endif %}\n\n{% if mutations != blank %}\n {% action \"shopify\" %}\n mutation {\n {{ mutations | join: newline }}\n }\n {% endaction %}\n{% else %}\n {% log \"This collection already has the correct products.\" %}\n{% endif %}", + "script": "{% assign collection_id = options.collection_id__number_required | prepend: \"gid://shopify/Collection/\" %}\n{% assign days_to_keep_products = options.number_of_days_to_keep_a_product_in_this_collection__number %}\n{% assign product_count = options.number_of_products_to_keep_in_this_collection__number %}\n\n{% if days_to_keep_products == blank and product_count == blank %}\n {% error %}\n \"Please fill in either \\\"Number of days to keep a product in this collection\\\" or \\\"Number of products to keep in this collection\\\" (but not both!).\"\n {% enderror %}\n\n{% elsif days_to_keep_products != blank and product_count != blank %}\n {% error \"Please choose one of the options, but not both! :)\" %}\n{% endif %}\n\n{% comment %}\n -- get IDs of all products currently in the collection\n{% endcomment %}\n\n{% assign cursor = nil %}\n{% assign current_product_ids = array %}\n\n{% for n in (1..100) %}\n {% capture query %}\n query {\n collection(id: {{ collection_id | json }}) {\n products(\n first: 250\n after: {{ cursor | json }}\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n id\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = query | shopify %}\n\n {% if event.preview %}\n {% capture result_json %}\n {\n \"data\": {\n \"collection\": {\n \"products\": {\n \"nodes\": [\n {\n \"id\": \"gid://shopify/Product/1234567890\"\n }\n ]\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n {% endif %}\n\n {% assign current_product_ids\n = result.data.collection.products.nodes\n | map: \"id\"\n | concat: current_product_ids\n %}\n\n {% if result.data.collection.products.pageInfo.hasNextPage %}\n {% assign cursor = result.data.collection.products.pageInfo.endCursor %}\n {% else %}\n {% break %}\n {% endif %}\n{% endfor %}\n\n{% assign loop_count = 100 %}\n{% assign first_elements = 250 %}\n{% assign products_query = \"status:active\" %}\n\n{% if days_to_keep_products != blank %}\n {% comment %}\n -- calculate the \"created at\" threshold for products to keep in the collection\n {% endcomment %}\n\n {% if days_to_keep_products < 1 %}\n {% error %}\n \"\\\"Number of days to keep a product in this collection\\\" should be > 0\"\n {% enderror %}\n {% endif %}\n\n {% assign now_s = \"now\" | date: \"%s\" | times: 1 %}\n {% assign days_to_keep_products_s = days_to_keep_products | times: 86400 %}\n {% assign created_at_threshold_s = now_s | minus: days_to_keep_products_s %}\n {% assign created_at_threshold = created_at_threshold_s | date: \"%Y-%m-%d\" %}\n\n {% assign products_query = products_query\n | append: \" created_at:>=\"\n | append: created_at_threshold\n %}\n\n{% elsif product_count != blank %}\n {% comment %}\n -- calculate the product pagination loops based on number of products to keep in the collection\n {% endcomment %}\n\n {% if product_count < 1 %}\n {% error %}\n \"\\\"Number of products to keep in this collection\\\" should be > 0\"\n {% enderror %}\n {% endif %}\n\n {% assign loop_count = product_count | divided_by: 250 %}\n {% assign remaining = product_count | modulo: 250 %}\n{% endif %}\n\n{% comment %}\n -- get qualifying product IDs based on chosen criteria\n{% endcomment %}\n\n{% assign cursor = nil %}\n{% assign qualifying_product_ids = array %}\n\n{% for n in (0..loop_count) %}\n {% if product_count != blank %}\n {% if forloop.last %}\n {% if remaining == 0 %}\n {% break %}\n {% endif %}\n\n {% assign first_elements = remaining %}\n {% endif %}\n {% endif %}\n\n {% capture query %}\n query {\n products(\n first: {{ first_elements }}\n after: {{ cursor | json }}\n query: {{ products_query | json }}\n reverse: true\n sortKey: CREATED_AT\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n id\n }\n }\n }\n {% endcapture %}\n\n {% assign result = query | shopify %}\n\n {% if event.preview %}\n {% capture result_json %}\n {\n \"data\": {\n \"products\": {\n \"nodes\": [\n {\n \"id\": \"gid://shopify/Product/2345678901\"\n }\n ]\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n {% endif %}\n\n {% assign result_product_ids = result.data.products.nodes | map: \"id\" %}\n {% assign qualifying_product_ids = qualifying_product_ids | concat: result_product_ids %}\n\n {% if result.data.products.pageInfo.hasNextPage %}\n {% assign cursor = result.data.products.pageInfo.endCursor %}\n {% else %}\n {% break %}\n {% endif %}\n{% endfor %}\n\n{% comment %}\n -- create arrays for the products to add and to remove, and split them into groups of 250\n{% endcomment %}\n\n{% assign product_ids_to_add = array %}\n{% assign product_ids_to_remove = array %}\n\n{% for qualifying_product_id in qualifying_product_ids %}\n {% unless current_product_ids contains qualifying_product_id %}\n {% assign product_ids_to_add = product_ids_to_add | push: qualifying_product_id %}\n {% endunless %}\n{% endfor %}\n\n{% for current_product_id in current_product_ids %}\n {% unless qualifying_product_ids contains current_product_id %}\n {% assign product_ids_to_remove = product_ids_to_remove | push: current_product_id %}\n {% endunless %}\n{% endfor %}\n\n{% assign mutations = array %}\n\n{% if product_ids_to_add != blank %}\n {% assign groups_of_product_ids_to_add = product_ids_to_add | in_groups_of: 250, fill_with: false %}\n\n {% for group_of_product_ids_to_add in groups_of_product_ids_to_add %}\n {% capture mutation %}\n collectionAddProducts{{ forloop.index0 }}: collectionAddProducts(\n id: {{ collection_id | json }}\n productIds: {{ group_of_product_ids_to_add | json }}\n ) {\n userErrors {\n field\n message\n }\n }\n {% endcapture %}\n\n {% assign mutations = mutations | push: mutation %}\n {% endfor %}\n{% endif %}\n\n{% if product_ids_to_remove != blank %}\n {% assign groups_of_product_ids_to_remove = product_ids_to_remove | in_groups_of: 250, fill_with: false %}\n\n {% for group_of_product_ids_to_remove in groups_of_product_ids_to_remove %}\n {% capture mutation %}\n collectionRemoveProducts{{ forloop.index0 }}: collectionRemoveProducts(\n id: {{ collection_id | json }}\n productIds: {{ group_of_product_ids_to_remove | json }}\n ) {\n userErrors {\n field\n message\n }\n }\n {% endcapture %}\n\n {% assign mutations = mutations | push: mutation %}\n {% endfor %}\n{% endif %}\n\n{% if mutations != blank %}\n {% action \"shopify\" %}\n mutation {\n {{ mutations | join: newline }}\n }\n {% endaction %}\n\n{% else %}\n {% log \"This collection already has the correct products.\" %}\n{% endif %}\n", "subscriptions": [ "mechanic/user/trigger", "mechanic/scheduler/daily"