¨4.0.1¨

This commit is contained in:
¨NW¨
2023-12-03 14:07:47 +00:00
parent c08b36d1b6
commit f35052522d
1112 changed files with 43019 additions and 24987 deletions

View File

@@ -0,0 +1,153 @@
import Vue from "vue";
import ProductMixin from "./mixins/ProductMixin";
import Errors from "@admin/js/Errors";
import { generateSlug } from "@admin/js/functions";
Vue.prototype.route = route;
new Vue({
el: "#app",
mixins: [ProductMixin],
data: {
formSubmissionType: null,
form: {
brand_id: "",
tax_class_id: "",
is_active: true,
media: [],
is_virtual: false,
manage_stock: 0,
in_stock: 1,
special_price_type: "fixed",
meta: {},
attributes: [],
downloads: [],
variations: [],
variants: [],
options: [],
slug: null,
},
errors: new Errors(),
selectizeConfig: {
plugins: ["remove_button"],
},
searchableSelectizeConfig: {},
categoriesSelectizeConfig: {
plugins: ["remove_button"],
onItemAdd(value) {
this.getItem(value)[0].innerHTML = this.getItem(
value
)[0].innerHTML.replace(/¦––\s/g, "");
},
},
flatPickrConfig: {
mode: "single",
enableTime: true,
altInput: true,
},
},
created() {
this.setSearchableSelectizeConfig();
},
methods: {
setProductSlug(value) {
this.form.slug = generateSlug(value);
},
setFormDefaultData() {
this.form = {
brand_id: "",
tax_class_id: "",
is_active: true,
media: [],
is_virtual: false,
manage_stock: 0,
in_stock: 1,
special_price_type: "fixed",
meta: {},
attributes: [],
downloads: [],
variations: [],
variants: [],
options: [],
slug: null,
};
},
resetForm() {
this.setFormDefaultData();
tinyMCE.get("description").setContent("");
this.resetBulkEditVariantFields();
this.focusField({
selector: "#name",
});
},
submit({ submissionType }) {
this.formSubmissionType = submissionType;
$.ajax({
type: "POST",
url: route("admin.products.store", {
...((submissionType === "save_and_edit" ||
submissionType === "save_and_exit") && {
exit_flash: true,
}),
}),
data: this.transformData(this.form),
dataType: "json",
success: ({ message, product_id }) => {
if (submissionType === "save_and_edit") {
location.href = route(
"admin.products.edit",
product_id
);
return;
}
if (submissionType === "save_and_exit") {
location.href = route("admin.products.index");
return;
}
toaster(message, {
type: "success",
});
this.resetForm();
},
})
.catch((error) => {
this.formSubmissionType = null;
toaster(error.responseJSON.message, {
type: "default",
});
if (error.status === 422) {
this.errors.reset();
this.errors.record(error.responseJSON.errors);
this.scrollToFirstErrorField(this.$refs.form.elements);
return;
}
if (this.hasAnyVariant) {
this.regenerateVariationsAndVariantsUid();
}
})
.always(() => {
if (submissionType === "save") {
this.formSubmissionType = null;
}
});
},
},
});

View File

@@ -0,0 +1,145 @@
import Vue from "vue";
import ProductMixin from "./mixins/ProductMixin";
import Errors from "@admin/js/Errors";
Vue.prototype.route = route;
new Vue({
el: "#app",
mixins: [ProductMixin],
data: {
formSubmissionType: null,
form: {
brand_id: "",
categories: [],
tags: [],
media: [],
name: "lorem ipsum",
description: "<p>lorem ipsum dolor</p>",
is_active: 1,
price: 999,
tax_class_id: "",
is_virtual: 0,
manage_stock: 0,
in_stock: 1,
special_price_type: "fixed",
meta: {},
variations: [],
variants: [],
attributes: [],
options: [],
downloads: [],
},
errors: new Errors(),
selectizeConfig: {
plugins: ["remove_button"],
},
searchableSelectizeConfig: {},
categoriesSelectizeConfig: {
plugins: ["remove_button"],
onItemAdd(value) {
this.getItem(value)[0].innerHTML = this.getItem(
value
)[0].innerHTML.replace(/¦––\s/g, "");
},
},
flatPickrConfig: {
mode: "single",
enableTime: true,
altInput: true,
},
},
created() {
this.setFormData();
this.setSearchableSelectizeConfig();
this.setDefaultVariantUid();
this.setVariantsLength();
},
methods: {
prepareFormData(formData) {
this.prepareAttributes(formData.attributes);
this.prepareVariations(formData.variations);
this.prepareVariants(formData.variants);
this.prepareOptions(formData.options);
return formData;
},
setFormData() {
this.form = { ...this.prepareFormData(FleetCart.data["product"]) };
},
setDefaultVariantUid() {
if (this.hasAnyVariant) {
this.defaultVariantUid = this.form.variants.find(
({ is_default }) => is_default === true
).uid;
}
},
setVariantsLength() {
this.variantsLength = this.form.variants.length;
},
submit({ submissionType }) {
this.formSubmissionType = submissionType;
$.ajax({
type: "PUT",
url: route("admin.products.update", {
id: this.form.id,
...(submissionType === "save_and_exit" && {
exit_flash: true,
}),
}),
data: this.transformData(this.form),
dataType: "json",
success: (response) => {
if (submissionType === "save_and_exit") {
location.href = route("admin.products.index");
return;
}
this.form = { ...response.product_resource };
this.errors.reset();
this.prepareFormData(this.form);
this.resetBulkEditVariantFields();
toaster(response.message, {
type: "success",
});
},
})
.catch((error) => {
this.formSubmissionType = null;
toaster(error.responseJSON.message, {
type: "default",
});
if (error.status === 422) {
this.errors.reset();
this.errors.record(error.responseJSON.errors);
this.scrollToFirstErrorField(this.$refs.form.elements);
return;
}
if (this.hasAnyVariant) {
this.regenerateVariationsAndVariantsUid();
}
})
.always(() => {
if (submissionType === "save") {
this.formSubmissionType = null;
}
});
},
},
});

View File

@@ -0,0 +1,63 @@
export default {
watch: {
"form.attributes": {
immediate: true,
handler(newValue) {
if (newValue.length === 0) {
this.addAttribute();
}
},
},
},
methods: {
prepareAttributes(attributes) {
attributes.forEach((attribute) => {
this.$set(attribute, "uid", this.uid());
});
},
getAttributeValuesById(id) {
let values = null;
let matched = false;
if (id === "") return;
Object.values(FleetCart.data["attribute-sets"]).some(
(attributeSet) => {
attributeSet.attributes.some((attribute) => {
if (attribute.id === id) {
matched = true;
values = attribute.values;
return true;
}
});
if (matched) {
return true;
}
}
);
return values;
},
focusAttributeValueField(index) {
this.$refs.attributeValues[index].$el.selectize.focus();
},
addAttribute() {
this.form.attributes.push({
attribute_id: "",
uid: this.uid(),
values: [],
});
},
deleteAttribute(index, uid) {
this.form.attributes.splice(index, 1);
this.clearErrors({ name: "attributes", uid });
},
},
};

View File

@@ -0,0 +1,224 @@
export default {
data() {
return {
bulkEditVariantsUid: "",
bulkEditVariantsField: "",
bulkEditVariants: {},
};
},
computed: {
hasBulkEditVariantsUid() {
return this.bulkEditVariantsUid !== "";
},
hasBulkEditVariantsField() {
return this.bulkEditVariantsField !== "";
},
},
methods: {
changeBulkEditVariantsUid(uid) {
this.resetVariantsSelection();
if (uid === "") {
this.resetBulkEditVariantsField();
return;
}
this.selectVariants(uid);
},
selectVariants(uid) {
this.resetVariantsSelection();
if (uid === "") return;
if (uid !== "all") {
this.selectSpecificVariants(uid);
return;
}
this.selectAllVariants();
},
selectAllVariants() {
this.form.variants.forEach((variant) => {
this.$set(variant, "is_selected", true);
});
},
selectSpecificVariants(uid) {
this.form.variants.forEach((variant) => {
if (variant.uids.includes(uid)) {
this.$set(variant, "is_selected", true);
}
});
},
changeBulkEditVariantsField(fieldName) {
const focusableFieldNames = ["sku", "price", "special_price"];
if (focusableFieldNames.includes(fieldName)) {
this.focusField({
selector: `#bulk-edit-variants-${fieldName.replace(
/_/g,
"-"
)}`,
});
}
this.resetBulkEditVariants();
},
addBulkEditVariantsMedia() {
const picker = new MediaPicker({ type: "image", multiple: true });
picker.on("select", ({ id, path }) => {
this.bulkEditVariants.media.push({
id: +id,
path,
});
});
},
removeBulkEditVariantsMedia(index) {
this.bulkEditVariants.media.splice(index, 1);
},
bulkEditvariantsDefaultData() {
return {
is_active: true,
media: [],
price: null,
special_price: null,
special_price_type: "fixed",
special_price_start: null,
special_price_end: null,
manage_stock: 0,
qty: null,
in_stock: 1,
};
},
resetBulkEditVariantFields() {
this.bulkEditVariantsUid = "";
this.resetVariantsSelection();
this.resetBulkEditVariantsField();
},
resetBulkEditVariantsField() {
this.bulkEditVariantsField = "";
this.resetBulkEditVariants();
},
resetBulkEditVariants() {
this.bulkEditVariants = {
...this.bulkEditvariantsDefaultData(),
};
},
resetVariantsSelection() {
this.form.variants.forEach((variant) => {
this.$set(variant, "is_selected", false);
});
},
clearVariantsSpecialPriceErrors(uid) {
Object.keys(this.errors).forEach((key) => {
if (
key.startsWith(`variants.${uid}`) &&
key.includes("special_price")
) {
this.errors.clear(key);
}
});
},
updateVariantsField(variant, { key, value }) {
this.$set(variant, key, value);
this.errors.clear(`variants.${variant.uid}.${key}`);
},
updateVariantsStatus(variant, { key, value }) {
if (variant.is_default === true) return;
this.$set(variant, key, value);
this.errors.clear(`variants.${variant.uid}.${key}`);
},
updateVariantsSpecialPrice(
variant,
{ key, value },
{ special_price_type, special_price_start, special_price_end }
) {
this.$set(variant, key, value);
this.$set(variant, "special_price_type", special_price_type);
this.$set(variant, "special_price_start", special_price_start);
this.$set(variant, "special_price_end", special_price_end);
this.clearVariantsSpecialPriceErrors(variant.uid);
},
updateVariantsManageStock(variant, { key, value }, { qty }) {
this.$set(variant, key, value);
this.$set(variant, "qty", qty);
this.errors.clear([
`variants.${variant.uid}.${key}`,
`variants.${variant.uid}.qty`,
]);
},
callUpdateVariantsMethodByField(key) {
return {
media: this.updateVariantsField,
sku: this.updateVariantsField,
is_active: this.updateVariantsStatus,
price: this.updateVariantsField,
special_price: this.updateVariantsSpecialPrice,
manage_stock: this.updateVariantsManageStock,
in_stock: this.updateVariantsField,
}[key];
},
updateVariants(field) {
this.form.variants.forEach((variant) => {
if (variant.is_selected) {
this.callUpdateVariantsMethodByField(field.key)(
variant,
field,
this.bulkEditVariants
);
}
});
},
bulkUpdateVariants() {
if (
!this.hasBulkEditVariantsUid &&
!this.hasBulkEditVariantsField
) {
return;
}
const field = {
key: this.bulkEditVariantsField,
value: this.bulkEditVariants[this.bulkEditVariantsField],
};
this.updateVariants(field);
this.resetBulkEditVariantFields();
toaster(trans("product::products.variants.bulk_variants_updated"), {
type: "default",
});
},
},
};

View File

@@ -0,0 +1,112 @@
export default {
methods: {
transformMedia(formData) {
formData.media = formData.media.map((data) => data.id);
},
transformAttributes(formData) {
formData.attributes = formData.attributes
.filter(({ attribute_id }) => attribute_id !== "")
.reduce((accumulator, { attribute_id, uid, values }) => {
return { ...accumulator, [uid]: { attribute_id, values } };
}, {});
},
transformDownloads(formData) {
formData.downloads = formData.downloads
.filter(({ id }) => id !== null)
.map(({ id }) => id);
},
transformVariations(formData) {
const PATHS = {
text: ["id", "uid", "label"],
color: ["id", "uid", "label", "color"],
image: ["id", "uid", "label", "image"],
};
formData.variations = formData.variations
.filter(({ name, type }) => Boolean(name) || type !== "")
.reduce((accumulator, variation) => {
if (variation.type === "") {
variation.values = [];
} else {
variation.values = variation.values.reduce(
(valueAccumulator, value) => {
value = _.pick(value, PATHS[variation.type]);
if (variation.type === "image") {
value.image = value.image.id;
}
return {
...valueAccumulator,
[value.uid]: value,
};
},
{}
);
}
return { ...accumulator, [variation.uid]: variation };
}, {});
},
transformVariants(formData) {
formData.variants = formData.variants.reduce(
(accumulator, variant) => {
variant.media = variant.media.map(({ id }) => id);
return { ...accumulator, [variant.uid]: variant };
},
{}
);
},
transformOptions(formData) {
const PATHS = {
text: ["id", "uid", "price", "price_type"],
select: ["id", "uid", "label", "price", "price_type"],
};
formData.options = formData.options
.filter(({ name, type }) => Boolean(name) || type !== "")
.reduce((accumulator, option) => {
if (option.type === "") {
option.values = [];
} else {
option.values = option.values.reduce(
(valueAccumulator, value) => {
const optionType = this.isOptionTypeText(option)
? "text"
: "select";
value = _.pick(value, PATHS[optionType]);
return {
...valueAccumulator,
[value.uid]: value,
};
},
{}
);
}
return { ...accumulator, [option.uid]: option };
}, {});
},
transformData(data) {
const formData = JSON.parse(JSON.stringify(data));
this.transformMedia(formData);
this.transformAttributes(formData);
this.transformDownloads(formData);
this.transformVariations(formData);
this.transformVariants(formData);
this.transformOptions(formData);
return formData;
},
},
};

View File

@@ -0,0 +1,42 @@
export default {
watch: {
"form.downloads": {
immediate: true,
handler(newValue) {
if (newValue.length === 0) {
this.addDownload({ preventMediaPicker: true });
}
},
},
},
methods: {
addDownload({ preventMediaPicker }) {
const downloads = this.form.downloads;
downloads.push({
id: null,
filename: null,
});
if (!preventMediaPicker) {
this.chooseDownloadableFile(downloads.length - 1);
}
},
deleteDownload(index) {
this.form.downloads.splice(index, 1);
},
chooseDownloadableFile(index) {
let picker = new MediaPicker();
picker.on("select", ({ id, filename }) => {
this.form.downloads.splice(index, 1, {
id,
filename,
});
});
},
},
};

View File

@@ -0,0 +1,81 @@
export default {
data() {
return {
isLeftColumnSectionDragging: false,
isRightColumnSectionDragging: false,
formLeftSections: [],
formRightSections: [],
};
},
computed: {
storeFormSections() {
return {
get: (sortable) => {
return this.getSectionOrder(sortable.options.dataName);
},
set: (sortable) => {
return this.setSectionOrder(sortable);
},
};
},
},
created() {
this.formLeftSections = this.getSectionOrder(
"product-form-left-sections"
);
this.formRightSections = this.getSectionOrder(
"product-form-right-sections"
);
},
methods: {
getDefaultSectionOrder(key) {
return {
"product-form-left-sections": [
"attributes",
"downloads",
"variations",
"variants",
"options",
],
"product-form-right-sections": [
"price",
"inventory",
"media",
"linked_products",
"seo",
"additional",
],
}[key];
},
getSectionOrder(key) {
const sectionsOrder = JSON.parse(localStorage.getItem(key));
return sectionsOrder === null
? this.getDefaultSectionOrder(key)
: sectionsOrder;
},
setSectionOrder(sortable) {
this.$nextTick(() => {
localStorage.setItem(
sortable.options.dataName,
JSON.stringify(sortable.toArray())
);
});
},
notifySectionOrderChange() {
toaster(trans("product::products.section.order_saved"), {
type: "default",
});
this.$nextTick(() => {
this.initColorPicker();
});
},
},
};

View File

@@ -0,0 +1,198 @@
export default {
data() {
return {
globalOptionId: "",
addingGlobalOption: false,
};
},
computed: {
isAddGlobalOptionDisabled() {
return this.globalOptionId === "" || this.addingGlobalOption;
},
isCollapsedOptionsAccordion() {
return this.form.options.every(({ is_open }) => is_open === false);
},
},
watch: {
"form.options": {
immediate: true,
handler(newValue) {
if (newValue.length === 0) {
this.addOption({ preventFocus: true });
}
},
},
},
methods: {
prepareOptions(options) {
options.forEach((option) => {
this.$set(option, "uid", this.uid());
this.$set(option, "is_open", false);
option.values.forEach((_, valueIndex) => {
this.$set(option.values[valueIndex], "uid", this.uid());
});
});
},
addOption({ preventFocus }) {
const options = this.form.options;
const uid = this.uid();
options.push({
uid,
type: "",
is_open: true,
is_global: false,
is_required: false,
values: [
{
uid: this.uid(),
price_type: "fixed",
},
],
});
if (!preventFocus) {
this.focusField({
selector: `#options-${uid}-name`,
});
}
},
deleteOption(index, uid) {
this.form.options.splice(index, 1);
this.clearErrors({ name: "options", uid });
},
changeOptionType(index, uid) {
let option = this.form.options[index];
const fieldName = this.isOptionTypeText(option) ? "price" : "label";
this.clearValuesError({ name: "options", uid });
if (option.values.length === 1) {
this.focusField({
selector: `#options-${uid}-values-${option.values[0].uid}-${fieldName}`,
});
}
},
isOptionTypeText(option) {
const TYPES = ["field", "textarea", "date", "date_time", "time"];
if (TYPES.includes(option.type)) {
option.values.length > 1 && option.values.splice(1);
return true;
}
return false;
},
isOptionTypeSelect({ type }) {
const TYPES = [
"dropdown",
"checkbox",
"checkbox_custom",
"radio",
"radio_custom",
"multiple_select",
];
return TYPES.includes(type) ? true : false;
},
addOptionRow(index, optionUid) {
const valueUid = this.uid();
this.form.options[index].values.push({
uid: valueUid,
price_type: "fixed",
});
this.focusField({
selector: `#options-${optionUid}-values-${valueUid}-label`,
});
},
addOptionRowOnPressEnter(event, optionIndex, valueIndex) {
const option = this.form.options[optionIndex];
const values = option.values;
if (event.target.value === "") return;
if (values.length - 1 === valueIndex) {
this.addOptionRow(optionIndex, option.uid);
return;
}
if (valueIndex < values.length - 1) {
$(
`#options-${option.uid}-values-${
values[valueIndex + 1].uid
}-label`
).trigger("focus");
}
},
deleteOptionRow(optionIndex, optionUid, valueIndex, valueUid) {
const option = this.form.options[optionIndex];
option.values.splice(valueIndex, 1);
this.clearValueRowErrors({ name: "options", optionUid, valueUid });
if (option.values.length === 0) {
this.addOptionRow(optionIndex, optionUid);
}
},
addGlobalOption() {
if (this.globalOptionId === "") return;
this.addingGlobalOption = true;
$.ajax({
type: "GET",
url: route("admin.options.show", this.globalOptionId),
dataType: "json",
success: (option) => {
option.uid = this.uid();
option.is_open = true;
option.values.forEach((value) => {
value.uid = this.uid();
});
this.form.options.push(option);
this.$nextTick(() => {
$(`#options-${option.uid}-name`).trigger("focus");
});
toaster(
trans("product::products.options.option_inserted"),
{
type: "default",
}
);
},
})
.catch((error) => {
toaster(error.responseJSON.message, {
type: "error",
});
})
.always(() => {
this.globalOptionId = "";
this.addingGlobalOption = false;
});
},
},
};

View File

@@ -0,0 +1,243 @@
import Admin from "@admin/js/Admin";
import md5 from "blueimp-md5";
import tinyMCE from "tinymce";
import draggable from "vuedraggable";
import Selectize from "vue2-selectize";
import flatPickr from "vue-flatpickr-component";
import FormSectionMixin from "./FormSectionMixin";
import AttributeMixin from "./AttributeMixin";
import VariationMixin from "./VariationMixin";
import VariantMixin from "./VariantMixin";
import BulkEditVariantsMixin from "./BulkEditVariantsMixin";
import OptionMixin from "./OptionMixin";
import DownloadMixin from "./DownloadMixin";
import DataTransformMixin from "./DataTransformMixin";
import { toaster } from "@admin/js/Toaster";
window.md5 = md5;
window.toaster = toaster;
export default {
components: {
draggable,
Selectize,
flatPickr,
},
mixins: [
FormSectionMixin,
AttributeMixin,
VariationMixin,
VariantMixin,
BulkEditVariantsMixin,
OptionMixin,
DownloadMixin,
DataTransformMixin,
],
mounted() {
this.initTinyMCE();
window.admin = new Admin();
},
methods: {
uid() {
return Math.random().toString(36).slice(3);
},
focusEditor() {
tinyMCE.get("description").focus();
},
focusField({ selector, key }) {
if (key !== undefined) {
this.errors.clear(key);
}
this.$nextTick(() => {
$(`${selector}`).trigger("focus");
});
},
regenerateVariationsAndVariantsUid() {
this.regenerateVariationsUid();
const variations = this.getFilteredVariations();
const newVariants = this.generateNewVariants(variations);
newVariants.forEach(({ uids }, index) => {
this.$set(this.form.variants[index], "uid", md5(uids));
this.$set(this.form.variants[index], "uids", uids);
});
},
hasAnyError({ name, uid }) {
return Object.keys(this.errors.errors).some((key) =>
key.startsWith(`${name}.${uid}`)
);
},
clearErrors({ name, uid }) {
this.clearMatchedErrors(`${name}.${uid}`);
},
clearValuesError({ name, uid }) {
this.clearMatchedErrors(`${name}.${uid}.values`);
},
clearValueRowErrors({ name, uid, valueUid }) {
this.clearMatchedErrors(`${name}.${uid}.values.${valueUid}`);
},
clearMatchedErrors(str) {
Object.keys(this.errors.errors).forEach((key) => {
if (key.startsWith(str)) {
this.errors.clear(key);
}
});
},
scrollToFirstErrorField(elements) {
this.$nextTick(() => {
[...elements]
.find(
(el) => el.name === Object.keys(this.errors.errors)[0]
)
.focus();
});
},
addMedia() {
const picker = new MediaPicker({ type: "image", multiple: true });
picker.on("select", ({ id, path }) => {
this.form.media.push({
id: +id,
path,
});
});
},
removeMedia(index) {
this.form.media.splice(index, 1);
},
preventLastSlideDrag(event) {
return event.related.className.indexOf("disabled") === -1;
},
toggleAccordions({ selector, state, data }) {
const event = new Event("click");
const elements = document.querySelectorAll(selector);
if (!state) {
data.forEach(({ is_open }, index) => {
if (is_open) {
elements[index].dispatchEvent(event);
}
});
return;
}
[...elements].forEach((element) => {
element.dispatchEvent(event);
});
},
toggleAccordion(event, data) {
const target = $(event.currentTarget);
const panelTitle = target.find('[data-toggle="collapse"]');
const panelBody = target.next();
if (data.is_open) {
panelBody.css("display", "block");
}
panelTitle.attr("data-transition", true);
this.$set(data, "is_open", !data.is_open);
panelBody.slideToggle(300, () => {
panelTitle.attr("data-transition", false);
panelBody.removeAttr("style");
});
},
setSearchableSelectizeConfig() {
this.searchableSelectizeConfig = {
valueField: "id",
labelField: "name",
searchField: "name",
load: function (query, callback) {
var url = route("admin.products.index");
if (url === undefined || query.length === 0) {
return callback();
}
$.get(url + "?query=" + query, callback, "json");
},
plugins: ["remove_button"],
};
},
initTinyMCE() {
tinyMCE.baseURL = `${FleetCart.baseUrl}/build/assets/tinymce`;
tinyMCE.init({
selector: ".wysiwyg",
theme: "silver",
height: 350,
menubar: false,
branding: false,
image_advtab: true,
automatic_uploads: true,
media_alt_source: false,
media_poster: false,
relative_urls: false,
toolbar_mode: "sliding", // Possible values: floating, sliding, scrolling, wrap
directionality: FleetCart.rtl ? "rtl" : "ltr",
cache_suffix: `?v=${FleetCart.version}`,
plugins:
"lists, link, table, image, media, paste, autosave, autolink,quickbars, wordcount, code, fullscreen",
toolbar:
"styleselect | bold italic underline strikethrough blockquote | bullist numlist | alignleft aligncenter alignright alignjustify | outdent indent | forecolor removeformat | table | image media link | code fullscreen",
quickbars_selection_toolbar:
"bold italic | quicklink h2 h3 blockquote quickimage quicktable",
setup: (editor) => {
editor.on("change", () => {
editor.save();
editor.getElement().dispatchEvent(new Event("input"));
this.errors.clear("description");
});
},
images_upload_handler: (blobInfo, success, failure) => {
let formData = new FormData();
formData.append(
"file",
blobInfo.blob(),
blobInfo.filename()
);
$.ajax({
method: "POST",
url: route("admin.media.store"),
data: formData,
processData: false,
contentType: false,
})
.then((file) => {
success(file.path);
})
.catch((xhr) => {
failure(xhr.responseJSON.message);
});
},
});
},
},
};

View File

@@ -0,0 +1,400 @@
export default {
data() {
return {
defaultVariantUid: "",
variantPosition: 0,
variantsLength: 0,
};
},
computed: {
hasAnyVariant() {
return this.form.variants.length !== 0;
},
isCollapsedVariantsAccordion() {
return this.form.variants.every(({ is_open }) => is_open === false);
},
},
methods: {
prepareVariants(variants) {
variants.forEach((variant) => {
this.$set(variant, "position", this.variantPosition++);
this.$set(variant, "is_open", false);
this.$set(variant, "is_selected", false);
});
},
changeDefaultVariant(uid) {
const variants = this.form.variants;
const index = variants.findIndex((variant) => variant.uid === uid);
if (variants[index].is_active === true) {
this.resetDefaultVariant();
this.defaultVariantUid = variants[index].uid;
this.$set(variants[index], "is_default", true);
return;
}
this.defaultVariantUid = this.defaultVariantUid;
},
setDefaultVariant() {
const variants = this.form.variants;
const index = variants.findIndex(
({ uid }) => uid === this.defaultVariantUid
);
this.resetDefaultVariant();
const variant = variants[index === -1 ? 0 : index];
this.defaultVariantUid = variant.uid;
this.$set(variant, "is_default", true);
if (index === -1) {
this.defaultVariantUid = variants[0].uid;
this.$set(variants[0], "is_active", true);
}
},
isActiveVariant(index) {
return this.form.variants[index].is_active;
},
changeVariantStatus(variantUid) {
if (this.defaultVariantUid === variantUid) {
toaster(
trans("product::products.variants.disable_default_variant"),
{
type: "default",
}
);
return;
}
this.clearErrors({
name: "variants",
uid: variantUid,
});
},
resetDefaultVariant() {
this.form.variants.some((variant) => {
if (variant.is_default === true) {
this.$set(variant, "is_default", false);
return true;
}
});
},
getFilteredVariations() {
return this.form.variations
.map(({ type, values }) =>
values
.map(({ uid, label }) => {
if (type !== "" && Boolean(label)) {
return { uid, label };
}
})
.filter(Boolean)
)
.filter((data) => data.length !== 0);
},
generateNewVariants(variations) {
return variations
.reduce((accumulator, currentValue) =>
accumulator.flatMap((x) =>
currentValue.map((y) => {
return {
uid: x.uid + "." + y.uid,
label: x.label + " / " + y.label,
};
})
)
)
.map(({ uid, label }) => {
return {
uids: uid.split(".").sort().join("."),
name: label,
};
});
},
generateVariants() {
this.$nextTick(() => {
this.initColorPicker();
this.updateColorThumbnails();
});
// Filter empty variation values
const variations = this.getFilteredVariations();
if (variations.length === 0) {
this.form.variants = [];
this.variantsLength = 0;
return;
}
const newVariants = this.generateNewVariants(variations);
const oldVariants = this.form.variants.map(({ uids }) => {
return {
uids,
};
});
if (newVariants.length > this.variantsLength) {
// Variation added
this.addVariants(newVariants, oldVariants);
} else if (newVariants.length < this.variantsLength) {
// Variation removed
this.removeVariants(newVariants, oldVariants);
} else if (newVariants.length === this.variantsLength) {
// Variations reordered
this.reorderVariants(newVariants, oldVariants);
}
this.variantsLength = newVariants.length;
this.setDefaultVariant();
},
addVariants(newVariants, oldVariants) {
this.notifyVariantsCreated(newVariants.length);
// Add initial variation with single or multiple values when variants are empty
if (oldVariants.length === 0) {
newVariants.forEach((newVariant) => {
this.form.variants.push(
this.variantDefaultData(newVariant)
);
});
return;
}
// A new single value has been added with existing variation values
if (this.hasCommonVariantUids(newVariants, oldVariants)) {
const oldVariantsUids = oldVariants.map(({ uids }) => uids);
newVariants.forEach((newVariant, index) => {
if (!oldVariantsUids.includes(newVariant.uids)) {
this.form.variants.splice(
index,
0,
this.variantDefaultData(newVariant)
);
}
});
return;
}
// A new variation with multiple values has been added
const matchedUids = [];
oldVariants.forEach(({ uids }) => {
newVariants.forEach((newVariant, index) => {
const doesUidExist = uids
.split(".")
.every((uids) =>
newVariant.uids.split(".").includes(uids)
);
if (doesUidExist && !matchedUids.includes(uids)) {
matchedUids.push(uids);
this.setVariantData(newVariant, index);
return;
}
if (doesUidExist) {
this.form.variants.splice(
index,
0,
this.variantDefaultData(newVariant)
);
}
});
});
},
removeVariants(newVariants, oldVariants) {
this.resetBulkEditVariantFields();
this.notifyVariantsRemoved(oldVariants.length - newVariants.length);
// Variation single value has been removed
if (this.hasCommonVariantUids(newVariants, oldVariants)) {
const newVariantsUids = newVariants.map(({ uids }) => uids);
oldVariants.forEach(({ uids }) => {
if (!newVariantsUids.includes(uids)) {
const index = this.form.variants.findIndex(
(variant) => variant.uids === uids
);
this.clearErrors({
name: "variants",
uid: this.form.variants[index].uid,
});
this.form.variants.splice(index, 1);
}
});
return;
}
// A variation with multiple values has been removed
const matchedUids = [];
newVariants.forEach(({ uids, name }) => {
oldVariants.forEach((oldVariant) => {
const index = this.form.variants.findIndex(
(variant) => variant.uids === oldVariant.uids
);
const doesUidExist = uids
.split(".")
.every((uids) =>
oldVariant.uids.split(".").includes(uids)
);
if (doesUidExist && !matchedUids.includes(uids)) {
matchedUids.push(uids);
this.setVariantData({ uids, name }, index);
return;
}
if (doesUidExist) {
this.clearErrors({
name: "variants",
uid: this.form.variants[index].uid,
});
this.form.variants.splice(index, 1);
}
});
});
},
reorderVariants(newVariants, oldVariants) {
// Reordered variations or variation values
const newVariantUids = newVariants.map(({ uids }) => uids);
if (this.hasCommonVariantUids(newVariants, oldVariants)) {
oldVariants.forEach(({ uids }) => {
const index = newVariantUids.indexOf(uids);
const formIndex = this.form.variants.findIndex(
(variant) => variant.uids === uids
);
// Update variant data before swap
this.setVariantData(
{ name: newVariants[index].name },
formIndex
);
// Swap variant elements
this.form.variants[formIndex] = this.form.variants.splice(
index,
1,
this.form.variants[formIndex]
)[0];
});
return;
}
// A new variation with a single value has been added
newVariants.forEach((newVariant, index) => {
this.setVariantData(newVariant, index);
});
},
hasCommonVariantUids(newVariants, oldVariants) {
// Check if the old variants UID is present in the new variants
return oldVariants.some(({ uids }) =>
newVariants.map(({ uids }) => uids).includes(uids)
);
},
setVariantData({ uids, name }, index) {
if (uids !== undefined) {
this.$set(this.form.variants[index], "uid", md5(uids));
this.$set(this.form.variants[index], "uids", uids);
}
this.$set(this.form.variants[index], "name", name);
},
variantDefaultData({ uids, name }) {
return {
position: this.variantPosition++,
uid: md5(uids),
uids,
name,
media: [],
is_active: true,
is_open: false,
is_default: false,
is_selected: false,
special_price_type: "fixed",
manage_stock: 0,
in_stock: 1,
};
},
resetVariants() {
this.form.variants = [];
},
addVariantMedia(index) {
const picker = new MediaPicker({ type: "image", multiple: true });
picker.on("select", ({ id, path }) => {
this.form.variants[index].media.push({
id: +id,
path,
});
});
},
removeVariantMedia(variantIndex, mediaIndex) {
this.form.variants[variantIndex].media.splice(mediaIndex, 1);
},
notifyVariantChanges({ count, status }) {
toaster(
trans(`product::products.variants.variants_${status}`, {
count,
suffix: trans(
`product::products.variants.${
count > 1 ? "variants" : "variant"
}`
),
}).toLowerCase(),
{
type: "default",
}
);
},
notifyVariantsCreated(count) {
this.notifyVariantChanges({ count, status: "created" });
},
notifyVariantsRemoved(count) {
this.notifyVariantChanges({ count, status: "removed" });
},
notifyVariantsReordered() {
toaster(trans("product::products.variants.variants_reordered"), {
type: "default",
});
},
},
};

View File

@@ -0,0 +1,316 @@
import Coloris from "@melloware/coloris";
export default {
data() {
return {
globalVariationId: "",
addingGlobalVariation: false,
variations: [],
};
},
computed: {
isAddGlobalVariationDisabled() {
return this.globalVariationId === "" || this.addingGlobalVariation;
},
isCollapsedVariationsAccordion() {
return this.form.variations.every(
({ is_open }) => is_open === false
);
},
},
watch: {
"form.variations": {
immediate: true,
handler(newValue) {
if (newValue.length === 0) {
this.addVariation({ preventFocus: true });
}
},
},
},
mounted() {
this.initColorPicker();
this.hideColorPicker();
},
methods: {
prepareVariations(variations) {
variations.forEach((variation) => {
this.$set(variation, "is_open", false);
});
},
regenerateVariationsUid() {
this.form.variations.forEach((variation) => {
this.$set(variation, "uid", this.uid());
variation.values.forEach((_, valueIndex) => {
this.$set(variation.values[valueIndex], "uid", this.uid());
});
});
},
reorderVariations() {
this.generateVariants();
this.notifyVariantsReordered();
},
reorderVariationValues() {
this.generateVariants();
this.notifyVariantsReordered();
},
addVariation({ preventFocus }) {
const uid = this.uid();
this.form.variations.push({
uid,
type: "",
is_global: false,
is_open: true,
values: [
{
uid: this.uid(),
image: {
id: null,
path: null,
},
},
],
});
if (!preventFocus) {
console.log("add");
this.focusField({
selector: `#variations-${uid}-name`,
});
}
},
deleteVariation(index, uid) {
this.form.variations.splice(index, 1);
this.clearErrors({ name: "variations", uid });
this.generateVariants();
},
changeVariationType(value, index, uid) {
const variation = this.form.variations[index];
if (value !== "" && variation.values.length === 1) {
this.focusField({
selector: `#variations-${uid}-values-${variation.values[0].uid}-label`,
});
}
if (value === "text") {
variation.values.forEach((value) => {
this.errors.clear([
`variations.${uid}.values.${value.uid}.color`,
`variations.${uid}.values.${value.uid}.image`,
]);
});
} else if (value === "color") {
variation.values.forEach((value) => {
this.errors.clear(
`variations.${uid}.values.${value.uid}.image`
);
});
this.$nextTick(() => {
this.initColorPicker();
});
} else if (value === "image") {
variation.values.forEach((value, valueIndex) => {
if (!value.image) {
this.$set(variation.values[valueIndex], "image", {
id: null,
path: null,
});
}
});
variation.values.forEach((value) => {
this.errors.clear(
`variations.${uid}.values.${value.uid}.color`
);
});
} else {
this.clearValuesError(index, uid);
}
},
chooseVariationImage(
variationIndex,
variationUid,
valueIndex,
valueUid
) {
let picker = new MediaPicker({ type: "image" });
picker.on("select", ({ id, path }) => {
this.form.variations[variationIndex].values[valueIndex].image =
{
id: +id,
path,
};
this.errors.clear(
`variations.${variationUid}.values.${valueUid}.image`
);
});
},
addVariationRow(index, variationUid) {
const valueUid = this.uid();
this.form.variations[index].values.push({
uid: valueUid,
image: {
id: null,
path: null,
},
});
this.$nextTick(() => {
this.initColorPicker();
$(
`#variations-${variationUid}-values-${valueUid}-label`
).trigger("focus");
});
},
addVariationRowOnPressEnter(event, variationIndex, valueIndex) {
const variation = this.form.variations[variationIndex];
const values = variation.values;
if (event.target.value === "") return;
if (values.length - 1 === valueIndex) {
this.addVariationRow(variationIndex, variation.uid);
return;
}
if (valueIndex < values.length - 1) {
$(
`#variations-${variation.uid}-values-${
values[valueIndex + 1].uid
}-label`
).trigger("focus");
}
},
deleteVariationRow(variationIndex, variationUid, valueIndex, valueUid) {
const variation = this.form.variations[variationIndex];
variation.values.splice(valueIndex, 1);
this.clearValueRowErrors({
name: "variations",
variationUid,
valueUid,
});
if (variation.values.length === 0) {
this.addVariationRow(variationIndex, variationUid);
}
this.generateVariants();
},
updateColorThumbnails() {
this.form.variations.forEach(({ uid, type, values }) => {
if (type !== "color") return;
const elements = document.querySelectorAll(
`.variation-${uid} .clr-field`
);
values.forEach(({ color }, valueIndex) => {
elements[valueIndex].style.color = color || "";
});
});
},
initColorPicker() {
Coloris.init();
Coloris({
el: ".color-picker",
alpha: false,
rtl: FleetCart.rtl,
theme: "large",
wrap: true,
format: "hex",
selectInput: true,
swatches: [
"#D01C1F",
"#3AA845",
"#118257",
"#0A33AE",
"#0D46A0",
"#000000",
"#5F4C3A",
"#726E6E",
"#F6D100",
"#C0E506",
"#FF540A",
"#C5A996",
"#4B80BE",
"#A1C3DA",
"#C8BFC2",
"#A9A270",
],
});
},
hideColorPicker() {
$(document).on("click", "#clr-swatches button", (e) => {
$(e.currentTarget)
.parents("#clr-picker")
.removeClass("clr-open");
});
},
addGlobalVariation() {
if (this.globalVariationId === "") return;
this.addingGlobalVariation = true;
$.ajax({
type: "GET",
url: route("admin.variations.show", this.globalVariationId),
dataType: "json",
success: (variation) => {
variation.uid = this.uid();
variation.is_open = true;
variation.values.forEach((value) => {
value.uid = this.uid();
});
this.form.variations.push(variation);
this.generateVariants();
this.$nextTick(() => {
$(`#variations-${variation.uid}-name`).trigger("focus");
});
},
})
.catch((error) => {
toaster(error.responseJSON.message, {
type: "error",
});
})
.always(() => {
this.globalVariationId = "";
this.addingGlobalVariation = false;
});
},
},
};

View File

@@ -1,5 +1,379 @@
#images, #attachments {
margin-bottom: 15px;
@import "@admin/sass/wysiwyg";
.v-toast {
padding: 20px 20px 60px;
}
.product-form {
.box {
.box-header {
.toggle-accordion {
font-size: 16px;
font-weight: 500;
margin: 1px 10px 0 0;
padding: 0 5px;
color: #333333;
cursor: pointer;
&.collapsed {
i {
transform: rotateX(180deg);
}
}
i {
transition: 150ms ease-in-out;
}
}
}
.box-body {
> .form-group {
&:last-child {
margin-bottom: 0;
}
}
}
}
.table-responsive {
margin-bottom: 15px;
}
.accordion-box-content {
.panel-heading {
[data-transition="false"] {
&:after {
transition: none;
}
}
}
.accordion-box-footer {
display: flex;
justify-content: space-between;
}
.insert-template {
display: flex;
justify-content: space-between;
select {
margin-right: 10px;
}
}
}
.product-media-grid {
display: grid;
grid-template-columns: repeat(5, 20%);
grid-template-rows: 1fr 1fr;
margin: -6px;
}
.media-grid-item {
margin: 6px;
border: 1px solid #d2d6de;
border-radius: 3px;
&:first-child {
grid-column-start: 1;
grid-column-end: 3;
grid-row-start: 1;
grid-row-end: 3;
}
&.handle {
.image-holder {
cursor: move;
}
}
&.media-picker {
border-width: 2px;
border-style: dashed;
}
.image-holder {
height: auto;
width: 100%;
margin: 0;
padding-bottom: 90%;
border: none;
float: none;
}
}
.variants {
position: relative;
.fade {
&-enter-active,
&-leave-active {
position: absolute;
transition: 150ms ease-in-out;
}
&-enter,
&-leave-to {
position: absolute;
opacity: 0;
}
}
}
.bulk-edit-variants {
.product-media-grid {
margin-bottom: -6px;
}
}
.variants-group {
position: relative;
display: flex;
flex-direction: column;
.panel-group {
width: 100%;
&:last-child {
margin-bottom: 0;
}
}
.panel-title {
[data-toggle="collapse"] {
padding: 12px 15px 13px;
}
.checkbox {
display: flex;
min-height: 0;
padding-top: 0;
label {
min-height: 18px;
}
[type="checkbox"]:checked + label:before,
[type="checkbox"]:not(:checked) + label:before {
height: 16px;
width: 16px;
}
[type="checkbox"]:checked + label:after,
[type="checkbox"]:not(:checked) + label:after {
font-size: 12px;
line-height: 17px;
}
}
.switch {
display: inline-flex;
margin-right: 25px;
padding-top: 0;
[type="checkbox"] {
&:checked + label::after {
left: 14px;
}
}
label {
padding-left: 30px;
&:before {
top: 5px;
height: 10px;
width: 26px;
}
&:after {
height: 16px;
width: 16px;
top: 2px;
}
}
}
}
.panel-body {
> .row {
> .col-sm-4 {
padding-right: 10px;
}
> .col-sm-8 {
padding-left: 10px;
}
}
}
.variant-fields {
margin-bottom: -10px;
overflow: hidden;
> .row {
> .col-sm-6 {
&:nth-child(1) {
padding-right: 10px;
}
&:nth-child(2) {
padding-left: 10px;
}
&:nth-child(3) {
padding-right: 10px;
}
}
}
}
.variant-name {
line-height: 18px;
}
.variant-badge {
margin-left: 5px;
li {
display: inline-flex;
padding-left: 4px;
padding-right: 4px;
}
.label {
font-size: 11px;
padding: 5px 8px;
border-radius: 15px;
}
}
.variant {
&-enter-active,
&-leave-active,
&-move {
transition: 150ms ease-in-out;
}
&-enter,
&-leave-to {
opacity: 0;
transform: translateX(20px);
}
&-enter-from {
opacity: 0;
scale: 0;
}
&-enter-to {
opacity: 1;
scale: 1;
}
&-leave-from {
opacity: 1;
}
}
.product-media-grid {
grid-template-columns: repeat(4, 25%);
}
.form-group {
margin-bottom: 15px;
}
}
.product-form-footer {
position: fixed;
bottom: 0;
display: flex;
justify-content: flex-end;
padding: 10px 20px;
background: #ffffff;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.18);
transition: 150ms ease-in-out;
z-index: 10;
.btn {
margin-right: 10px;
padding: 6px 14px;
&:last-child {
margin-right: 0;
}
}
.btn-default {
border: 1px solid #d9d9d9;
background: #ffffff;
&:hover {
color: #0068e1;
border-color: #0074fb;
}
}
.btn-secondary {
color: #0068e1;
background: #ffffff;
border: 1px solid #0068e1;
&.btn-loading {
&:after {
border-color: #0068e1;
border-right-color: transparent;
}
&:hover {
&:after {
border-color: #ffffff;
border-right-color: #0068e1;
}
}
}
&:hover {
color: #ffffff;
background: #0068e1;
}
}
}
}
.product-form-left-column {
padding-right: 10px;
.box {
&:last-child {
margin-bottom: 0;
}
}
}
.product-form-right-column {
padding-left: 10px;
.box {
&:last-child {
margin-bottom: 0;
}
}
}
.product-form-column {
&.dragging {
border-radius: 2px;
outline: 2px dashed #a3b0c2;
}
.alert {
margin-bottom: 0;
}
}
.attachment-wrapper {
@@ -20,3 +394,350 @@
padding: 10px 15px;
}
}
.product-downloads-wrapper {
.slide {
margin-bottom: 20px;
}
.table {
> tbody {
> tr {
> td {
vertical-align: middle;
}
}
}
}
.options {
.drag-handle {
margin-top: 3px;
}
}
.choose-file-group {
display: flex;
}
.downloadable-file-name {
flex-grow: 1;
}
.btn-choose-file {
margin-left: 8px;
}
}
.ltr {
&.sidebar-collapse {
.product-form {
.product-form-footer {
left: 50px;
}
}
}
.product-form {
.product-form-footer {
left: 250px;
right: 0;
.btn-default {
margin-right: 10px;
}
}
}
}
.rtl {
&.sidebar-collapse {
.product-form {
.product-form-footer {
right: 50px;
}
}
}
.product-form {
.product-form-footer {
right: 250px;
left: 0;
.btn-default {
margin-left: 10px;
}
}
}
}
@media screen and (min-width: 1200px) and (max-width: 1649px) {
.product-form-right-column {
.box-body {
> .form-group {
margin-bottom: 15px;
> .col-sm-3,
> .col-sm-9 {
width: 100%;
}
label {
margin-bottom: 5px;
padding-top: 0;
}
}
}
}
}
@media screen and (max-width: 1650px) {
.product-form {
.variants-group {
.product-media-grid {
grid-template-columns: repeat(3, 33.33333333333333%);
}
}
}
.product-form-right-column {
.product-media-grid {
grid-template-columns: repeat(4, 25%);
}
}
}
@media screen and (max-width: 1500px) {
.product-form {
.bulk-edit-variants {
.product-media-grid {
grid-template-columns: repeat(4, 25%);
}
}
}
}
@media screen and (max-width: 1380px) {
.product-form-right-column {
.product-media-grid {
grid-template-columns: repeat(3, 33.33333333333333%);
}
}
}
@media screen and (max-width: 1199px) {
.product-form-left-column {
padding-right: 15px;
.box {
&:last-child {
margin-bottom: 20px;
}
}
}
.product-form-right-column {
padding-left: 15px;
.product-media-grid {
grid-template-columns: repeat(8, 12.5%);
}
}
}
@media screen and (max-width: 991px) {
.form-horizontal {
.form-group {
margin-bottom: 20px;
.control-label {
margin-bottom: 0;
padding-top: 8px;
}
}
.checkbox,
.switch {
padding-top: 8px;
label {
padding-top: 0;
}
}
}
.product-form {
.product-form-footer {
position: relative;
bottom: auto;
margin-top: 20px;
padding: 0;
background: transparent;
box-shadow: none;
z-index: 0;
}
}
.ltr,
.rtl {
.product-form {
.product-form-footer {
left: auto;
right: auto;
}
}
}
}
@media screen and (max-width: 767px) {
.form-horizontal {
.form-group {
margin-bottom: 15px;
.control-label {
margin-bottom: 6px;
padding-top: 0;
}
}
.checkbox,
.switch {
padding-top: 0;
}
}
.product-form {
.bulk-edit-variants {
margin-bottom: 10px;
}
.bulk-edit-variants,
.variants-group {
.product-media-grid {
grid-template-columns: repeat(7, 14.28571428571429%);
}
}
.variants-group {
.panel-body {
> .row {
> .col-sm-4 {
padding-right: 15px;
}
> .col-sm-8 {
padding-left: 15px;
}
}
}
.variant-fields {
margin-top: 15px;
> .row {
> .col-sm-6 {
&:nth-child(1) {
padding-right: 15px;
}
&:nth-child(2) {
padding-left: 15px;
}
}
}
}
}
}
.product-form-right-column {
.product-media-grid {
grid-template-columns: repeat(6, 16.66666666666667%);
}
}
}
@media screen and (max-width: 650px) {
.product-form {
.bulk-edit-variants,
.variants-group {
.product-media-grid {
grid-template-columns: repeat(6, 16.66666666666667%);
}
}
}
}
@media screen and (max-width: 560px) {
.product-form .bulk-edit-variants,
.product-form .variants-group,
.product-form-right-column {
.product-media-grid {
grid-template-columns: repeat(5, 20%);
}
}
}
@media screen and (max-width: 420px) {
.product-form .bulk-edit-variants,
.product-form .variants-group,
.product-form-right-column {
.product-media-grid {
grid-template-columns: repeat(4, 25%);
}
}
}
@media screen and (max-width: 767px) {
.box-header {
padding: 8px 15px;
}
.box-body {
padding: 15px;
}
.product-downloads-wrapper {
.table {
> tbody {
> tr {
> td {
&:nth-child(2) {
min-width: 280px;
}
}
}
}
}
}
}
@media screen and (max-width: 435px) {
.product-form {
.accordion-box-content {
.accordion-box-footer {
flex-direction: column;
> .btn {
margin-bottom: 10px;
}
}
}
}
}
@media screen and (max-width: 370px) {
.product-form {
.product-form-footer {
flex-direction: column;
.btn {
margin-right: 0;
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
}
}
}

View File

@@ -20,6 +20,50 @@ return [
'manage_stock' => 'Inventory Management',
'qty' => 'Qty',
'in_stock' => 'Stock Availability',
'new_from' => 'Product New From',
'new_to' => 'Product New To',
'new_from' => 'New From',
'new_to' => 'New To',
'up_sells' => 'Up-Sells',
'cross_sells' => 'Cross-Sells',
'related_products' => 'Related Products',
# product attributes
'attributes' => [
'*.attribute_id' => 'Attribute',
'*.values' => 'Values',
],
# product options
'options' => [
'*.name' => 'Name',
'*.type' => 'Type',
'*.values.*.label' => 'Label',
'*.values.*.price' => 'Price',
'*.values.*.price_type' => 'Price Type',
],
# product variations
'variations' => [
'*.name' => 'Name',
'*.type' => 'Type',
'*.values' => 'Values',
'*.values.*.label' => 'Label',
'*.values.*.color' => 'Color',
'*.values.*.image' => 'Image',
],
# product variants
'variants' => [
'*.name' => 'Name',
'*.sku' => 'SKU',
'*.is_active' => 'Status',
'*.is_default' => 'Default',
'*.price' => 'Price',
'*.special_price' => 'Special Price',
'*.special_price_type' => 'Special Price Type',
'*.special_price_start' => 'Special Price Start',
'*.special_price_end' => 'Special Price End',
'*.manage_stock' => 'Inventory Management',
'*.qty' => 'Quantity',
'*.in_stock' => 'Stock Availability',
],
];

View File

@@ -3,31 +3,85 @@
return [
'product' => 'Product',
'products' => 'Products',
'save' => 'Save',
'save_and_exit' => 'Save & Exit',
'save_and_edit' => 'Save & Edit',
'section' => [
'expand_all' => 'Expand All',
'collapse_all' => 'Collapse All',
'order_saved' => 'Order saved',
],
'table' => [
'thumbnail' => 'Thumbnail',
'name' => 'Name',
'price' => 'Price',
],
'tabs' => [
'group' => [
'basic_information' => 'Basic Information',
'advanced_information' => 'Advanced Information',
],
'group' => [
'general' => 'General',
'price' => 'Price',
'inventory' => 'Inventory',
'images' => 'Images',
'attributes' => 'Attributes',
'downloads' => 'Downloads',
'variations' => 'Variations',
'variants' => 'Variants',
'options' => 'Options',
'pricing' => 'Pricing',
'inventory' => 'Inventory',
'media' => 'Media',
'linked_products' => 'Linked Products',
'seo' => 'SEO',
'related_products' => 'Related Products',
'up_sells' => 'Up-Sells',
'cross_sells' => 'Cross-Sells',
'additional' => 'Additional',
],
'attributes' => [
'attribute' => 'Attribute',
'values' => 'Values',
'add_attribute' => 'Add Attribute',
],
'downloads' => [
'file' => 'File',
'choose' => 'Choose',
'add_file' => 'Add File',
],
'variations' => [
'new_variation' => 'New Variation',
'add_variation' => 'Add Variation',
'insert' => 'Insert',
'add_row' => 'Add Row',
'please_add_some_variations' => 'Please add some variations to generate variants',
],
'variants' => [
'variants' => 'Variants',
'variant' => 'Variant',
'default' => 'Default',
'inactive' => 'Inactive',
'out_of_stock' => 'Out of Stock',
'apply' => 'Apply',
'has_product_variant' => 'Managed from individual variants',
'bulk_variants_updated' => 'Variants updated',
'variants_created' => ':count :suffix created',
'variants_updated' => ':count :suffix updated',
'variants_removed' => ':count :suffix removed',
'variants_reordered' => 'Variants reordered',
'disable_default_variant' => 'The default variant cannot be disabled',
],
'options' => [
'new_option' => 'New Option',
'add_option' => 'Add Option',
'add_row' => 'Add Row',
'insert' => 'Insert',
'option_inserted' => 'Option inserted',
],
'form' => [
'the_product_won\'t_be_shipped' => 'The product won\'t be shipped',
'enable_the_product' => 'Enable the product',
'price_types' => [
'special_price_types' => [
'fixed' => 'Fixed',
'percent' => 'Percent',
],
@@ -39,12 +93,93 @@ return [
'1' => 'In Stock',
'0' => 'Out of Stock',
],
'base_image' => 'Base Image',
'additional_images' => 'Additional Images',
'downloadable_files' => 'Downloadable Files',
'file' => 'File',
'choose' => 'Choose',
'delete_file' => 'Delete File',
'add_new_file' => 'Add New File',
'variations' => [
'name' => 'Name',
'type' => 'Type',
'values' => 'Values',
'label' => 'Label',
'color' => 'Color',
'image' => 'Image',
'variation_types' => [
'please_select' => 'Please Select',
'text' => 'Text',
'color' => 'Color',
'image' => 'Image',
],
'select_template' => 'Select Template',
],
'variants' => [
'default_variant' => 'Default Variant',
'bulk_edit' => 'Bulk Edit',
'all_variants' => 'All Variants',
'field_type' => 'Field Type',
'name' => 'Name',
'media' => 'Media',
'sku' => 'SKU',
'is_active' => 'Status',
'enable_the_variants' => 'Enable the variants',
'price' => 'Price',
'special_price' => 'Special Price',
'special_price_type' => 'Special Price Type',
'special_price_types' => [
'fixed' => 'Fixed',
'percent' => 'Percent',
],
'special_price_start' => 'Special Price Start',
'special_price_end' => 'Special Price End',
'manage_stock' => 'Inventory Management',
'manage_stock_states' => [
0 => 'Don\'t Track Inventory',
1 => 'Track Inventory',
],
'qty' => 'Qty',
'in_stock' => 'Stock Availability',
'stock_availability_states' => [
0 => 'Out of Stock',
1 => 'In Stock',
],
],
'options' => [
'name' => 'Name',
'type' => 'Type',
'is_required' => 'Required',
'label' => 'Label',
'price' => 'Price',
'price_type' => 'Price Type',
'option_types' => [
'please_select' => 'Please Select',
'text' => 'Text',
'field' => 'Field',
'textarea' => 'Textarea',
'select' => 'Select',
'dropdown' => 'Dropdown',
'checkbox' => 'Checkbox',
'checkbox_custom' => 'Custom Checkbox',
'radio' => 'Radio Button',
'radio_custom' => 'Custom Radio Button',
'multiple_select' => 'Multiple Select',
'date' => 'Date',
'date_time' => 'Date & Time',
'time' => 'Time',
],
'price' => 'Price',
'price_types' => [
'fixed' => 'Fixed',
'percent' => 'Percent',
],
'select_template' => 'Select Template',
],
],
];

View File

@@ -2,5 +2,6 @@
return [
'products' => 'Products',
'catalog' => 'Catalog',
'create_product' => 'Create Product',
'all_products' => 'All Products',
];

View File

@@ -0,0 +1,5 @@
<?php
return [
'price_field_is_required' => 'The price field is required',
];

View File

@@ -8,11 +8,81 @@
@endcomponent
@section('content')
<form method="POST" action="{{ route('admin.products.store') }}" class="form-horizontal" id="product-create-form" enctype="multipart/form-data" novalidate>
{{ csrf_field() }}
<div id="app" v-cloak>
<form
class="product-form form-horizontal"
@input="errors.clear($event.target.name)"
@submit.prevent
ref="form"
>
<div class="row">
<div class="product-form-left-column col-lg-8 col-md-12">
@include('product::admin.products.layouts.left_column')
</div>
{!! $tabs->render(compact('product')) !!}
</form>
<div class="product-form-right-column col-lg-4 col-md-12">
@include('product::admin.products.layouts.right_column')
</div>
</div>
<div class="product-form-footer">
<button
type="button"
class="btn btn-default"
:class="{ 'btn-loading': formSubmissionType === 'save' }"
:disabled="formSubmissionType === 'save'"
@click="submit({ submissionType: 'save' })"
>
{{ trans('product::products.save') }}
</button>
<button
type="button"
class="btn btn-secondary"
:class="{ 'btn-loading': formSubmissionType === 'save_and_edit' }"
:disabled="formSubmissionType === 'save_and_edit'"
@click="submit({ submissionType: 'save_and_edit' })"
>
{{ trans('product::products.save_and_edit') }}
</button>
<button
type="button"
class="btn btn-primary"
:class="{ 'btn-loading': formSubmissionType === 'save_and_exit' }"
:disabled="formSubmissionType === 'save_and_exit'"
@click="submit({ submissionType: 'save_and_exit' })"
>
{{ trans('product::products.save_and_exit') }}
</button>
</div>
</form>
</div>
@endsection
@include('product::admin.products.partials.shortcuts')
@push('globals')
<script>
FleetCart.data['attribute-sets'] = @json($attributeSets);
FleetCart.langs['product::products.section.order_saved'] = '{{ trans('product::products.section.order_saved') }}';
FleetCart.langs['product::products.variants.variants'] = '{{ trans('product::products.variants.variants') }}';
FleetCart.langs['product::products.variants.variant'] = '{{ trans('product::products.variants.variant') }}';
FleetCart.langs['product::products.variants.bulk_variants_updated'] = '{{ trans('product::products.variants.bulk_variants_updated') }}';
FleetCart.langs['product::products.variants.variants_created'] = '{{ trans('product::products.variants.variants_created') }}';
FleetCart.langs['product::products.variants.variants_removed'] = '{{ trans('product::products.variants.variants_removed') }}';
FleetCart.langs['product::products.variants.variants_reordered'] = '{{ trans('product::products.variants.variants_reordered') }}';
FleetCart.langs['product::products.variants.disable_default_variant'] = '{{ trans('product::products.variants.disable_default_variant') }}';
FleetCart.langs['product::products.options.option_inserted'] = '{{ trans('product::products.options.option_inserted') }}';
</script>
@vite([
'Modules/Product/Resources/assets/admin/sass/main.scss',
'Modules/Product/Resources/assets/admin/js/create.js',
'Modules/Attribute/Resources/assets/admin/sass/main.scss',
'Modules/Variation/Resources/assets/admin/sass/main.scss',
'Modules/Option/Resources/assets/admin/sass/main.scss',
'Modules/Media/Resources/assets/admin/sass/main.scss',
'Modules/Media/Resources/assets/admin/js/main.js',
])
@endpush

View File

@@ -9,12 +9,82 @@
@endcomponent
@section('content')
<form method="POST" action="{{ route('admin.products.update', $product) }}" class="form-horizontal" id="product-edit-form" enctype="multipart/form-data" novalidate>
{{ csrf_field() }}
{{ method_field('put') }}
<div id="app" v-cloak>
<form
class="product-form form-horizontal"
@input="errors.clear($event.target.name)"
@submit.prevent
ref="form"
>
<div class="row">
<div class="product-form-left-column col-lg-8 col-md-12">
@include('product::admin.products.layouts.left_column')
</div>
{!! $tabs->render(compact('product')) !!}
</form>
<div class="product-form-right-column col-lg-4 col-md-12">
@include('product::admin.products.layouts.right_column')
</div>
</div>
<div class="product-form-footer">
<button
type="button"
class="btn btn-default"
:class="{ 'btn-loading': formSubmissionType === 'save' }"
:disabled="formSubmissionType === 'save'"
@click="submit({ submissionType: 'save' })"
>
{{ trans('product::products.save') }}
</button>
<button
type="button"
class="btn btn-primary"
:class="{ 'btn-loading': formSubmissionType === 'save_and_exit' }"
:disabled="formSubmissionType === 'save_and_exit'"
@click="submit({ submissionType: 'save_and_exit' })"
>
{{ trans('product::products.save_and_exit') }}
</button>
</div>
</form>
</div>
@endsection
@include('product::admin.products.partials.shortcuts')
@if (session()->has('exit_flash'))
@push('notifications')
<div class="alert alert-success fade in alert-dismissible clearfix">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<span class="alert-text">{{ session('exit_flash') }}</span>
</div>
@endpush
@endif
@push('globals')
<script>
FleetCart.data['product'] = {!! $product_resource !!};
FleetCart.data['attribute-sets'] = @json($attributeSets);
FleetCart.langs['product::products.section.order_saved'] = '{{ trans('product::products.section.order_saved') }}';
FleetCart.langs['product::products.variants.variants'] = '{{ trans('product::products.variants.variants') }}';
FleetCart.langs['product::products.variants.variant'] = '{{ trans('product::products.variants.variant') }}';
FleetCart.langs['product::products.variants.bulk_variants_updated'] = '{{ trans('product::products.variants.bulk_variants_updated') }}';
FleetCart.langs['product::products.variants.variants_created'] = '{{ trans('product::products.variants.variants_created') }}';
FleetCart.langs['product::products.variants.variants_removed'] = '{{ trans('product::products.variants.variants_removed') }}';
FleetCart.langs['product::products.variants.variants_reordered'] = '{{ trans('product::products.variants.variants_reordered') }}';
FleetCart.langs['product::products.variants.disable_default_variant'] = '{{ trans('product::products.variants.disable_default_variant') }}';
FleetCart.langs['product::products.options.option_inserted'] = '{{ trans('product::products.options.option_inserted') }}';
</script>
@vite([
'Modules/Product/Resources/assets/admin/sass/main.scss',
'Modules/Product/Resources/assets/admin/js/edit.js',
'Modules/Attribute/Resources/assets/admin/sass/main.scss',
'Modules/Variation/Resources/assets/admin/sass/main.scss',
'Modules/Option/Resources/assets/admin/sass/main.scss',
'Modules/Media/Resources/assets/admin/sass/main.scss',
'Modules/Media/Resources/assets/admin/js/main.js',
])
@endpush

View File

@@ -16,8 +16,18 @@
@endslot
@endcomponent
@if (session()->has('exit_flash'))
@push('notifications')
<div class="alert alert-success fade in alert-dismissible clearfix">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<span class="alert-text">{{ session('exit_flash') }}</span>
</div>
@endpush
@endif
@push('scripts')
<script>
<script type="module">
new DataTable('#products-table .table', {
columns: [
{ data: 'checkbox', orderable: false, searchable: false, width: '3%' },
@@ -26,7 +36,7 @@
{ data: 'name', name: 'translations.name', orderable: false, defaultContent: '' },
{ data: 'price', searchable: false },
{ data: 'status', name: 'is_active', searchable: false },
{ data: 'created', name: 'created_at' },
{ data: 'updated', name: 'updated_at' },
],
});
</script>

View File

@@ -0,0 +1,23 @@
@include('product::admin.products.layouts.sections.general')
<draggable
animation="150"
class="product-form-column"
:class="{ dragging: isLeftColumnSectionDragging }"
data-name="product-form-left-sections"
force-fallback="true"
handle=".drag-handle"
:list="formLeftSections"
:store="storeFormSections"
@choose="isLeftColumnSectionDragging = true"
@unchoose="isLeftColumnSectionDragging = false"
@change="notifySectionOrderChange"
>
<div class="box" v-for="(section, sectionIndex) in formLeftSections" :data-id="section" :key="sectionIndex">
@include('product::admin.products.layouts.sections.attributes')
@include('product::admin.products.layouts.sections.downloads')
@include('product::admin.products.layouts.sections.variations')
@include('product::admin.products.layouts.sections.variants')
@include('product::admin.products.layouts.sections.options')
</div>
</draggable>

View File

@@ -0,0 +1,22 @@
<draggable
animation="150"
class="product-form-column"
:class="{ dragging: isRightColumnSectionDragging }"
data-name="product-form-right-sections"
force-fallback="true"
handle=".drag-handle"
:list="formRightSections"
:store="storeFormSections"
@choose="isRightColumnSectionDragging = true"
@unchoose="isRightColumnSectionDragging = false"
@change="notifySectionOrderChange"
>
<div class="box" v-for="(section, index) in formRightSections" :data-id="section" :key="index">
@include('product::admin.products.layouts.sections.pricing')
@include('product::admin.products.layouts.sections.inventory')
@include('product::admin.products.layouts.sections.media')
@include('product::admin.products.layouts.sections.linked_products')
@include('product::admin.products.layouts.sections.seo')
@include('product::admin.products.layouts.sections.additional')
</div>
</draggable>

View File

@@ -0,0 +1,74 @@
<template v-else-if="section === 'additional'">
<div class="box-header">
<h5>{{ trans('product::products.group.additional') }}</h5>
<div class="drag-handle">
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
</div>
</div>
<div class="box-body">
<div class="form-group">
<label for="short-description" class="col-sm-3 control-label text-left">
{{ trans('product::attributes.short_description') }}
</label>
<div class="col-sm-9">
<textarea name="short_description" rows="6" cols="10" id="short-description" class="form-control" v-model="form.short_description"></textarea>
<span class="help-block text-red" v-if="errors.has('short_description')" v-text="errors.get('short_description')"></span>
</div>
</div>
<div class="form-group">
<label for="new-from" class="col-sm-3 control-label text-left">
{{ trans('product::attributes.new_from') }}
</label>
<div class="col-sm-9">
<div class="input-group">
<span class="input-group-addon">
<i class="fa fa-calendar" aria-hidden="true"></i>
</span>
<flat-pickr
name="new_from"
id="new-from"
class="form-control"
:config="flatPickrConfig"
v-model="form.new_from"
>
</flat-pickr>
</div>
<span class="help-block text-red" v-if="errors.has('new_from')" v-text="errors.get('new_from')"></span>
</div>
</div>
<div class="form-group">
<label for="new-to" class="col-sm-3 control-label text-left">
{{ trans('product::attributes.new_to') }}
</label>
<div class="col-sm-9">
<div class="input-group">
<span class="input-group-addon">
<i class="fa fa-calendar" aria-hidden="true"></i>
</span>
<flat-pickr
name="new_to"
id="new-to"
class="form-control"
:config="flatPickrConfig"
v-model="form.new_to"
>
</flat-pickr>
</div>
<span class="help-block text-red" v-if="errors.has('new_to')" v-text="errors.get('new_to')"></span>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,122 @@
<template v-if="section === 'attributes'">
<div class="box-header">
<h5>{{ trans('product::products.group.attributes') }}</h5>
<div class="drag-handle">
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
</div>
</div>
<div class="box-body">
<div id="product-attributes-wrapper">
<div class="table-responsive">
<table class="options table table-bordered">
<thead class="hidden-xs">
<tr>
<th></th>
<th>{{ trans('product::products.attributes.attribute') }}</th>
<th>{{ trans('product::products.attributes.values') }}</th>
<th></th>
</tr>
</thead>
<tbody
animation="150"
handle=".drag-handle"
is="draggable"
tag="tbody"
:list="form.attributes"
>
<tr v-for="(attribute, index) in form.attributes" :key="index">
<td class="text-center">
<span class="drag-handle">
<i class="fa">&#xf142;</i>
<i class="fa">&#xf142;</i>
</span>
</td>
<td>
<div class="form-group">
<label :for="`attributes-${attribute.uid}-attribute-id`" class="visible-xs">
{{ trans('product::products.attributes.attribute') }}
</label>
<select
:name="`attributes.${attribute.uid}.attribute_id`"
:id="`attributes-${attribute.uid}-attribute-id`"
class="form-control attribute custom-select-black"
@change="focusAttributeValueField(index)"
v-model.number="attribute.attribute_id"
>
<option value="">{{ trans('admin::admin.form.please_select') }}</option>
@foreach ($attributeSets as $attributeSet)
<optgroup label="{{ $attributeSet->name }}">
@foreach ($attributeSet->attributes as $attribute)
<option value="{{ $attribute->id }}">
{{ $attribute->name }}
</option>
@endforeach
</optgroup>
@endforeach
</select>
<span
class="help-block text-red"
v-if="errors.has(`attributes.${attribute.uid}.attribute_id`)"
v-text="errors.get(`attributes.${attribute.uid}.attribute_id`)"
>
</span>
</div>
</td>
<td>
<div class="form-group">
<label :for="`attributes-${attribute.uid}-values`" class="visible-xs">
{{ trans('product::products.attributes.values') }}
</label>
<selectize
:name="`attributes.${attribute.uid}.values`"
:id="`attributes-${attribute.uid}-values`"
:settings="selectizeConfig"
@input="clearValuesError({ name: 'attributes', uid: attribute.uid })"
v-model="attribute.values"
multiple
ref="attributeValues"
>
<option
v-for="(value, valueIndex) in getAttributeValuesById(attribute.attribute_id)"
:key="valueIndex"
:value="value.id"
>
@{{ value.value }}
</option>
</selectize>
<span
class="help-block text-red"
v-if="errors.has(`attributes.${attribute.uid}.values`)"
v-text="errors.get(`attributes.${attribute.uid}.values`)"
>
</span>
</div>
</td>
<td class="text-center">
<button type="button" class="btn btn-default delete-row" @click="deleteAttribute(index, attribute.uid)">
<i class="fa fa-trash"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
<button type="button" class="btn btn-default" @click="addAttribute">
{{ trans('product::products.attributes.add_attribute') }}
</button>
</div>
</div>
</template>

View File

@@ -0,0 +1,76 @@
<template v-else-if="section === 'downloads'">
<div class="box-header">
<h5>{{ trans('product::products.group.downloads') }}</h5>
<div class="drag-handle">
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
</div>
</div>
<div class="box-body">
<div class="product-downloads-wrapper clearfix">
<div class="table-responsive">
<table class="options table table-bordered">
<thead>
<tr>
<th></th>
<th>{{ trans('product::products.downloads.file') }}</th>
<th></th>
</tr>
</thead>
<tbody
animation="150"
handle=".drag-handle"
is="draggable"
tag="tbody"
:list="form.downloads"
>
<tr v-for="(download, index) in form.downloads" :key="index">
<td class="text-center">
<span class="drag-handle">
<i class="fa">&#xf142;</i>
<i class="fa">&#xf142;</i>
</span>
</td>
<td>
<div class="choose-file-group">
<input
type="text"
:value="download.filename"
class="form-control downloadable-file-name"
readonly
>
<button
type="button"
class="btn btn-default btn-choose-file"
@click="chooseDownloadableFile(index)"
>
{{ trans('product::products.downloads.choose') }}
</button>
</div>
</td>
<td class="text-center">
<button
type="button"
class="btn btn-default delete-row"
@click="deleteDownload(index)"
>
<i class="fa fa-trash"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
<button type="button" class="btn btn-default" @click="addDownload">
{{ trans('product::products.downloads.add_file') }}
</button>
</div>
</div>
</template>

View File

@@ -0,0 +1,171 @@
<div class="box">
<div class="box-header">
<h5>{{ trans('product::products.group.general') }}</h5>
</div>
<div class="box-body">
<div class="form-group">
<label for="name" class="col-sm-3 control-label text-left">
{{ trans('product::attributes.name') }}
<span class="text-red">*</span>
</label>
<div class="col-sm-9">
<input
type="text"
name="name"
id="name"
class="form-control"
v-model="form.name"
@if (request()->routeIs('admin.products.create'))
@change="setProductSlug($event.target.value)"
@endif
>
<span class="help-block text-red" v-if="errors.has('name')" v-text="errors.get('name')"></span>
</div>
</div>
<div class="form-group">
<label for="description" class="col-sm-3 control-label text-left" @click="focusEditor">
{{ trans('product::attributes.description') }}
<span class="text-red">*</span>
</label>
<div class="col-sm-9">
<textarea
name="description"
id="description"
class="form-control wysiwyg"
v-model="form.description"
>
</textarea>
<span class="help-block text-red" v-if="errors.has('description')" v-text="errors.get('description')"></span>
</div>
</div>
<div class="form-group">
<label for="brand-id" class="col-sm-3 control-label text-left">
{{ trans('product::attributes.brand_id') }}
</label>
<div class="col-sm-5">
<select name="brand_id" id="brand-id" class="form-control custom-select-black" v-model="form.brand_id">
@foreach ($brands as $id => $label)
<option value="{{ $id }}">{{ $label }}</option>
@endforeach
</select>
<span class="help-block text-red" v-if="errors.has('brand_id')" v-text="errors.get('brand_id')"></span>
</div>
</div>
<div class="form-group">
<label for="categories" class="col-sm-3 control-label text-left">
{{ trans('product::attributes.categories') }}
</label>
<div class="col-sm-5">
<selectize
name="categories"
id="categories"
:settings="categoriesSelectizeConfig"
v-model="form.categories"
multiple
>
@foreach ($categories as $id => $label)
<option value="{{ $id }}">{{ $label }}</option>
@endforeach
</selectize>
<span class="help-block text-red" v-if="errors.has('categories')" v-text="errors.get('categories')"></span>
</div>
</div>
<div class="form-group">
<label for="tax-class-id" class="col-sm-3 control-label text-left">
{{ trans('product::attributes.tax_class_id') }}
</label>
<div class="col-sm-5">
<select name="tax_class_id" id="tax-class-id" class="form-control custom-select-black" v-model="form.tax_class_id">
@foreach ($taxClasses as $id => $label)
<option value="{{ $id }}">{{ $label }}</option>
@endforeach
</select>
<span class="help-block text-red" v-if="errors.has('tax_class_id')" v-text="errors.get('tax_class_id')"></span>
</div>
</div>
<div class="form-group">
<label for="tags" class="col-sm-3 control-label text-left">
{{ trans('product::attributes.tags') }}
</label>
<div class="col-sm-5">
<selectize
name="tags"
id="tags"
:settings="selectizeConfig"
v-model="form.tags"
multiple
>
@foreach ($tags as $id => $label)
<option value="{{ $id }}">{{ $label }}</option>
@endforeach
</selectize>
<span class="help-block text-red" v-if="errors.has('tags')" v-text="errors.get('tags')"></span>
</div>
</div>
<div class="form-group">
<label for="is_virtual" class="col-sm-3 control-label text-left">
{{ trans('product::attributes.is_virtual') }}
</label>
<div class="col-sm-5">
<div class="switch">
<input
type="checkbox"
name="is_virtual"
id="is-virtual"
v-model="form.is_virtual"
>
<label for="is-virtual">
{{ trans('product::products.form.the_product_won\'t_be_shipped') }}
</label>
</div>
<span class="help-block text-red" v-if="errors.has('is_virtual')" v-text="errors.get('is_virtual')"></span>
</div>
</div>
<div class="form-group">
<label for="is-active" class="col-sm-3 control-label text-left">
{{ trans('product::attributes.is_active') }}
</label>
<div class="col-sm-9">
<div class="switch">
<input
type="checkbox"
name="is_active"
id="is-active"
v-model="form.is_active"
>
<label for="is-active">
{{ trans('product::products.form.enable_the_product') }}
</label>
<span class="help-block text-red" v-if="errors.has('is_active')" v-text="errors.get('is_active')"></span>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,101 @@
<template v-else-if="section === 'inventory'">
<div class="box-header">
<h5>{{ trans('product::products.group.inventory') }}</h5>
<div class="drag-handle">
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
</div>
</div>
<div class="box-body">
<div v-if="hasAnyVariant" class="alert alert-info">
{{ trans('product::products.variants.has_product_variant') }}
</div>
<template v-else>
<div class="form-group">
<label for="sku" class="col-sm-3 control-label text-left">
{{ trans('product::attributes.sku') }}
</label>
<div class="col-sm-9">
<input
type="text"
name="sku"
id="sku"
class="form-control"
v-model="form.sku"
>
<span class="help-block text-red" v-if="errors.has('sku')" v-text="errors.get('sku')"></span>
</div>
</div>
<div class="form-group">
<label for="manage-stock" class="col-sm-3 control-label text-left">
{{ trans('product::attributes.manage_stock') }}
</label>
<div class="col-sm-9">
<select
name="manage_stock"
id="manage-stock"
class="form-control custom-select-black"
@change="focusField({
selector: '#qty'
})"
v-model.number="form.manage_stock"
>
<option value="0">{{ trans('product::products.form.manage_stock_states.0') }}</option>
<option value="1">{{ trans('product::products.form.manage_stock_states.1') }}</option>
</select>
<span class="help-block text-red" v-if="errors.has('manage_stock')" v-text="errors.get('manage_stock')"></span>
</div>
</div>
<div class="form-group" v-if="form.manage_stock == 1">
<label for="qty" class="col-sm-3 control-label text-left">
{{ trans('product::attributes.qty') }}
<span class="text-red">*</span>
</label>
<div class="col-sm-9">
<input
type="number"
min="0"
name="qty"
step="1"
id="qty"
class="form-control"
@wheel="$event.target.blur()"
v-model.number="form.qty"
>
<span class="help-block text-red" v-if="errors.has('qty')" v-text="errors.get('qty')"></span>
</div>
</div>
<div class="form-group">
<label for="in-stock" class="col-sm-3 control-label text-left">
{{ trans('product::attributes.in_stock') }}
</label>
<div class="col-sm-9">
<select
name="in_stock"
id="in-stock"
class="form-control custom-select-black"
v-model.number="form.in_stock"
>
<option value="1">{{ trans('product::products.form.stock_availability_states.1') }}</option>
<option value="0">{{ trans('product::products.form.stock_availability_states.0') }}</option>
</select>
<span class="help-block text-red" v-if="errors.has('in_stock')" v-text="errors.get('in_stock')"></span>
</div>
</div>
</template>
</div>
</template>

View File

@@ -0,0 +1,72 @@
<template v-else-if="section === 'linked_products'">
<div class="box-header">
<h5>{{ trans('product::products.group.linked_products') }}</h5>
<div class="drag-handle">
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
</div>
</div>
<div class="box-body">
<div class="form-group">
<label for="up-sells" class="col-sm-3 control-label text-left">
{{ trans('product::attributes.up_sells') }}
</label>
<div class="col-sm-9">
<selectize
name="up_sells"
id="up-sells"
:settings="searchableSelectizeConfig"
v-model="form.up_sells"
multiple
>
@foreach ($product->upSellProducts as $upSellProduct)
<option value="{{ $upSellProduct->id }}">{{ $upSellProduct->name }}</option>
@endforeach
</selectize>
</div>
</div>
<div class="form-group">
<label for="cross-sells" class="col-sm-3 control-label text-left">
{{ trans('product::attributes.cross_sells') }}
</label>
<div class="col-sm-9">
<selectize
name="cross_sells"
id="cross-sells"
:settings="searchableSelectizeConfig"
v-model="form.cross_sells"
multiple
>
@foreach ($product->crossSellProducts as $crossSellProduct)
<option value="{{ $crossSellProduct->id }}">{{ $crossSellProduct->name }}</option>
@endforeach
</selectize>
</div>
</div>
<div class="form-group">
<label for="related-products" class="col-sm-3 control-label text-left">
{{ trans('product::attributes.related_products') }}
</label>
<div class="col-sm-9">
<selectize
name="related_products"
id="related-products"
:settings="searchableSelectizeConfig"
v-model="form.related_products"
multiple
>
@foreach ($product->relatedProducts as $relatedProduct)
<option value="{{ $relatedProduct->id }}">{{ $relatedProduct->name }}</option>
@endforeach
</selectize>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,43 @@
<template v-if="section === 'media'">
<div class="box-header">
<h5>{{ trans('product::products.group.media') }}</h5>
<div class="drag-handle">
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
</div>
</div>
<div class="box-body">
<div v-if="hasAnyVariant" class="alert alert-info">
{{ trans('product::products.variants.has_product_variant') }}
</div>
<div v-else class="row">
<div class="col-md-12">
<draggable
animation="200"
class="product-media-grid"
force-fallback="true"
handle=".handle"
:move="preventLastSlideDrag"
:list="form.media"
>
<div class="media-grid-item handle" v-for="(media, index) in form.media" :key="index">
<div class="image-holder">
<img :src="media.path" alt="product media">
<button type="button" class="btn remove-image" @click="removeMedia(index)"></button>
</div>
</div>
<div class="media-grid-item media-picker disabled" @click="addMedia">
<div class="image-holder">
<img src="{{ asset('build/assets/placeholder_image.png') }}" class="placeholder-image" alt="Placeholder image">
</div>
</div>
</draggable>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,436 @@
<template v-else-if="section === 'options'">
<div class="box-header">
<h5>{{ trans('product::products.group.options') }}</h5>
<div class="d-flex">
<span
class="toggle-accordion"
:class="{ 'collapsed': isCollapsedOptionsAccordion }"
data-toggle="tooltip"
data-placement="top"
:data-original-title="
isCollapsedOptionsAccordion ?
'{{ trans('product::products.section.expand_all') }}' :
'{{ trans('product::products.section.collapse_all') }}'
"
@click="toggleAccordions({
selector: '.options-group .panel-heading',
state: isCollapsedOptionsAccordion,
data: form.options
})"
>
<i class="fa fa-angle-double-up" aria-hidden="true"></i>
</span>
<div class="drag-handle">
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
</div>
</div>
</div>
<div class="box-body clearfix">
<div class="accordion-box-content">
<draggable
animation="150"
class="options-group"
force-fallback="true"
handle=".drag-handle"
:list="form.options"
>
<div
v-for="(option, index) in form.options"
:id="`option-${option.uid}`"
class="content-accordion panel-group options-group-wrapper"
:class="`option-${option.uid}`"
:key="index"
>
<div class="panel panel-default option">
<div class="panel-heading" @click.stop="toggleAccordion($event, option)">
<h4 class="panel-title">
<div
:aria-expanded="option.is_open"
data-toggle="collapse"
data-transition="false"
:href="`#custom-collapse-${option.uid}`"
:class="{
'collapsed': !option.is_open,
'has-error': hasAnyError({
name: 'options',
uid: option.uid
})
}"
>
<div class="d-flex align-items-center">
<span class="drag-handle">
<i class="fa">&#xf142;</i>
<i class="fa">&#xf142;</i>
</span>
<span v-text="option.name || '{{ trans('product::products.options.new_option') }}'"></span>
</div>
<span
class="delete-option"
@click.stop="deleteOption(index, option.uid)"
>
<i class="fa fa-trash"></i>
</span>
</div>
</h4>
</div>
<div class="panel-collapse" :class="{ 'collapse': !option.is_open }">
<div class="panel-body">
<div class="new-option">
<div class="row">
<div class="col-sm-6">
<div class="form-group">
<label :for="`options-${option.uid}-name`">
{{ trans('product::products.form.options.name') }}
<span v-if="option.name || option.type" class="text-red">*</span>
</label>
<input
type="text"
:name="`options.${option.uid}.name`"
class="form-control option-name-field"
:id="`options-${option.uid}-name`"
v-model="option.name"
>
<span
class="help-block text-red"
v-if="errors.has(`options.${option.uid}.name`)"
v-text="errors.get(`options.${option.uid}.name`)"
>
</span>
</div>
</div>
<div class="col-sm-3">
<div class="form-group">
<label :for="`options-${option.uid}-type`">
{{ trans('product::products.form.options.type') }}
<span v-if="option.name || option.type" class="text-red">*</span>
</label>
<select
:name="`options.${option.uid}.type`"
:id="`options-${option.uid}-type`"
class="form-control custom-select-black"
@change="changeOptionType(index, option.uid)"
v-model="option.type"
>
<option value="">
{{ trans('product::products.form.options.option_types.please_select') }}
</option>
<optgroup label="{{ trans('product::products.form.options.option_types.text') }}">
<option value="field">
{{ trans('product::products.form.options.option_types.field') }}
</option>
<option value="textarea">
{{ trans('product::products.form.options.option_types.textarea') }}
</option>
</optgroup>
<optgroup label="{{ trans('product::products.form.options.option_types.select') }}">
<option value="dropdown">
{{ trans('product::products.form.options.option_types.dropdown') }}
</option>
<option value="checkbox">
{{ trans('product::products.form.options.option_types.checkbox') }}
</option>
<option value="checkbox_custom">
{{ trans('product::products.form.options.option_types.checkbox_custom') }}
</option>
<option value="radio">
{{ trans('product::products.form.options.option_types.radio') }}
</option>
<option value="radio_custom">
{{ trans('product::products.form.options.option_types.radio_custom') }}
</option>
<option value="multiple_select">
{{ trans('product::products.form.options.option_types.multiple_select') }}
</option>
</optgroup>
<optgroup label="{{ trans('product::products.form.options.option_types.date') }}">
<option value="date">
{{ trans('product::products.form.options.option_types.date') }}
</option>
<option value="date_time">
{{ trans('product::products.form.options.option_types.date_time') }}
</option>
<option value="time">
{{ trans('product::products.form.options.option_types.time') }}
</option>
</optgroup>
</select>
<span
class="help-block text-red"
v-if="errors.has(`options.${option.uid}.type`)"
v-text="errors.get(`options.${option.uid}.type`)"
>
</span>
</div>
</div>
<div class="col-sm-3">
<div class="form-group">
<div class="checkbox">
<input
type="checkbox"
:name="`options.${option.uid}.is_required`"
:id="`options-${option.uid}-is-required`"
class="form-control"
v-model="option.is_required"
>
<label :for="`options-${option.uid}-is-required`">
{{ trans('product::products.form.options.is_required') }}
</label>
</div>
</div>
</div>
</div>
</div>
<template v-if="isOptionTypeText(option)">
<div class="option-values" :id="`options.${option.uid}.values`">
<div class="table-responsive option-text">
<table class="table table-bordered">
<thead>
<tr>
<th>{{ trans('product::products.form.options.price') }}</th>
<th>{{ trans('product::products.form.options.price_type') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(value, valueIndex) in option.values" :key="valueIndex">
<td>
<div class="input-group">
<span class="input-group-addon">
@{{ value.price_type === 'fixed' ? '$' : '%' }}
</span>
<input
type="number"
min="0"
step="0.1"
:name="`options.${option.uid}.values.${value.uid}.price`"
:id="`options-${option.uid}-values-${value.uid}-price`"
class="form-control"
@wheel="$event.target.blur()"
v-model="value.price"
>
</div>
<span
class="help-block text-red"
v-if="errors.has(`options.${option.uid}.values.${value.uid}.price`)"
v-text="errors.get(`options.${option.uid}.values.${value.uid}.price`)"
>
</span>
</td>
<td>
<select
:name="`options.${option.uid}.values.${value.uid}.price_type`"
:id="`options-${option.uid}-values-${value.uid}-price-type`"
class="form-control custom-select-black"
v-model="value.price_type"
>
<option value="fixed">
{{ trans('product::products.form.options.price_types.fixed') }}
</option>
<option value="percent">
{{ trans('product::products.form.options.price_types.fixed') }}
</option>
</select>
<span
class="help-block text-red"
v-if="errors.has(`options.${option.uid}.values.${value.uid}.price_type`)"
v-text="errors.get(`options.${option.uid}.values.${value.uid}.price_type`)"
>
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<template v-else-if="isOptionTypeSelect(option)">
<div class="option-values" :id="`options.${option.uid}.values`">
<div class="option-select">
<div class="table-responsive option-text">
<table class="options table table-bordered">
<thead>
<tr>
<th></th>
<th>
{{ trans('product::products.form.options.label') }}
<span class="text-red">*</span>
</th>
<th>{{ trans('product::products.form.options.price') }}</th>
<th>{{ trans('product::products.form.options.price_type') }}</th>
<th></th>
</tr>
</thead>
<tbody
animation="150"
handle=".drag-handle"
is="draggable"
tag="tbody"
:list="option.values"
>
<tr v-for="(value, valueIndex) in option.values" :key="valueIndex">
<td class="text-center">
<span class="drag-handle">
<i class="fa">&#xf142;</i>
<i class="fa">&#xf142;</i>
</span>
</td>
<td>
<input
type="text"
:name="`options.${option.uid}.values.${value.uid}.label`"
:id="`options-${option.uid}-values-${value.uid}-label`"
class="form-control"
@keyup.enter="addOptionRowOnPressEnter($event, index, valueIndex)"
v-model="value.label"
>
<span
class="help-block text-red"
v-if="errors.has(`options.${option.uid}.values.${value.uid}.label`)"
v-text="errors.get(`options.${option.uid}.values.${value.uid}.label`)"
>
</span>
</td>
<td>
<div class="input-group">
<span class="input-group-addon">
@{{ value.price_type === 'fixed' ? '$' : '%' }}
</span>
<input
type="number"
min="0"
step="0.1"
:name="`options.${option.uid}.values.${value.uid}.price`"
:id="`options-${option.uid}-values-${value.uid}-price`"
class="form-control"
@wheel="$event.target.blur()"
v-model="value.price"
>
</div>
<span
class="help-block text-red"
v-if="errors.has(`options.${option.uid}.values.${value.uid}.price`)"
v-text="errors.get(`options.${option.uid}.values.${value.uid}.price`)"
>
</span>
</td>
<td>
<select
:name="`options.${option.uid}.values.${value.uid}.price_type`"
:id="`options-${option.uid}-values-${value.uid}-price-type`"
class="form-control custom-select-black"
v-model="value.price_type"
>
<option value="fixed">
{{ trans('product::products.form.options.price_types.fixed') }}
</option>
<option value="percent">
{{ trans('product::products.form.options.price_types.percent') }}
</option>
</select>
<span
class="help-block text-red"
v-if="errors.has(`options.${option.uid}.values.${value.uid}.price_type`)"
v-text="errors.get(`options.${option.uid}.values.${value.uid}.price_type`)"
>
</span>
</td>
<td>
<button
type="button"
tabindex="-1"
class="btn btn-default delete-row"
@click="deleteOptionRow(index, option.uid, valueIndex, value.uid)"
>
<i class="fa fa-trash"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
<button
type="button"
class="btn btn-default"
@click="addOptionRow(index, option.uid)"
>
{{ trans('product::products.options.add_row') }}
</button>
</div>
</div>
</template>
</div>
</div>
</div>
</div>
</draggable>
<div class="accordion-box-footer">
<button type="button" class="btn btn-default" @click="addOption">
{{ trans('product::products.options.add_option') }}
</button>
@hasAccess('admin.options.index')
@if ($globalOptions->isNotEmpty())
<div class="insert-template">
<select class="form-control custom-select-black" v-model="globalOptionId">
<option value="">{{ trans('product::products.form.options.select_template') }}</option>
@foreach ($globalOptions as $globalOption)
<option value="{{ $globalOption->id }}">{{ $globalOption->name }}</option>
@endforeach
</select>
<button
type="button"
class="btn btn-default"
:class="{ 'btn-loading': addingGlobalOption }"
:disabled="isAddGlobalOptionDisabled"
@click="addGlobalOption"
>
{{ trans('product::products.options.insert') }}
</button>
</div>
@endif
@endHasAccess
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,147 @@
<template v-if="section === 'price'">
<div class="box-header">
<h5>{{ trans('product::products.group.pricing') }}</h5>
<div class="drag-handle">
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
</div>
</div>
<div class="box-body">
<div v-if="hasAnyVariant" class="alert alert-info">
{{ trans('product::products.variants.has_product_variant') }}
</div>
<template v-else>
<div class="form-group">
<label for="price" class="col-sm-3 control-label text-left">
{{ trans('product::attributes.price') }}
<span class="text-red">*</span>
</label>
<div class="col-sm-9">
<div class="input-group">
<span class="input-group-addon">$</span>
<input
type="number"
min="0"
name="price"
step="0.1"
id="price"
class="form-control"
@wheel="$event.target.blur()"
v-model="form.price"
>
</div>
<span class="help-block text-red" v-if="errors.has('price')" v-text="errors.get('price')"></span>
</div>
</div>
<div class="form-group">
<label for="special-price" class="col-sm-3 control-label text-left">
{{ trans('product::attributes.special_price') }}
</label>
<div class="col-sm-9">
<div class="input-group">
<span class="input-group-addon">
@{{ form.special_price_type === 'fixed' ? '$' : '%' }}
</span>
<input
type="number"
min="0"
name="special_price"
step="0.1"
id="special-price"
class="form-control"
@wheel="$event.target.blur()"
v-model="form.special_price"
>
</div>
<span class="help-block text-red" v-if="errors.has('special_price')" v-text="errors.get('special_price')"></span>
</div>
</div>
<div class="form-group">
<label for="special-price-type" class="col-sm-3 control-label text-left">
{{ trans('product::attributes.special_price_type') }}
</label>
<div class="col-sm-9">
<select
name="special_price_type"
id="special-price-type"
class="form-control custom-select-black"
v-model="form.special_price_type"
>
<option value="fixed">
{{ trans('product::products.form.special_price_types.fixed') }}
</option>
<option value="percent">
{{ trans('product::products.form.special_price_types.percent') }}
</option>
</select>
<span class="help-block text-red" v-if="errors.has('special_price_type')" v-text="errors.get('special_price_type')"></span>
</div>
</div>
<div class="form-group">
<label for="special-price-start" class="col-sm-3 control-label text-left">
{{ trans('product::attributes.special_price_start') }}
</label>
<div class="col-sm-9">
<div class="input-group">
<span class="input-group-addon">
<i class="fa fa-calendar" aria-hidden="true"></i>
</span>
<flat-pickr
name="special_price_start"
id="special-price-start"
class="form-control"
:config="flatPickrConfig"
v-model="form.special_price_start"
>
</flat-pickr>
</div>
<span class="help-block text-red" v-if="errors.has('special_price_start')" v-text="errors.get('special_price_start')"></span>
</div>
</div>
<div class="form-group">
<label for="special-price-end" class="col-sm-3 control-label text-left">
{{ trans('product::attributes.special_price_end') }}
</label>
<div class="col-sm-9">
<div class="input-group">
<span class="input-group-addon">
<i class="fa fa-calendar" aria-hidden="true"></i>
</span>
<flat-pickr
name="special_price_end"
id="special-price-end"
class="form-control"
:config="flatPickrConfig"
v-model="form.special_price_end"
>
</flat-pickr>
</div>
<span class="help-block text-red" v-if="errors.has('special_price_end')" v-text="errors.get('special_price_end')"></span>
</div>
</div>
</template>
</div>
</template>

View File

@@ -0,0 +1,49 @@
<template v-else-if="section === 'seo'">
<div class="box-header">
<h5>{{ trans('product::products.group.seo') }}</h5>
<div class="drag-handle">
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
</div>
</div>
<div class="box-body">
<div class="form-group">
<label for="slug" class="col-sm-3 control-label text-left">
{{ trans('product::attributes.slug') }}
<span v-if="route().current('admin.products.edit')" class="text-red">*</span>
</label>
<div class="col-sm-9">
<input type="text" name="slug" id="slug" class="form-control" @change="setProductSlug($event.target.value)" v-model="form.slug">
<span class="help-block text-red" v-if="errors.has('slug')" v-text="errors.get('slug')"></span>
</div>
</div>
<div class="form-group">
<label for="meta-title" class="col-sm-3 control-label text-left">
{{ trans('meta::attributes.meta_title') }}
</label>
<div class="col-sm-9">
<input type="text" name="meta.meta_title" id="meta-title" class="form-control" v-model="form.meta.meta_title">
<span class="help-block text-red" v-if="errors.has('meta.meta_title')" v-text="errors.get('meta.meta_title')"></span>
</div>
</div>
<div class="form-group">
<label for="meta-description" class="col-sm-3 control-label text-left">
{{ trans('meta::attributes.meta_description') }}
</label>
<div class="col-sm-9">
<textarea name="meta.meta_description" rows="6" cols="10" id="meta-description" class="form-control" v-model="form.meta.meta_description"></textarea>
<span class="help-block text-red" v-if="errors.has('meta.meta_description')" v-text="errors.get('meta.meta_description')"></span>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,439 @@
<template v-else-if="section === 'variants'">
<div class="box-header">
<h5>{{ trans('product::products.group.variants') }}</h5>
<div class="d-flex">
<span
v-if="hasAnyVariant"
class="toggle-accordion"
:class="{ 'collapsed': isCollapsedVariantsAccordion }"
data-toggle="tooltip"
data-placement="top"
:data-original-title="
isCollapsedVariantsAccordion ?
'{{ trans('product::products.section.expand_all') }}' :
'{{ trans('product::products.section.collapse_all') }}'
"
@click="toggleAccordions({
selector: '.variants-group .panel-heading',
state: isCollapsedVariantsAccordion,
data: form.variants
})"
>
<i class="fa fa-angle-double-up" aria-hidden="true"></i>
</span>
<div class="drag-handle">
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
</div>
</div>
</div>
<div class="box-body">
<div class="accordion-box-content">
<div v-if="!hasAnyVariant" class="alert alert-info">
{{ trans('product::products.variations.please_add_some_variations') }}
</div>
<template v-else>
<div class="form-group">
<label for="default-variant" class="col-sm-3 control-label text-left">
{{ trans('product::products.form.variants.default_variant') }}
</label>
<div class="col-sm-5">
<select
name="default_variant"
id="default-variant"
class="form-control custom-select-black"
@change="changeDefaultVariant($event.target.value)"
>
<option
v-for="(variant, index) in form.variants"
:value="variant.uid"
:selected="defaultVariantUid === variant.uid"
:disabled="!isActiveVariant(index)"
>
@{{ variant.name }}
</option>
</select>
</div>
</div>
@include('product::admin.products.partials.bulk_edit_variants')
<transition-group tag="div" name="variant" class="variants-group">
<div
v-for="(variant, index) in form.variants"
:id="`variant-${variant.uid}`"
class="content-accordion panel-group options-group-wrapper"
:key="variant.position"
>
<div class="panel panel-default">
<div class="panel-heading" @click.stop="toggleAccordion($event, variant)">
<h4 class="panel-title">
<div
:aria-expanded="variant.is_open"
data-toggle="collapse"
:href="`#variant-collapse-${variant.uid}`"
:class="{
'collapsed': !variant.is_open,
'has-error': hasAnyError({
name: 'variants',
uid: variant.uid
})
}"
>
<div class="d-flex align-items-center">
<div v-if="variant.is_selected" class="checkbox">
<input
type="checkbox"
:name="`variants.${variant.uid}.is_selected`"
:id="`variants-${variant.uid}-is-selected`"
:checked="variant.is_selected"
disabled
>
<label :for="`variants-${variant.uid}-is-selected`"></label>
</div>
<span class="variant-name">@{{ variant.name }}</span>
<ul class="variant-badge list-inline d-flex">
<li v-if="variant.is_default">
<span class="label label-primary">
{{ trans('product::products.variants.default') }}
</span>
</li>
<li v-else-if="!variant.is_active">
<span class="label label-default">
{{ trans('product::products.variants.inactive') }}
</span>
</li>
<li v-if="variant.is_active && variant.in_stock == 0">
<span class="label label-warning">
{{ trans('product::products.variants.out_of_stock') }}
</span>
</li>
</ul>
</div>
<div class="switch" @click.stop>
<input
type="checkbox"
:name="`variants.${variant.uid}.is_active`"
:id="`variants-${variant.uid}-is-active`"
:disabled="defaultVariantUid === variant.uid"
v-model="variant.is_active"
>
<label :for="`variants-${variant.uid}-is-active`" @click="changeVariantStatus(variant.uid)"></label>
</div>
</div>
</h4>
</div>
<div class="panel-collapse" :class="{ 'collapse': !variant.is_open }">
<div class="panel-body">
<div class="row">
<div class="col-sm-4">
<draggable
animation="200"
class="product-media-grid"
force-fallback="true"
handle=".handle"
:move="preventLastSlideDrag"
:list="variant.media"
>
<div class="media-grid-item handle" v-for="(media, mediaIndex) in variant.media" :key="mediaIndex">
<div class="image-holder">
<img :src="media.path" alt="product variant media">
<button type="button" class="btn remove-image" @click="removeVariantMedia(index, mediaIndex)"></button>
</div>
</div>
<div class="media-grid-item media-picker disabled" @click="addVariantMedia(index)">
<div class="image-holder">
<img src="{{ asset('build/assets/placeholder_image.png') }}" class="placeholder-image" alt="Placeholder image">
</div>
</div>
</draggable>
</div>
<div class="col-sm-8">
<div class="variant-fields">
<div class="row">
<div class="col-sm-6">
<div class="form-group">
<label :for="`variants-${variant.uid}-sku`">
{{ trans('product::products.form.variants.sku') }}
</label>
<input
type="text"
:name="`variants.${variant.uid}.sku`"
:id="`variants-${variant.uid}-sku`"
class="form-control"
v-model="variant.sku"
>
<span
class="help-block text-red"
v-if="errors.has(`variants.${variant.uid}.sku`)"
v-text="errors.get(`variants.${variant.uid}.sku`)"
>
</span>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label :for="`variants-${variant.uid}-price`">
{{ trans('product::products.form.variants.price') }}
<span class="text-red">*</span>
</label>
<div class="input-group">
<span class="input-group-addon">$</span>
<input
type="number"
:name="`variants.${variant.uid}.price`"
min="0"
step="0.1"
:id="`variants-${variant.uid}-price`"
class="form-control"
@wheel="$event.target.blur()"
v-model.number="variant.price"
>
</div>
<span
class="help-block text-red"
v-if="errors.has(`variants.${variant.uid}.price`)"
v-text="errors.get(`variants.${variant.uid}.price`)"
>
</span>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group">
<label :for="`variants-${variant.uid}-special-price`">
{{ trans('product::products.form.variants.special_price') }}
</label>
<div class="input-group">
<span class="input-group-addon">
@{{ variant.special_price_type === 'fixed' ? '$' : '%' }}
</span>
<input
type="number"
:name="`variants.${variant.uid}.special_price`"
min="0"
step="0.1"
:id="`variants-${variant.uid}-special-price`"
class="form-control"
@wheel="$event.target.blur()"
v-model="variant.special_price"
>
</div>
<span
class="help-block text-red"
v-if="errors.has(`variants.${variant.uid}.special_price`)"
v-text="errors.get(`variants.${variant.uid}.special_price`)"
>
</span>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label :for="`variants-${variant.uid}-special-price-type`">
{{ trans('product::products.form.variants.special_price_type') }}
</label>
<select
:name="`variants.${variant.uid}.special_price_type`"
:id="`variants-${variant.uid}-special-price-type`"
class="form-control custom-select-black"
v-model="variant.special_price_type"
>
<option value="fixed">
{{ trans('product::products.form.variants.special_price_types.fixed') }}
</option>
<option value="percent">
{{ trans('product::products.form.variants.special_price_types.percent') }}
</option>
</select>
<span
class="help-block text-red"
v-if="errors.has(`variants.${variant.uid}.special_price_type`)"
v-text="errors.get(`variants.${variant.uid}.special_price_type`)"
>
</span>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group">
<label :for="`variants-${variant.uid}-special-price-start`">
{{ trans('product::products.form.variants.special_price_start') }}
</label>
<flat-pickr
:name="`variants.${variant.uid}.special_price_start`"
:id="`variants-${variant.uid}-special-price-start`"
class="form-control"
:config="flatPickrConfig"
v-model="variant.special_price_start"
>
</flat-pickr>
<span
class="help-block text-red"
v-if="errors.has(`variants.${variant.uid}.special_price_start`)"
v-text="errors.get(`variants.${variant.uid}.special_price_start`)"
>
</span>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label :for="`variants-${variant.uid}-special-price-end`">
{{ trans('product::products.form.variants.special_price_end') }}
</label>
<flat-pickr
:name="`variants.${variant.uid}.special_price_end`"
:id="`variants-${variant.uid}-special-price-end`"
class="form-control"
:config="flatPickrConfig"
v-model="variant.special_price_end"
>
</flat-pickr>
<span
class="help-block text-red"
v-if="errors.has(`variants.${variant.uid}.special_price_end`)"
v-text="errors.get(`variants.${variant.uid}.special_price_end`)"
>
</span>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group">
<label :for="`variants-${variant.uid}-manage-stock`">
{{ trans('product::products.form.variants.manage_stock') }}
</label>
<select
:name="`variants.${variant.uid}.manage_stock`"
:id="`variants-${variant.uid}-manage-stock`"
class="form-control custom-select-black"
@change="focusField({
selector: `#variants-${variant.uid}-qty`,
key: `variants.${variant.uid}.qty`
})"
v-model.number="variant.manage_stock"
>
<option value="0">
{{ trans('product::products.form.variants.manage_stock_states.0') }}
</option>
<option value="1">
{{ trans('product::products.form.variants.manage_stock_states.1') }}
</option>
</select>
<span
class="help-block text-red"
v-if="errors.has(`variants.${variant.uid}.manage_stock`)"
v-text="errors.get(`variants.${variant.uid}.manage_stock`)"
>
</span>
</div>
</div>
<div v-if="variant.manage_stock == 1" class="col-sm-6">
<div class="form-group">
<label :for="`variants-${variant.uid}-qty`">
{{ trans('product::products.form.variants.qty') }}<span class="text-red">*</span>
</label>
<input
type="number"
:name="`variants.${variant.uid}.qty`"
min="0"
step="1"
:id="`variants-${variant.uid}-qty`"
class="form-control"
@wheel="$event.target.blur()"
v-model="variant.qty"
>
<span
class="help-block text-red"
v-if="errors.has(`variants.${variant.uid}.qty`)"
v-text="errors.get(`variants.${variant.uid}.qty`)"
>
</span>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label :for="`variants-${variant.uid}-in-stock`">
{{ trans('product::products.form.variants.in_stock') }}
</label>
<select
:name="`variants.${variant.uid}.in_stock`"
:id="`variants-${variant.uid}-in-stock`"
class="form-control custom-select-black"
v-model="variant.in_stock"
>
<option value="0">
{{ trans('product::products.form.variants.stock_availability_states.0') }}
</option>
<option value="1">
{{ trans('product::products.form.variants.stock_availability_states.1') }}
</option>
</select>
<span
class="help-block text-red"
v-if="errors.has(`variants.${variant.uid}.in_stock`)"
v-text="errors.get(`variants.${variant.uid}.in_stock`)"
>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</transition-group>
</template>
</div>
</div>
</template>

View File

@@ -0,0 +1,310 @@
<template v-else-if="section === 'variations'">
<div class="box-header">
<h5>{{ trans('product::products.group.variations') }}</h5>
<div class="d-flex">
<span
class="toggle-accordion"
:class="{ 'collapsed': isCollapsedVariationsAccordion }"
data-toggle="tooltip"
data-placement="top"
:data-original-title="
isCollapsedVariationsAccordion ?
'{{ trans('product::products.section.expand_all') }}' :
'{{ trans('product::products.section.collapse_all') }}'
"
@click="toggleAccordions({
selector: '.variations-group .panel-heading',
state: isCollapsedVariationsAccordion,
data: form.variations
})"
>
<i class="fa fa-angle-double-up" aria-hidden="true"></i>
</span>
<div class="drag-handle">
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
<i class="fa fa-ellipsis-h" aria-hidden="true"></i>
</div>
</div>
</div>
<div class="box-body">
<div class="accordion-box-content">
<draggable
animation="150"
class="variations-group"
force-fallback="true"
handle=".drag-handle"
@change="reorderVariations"
:list="form.variations"
>
<div
v-for="(variation, index) in form.variations"
:id="`variation-${variation.uid}`"
class="content-accordion panel-group options-group-wrapper"
:class="`variation-${variation.uid}`"
:key="index"
>
<div class="panel panel-default">
<div class="panel-heading" @click.stop="toggleAccordion($event, variation)">
<h4 class="panel-title">
<div
:aria-expanded="variation.is_open"
data-toggle="collapse"
data-transition="false"
:class="{
'collapsed': !variation.is_open,
'has-error': hasAnyError({
name: 'variations',
uid: variation.uid
})
}"
>
<div class="d-flex align-items-center">
<span class="drag-handle">
<i class="fa">&#xf142;</i>
<i class="fa">&#xf142;</i>
</span>
<span v-text="variation.name || '{{ trans('product::products.variations.new_variation') }}'"></span>
</div>
<span
class="delete-option"
@click.stop="deleteVariation(index, variation.uid)"
>
<i class="fa fa-trash"></i>
</span>
</div>
</h4>
</div>
<div class="panel-collapse" :class="{ 'collapse': !variation.is_open }">
<div class="panel-body">
<div class="new-option">
<div class="row">
<div class="col-sm-6">
<div class="form-group">
<label :for="`variations-${variation.uid}-name`">
{{ trans('product::products.form.variations.name') }}
<span v-if="variation.name || variation.type" class="text-red">*</span>
</label>
<input
type="text"
:name="`variations.${variation.uid}.name`"
:id="`variations-${variation.uid}-name`"
class="form-control"
v-model="variation.name"
>
<span
class="help-block text-red"
v-if="errors.has(`variations.${variation.uid}.name`)"
v-text="errors.get(`variations.${variation.uid}.name`)"
>
</span>
</div>
</div>
<div class="col-sm-3">
<div class="form-group">
<label :for="`variations-${variation.uid}-type`">
{{ trans('product::products.form.variations.type') }}
<span v-if="variation.name || variation.type" class="text-red">*</span>
</label>
<select
:name="`variations.${variation.uid}.type`"
:id="`variations-${variation.uid}-type`"
class="form-control custom-select-black"
@change="changeVariationType($event.target.value, index, variation.uid)"
v-model="variation.type"
>
<option value="">
{{ trans('product::products.form.variations.variation_types.please_select') }}
</option>
<option value="text">
{{ trans('product::products.form.variations.variation_types.text') }}
</option>
<option value="color">
{{ trans('product::products.form.variations.variation_types.color') }}
</option>
<option value="image">
{{ trans('product::products.form.variations.variation_types.image') }}
</option>
</select>
<span
class="help-block text-red"
v-if="errors.has(`variations.${variation.uid}.type`)"
v-text="errors.get(`variations.${variation.uid}.type`)"
>
</span>
</div>
</div>
</div>
</div>
<div class="variation-values" v-if="variation.type !== ''">
<div class="table-responsive">
<table
class="options table table-bordered table-striped"
:class="variation.type !== '' ? `type-${variation.type}` : ''"
>
<thead>
<tr>
<th></th>
<th>
{{ trans('product::products.form.variations.label') }}
<span class="text-red">*</span>
</th>
<th v-if="variation.type === 'color'">
{{ trans('product::products.form.variations.color') }}
<span class="text-red">*</span>
</th>
<th v-else-if="variation.type === 'image'">
{{ trans('product::products.form.variations.image') }}
<span class="text-red">*</span>
</th>
<th></th>
</tr>
</thead>
<tbody
animation="150"
handle=".drag-handle"
is="draggable"
tag="tbody"
@change="reorderVariationValues"
:list="variation.values"
>
<tr class="option-row" v-for="(value, valueIndex) in variation.values" :key="valueIndex">
<td class="text-center">
<span class="drag-handle">
<i class="fa">&#xf142;</i>
<i class="fa">&#xf142;</i>
</span>
</td>
<td>
<input
type="text"
:name="`variations.${variation.uid}.values.${value.uid}.label`"
:id="`variations-${variation.uid}-values-${value.uid}-label`"
class="form-control"
@input="generateVariants"
@keyup.enter="addVariationRowOnPressEnter($event, index, valueIndex)"
v-model="value.label"
>
<span
class="help-block text-red"
v-if="errors.has(`variations.${variation.uid}.values.${value.uid}.label`)"
v-text="errors.get(`variations.${variation.uid}.values.${value.uid}.label`)"
>
</span>
</td>
<td v-if="variation.type === 'color'">
<div>
<input
type="text"
:name="`variations.${variation.uid}.values.${value.uid}.color`"
:id="`variations-${variation.uid}-values-${value.uid}-color`"
class="form-control color-picker"
v-model="value.color"
>
</div>
<span
class="help-block text-red"
v-if="errors.has(`variations.${variation.uid}.values.${value.uid}.color`)"
v-text="errors.get(`variations.${variation.uid}.values.${value.uid}.color`)"
>
</span>
</td>
<td v-else-if="variation.type === 'image'">
<div class="d-flex">
<div
class="image-holder"
@click="chooseVariationImage(index, variation.uid, valueIndex, value.uid)"
>
<template v-if="value.image.id">
<img :src="value.image.path" alt="variation image">
</template>
<img v-else src="{{ asset('build/assets/placeholder_image.png') }}" class="placeholder-image" alt="Placeholder image">
</div>
</div>
<span
class="help-block text-red"
v-if="errors.has(`variations.${variation.uid}.values.${value.uid}.image`)"
v-text="errors.get(`variations.${variation.uid}.values.${value.uid}.image`)"
>
</span>
</td>
<td class="text-center">
<button
type="button"
tabindex="-1"
class="btn btn-default delete-row"
@click="deleteVariationRow(index, variation.uid, valueIndex, value.uid)"
>
<i class="fa fa-trash"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
<button
type="button"
class="btn btn-default"
@click="addVariationRow(index, variation.uid)"
>
{{ trans('product::products.variations.add_row') }}
</button>
</div>
</div>
</div>
</div>
</div>
</draggable>
<div class="accordion-box-footer">
<button type="button" class="btn btn-default" @click="addVariation">
{{ trans('product::products.variations.add_variation') }}
</button>
@hasAccess('admin.variations.index')
<div class="insert-template">
<select class="form-control custom-select-black" v-model="globalVariationId">
<option value="">
{{ trans('product::products.form.variations.select_template') }}
</option>
@foreach ($globalVariations as $globalVariation)
<option value="{{ $globalVariation->id }}">{{ $globalVariation->name }}</option>
@endforeach
</select>
<button
type="button"
class="btn btn-default"
:class="{ 'btn-loading': addingGlobalVariation }"
:disabled="isAddGlobalVariationDisabled"
@click="addGlobalVariation"
>
{{ trans('product::products.variations.insert') }}
</button>
</div>
@endHasAccess
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,308 @@
<div class="bulk-edit-variants overflow-hidden">
<div class="form-group">
<label for="variation-values-list" class="col-sm-3 control-label text-left">
{{ trans('product::products.form.variants.bulk_edit') }}
</label>
<div class="col-sm-5">
<select
name="variation_values_list"
id="variation-values-list"
class="form-control custom-select-black"
@change="changeBulkEditVariantsUid($event.target.value)"
v-model="bulkEditVariantsUid"
>
<option value="">{{ trans('admin::admin.form.please_select') }}</option>
<option value="all">{{ trans('product::products.form.variants.all_variants') }}</option>
<template v-for="(variation, index) in form.variations">
<template v-for="(value, valueIndex) in variation.values">
<option v-if="variation.type !== '' && Boolean(value.label)" :key="value.uid" :value="value.uid">
@{{ value.label }}
</option>
</template>
</template>
</select>
</div>
</div>
<div v-if="hasBulkEditVariantsUid" class="form-group">
<label for="bulk-edit-variants-field-type" class="col-sm-3 control-label text-left">
{{ trans('product::products.form.variants.field_type') }}
</label>
<div class="col-sm-5">
<select
name="bulk_edit_variants_field_type"
id="bulk-edit-variants-field-type"
class="form-control custom-select-black"
@change="changeBulkEditVariantsField($event.target.value)"
v-model="bulkEditVariantsField"
>
<option value="">{{ trans('admin::admin.form.please_select') }}</option>
<option value="is_active">{{ trans('product::products.form.variants.is_active') }}</option>
<option value="media">{{ trans('product::products.form.variants.media') }}</option>
<option value="sku">{{ trans('product::products.form.variants.sku') }}</option>
<option value="price">{{ trans('product::products.form.variants.price') }}</option>
<option value="special_price">{{ trans('product::products.form.variants.special_price') }}</option>
<option value="manage_stock">{{ trans('product::products.form.variants.manage_stock') }}</option>
<option value="in_stock">{{ trans('product::products.form.variants.in_stock') }}</option>
</select>
</div>
</div>
<template v-if="hasBulkEditVariantsUid && hasBulkEditVariantsField">
<div v-if="bulkEditVariantsField === 'is_active'" class="form-group">
<label for="bulk-edit-variants-is-active" class="col-sm-3 control-label text-left">
{{ trans('product::products.form.variants.is_active') }}
</label>
<div class="col-sm-5">
<div class="checkbox">
<input
type="checkbox"
name="bulk_edit_variants_is_active"
id="bulk-edit-variants-is-active"
v-model="bulkEditVariants.is_active"
>
<label for="bulk-edit-variants-is-active">
{{ trans('product::products.form.variants.enable_the_variants') }}
</label>
</div>
</div>
</div>
<div v-else-if="bulkEditVariantsField === 'media'" class="form-group">
<label class="col-sm-3 control-label text-left">
{{ trans('product::products.form.variants.media') }}
</label>
<div class="col-sm-5">
<draggable
animation="200"
class="product-media-grid"
force-fallback="true"
handle=".handle"
:move="preventLastSlideDrag"
:list="bulkEditVariants.media"
>
<div class="media-grid-item handle" v-for="(media, index) in bulkEditVariants.media" :key="index">
<div class="image-holder">
<img :src="media.path" alt="product variants media">
<button type="button" class="btn remove-image" @click="removeBulkEditVariantsMedia(index)"></button>
</div>
</div>
<div class="media-grid-item media-picker disabled" @click="addBulkEditVariantsMedia">
<div class="image-holder">
<img src="{{ asset('build/assets/placeholder_image.png') }}" class="placeholder-image" alt="Placeholder image">
</div>
</div>
</draggable>
</div>
</div>
<div v-else-if="bulkEditVariantsField === 'sku'" class="form-group">
<label for="bulk-edit-variants-sku" class="col-sm-3 control-label text-left">
{{ trans('product::products.form.variants.sku') }}
</label>
<div class="col-sm-5">
<input
type="text"
name="bulk_edit_variants_sku"
id="bulk-edit-variants-sku"
class="form-control"
v-model="bulkEditVariants.sku"
>
</div>
</div>
<div v-else-if="bulkEditVariantsField === 'price'" class="form-group">
<label for="bulk-edit-variants-price" class="col-sm-3 control-label text-left">
{{ trans('product::products.form.variants.price') }}
</label>
<div class="col-sm-5">
<div class="input-group">
<span class="input-group-addon">$</span>
<input
type="number"
name="bulk_edit_variants_price"
min="0"
step="0.1"
id="bulk-edit-variants-price"
class="form-control"
@wheel="$event.target.blur()"
v-model.number="bulkEditVariants.price"
>
</div>
</div>
</div>
<template v-else-if="bulkEditVariantsField === 'special_price'">
<div class="form-group">
<label for="bulk-edit-variants-special-price" class="col-sm-3 control-label text-left">
{{ trans('product::products.form.variants.special_price') }}
</label>
<div class="col-sm-5">
<div class="input-group">
<span class="input-group-addon">
@{{ bulkEditVariants.special_price_type === 'fixed' ? '$' : '%' }}
</span>
<input
type="number"
name="bulk_edit_variants_special_price"
min="0"
step="0.1"
id="bulk-edit-variants-special-price"
class="form-control"
@wheel="$event.target.blur()"
v-model="bulkEditVariants.special_price"
>
</div>
</div>
</div>
<div class="form-group">
<label for="bulk-edit-variants-special-price-type" class="col-sm-3 control-label text-left">
{{ trans('product::products.form.variants.special_price_type') }}
</label>
<div class="col-sm-5">
<select
name="bulk_edit_variants_special_price_type"
id="bulk-edit-variants-special-price-type"
class="form-control custom-select-black"
v-model="bulkEditVariants.special_price_type"
>
<option value="fixed">
{{ trans('product::products.form.variants.special_price_types.fixed') }}
</option>
<option value="percent">
{{ trans('product::products.form.variants.special_price_types.percent') }}
</option>
</select>
</div>
</div>
<div class="form-group">
<label for="bulk-edit-variants-special-price-start" class="col-sm-3 control-label text-left">
{{ trans('product::products.form.variants.special_price_start') }}
</label>
<div class="col-sm-5">
<flat-pickr
name="bulk_edit_variants_special_price_start"
id="bulk-edit-variants-special-price-start"
class="form-control"
:config="flatPickrConfig"
v-model="bulkEditVariants.special_price_start"
>
</flat-pickr>
</div>
</div>
<div class="form-group">
<label for="bulk-edit-variants-special-price-end" class="col-sm-3 control-label text-left">
{{ trans('product::products.form.variants.special_price_end') }}
</label>
<div class="col-sm-5">
<flat-pickr
name="bulk_edit_variants_special_price_end"
id="bulk-edit-variants-special-price-end"
class="form-control"
:config="flatPickrConfig"
v-model="bulkEditVariants.special_price_end"
>
</flat-pickr>
</div>
</div>
</template>
<template v-else-if="bulkEditVariantsField === 'manage_stock'">
<div class="form-group">
<label for="bulk-edit-variants-manage-stock" class="col-sm-3 control-label text-left">
{{ trans('product::products.form.variants.manage_stock') }}
</label>
<div class="col-sm-5">
<select
name="bulk_edit_variants_manage_stock`"
id="bulk-edit-variants-manage-stock"
class="form-control custom-select-black"
@change="focusField({
selector: '#bulk-edit-variants-qty'
})"
v-model="bulkEditVariants.manage_stock"
>
<option value="0">
{{ trans('product::products.form.variants.manage_stock_states.0') }}
</option>
<option value="1">
{{ trans('product::products.form.variants.manage_stock_states.1') }}
</option>
</select>
</div>
</div>
<div v-if="bulkEditVariants.manage_stock == 1" class="form-group">
<label for="bulk-edit-variants-qty" class="col-sm-3 control-label text-left">
{{ trans('product::products.form.variants.qty') }}
</label>
<div class="col-sm-5">
<input
type="number"
name="bulk_edit_variants_qty"
min="0"
step="1"
id="bulk-edit-variants-qty"
class="form-control"
@wheel="$event.target.blur()"
v-model.number="bulkEditVariants.qty"
>
</div>
</div>
</template>
<div v-else-if="bulkEditVariantsField === 'in_stock'" class="form-group">
<label for="bulk-edit-variants-in-stock`" class="col-sm-3 control-label text-left">
{{ trans('product::products.form.variants.in_stock') }}
</label>
<div class="col-sm-5">
<select
name="bulk_edit_variants_in_stock`"
id="bulk-edit-variants-in-stock`"
class="form-control custom-select-black"
v-model="bulkEditVariants.in_stock"
>
<option value="0">
{{ trans('product::products.form.variants.stock_availability_states.0') }}
</option>
<option value="1">
{{ trans('product::products.form.variants.stock_availability_states.1') }}
</option>
</select>
</div>
</div>
<div class="form-group">
<div class="col-sm-5 col-sm-offset-3">
<button type="button" class="btn btn-default" @click="bulkUpdateVariants">
{{ trans('product::products.variants.apply') }}
</button>
</div>
</div>
</template>
</div>

View File

@@ -6,7 +6,7 @@
@endpush
@push('scripts')
<script>
<script type="module">
keypressAction([
{ key: 'b', route: "{{ route('admin.products.index') }}" },
]);

View File

@@ -6,5 +6,5 @@
<th>{{ trans('product::products.table.name') }}</th>
<th>{{ trans('product::products.table.price') }}</th>
<th>{{ trans('admin::admin.table.status') }}</th>
<th data-sort>{{ trans('admin::admin.table.created') }}</th>
<th data-sort>{{ trans('admin::admin.table.updated') }}</th>
</tr>