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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions addon/components/modals/add-store-to-category.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
<div class="modal-body-container">
<NetworkCategoryPicker @network={{@options.network}} @onSelect={{@options.onSelectCategory}} @onCreateNewCategory={{@options.createNewCategory}} @wrapperClass="mr-2" />
{{#if @options.categories}}
{{! Products: a store's own category tree. }}
<Storefront::Product::CategoryTreeSelect
@categories={{@options.categories}}
@selected={{@options.category}}
@placeholder={{t "storefront.products.bulk.pick-category"}}
@onSelect={{@options.setCategory}}
/>
{{else}}
<NetworkCategoryPicker @network={{@options.network}} @onSelect={{@options.onSelectCategory}} @onCreateNewCategory={{@options.createNewCategory}} @wrapperClass="mr-2" />
{{/if}}
</div>
</Modal::Default>
</Modal::Default>
2 changes: 1 addition & 1 deletion addon/components/modals/create-product-category.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
<div class="modal-body-container">
<Storefront::Product::CategoryForm @category={{@options.category}} @uploadNewPhoto={{@options.uploadNewPhoto}} />
<Storefront::Product::CategoryForm @category={{@options.category}} @categories={{@options.categories}} @uploadNewPhoto={{@options.uploadNewPhoto}} />
</div>
</Modal::Default>
13 changes: 13 additions & 0 deletions addon/components/storefront/product/category-form.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@
@value={{@category.description}}
@helpText={{t "storefront.component.modals.create-product-category.description-text"}}
/>
{{#if this.hasParentPicker}}
<InputGroup @name={{t "storefront.component.modals.create-product-category.parent"}} @helpText={{t "storefront.component.modals.create-product-category.parent-text"}}>
<Select
@options={{this.parentOptions}}
@optionValue="value"
@optionLabel="label"
@value={{or @category.parent_uuid ""}}
@onSelect={{this.selectParent}}
class="w-full"
data-test-category-parent
/>
</InputGroup>
{{/if}}
<InputGroup @name={{t "storefront.component.modals.create-product-category.icon"}}>
<div class="storefront-product-category-form__media">
<Image
Expand Down
36 changes: 36 additions & 0 deletions addon/components/storefront/product/category-form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { parentCategoryOptions } from '../../../utils/category-tree';

/**
* A product category's name, description and icon, and, given the store's other categories,
* the category it sits under (up to three levels deep).
*/
export default class StorefrontProductCategoryFormComponent extends Component {
@service intl;

get hasParentPicker() {
return Array.isArray(this.args.categories) || typeof this.args.categories?.toArray === 'function';
}

get parentOptions() {
const indent = '    ';

return [
{ value: '', label: this.intl.t('storefront.component.modals.create-product-category.top-level') },
...parentCategoryOptions(this.args.categories, this.args.category).map((row) => ({
value: row.category.id,
label: `${indent.repeat(row.depth)}${row.path}`,
})),
];
}

/** Sets the parent and its id together: the serializer writes the parent's id when one is set. */
@action selectParent(value) {
const parent = parentCategoryOptions(this.args.categories, this.args.category).find((row) => row.category.id === value)?.category ?? null;

this.args.category.setProperties({ parent_uuid: parent ? value : null, parent });
this.args.onChange?.();
}
}
9 changes: 9 additions & 0 deletions addon/components/storefront/product/category-sidebar.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@
<strong>{{row.category.name}}</strong>
{{#if (eq row.depth 0)}}<small>{{n-a row.category.description ""}}</small>{{/if}}
</span>
{{#if (and row.canHaveChildren (can "storefront create product-category"))}}
<span
class="storefront-product-category__add"
title={{t "storefront.products.sidebar.new-subcategory"}}
aria-hidden="true"
data-test-storefront-product-subcategory={{row.category.id}}
{{on "click" (fn this.createSubcategory row.category)}}
><FaIcon @icon="plus" @size="xs" /></span>
{{/if}}
<span class="storefront-product-category__count">{{row.count}}</span>
</button>
{{else}}
Expand Down
8 changes: 8 additions & 0 deletions addon/components/storefront/product/category-sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action, get } from '@ember/object';
import { inject as service } from '@ember/service';
import { MAX_CATEGORY_DEPTH } from '../../../utils/category-tree';

/**
* The category sidebar: nested categories with product counts, plus the smart groups a
Expand Down Expand Up @@ -48,6 +49,8 @@ export default class StorefrontProductCategorySidebarComponent extends Component
depth,
count: this.countFor(category),
hasChildren: children.length > 0,
// A third-level category takes no subcategories.
canHaveChildren: depth < MAX_CATEGORY_DEPTH - 1 && typeof this.args.onCreateSubcategory === 'function',
isCollapsed: this.collapsed.has(category.id),
isActive: this.args.activeCategory?.id === category.id,
});
Expand Down Expand Up @@ -79,6 +82,11 @@ export default class StorefrontProductCategorySidebarComponent extends Component
return this.args.onSelectGroup?.(group);
}

@action createSubcategory(category, event) {
event?.stopPropagation?.();
return this.args.onCreateSubcategory?.(category);
}

@action toggleCollapse(category, event) {
event?.stopPropagation?.();
const next = new Set(this.collapsed);
Expand Down
11 changes: 11 additions & 0 deletions addon/components/storefront/product/category-tree-select.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Select
@options={{this.options}}
@optionValue="value"
@optionLabel="label"
@value={{@selected.id}}
@placeholder={{@placeholder}}
@onSelect={{this.select}}
class="w-full"
data-test-category-tree-select
...attributes
/>
24 changes: 24 additions & 0 deletions addon/components/storefront/product/category-tree-select.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { flattenCategoryTree } from '../../../utils/category-tree';

/**
* Picks a category from a store's tree, each option indented and named by its path
* ("Drinks › Coffee › Cold brew").
*/
export default class StorefrontProductCategoryTreeSelectComponent extends Component {
get rows() {
return flattenCategoryTree(this.args.categories);
}

get options() {
const indent = '    ';

return this.rows.map((row) => ({ value: row.category.id, label: `${indent.repeat(row.depth)}${row.path}` }));
}

@action select(value) {
const row = this.rows.find((candidate) => candidate.category.id === value);
this.args.onSelect?.(row?.category ?? null);
}
}
2 changes: 1 addition & 1 deletion addon/components/storefront/product/form.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@
@onChangeId={{fn (mut @product.category_uuid)}}
as |model|
>
{{model.name}}
{{this.categoryLabel model}}
</ModelSelect>
</div>
<div class="storefront-create-order__field">
Expand Down
10 changes: 10 additions & 0 deletions addon/components/storefront/product/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action, get } from '@ember/object';
import { inject as service } from '@ember/service';
import { categoryPath } from '../../../utils/category-tree';

/** Which section of the page holds each field an issue can point at. */
const SECTION_FOR_FIELD = { name: 'product', price: 'pricing', sale_price: 'pricing', category: 'organization', media: 'media' };
Expand Down Expand Up @@ -33,6 +34,15 @@ function number(value) {
*/
export default class StorefrontProductFormComponent extends Component {
@service intl;
@service store;

/** A category option's place in the store's tree: "Drinks › Coffee › Cold brew". */
categoryLabel = (category) => {
const ownerId = get(category, 'owner_uuid');
const categories = this.store.peekAll('category').filter((record) => get(record, 'for') === 'storefront_product' && get(record, 'owner_uuid') === ownerId);

return categoryPath(categories, category);
};
@tracked moreOpen = false;
/** The language whose fields are on screen in the translations section. */
@tracked translationLanguage = null;
Expand Down
Loading