Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 237 additions & 0 deletions docs/.vitepress/theme/components/ScopeTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
<template>
<div class="scope-table">
<div class="scope-header">
<span class="scope-title">{{ title }}</span>
<button
class="scope-copy-all"
type="button"
@click="copy(allPermissions, 'all')"
>
<component :is="copied === 'all' ? CheckIcon : CopyIcon" />
{{ copied === 'all' ? 'Copied!' : `Copy all ${allPermissions.length}` }}
</button>
</div>

<ul class="scope-list">
<li v-for="(scope, i) in scopes" :key="scope.name" class="scope-row">
<div class="scope-info">
<span class="scope-index">{{ i + 1 }}</span>
<div>
<div class="scope-name">{{ scope.name }}</div>
<code class="scope-perms">{{ scope.permissions.join(', ') }}</code>
</div>
</div>
<button
class="scope-copy"
type="button"
:aria-label="`Copy ${scope.name} permissions`"
@click="copy(scope.permissions, scope.name)"
>
<component :is="copied === scope.name ? CheckIcon : CopyIcon" />
<span>{{ copied === scope.name ? 'Copied!' : 'Copy' }}</span>
</button>
</li>
</ul>
</div>
</template>

<script setup>
import { computed, h, onUnmounted, ref } from 'vue'

// Renders a list of API access scopes with copy-to-clipboard buttons —
// one per scope, plus a "copy all" in the header. Permissions are copied
// comma-separated, which is the format Shopify's scopes field expects.
//
// Usage in markdown:
//
// <ScopeTable :scopes="[
// { name: 'Shop locales', permissions: ['write_locales', 'read_locales'] },
// { name: 'Inventory', permissions: ['write_inventory', 'read_inventory'] }
// ]" />

const props = defineProps({
scopes: { type: Array, required: true },
title: { type: String, default: 'Access scopes' },
})

const copied = ref(null)
let timer = null

const allPermissions = computed(() => props.scopes.flatMap((s) => s.permissions))

async function copy(permissions, key) {
const text = permissions.join(',')
try {
await navigator.clipboard.writeText(text)
} catch {
// Clipboard API needs a secure context (https or localhost). Fall back to
// a temporary textarea so copy still works over plain http on a LAN host.
const el = document.createElement('textarea')
el.value = text
el.style.position = 'fixed'
el.style.opacity = '0'
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
}

copied.value = key
clearTimeout(timer)
timer = setTimeout(() => (copied.value = null), 2000)
}

onUnmounted(() => clearTimeout(timer))

const CopyIcon = () =>
h(
'svg',
{
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'aria-hidden': 'true',
},
[
h('rect', { x: '9', y: '9', width: '13', height: '13', rx: '2' }),
h('path', { d: 'M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1' }),
],
)

const CheckIcon = () =>
h(
'svg',
{
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2.5',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'aria-hidden': 'true',
},
[h('path', { d: 'M20 6 9 17l-5-5' })],
)
</script>

<style scoped>
.scope-table {
margin: 1.25rem 0;
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
overflow: hidden;
}

.scope-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.75rem 1rem;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}

.scope-title {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
}

.scope-list {
list-style: none;
margin: 0;
padding: 0;
}

.scope-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--vp-c-divider);
}

.scope-row:last-child {
border-bottom: none;
}

.scope-info {
display: flex;
align-items: center;
gap: 0.75rem;
min-width: 0;
}

.scope-index {
flex-shrink: 0;
width: 24px;
height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--vp-c-brand, #8b5cf6);
color: #fff;
font-size: 12px;
font-weight: 700;
}

.scope-name {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
}

.scope-perms {
font-size: 12.5px;
color: var(--vp-c-text-2);
background: none;
padding: 0;
word-break: break-word;
}

.scope-copy,
.scope-copy-all {
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.625rem;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
font-size: 12.5px;
font-weight: 500;
cursor: pointer;
transition: color 0.2s, border-color 0.2s;
}

.scope-copy:hover,
.scope-copy-all:hover {
color: var(--vp-c-brand-1, #8b5cf6);
border-color: var(--vp-c-brand-1, #8b5cf6);
}

.scope-copy svg,
.scope-copy-all svg {
width: 14px;
height: 14px;
}

@media (max-width: 640px) {
.scope-row {
align-items: flex-start;
flex-direction: column;
}

.scope-copy span {
display: none;
}
}
</style>
2 changes: 2 additions & 0 deletions docs/.vitepress/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import FeatureGrid from './components/FeatureGrid.vue'
import FeatureCard from './components/FeatureCard.vue'
import VideoEmbed from './components/VideoEmbed.vue'
import GoogleTranslate from './components/GoogleTranslate.vue'
import ScopeTable from './components/ScopeTable.vue'
import './custom.css'

export default {
Expand All @@ -19,6 +20,7 @@ export default {
app.component('FeatureCard', FeatureCard)
app.component('VideoEmbed', VideoEmbed)
app.component('GoogleTranslate', GoogleTranslate)
app.component('ScopeTable', ScopeTable)

app.directive('click-outside', {
mounted(el, binding) {
Expand Down
73 changes: 41 additions & 32 deletions docs/shopify/attribute-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,48 +16,57 @@ On this screen, the left side lists all available Shopify product fields. For ea

## Available Field Mappings

Here's a breakdown of every Shopify field you can map, what it does, and when you'd use it:

| Shopify Field | Field Code | What it does |
|---|---|---|
| **Name** | `title` | The product title shown on your Shopify storefront |
| **Description** | `descriptionHtml` | Full product description — supports HTML formatting |
| **Price** | `price` | The selling price of the product |
| **Weight** | `weight` | Product weight used for shipping calculations |
| **Quantity** | `inventoryQuantity` | How many units are available in stock |
| **Inventory Tracked** | `inventoryTracked` | Turns on inventory tracking for the product |
| **Allow Purchase Out of Stock** | `inventoryPolicy` | Allows customers to still buy the product when stock hits zero |
| **Vendor** | `vendor` | The brand or supplier name |
| **Product Type** | `productType` | The category or type the product belongs to |
| **Tags** | `tags` | Keywords used for search and filtering in Shopify |
| **Barcode** | `barcode` | Product barcode or unique identifier for inventory scanning |
| **Compare Price** | `compareAtPrice` | The original price — shown as a strikethrough to highlight a discount |
| **SEO Title** | `metafields_global_title_tag` | Custom page title used by search engines |
| **SEO Description** | `metafields_global_description_tag` | Meta description shown in search engine results |
| **Handle** | `handle` | The URL-friendly slug for the product page (e.g. `blue-running-shoes`) |
| **Taxable** | `taxable` | Marks whether tax should be applied to this product |
| **Cost per Item** | `cost` | Cost of goods sold (COGS) — used for profit reporting |
Here's a breakdown of every Shopify field you can map, what it does, and which UnoPim attribute types it accepts:

| Shopify Field | Field Code | What it does | Supported attribute types |
|---|---|---|---|
| **Name** | `title` | The product title shown on your Shopify storefront. This field is required. | text |
| **Description** | `descriptionHtml` | Full product description — supports HTML formatting | text, textarea |
| **Price** | `price` | The selling price of the product | price |
| **Weight** | `weight` | Product weight used for shipping calculations | number, metric |
| **Inventory Tracked** | `inventoryTracked` | Turns on inventory tracking for the product | boolean |
| **Allow Purchase Out of Stock** | `inventoryPolicy` | Allows customers to still buy the product when stock hits zero | yes/no |
| **Vendor** | `vendor` | The brand or supplier name | text, simple select |
| **Product Type** | `productType` | The category or type the product belongs to | text, simple select |
| **Tags** | `tags` | Keywords used for search and filtering in Shopify | textarea, text, select, multiselect |
| **Barcode** | `barcode` | Product barcode or unique identifier for inventory scanning | text |
| **Compare Price** | `compareAtPrice` | The original price — shown as a strikethrough to highlight a discount | price |
| **SEO Title** | `metafields_global_title_tag` | Custom page title used by search engines | text |
| **SEO Description** | `metafields_global_description_tag` | Meta description shown in search engine results | text, textarea |
| **Handle** | `handle` | The URL-friendly slug for the product page (e.g. `blue-running-shoes`) | text |
| **Taxable** | `taxable` | Marks whether tax should be applied to this product | yes/no |
| **Cost per Item** | `cost` | Cost of goods sold (COGS) — used for profit reporting | price |

![Field Mapping Example](./images/export-mapping-fields.png)

> **Warning:** **Handle** must be unique. If several products end up with the same handle, only the **last** one is exported — the rest are silently overwritten.

---

## Using Fixed Values
## Shopify Status

Sometimes you don't want a field to pull from a UnoPim attribute — you just want every exported product to have the **same value** for that field. That's what the **Fixed Value** option is for.
**Shopify Status** (`status`) is not an attribute mapping — it's a fixed setting. Whatever you choose here is applied to **every product you export**, not to one product at a time.

**Example:** You want all exported products to have a quantity of `100` in Shopify, regardless of what's stored in UnoPim.
| Option | What it does |
|---|---|
| **Active** | The product is ready to sell and can be published to your sales channels |
| **Draft** | The product isn't ready to sell and stays hidden from customers — use this to review products before publishing |
| **Archived** | The product is no longer being sold and isn't available on any sales channel |
| **Unlisted** | The product is active, but customers need a **direct link** to reach it. It won't appear in search, collections, or product recommendations. |

Here's how to do it:
---

1. Find the **Quantity** field in the mapping list.
2. **Deselect** the UnoPim attribute from the dropdown — the Fixed Value input will become editable once the attribute is cleared.
3. Type `100` in the Fixed Value field.
4. Save your mapping.
## Unit Price

From now on, every product exported to Shopify will have its quantity set to `100` — no matter what value exists in UnoPim.
Some markets (notably the EU) require products to show a **unit price** — the price per standard unit of measure, like "€2.50 per litre". The **Unit Price** section maps the attributes that make this work.

![Fixed Value Example](./images/same-unit.png)
| Field | What it does | What to choose |
|---|---|---|
| **Total amount** | The total quantity contained in the product | A number or decimal type attribute |
| **Total amount unit** | The unit that the total amount is measured in | A text or select type attribute |
| **Base measure** | The reference quantity the unit price is calculated against | A number you type in |
| **Base measure unit** | The unit for the base measure | Select from the dropdown |

> **Tip:** Fixed values are useful for fields like **Taxable**, **Inventory Tracked**, or **Status** where you want a consistent default across your entire catalog.
> **Important:** The value of **Total amount unit** must match a valid Shopify unit — for example `ML`, `CL`, or `L`. Any other value is **not exported**, and the product ends up with no unit price at all.

**Example:** For a 750 ml bottle priced per litre, you'd set **Total amount** to `750`, **Total amount unit** to `ML`, **Base measure** to `1`, and **Base measure unit** to `L`.
Loading