init
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<div class="form-item-stack">
|
||||
<el-switch
|
||||
:model-value="modelValue.applyToAll"
|
||||
:validate-event="false"
|
||||
active-text="All products"
|
||||
@update:model-value="value => updateScope({ applyToAll: Boolean(value) })"
|
||||
/>
|
||||
|
||||
<template v-if="!modelValue.applyToAll">
|
||||
<div class="scope-field">
|
||||
<span class="secondary-text m-0">Applies to all products in category</span>
|
||||
<el-select
|
||||
:model-value="modelValue.categoryIds"
|
||||
:validate-event="false"
|
||||
multiple
|
||||
collapse-tags
|
||||
:max-collapse-tags="3"
|
||||
collapse-tags-tooltip
|
||||
class="cms-scope-select"
|
||||
placeholder="Select categories"
|
||||
@update:model-value="value => updateScope({ categoryIds: value })"
|
||||
>
|
||||
<el-option
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
:label="category.name"
|
||||
:value="category.id"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="scope-field">
|
||||
<span class="secondary-text m-0">Applies to all variants of the product</span>
|
||||
<el-select
|
||||
:model-value="modelValue.productIds"
|
||||
:validate-event="false"
|
||||
multiple
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
collapse-tags
|
||||
:max-collapse-tags="3"
|
||||
collapse-tags-tooltip
|
||||
class="cms-scope-select"
|
||||
:remote-method="loadProducts"
|
||||
:loading="productSearchLoading"
|
||||
placeholder="Search products"
|
||||
@update:model-value="value => updateScope({ productIds: value })"
|
||||
>
|
||||
<el-option
|
||||
v-for="product in productOptions"
|
||||
:key="product.id"
|
||||
:label="product.title"
|
||||
:value="product.id"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="scope-field">
|
||||
<span class="secondary-text m-0">Applies to selected variants of the product</span>
|
||||
<el-select
|
||||
:model-value="modelValue.variantIds"
|
||||
:validate-event="false"
|
||||
multiple
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
collapse-tags
|
||||
:max-collapse-tags="3"
|
||||
collapse-tags-tooltip
|
||||
class="cms-scope-select"
|
||||
:remote-method="loadVariants"
|
||||
:loading="variantSearchLoading"
|
||||
placeholder="Search variants"
|
||||
@update:model-value="value => updateScope({ variantIds: value })"
|
||||
>
|
||||
<el-option
|
||||
v-for="variant in variantOptions"
|
||||
:key="variant.id"
|
||||
:label="variant.label"
|
||||
:value="variant.id"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getProductTitle } from '@/utils/product/getProductTitle';
|
||||
import { getVariantLabel } from '@/utils/product/getVariantLabel';
|
||||
import { useCategoriesStore } from '@/stores/categories';
|
||||
import { useProductsStore } from '@/stores/products';
|
||||
import type { DiscountCode } from '@/types/discountCode/DiscountCode';
|
||||
import type { DiscountScope } from '@/types/discountCode/DiscountScope';
|
||||
import type { Product } from '@/types/product/Product';
|
||||
import type { ProductOption } from '@/types/product/ProductOption';
|
||||
import type { ProductVariant } from '@/types/product/ProductVariant';
|
||||
import type { VariantOption } from '@/types/product/VariantOption';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { onMounted, ref, type PropType } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object as PropType<DiscountScope>,
|
||||
required: true
|
||||
},
|
||||
discountCode: {
|
||||
type: Object as PropType<DiscountCode | null>,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: DiscountScope];
|
||||
change: [];
|
||||
}>();
|
||||
|
||||
const { fetchProducts, fetchProductVariants } = useProductsStore();
|
||||
const { categories } = storeToRefs(useCategoriesStore());
|
||||
|
||||
const productOptions = ref<ProductOption[]>([]);
|
||||
const variantOptions = ref<VariantOption[]>([]);
|
||||
const productSearchLoading = ref(false);
|
||||
const variantSearchLoading = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.discountCode) {
|
||||
mergeProductOptions(props.discountCode.products?.map(productOptionFromEntity) ?? []);
|
||||
mergeVariantOptions(props.discountCode.variants?.map(variantOptionFromEntity) ?? []);
|
||||
}
|
||||
});
|
||||
|
||||
const mergeProductOptions = (products: ProductOption[]): void => {
|
||||
const selectedIds = new Set(props.modelValue.productIds);
|
||||
|
||||
const byId = new Map(
|
||||
productOptions.value.filter(product => selectedIds.has(product.id)).map(product => [product.id, product])
|
||||
);
|
||||
|
||||
for (const product of products) {
|
||||
byId.set(product.id, product);
|
||||
}
|
||||
|
||||
productOptions.value = [...byId.values()].sort((a, b) => a.title.localeCompare(b.title));
|
||||
};
|
||||
|
||||
const productOptionFromEntity = (product: Pick<Product, 'id' | 'title'>): ProductOption => ({
|
||||
id: product.id,
|
||||
title: getProductTitle(product.title)
|
||||
});
|
||||
|
||||
const mergeVariantOptions = (variants: VariantOption[]): void => {
|
||||
const selectedIds = new Set(props.modelValue.variantIds);
|
||||
|
||||
const byId = new Map(
|
||||
variantOptions.value.filter(variant => selectedIds.has(variant.id)).map(variant => [variant.id, variant])
|
||||
);
|
||||
|
||||
for (const variant of variants) {
|
||||
byId.set(variant.id, variant);
|
||||
}
|
||||
|
||||
variantOptions.value = [...byId.values()].sort((a, b) => a.label.localeCompare(b.label));
|
||||
};
|
||||
|
||||
const variantOptionFromEntity = (variant: ProductVariant): VariantOption => ({
|
||||
id: variant.id,
|
||||
label: getVariantLabel(variant.title, variant.product?.title)
|
||||
});
|
||||
|
||||
const loadProducts = async (search: string) => {
|
||||
productSearchLoading.value = true;
|
||||
|
||||
try {
|
||||
const { items } = await fetchProducts({ search, page: 1, limit: 20 });
|
||||
|
||||
mergeProductOptions(items.map(product => productOptionFromEntity(product)));
|
||||
} catch {
|
||||
ElMessage.error('Failed to load products');
|
||||
} finally {
|
||||
productSearchLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadVariants = async (search: string) => {
|
||||
variantSearchLoading.value = true;
|
||||
|
||||
try {
|
||||
const { items } = await fetchProductVariants({ search, page: 1, limit: 20 });
|
||||
|
||||
mergeVariantOptions(items.map(variantOptionFromEntity));
|
||||
} catch {
|
||||
ElMessage.error('Failed to load variants');
|
||||
} finally {
|
||||
variantSearchLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const updateScope = (patch: Partial<DiscountScope>) => {
|
||||
emit('update:modelValue', { ...props.modelValue, ...patch });
|
||||
emit('change');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-item-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.scope-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.scope-field .cms-scope-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.scope-field :deep(.el-select__wrapper) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.scope-field :deep(.el-select__selected-item) {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.scope-field :deep(.el-select__tags-text) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: min(160px, 45vw);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user