From a59049360bfa7adbb75d1d079d54fba4c91ca7a3 Mon Sep 17 00:00:00 2001 From: Brad Hover Date: Thu, 28 Sep 2023 15:42:10 -0700 Subject: [PATCH 1/2] new task: set product types by title keywords --- docs/README.md | 2 + .../README.md | 51 +++++++ .../script.liquid | 135 ++++++++++++++++++ .../set-product-types-by-title-keywords.json | 18 +++ 4 files changed, 206 insertions(+) create mode 100644 docs/set-product-types-by-title-keywords/README.md create mode 100644 docs/set-product-types-by-title-keywords/script.liquid create mode 100644 tasks/set-product-types-by-title-keywords.json diff --git a/docs/README.md b/docs/README.md index 22d0b0d6..02a40ed7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -306,6 +306,7 @@ This directory is built automatically. Each task's documentation is generated fr * [Set a default tracking number for new fulfillments](./set-a-default-tracking-number-for-new-fulfillments) * [Set product or variant metafields values in bulk](./set-product-or-variant-metafields-in-bulk) * [Set product templates based on product tags](./set-product-templates-based-on-product-tags) +* [Set product types by title keywords](./set-product-types-by-title-keywords) * [Standardize UK postcodes](./standardize-uk-shipping-postcodes) * [Sync an inverse customer tag](./sync-an-inverse-customer-tag) * [Sync an inverse order tag](./sync-an-inverse-order-tag) @@ -1362,6 +1363,7 @@ This directory is built automatically. Each task's documentation is generated fr * [Send an email when a specific product is shipped](./send-an-email-when-a-specific-product-is-shipped) * [Set product or variant metafields values in bulk](./set-product-or-variant-metafields-in-bulk) * [Set product templates based on product tags](./set-product-templates-based-on-product-tags) +* [Set product types by title keywords](./set-product-types-by-title-keywords) * [Sync inventory across a product type](./sync-inventory-across-a-product-type) * [Sync inventory across product variants](./sync-inventory-across-product-variants) * [Sync inventory levels to variant metafields](./sync-inventory-levels-to-variant-metafields) diff --git a/docs/set-product-types-by-title-keywords/README.md b/docs/set-product-types-by-title-keywords/README.md new file mode 100644 index 00000000..dbebed00 --- /dev/null +++ b/docs/set-product-types-by-title-keywords/README.md @@ -0,0 +1,51 @@ +# Set product types by title keywords + +Tags: Products + +Use this task to quickly reset product types in bulk based on product titles. When run manually, it will query for all products in the shop and for each product with a matched keyword in the title, the task will assign the paired product type to it. + +* View in the task library: [tasks.mechanic.dev/set-product-types-by-title-keywords](https://tasks.mechanic.dev/set-product-types-by-title-keywords) +* Task JSON, for direct import: [task.json](../../tasks/set-product-types-by-title-keywords.json) +* Preview task code: [script.liquid](./script.liquid) + +## Default options + +```json +{ + "product_types_and_keywords__keyval_multiline_required": { + "Shirts": "shirt\nt-shirt\ntee" + } +} +``` + +[Learn about task options in Mechanic](https://learn.mechanic.dev/core/tasks/options) + +## Subscriptions + +```liquid +mechanic/user/trigger +``` + +[Learn about event subscriptions in Mechanic](https://learn.mechanic.dev/core/tasks/subscriptions) + +## Documentation + +Use this task to quickly reset product types in bulk based on product titles. When run manually, it will query for all products in the shop and for each product with a matched keyword in the title, the task will assign the paired product type to it. + +Configure the product types to set on the left, and the keyword(s) on the right. The task is pre-filled with a sample entry for "Shirts", which can be replaced if not needed. + +**Notes:** +- The task will search for keywords in the order of entry in the task configuration, and it will stop once a match is made. +- If a product title does not contain any of the configured keywords, then that product will be ignored. + +## Installing this task + +Find this task [in the library at tasks.mechanic.dev](https://tasks.mechanic.dev/set-product-types-by-title-keywords), and use the "Try this task" button. Or, import [this task's JSON export](../../tasks/set-product-types-by-title-keywords.json) – see [Importing and exporting tasks](https://learn.mechanic.dev/core/tasks/import-and-export) to learn how imports work. + +## Contributions + +Found a bug? Got an improvement to add? Start here: [../../CONTRIBUTING.md](../../CONTRIBUTING.md). + +## Task requests + +Submit your [task requests](https://mechanic.canny.io/task-requests) for consideration by the Mechanic community, and they may be chosen for development and inclusion in the [task library](https://tasks.mechanic.dev/)! diff --git a/docs/set-product-types-by-title-keywords/script.liquid b/docs/set-product-types-by-title-keywords/script.liquid new file mode 100644 index 00000000..d9df3d51 --- /dev/null +++ b/docs/set-product-types-by-title-keywords/script.liquid @@ -0,0 +1,135 @@ +{% assign product_types_and_keywords = options.product_types_and_keywords__keyval_multiline_required %} + +{% comment %} + -- set preview values for the configuration field that will work with the preview query data +{% endcomment %} + +{% if event.preview %} + {% capture product_types_and_keywords_json %} + { + "Shoes": "shoe\nshoes" + } + {% endcapture %} + + {% assign product_types_and_keywords = product_types_and_keywords_json | parse_json %} +{% endif %} + +{% assign cursor = nil %} + +{% comment %} + -- query for all products in the shop (if > 25K products, the "100" loop value can be adjusted upward) +{% endcomment %} + +{% for n in (1..100) %} + {% capture query %} + query { + products( + first: 250 + after: {{ cursor | json }} + ) { + pageInfo { + hasNextPage + endCursor + } + nodes { + id + title + productType + } + } + } + {% endcapture %} + + {% assign result = query | shopify %} + + {% if event.preview %} + {% capture result_json %} + { + "data": { + "products": { + "nodes": [ + { + "id": "gid://shopify/Product/1234567890", + "title": "Alpha Shoe", + "productType": "Shirts" + } + ] + } + } + } + {% endcapture %} + + {% assign result = result_json | parse_json %} + {% endif %} + + {% comment %} + -- process each product in this result before querying for more products + {% endcomment %} + + {% for product in result.data.products.nodes %} + {% comment %} + -- use downcase on product title and configured keywords since the "contains" operator is case-sensitive + {% endcomment %} + + {% assign product_title_downcase = product.title | downcase %} + {% assign product_type_to_set = nil %} + + {% for keyval in product_types_and_keywords %} + {% assign product_type = keyval.first %} + {% assign keywords = keyval.last | split: newline %} + + {% for keyword in keywords %} + {% if keyword == blank or keyword == "" %} + {% comment %} + -- protect against accidental empty keyword lines in the task config + {% endcomment %} + + {% continue %} + {% endif %} + + {% assign keyword_downcase = keyword | downcase %} + + {% if product_title_downcase contains keyword_downcase %} + {% assign product_type_to_set = product_type %} + {% break %} + {% endif %} + {% endfor %} + + {% if product_type_to_set != blank %} + {% break %} + {% endif %} + {% endfor %} + + {% comment %} + -- set a new product type if a keyword match was made and the product does not already have that type + {% endcomment %} + + {% if product_type_to_set != blank and product_type_to_set != product.productType %} + {% action "shopify" %} + mutation { + productUpdate( + input: { + id: {{ product.id | json }} + productType: {{ product_type_to_set | json }} + } + ) { + product { + title + productType + } + userErrors { + field + message + } + } + } + {% endaction %} + {% endif %} + {% endfor %} + + {% if result.data.products.pageInfo.hasNextPage %} + {% assign cursor = result.data.products.pageInfo.endCursor %} + {% else %} + {% break %} + {% endif %} +{% endfor %} diff --git a/tasks/set-product-types-by-title-keywords.json b/tasks/set-product-types-by-title-keywords.json new file mode 100644 index 00000000..8eea9858 --- /dev/null +++ b/tasks/set-product-types-by-title-keywords.json @@ -0,0 +1,18 @@ +{ + "docs": "Use this task to quickly reset product types in bulk based on product titles. When run manually, it will query for all products in the shop and for each product with a matched keyword in the title, the task will assign the paired product type to it.\n\nConfigure the product types to set on the left, and the keyword(s) on the right. The task is pre-filled with a sample entry for \"Shirts\", which can be replaced if not needed.\n\n**Notes:**\n- The task will search for keywords in the order of entry in the task configuration, and it will stop once a match is made.\n- If a product title does not contain any of the configured keywords, then that product will be ignored.", + "halt_action_run_sequence_on_error": false, + "name": "Set product types by title keywords", + "online_store_javascript": null, + "options": { + "product_types_and_keywords__keyval_multiline_required": { + "Shirts": "shirt\nt-shirt\ntee" + } + }, + "order_status_javascript": null, + "perform_action_runs_in_sequence": false, + "preview_event_definitions": [], + "script": "{% assign product_types_and_keywords = options.product_types_and_keywords__keyval_multiline_required %}\n\n{% comment %}\n -- set preview values for the configuration field that will work with the preview query data\n{% endcomment %}\n\n{% if event.preview %}\n {% capture product_types_and_keywords_json %}\n {\n \"Shoes\": \"shoe\\nshoes\"\n }\n {% endcapture %}\n\n {% assign product_types_and_keywords = product_types_and_keywords_json | parse_json %}\n{% endif %}\n\n{% assign cursor = nil %}\n\n{% comment %}\n -- query for all products in the shop (if > 25K products, the \"100\" loop value can be adjusted upward)\n{% endcomment %}\n\n{% for n in (1..100) %}\n {% capture query %}\n query {\n products(\n first: 250\n after: {{ cursor | json }}\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n id\n title\n productType\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/1234567890\",\n \"title\": \"Alpha Shoe\",\n \"productType\": \"Shirts\"\n }\n ]\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n {% endif %}\n\n {% comment %}\n -- process each product in this result before querying for more products\n {% endcomment %}\n\n {% for product in result.data.products.nodes %}\n {% comment %}\n -- use downcase on product title and configured keywords since the \"contains\" operator is case-sensitive\n {% endcomment %}\n\n {% assign product_title_downcase = product.title | downcase %}\n {% assign product_type_to_set = nil %}\n\n {% for keyval in product_types_and_keywords %}\n {% assign product_type = keyval.first %}\n {% assign keywords = keyval.last | split: newline %}\n\n {% for keyword in keywords %}\n {% if keyword == blank or keyword == \"\" %}\n {% comment %}\n -- protect against accidental empty keyword lines in the task config\n {% endcomment %}\n\n {% continue %}\n {% endif %}\n\n {% assign keyword_downcase = keyword | downcase %}\n\n {% if product_title_downcase contains keyword_downcase %}\n {% assign product_type_to_set = product_type %}\n {% break %}\n {% endif %}\n {% endfor %}\n\n {% if product_type_to_set != blank %}\n {% break %}\n {% endif %} \n {% endfor %}\n\n {% comment %}\n -- set a new product type if a keyword match was made and the product does not already have that type\n {% endcomment %}\n\n {% if product_type_to_set != blank and product_type_to_set != product.productType %}\n {% action \"shopify\" %}\n mutation {\n productUpdate(\n input: {\n id: {{ product.id | json }}\n productType: {{ product_type_to_set | json }}\n }\n ) {\n product {\n title\n productType\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %} \n {% endif %} \n {% endfor %}\n\n {% if result.data.products.pageInfo.hasNextPage %}\n {% assign cursor = result.data.products.pageInfo.endCursor %}\n {% else %}\n {% break %}\n {% endif %}\n{% endfor %}", + "subscriptions": ["mechanic/user/trigger"], + "subscriptions_template": "mechanic/user/trigger", + "tags": ["Products"] +} From e10bef82c44bfe1dc5c1d4d8ccdedd2f03e8ca2e Mon Sep 17 00:00:00 2001 From: Brad Hover Date: Mon, 2 Oct 2023 21:16:26 -0700 Subject: [PATCH 2/2] split product title by spaces for array comparison of words --- .../README.md | 2 +- .../script.liquid | 21 ++++++++++++------- .../set-product-types-by-title-keywords.json | 4 ++-- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/docs/set-product-types-by-title-keywords/README.md b/docs/set-product-types-by-title-keywords/README.md index dbebed00..e5f960ee 100644 --- a/docs/set-product-types-by-title-keywords/README.md +++ b/docs/set-product-types-by-title-keywords/README.md @@ -13,7 +13,7 @@ Use this task to quickly reset product types in bulk based on product titles. Wh ```json { "product_types_and_keywords__keyval_multiline_required": { - "Shirts": "shirt\nt-shirt\ntee" + "Shirts": "shirt\nshirts\nt-shirt\nt-shirts\ntee\ntees" } } ``` diff --git a/docs/set-product-types-by-title-keywords/script.liquid b/docs/set-product-types-by-title-keywords/script.liquid index d9df3d51..d8e97f82 100644 --- a/docs/set-product-types-by-title-keywords/script.liquid +++ b/docs/set-product-types-by-title-keywords/script.liquid @@ -7,7 +7,7 @@ {% if event.preview %} {% capture product_types_and_keywords_json %} { - "Shoes": "shoe\nshoes" + "Shirts": "shirt\nshirts\nt-shirt\nt-shirts\ntee\ntees" } {% endcapture %} @@ -50,8 +50,8 @@ "nodes": [ { "id": "gid://shopify/Product/1234567890", - "title": "Alpha Shoe", - "productType": "Shirts" + "title": "Super soft tees", + "productType": "Shoes" } ] } @@ -69,9 +69,14 @@ {% for product in result.data.products.nodes %} {% comment %} -- use downcase on product title and configured keywords since the "contains" operator is case-sensitive + -- split on spaces in title to create an array of words to be compared to keywords {% endcomment %} - {% assign product_title_downcase = product.title | downcase %} + {% assign product_title_words_downcase + = product.title + | downcase + | split: " " + %} {% assign product_type_to_set = nil %} {% for keyval in product_types_and_keywords %} @@ -89,7 +94,7 @@ {% assign keyword_downcase = keyword | downcase %} - {% if product_title_downcase contains keyword_downcase %} + {% if product_title_words_downcase contains keyword_downcase %} {% assign product_type_to_set = product_type %} {% break %} {% endif %} @@ -97,7 +102,7 @@ {% if product_type_to_set != blank %} {% break %} - {% endif %} + {% endif %} {% endfor %} {% comment %} @@ -123,8 +128,8 @@ } } } - {% endaction %} - {% endif %} + {% endaction %} + {% endif %} {% endfor %} {% if result.data.products.pageInfo.hasNextPage %} diff --git a/tasks/set-product-types-by-title-keywords.json b/tasks/set-product-types-by-title-keywords.json index 8eea9858..1559bf56 100644 --- a/tasks/set-product-types-by-title-keywords.json +++ b/tasks/set-product-types-by-title-keywords.json @@ -5,13 +5,13 @@ "online_store_javascript": null, "options": { "product_types_and_keywords__keyval_multiline_required": { - "Shirts": "shirt\nt-shirt\ntee" + "Shirts": "shirt\nshirts\nt-shirt\nt-shirts\ntee\ntees" } }, "order_status_javascript": null, "perform_action_runs_in_sequence": false, "preview_event_definitions": [], - "script": "{% assign product_types_and_keywords = options.product_types_and_keywords__keyval_multiline_required %}\n\n{% comment %}\n -- set preview values for the configuration field that will work with the preview query data\n{% endcomment %}\n\n{% if event.preview %}\n {% capture product_types_and_keywords_json %}\n {\n \"Shoes\": \"shoe\\nshoes\"\n }\n {% endcapture %}\n\n {% assign product_types_and_keywords = product_types_and_keywords_json | parse_json %}\n{% endif %}\n\n{% assign cursor = nil %}\n\n{% comment %}\n -- query for all products in the shop (if > 25K products, the \"100\" loop value can be adjusted upward)\n{% endcomment %}\n\n{% for n in (1..100) %}\n {% capture query %}\n query {\n products(\n first: 250\n after: {{ cursor | json }}\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n id\n title\n productType\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/1234567890\",\n \"title\": \"Alpha Shoe\",\n \"productType\": \"Shirts\"\n }\n ]\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n {% endif %}\n\n {% comment %}\n -- process each product in this result before querying for more products\n {% endcomment %}\n\n {% for product in result.data.products.nodes %}\n {% comment %}\n -- use downcase on product title and configured keywords since the \"contains\" operator is case-sensitive\n {% endcomment %}\n\n {% assign product_title_downcase = product.title | downcase %}\n {% assign product_type_to_set = nil %}\n\n {% for keyval in product_types_and_keywords %}\n {% assign product_type = keyval.first %}\n {% assign keywords = keyval.last | split: newline %}\n\n {% for keyword in keywords %}\n {% if keyword == blank or keyword == \"\" %}\n {% comment %}\n -- protect against accidental empty keyword lines in the task config\n {% endcomment %}\n\n {% continue %}\n {% endif %}\n\n {% assign keyword_downcase = keyword | downcase %}\n\n {% if product_title_downcase contains keyword_downcase %}\n {% assign product_type_to_set = product_type %}\n {% break %}\n {% endif %}\n {% endfor %}\n\n {% if product_type_to_set != blank %}\n {% break %}\n {% endif %} \n {% endfor %}\n\n {% comment %}\n -- set a new product type if a keyword match was made and the product does not already have that type\n {% endcomment %}\n\n {% if product_type_to_set != blank and product_type_to_set != product.productType %}\n {% action \"shopify\" %}\n mutation {\n productUpdate(\n input: {\n id: {{ product.id | json }}\n productType: {{ product_type_to_set | json }}\n }\n ) {\n product {\n title\n productType\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %} \n {% endif %} \n {% endfor %}\n\n {% if result.data.products.pageInfo.hasNextPage %}\n {% assign cursor = result.data.products.pageInfo.endCursor %}\n {% else %}\n {% break %}\n {% endif %}\n{% endfor %}", + "script": "{% assign product_types_and_keywords = options.product_types_and_keywords__keyval_multiline_required %}\n\n{% comment %}\n -- set preview values for the configuration field that will work with the preview query data\n{% endcomment %}\n\n{% if event.preview %}\n {% capture product_types_and_keywords_json %}\n {\n \"Shirts\": \"shirt\\nshirts\\nt-shirt\\nt-shirts\\ntee\\ntees\"\n }\n {% endcapture %}\n\n {% assign product_types_and_keywords = product_types_and_keywords_json | parse_json %}\n{% endif %}\n\n{% assign cursor = nil %}\n\n{% comment %}\n -- query for all products in the shop (if > 25K products, the \"100\" loop value can be adjusted upward)\n{% endcomment %}\n\n{% for n in (1..100) %}\n {% capture query %}\n query {\n products(\n first: 250\n after: {{ cursor | json }}\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n id\n title\n productType\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/1234567890\",\n \"title\": \"Super soft tees\",\n \"productType\": \"Shoes\"\n }\n ]\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n {% endif %}\n\n {% comment %}\n -- process each product in this result before querying for more products\n {% endcomment %}\n\n {% for product in result.data.products.nodes %}\n {% comment %}\n -- use downcase on product title and configured keywords since the \"contains\" operator is case-sensitive\n -- split on spaces in title to create an array of words to be compared to keywords\n {% endcomment %}\n\n {% assign product_title_words_downcase\n = product.title\n | downcase\n | split: \" \"\n %}\n {% assign product_type_to_set = nil %}\n\n {% for keyval in product_types_and_keywords %}\n {% assign product_type = keyval.first %}\n {% assign keywords = keyval.last | split: newline %}\n\n {% for keyword in keywords %}\n {% if keyword == blank or keyword == \"\" %}\n {% comment %}\n -- protect against accidental empty keyword lines in the task config\n {% endcomment %}\n\n {% continue %}\n {% endif %}\n\n {% assign keyword_downcase = keyword | downcase %}\n\n {% if product_title_words_downcase contains keyword_downcase %}\n {% assign product_type_to_set = product_type %}\n {% break %}\n {% endif %}\n {% endfor %}\n\n {% if product_type_to_set != blank %}\n {% break %}\n {% endif %}\n {% endfor %}\n\n {% comment %}\n -- set a new product type if a keyword match was made and the product does not already have that type\n {% endcomment %}\n\n {% if product_type_to_set != blank and product_type_to_set != product.productType %}\n {% action \"shopify\" %}\n mutation {\n productUpdate(\n input: {\n id: {{ product.id | json }}\n productType: {{ product_type_to_set | json }}\n }\n ) {\n product {\n title\n productType\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n {% endfor %}\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", "subscriptions": ["mechanic/user/trigger"], "subscriptions_template": "mechanic/user/trigger", "tags": ["Products"]