Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: file section for configurable products #1619

Open
wants to merge 24 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
94feb70
feat: update types
ivan-kalachikov Mar 4, 2025
0bb0c80
feat: update types
ivan-kalachikov Mar 5, 2025
8c0d208
feat: file section for configurable products
ivan-kalachikov Mar 5, 2025
ae1d5eb
feat: update jsdoc
ivan-kalachikov Mar 6, 2025
2980223
feat: refactor
ivan-kalachikov Mar 6, 2025
68b7496
feat: add file section validation test cases
ivan-kalachikov Mar 6, 2025
0ea4624
feat: add file section common test cases
ivan-kalachikov Mar 6, 2025
103adb7
feat: add file section common test cases
ivan-kalachikov Mar 6, 2025
94028c6
fix: remove file handling
ivan-kalachikov Mar 6, 2025
7ed5f51
feat: add locales
ivan-kalachikov Mar 6, 2025
8bfc84d
fix: refactor and improve UI
ivan-kalachikov Mar 6, 2025
8f451d8
fix: tests
ivan-kalachikov Mar 6, 2025
f90686c
feat: improve file section
ivan-kalachikov Mar 6, 2025
cba2ddc
fix: resolve comments
ivan-kalachikov Mar 7, 2025
3a398ad
fix: memoize once fetched options to avoid multi requests
ivan-kalachikov Mar 11, 2025
5e29e6e
fix: move fetching file options inside a lifecycle hook
ivan-kalachikov Mar 12, 2025
ca1017f
Merge branch 'dev' into feat/VCST-2796-file-section-configurable-prod…
ivan-kalachikov Mar 12, 2025
47d84a7
Merge branch 'dev' into feat/VCST-2796-file-section-configurable-prod…
ivan-kalachikov Mar 12, 2025
c00503d
Merge branch 'dev' into feat/VCST-2796-file-section-configurable-prod…
ivan-kalachikov Mar 14, 2025
d4fa0e9
fix: import path
ivan-kalachikov Mar 17, 2025
7e81957
fix: import path
ivan-kalachikov Mar 17, 2025
6b1cc38
fix: import path
ivan-kalachikov Mar 17, 2025
32a4692
fix: locales and minor refactor
ivan-kalachikov Mar 17, 2025
a78e469
fix: change files scope
ivan-kalachikov Mar 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,9 @@ fragment fullLineItem on LineItemType {
name
customText
type
files {
name
url
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ query GetConfigurationItems(
productId
sectionId
type
files {
contentType
name
size
url
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,9 @@ fragment orderLineItemFields on OrderLineItemType {
name
customText
type
files {
name
url
}
}
}
142 changes: 85 additions & 57 deletions client-app/core/api/graphql/types.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion client-app/core/composables/useThemeContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ function _useThemeContext() {

async function fetchPreset(themePresetName: string): Promise<IThemeConfigPreset | void> {
try {
const module = (await import(`@/assets/presets/${themePresetName}.json`)) as {
const module = (await import(`../../assets/presets/${themePresetName}.json`)) as {
default: IThemeConfigPreset;
};
return module.default;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<template>
<div class="option-file">
<VcFileUploader
:files="files"
v-bind="fileOptions"
removable
@add-files="onAddFiles"
@remove-files="onRemoveFiles"
@download="onFileDownload"
/>
<div class="option-file__errors">
<VcAlert
v-for="(file, index) in filesWithErrors"
:key="index"
color="danger"
variant="solid-light"
size="sm"
icon
>
{{ file.errorMessage }}
</VcAlert>
</div>
</div>
</template>

<script setup lang="ts">
import { computed, toRefs, onMounted } from "vue";
import { downloadFile, useFiles } from "@/shared/files";
import { toAttachedFile } from "@/ui-kit/utilities";
import type { CartConfigurationItemFileType } from "@/core/api/graphql/types";
import type { DeepReadonly } from "vue";

const emit = defineEmits<IEmits>();
const props = defineProps<IProps>();

const { value } = toRefs(props);

const DEFAULT_FILES_SCOPE = "product-configuration";

interface IProps {
value?: DeepReadonly<CartConfigurationItemFileType[]>;
}

interface IEmits {
(e: "input", value: CartConfigurationItemFileType[]): void;
}

const initialFiles = computed(
() => value.value?.map((file) => toAttachedFile(file.name, file.size, file.contentType, file.url)) ?? [],
);

const {
files,
attachedAndUploadedFiles,
addFiles,
validateFiles,
removeFiles,
uploadFiles,
fetchOptions: fetchFileOptions,
options: fileOptions,
} = useFiles(DEFAULT_FILES_SCOPE, initialFiles);

async function onAddFiles(items: INewFile[]) {
addFiles(items);
validateFiles();
await uploadFiles();
emit("input", attachedAndUploadedFiles.value);
}

const filesWithErrors = computed(() => files.value.filter((file) => file.errorMessage));

async function onRemoveFiles(filesToRemove: FileType[]) {
await removeFiles(filesToRemove);
validateFiles();
emit("input", attachedAndUploadedFiles.value);
}

function onFileDownload(file: FileType) {
if (file && file.url) {
void downloadFile(file.url, file.name);
}
}

onMounted(() => {
void fetchFileOptions();
});
</script>

<style lang="scss">
.option-file {
&__errors {
@apply mt-2 flex flex-col gap-2;
}
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
:collapsed="index !== 0"
>
<template #title>
{{ section.name }}
<div class="product-configuration__title">
{{ section.name }}
<span v-if="section.isRequired" class="product-configuration__required">*</span>
</div>

<div class="product-configuration__subtitle">
{{ section.description }}
Expand All @@ -25,7 +28,18 @@
{{ validationErrors.get(section.id) }}
</div>

<div v-else>{{ getSectionSubtitle(section) }}</div>
<div
v-else
class="product-configuration__value"
:class="[
hasSelectedOption(section.id)
? 'product-configuration__value--selected'
: 'product-configuration__value--not-selected',
section.isRequired ? 'product-configuration__value--required' : '',
]"
>
{{ getSectionSubtitle(section) }}
</div>
</div>
</template>

Expand All @@ -41,9 +55,10 @@
:extended-price="option.extendedPrice"
:name="section.id"
@input="
handleInput({
selectSectionValue({
sectionId: section.id,
option: { productId: option.product.id, quantity: option.quantity ?? 1 },
productId: option.product.id,
quantity: option.quantity ?? 1,
type: section.type,
})
"
Expand All @@ -55,9 +70,8 @@
:name="section.id"
:selected="selectedConfiguration[section.id]?.productId === undefined"
@input="
handleInput({
selectSectionValue({
sectionId: section.id,
option: undefined,
type: section.type,
})
"
Expand All @@ -71,7 +85,7 @@
:value="selectedConfiguration[section.id]?.selectedOptionTextValue"
:selected="!!selectedConfiguration[section.id]"
@input="
handleInput({
selectSectionValue({
sectionId: section.id,
customText: $event,
type: section.type,
Expand All @@ -84,9 +98,42 @@
:name="section.id"
:selected="selectedConfiguration[section.id]?.selectedOptionTextValue === undefined"
@input="
handleInput({
selectSectionValue({
sectionId: section.id,
type: section.type,
})
"
/>
</template>

<template v-if="section.type === CONFIGURABLE_SECTION_TYPES.file">
<OptionFile
:is-required="section.isRequired"
:name="section.id"
:value="selectedConfiguration[section.id]?.files"
@input="
selectSectionValue({
sectionId: section.id,
files: $event,
type: section.type,
})
"
@remove-files="
selectSectionValue({
sectionId: section.id,
files: $event,
type: section.type,
})
"
/>

<OptionNone
v-if="!section.isRequired"
:name="section.id"
:selected="selectedConfiguration[section.id]?.selectedOptionTextValue === undefined"
@input="
selectSectionValue({
sectionId: section.id,
customText: undefined,
type: section.type,
})
"
Expand All @@ -109,11 +156,12 @@ import { CONFIGURABLE_SECTION_TYPES } from "@/shared/catalog/constants/configura
import { SaveChangesModal } from "@/shared/common";
import { useModal } from "@/shared/modal";
import { useNotifications } from "@/shared/notification";
import OptionFile from "./option-file.vue";
import OptionNone from "./option-none.vue";
import OptionProductNone from "./option-product-none.vue";
import OptionProduct from "./option-product.vue";
import OptionText from "./option-text.vue";
import type { ConfigurationSectionInput, ConfigurationSectionType } from "@/core/api/graphql/types";
import type { ConfigurationSectionType } from "@/core/api/graphql/types";
import type { DeepReadonly } from "vue";

const props = defineProps<IProps>();
Expand Down Expand Up @@ -163,22 +211,17 @@ watch(
},
);

function handleInput(payload: ConfigurationSectionInput) {
selectSectionValue(payload);
function hasSelectedOption(sectionId: string) {
return !!selectedConfiguration.value?.[sectionId]?.selectedOptionTextValue;
}

function getSectionSubtitle(section: DeepReadonly<ConfigurationSectionType>) {
if (selectedConfiguration.value?.[section.id]?.selectedOptionTextValue) {
if (hasSelectedOption(section.id)) {
return selectedConfiguration.value?.[section.id]?.selectedOptionTextValue;
}
switch (section.type) {
case CONFIGURABLE_SECTION_TYPES.product:
return t("shared.catalog.product_details.product_configuration.nothing_selected");
case CONFIGURABLE_SECTION_TYPES.text:
return t("shared.catalog.product_details.product_configuration.no_text");
default:
return t("shared.catalog.product_details.product_configuration.nothing_selected");
}
return section.isRequired
? t("shared.catalog.product_details.product_configuration.required_no_selected")
: t("shared.catalog.product_details.product_configuration.optional_no_selected");
}

async function canChangeRoute(): Promise<boolean> {
Expand Down Expand Up @@ -228,10 +271,16 @@ async function openSaveChangesModal(): Promise<boolean> {

<style lang="scss">
.product-configuration {
$required: "";

&__widgets {
@apply space-y-5;
}

&__required {
@apply text-danger;
}

&__subtitle {
@apply mt-1 text-xs font-normal normal-case text-neutral max-w-3xl;
}
Expand All @@ -245,7 +294,25 @@ async function openSaveChangesModal(): Promise<boolean> {
}

&__error {
@apply text-danger;
@apply text-danger-700;
}

&__value {
&--selected {
@apply text-success-600;
}

&--required {
$required: &;
}

&--not-selected {
@apply text-info-800;

&#{$required} {
@apply text-danger-800;
}
}
}
}
</style>
Loading
Loading