-
Notifications
You must be signed in to change notification settings - Fork 22
feat(FilterBlock): add block that filters its subblocks #180
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
Changes from all commits
56468c8
be5903e
f563b89
3cb7f89
772267b
1e6b097
05127f7
23782e8
17c7384
2803aaf
428b03d
b570dce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| @import '../../../styles/variables'; | ||
| @import '../../../styles/mixins'; | ||
|
|
||
| $block: '.#{$ns}filter-block'; | ||
| $innerBlock: '.#{$ns}block-base'; | ||
|
|
||
| #{$block} { | ||
| &__title { | ||
| margin-bottom: $indentSM; | ||
|
|
||
| @include centerable-title(); | ||
| } | ||
|
|
||
| &__tabs { | ||
| margin-bottom: 0; | ||
|
|
||
| @include tab-panel(); | ||
| } | ||
|
|
||
| .row &__block-container.row { | ||
| margin: 0px; | ||
| } | ||
|
|
||
| --pc-first-block-indent: 0; | ||
| --pc-first-block-mobile-indent: 0; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| import React, {useMemo, useState} from 'react'; | ||
|
|
||
| import i18n from './i18n'; | ||
| import {block} from '../../utils'; | ||
| import {BlockType, ConstructorItem, FilterBlockProps, FilterItem} from '../../models'; | ||
| import {Row, Col} from '../../grid'; | ||
| import {BlockHeader, AnimateBlock} from '../../components'; | ||
| import ButtonTabs, {ButtonTabsItemProps} from '../../components/ButtonTabs/ButtonTabs'; | ||
| import {ConstructorBlocks} from '../../containers/PageConstructor/components/ConstructorBlocks'; | ||
|
|
||
| import './FilterBlock.scss'; | ||
|
|
||
| const b = block('filter-block'); | ||
|
|
||
| const FilterBlock: React.FC<FilterBlockProps> = ({ | ||
| title, | ||
| description, | ||
| tags, | ||
| tagButtonSize, | ||
| allTag, | ||
| items, | ||
| colSizes, | ||
| centered, | ||
| animated, | ||
| }) => { | ||
| const tabButtons = useMemo(() => { | ||
| const allButton: ButtonTabsItemProps | undefined = allTag | ||
| ? {id: null, title: typeof allTag === 'boolean' ? i18n('label-all-tag') : allTag} | ||
| : undefined; | ||
| const otherButtons: ButtonTabsItemProps[] | undefined = | ||
| tags && tags.map((tag) => ({id: tag.id, title: tag.label})); | ||
| return [...(allButton ? [allButton] : []), ...(otherButtons ? otherButtons : [])]; | ||
| }, [allTag, tags]); | ||
|
|
||
| const [selectedTag, setSelectedTag] = useState(tabButtons.length ? tabButtons[0].id : null); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we return
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes we can, but it will make the body of the memo more complex, I believe it's better to leave it as is. |
||
|
|
||
| const actualTag: string | null = useMemo(() => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Didn't catch why do we need this variable? I think
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's assume that we have 3 ids, 'A', 'B', 'C'. ` We change the value of selected tag to 'C', then remove 'C' from our id list. In this case we should choose a new id to display. The actualTag is this new selected value, or the selectedTag if it still exists.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But how can we reproduce this situation? We don't have multiple tags choice or an opportunity to let no tags be selected |
||
| return tabButtons.length && !tabButtons.find((tab) => tab.id === selectedTag) | ||
| ? tabButtons[0].id | ||
| : selectedTag; | ||
| }, [tabButtons, selectedTag]); | ||
|
|
||
| const container: ConstructorItem[] = useMemo(() => { | ||
| const itemsToShow: FilterItem[] = actualTag | ||
| ? items.filter((item) => item.tags.includes(actualTag)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't it be easier to create object with tags as keys once at the start?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Each item can have several tag values. In this case we should add a card object to array of several keys - this procedure will be more complex in code, and I'm not sure that the performance will change dramatically to make so. |
||
| : items; | ||
| return [ | ||
| { | ||
| type: BlockType.CardLayoutBlock, | ||
| title: '', | ||
| colSizes: colSizes, | ||
| children: itemsToShow.map((item) => item.card), | ||
| }, | ||
| ]; | ||
| }, [actualTag, items, colSizes]); | ||
|
|
||
| return ( | ||
| <AnimateBlock className={b()} animate={animated}> | ||
| {title && ( | ||
| <BlockHeader | ||
| className={b('title', {centered: centered})} | ||
| title={title} | ||
| description={description} | ||
| /> | ||
| )} | ||
| {tabButtons.length && ( | ||
| <Row> | ||
| <Col> | ||
| <ButtonTabs | ||
| className={b('tabs', {centered: centered})} | ||
| items={tabButtons} | ||
| activeTab={selectedTag} | ||
| onSelectTab={setSelectedTag} | ||
| tabSize={tagButtonSize} | ||
| /> | ||
| </Col> | ||
| </Row> | ||
| )} | ||
| <Row className={b('block-container')}> | ||
| <ConstructorBlocks items={container} /> | ||
| </Row> | ||
| </AnimateBlock> | ||
| ); | ||
| }; | ||
| export default FilterBlock; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| `type: "filter-block"` | ||
|
|
||
| `title?: Title | string` — Title. | ||
|
|
||
| `description?: string` — Text. | ||
|
|
||
| `filterTags: []` - Tags by which content can be filtered. | ||
|
|
||
| `tagButtonSize: 's' | 'm' | 'l' | 'xl'` - Size of filter tags. | ||
|
|
||
| `allTag: boolean | string` - Specifies whether to show the 'All' tag. If the value is a non-falsy string, the block uses the value as label for the `All` tag. | ||
|
|
||
| `items:` — Items, the block displays. | ||
|
|
||
| - `tags: string[]` - tags assigned to the card. | ||
|
|
||
| - `card: SubBlock` - card to show. | ||
|
|
||
| `centered?: boolean` - Specifies whether the header and the tab panel are centered. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import React from 'react'; | ||
| import {Meta, Story} from '@storybook/react/types-6-0'; | ||
|
|
||
| import FilterBlock from '../FilterBlock'; | ||
| import { | ||
| FilterBlockModel, | ||
| CardWithImageProps, | ||
| FilterBlockProps, | ||
| FilterItem, | ||
| FilterTag, | ||
| } from '../../../models'; | ||
| import {CardWithImageModel} from '../../../models/constructor-items/sub-blocks'; | ||
| import {PageConstructor} from '../../../containers/PageConstructor'; | ||
|
|
||
| import data from './data.json'; | ||
|
|
||
| export default { | ||
| title: 'Blocks/Filter Block', | ||
| component: FilterBlock, | ||
| } as Meta; | ||
|
|
||
| const createItemList: ( | ||
| count: number, | ||
| shared: CardWithImageProps, | ||
| tagList: FilterTag[], | ||
| ) => FilterItem[] = (count, shared, tagList) => | ||
| Array.from({length: count}, (_, index) => ({ | ||
| tags: [tagList[index % tagList.length].id], | ||
| card: { | ||
| ...shared, | ||
| title: shared.title ? `${shared.title} ${index + 1}` : `${index + 1}`, | ||
| } as CardWithImageModel, | ||
| })); | ||
|
|
||
| const createArgs = (overrides: Partial<FilterBlockProps>) => | ||
| ({ | ||
| type: 'filter-block', | ||
| title: data.default.content.title, | ||
| description: data.default.content.description, | ||
| tags: data.default.filters, | ||
| items: createItemList(6, data.default.card, data.default.filters), | ||
| ...overrides, | ||
| } as FilterBlockProps); | ||
|
|
||
| const DefaultTemplate: Story<FilterBlockModel> = (args) => ( | ||
| <PageConstructor content={{blocks: [args]}} /> | ||
| ); | ||
|
|
||
| export const Default = DefaultTemplate.bind({}); | ||
| Default.args = createArgs({allTag: false}); | ||
|
|
||
| export const WithDefaultAllTag = DefaultTemplate.bind({}); | ||
| WithDefaultAllTag.args = createArgs({allTag: true}); | ||
|
|
||
| export const WithCustomAllTag = DefaultTemplate.bind({}); | ||
| WithCustomAllTag.args = createArgs({allTag: 'Custom All Tag Label'}); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| { | ||
| "default": { | ||
| "card": { | ||
| "type": "card-with-image", | ||
yuberdysheva marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| "image": "https://storage.yandexcloud.net/cloud-www-assets/constructor/storybook/images/img-mini_4-12_light.png", | ||
| "title": "Lorem ipsum", | ||
| "description": "Dolor sit amet" | ||
| }, | ||
| "content": { | ||
| "type": "card-layout-block", | ||
| "title": "Card Layout", | ||
| "description": "Three cards in a row on the desktop, two cards in a row on a tablet, one card in a row on a mobile phone." | ||
| }, | ||
| "filters": [ | ||
| { | ||
| "id": "one", | ||
| "label": "First very long label" | ||
| }, | ||
| { | ||
| "id": "two", | ||
| "label": "Second very long label" | ||
| }, | ||
| { | ||
| "id": "three", | ||
| "label": "Third very long label" | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "label-all-tag": "All" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import {registerKeyset} from '../../../utils/registerKeyset'; | ||
| import en from './en.json'; | ||
| import ru from './ru.json'; | ||
|
|
||
| const COMPONENT = 'FilterBlock'; | ||
|
|
||
| export default registerKeyset({en, ru}, COMPONENT); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "label-all-tag": "Все" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import { | ||
| AnimatableProps, | ||
| BlockBaseProps, | ||
| BlockHeaderProps, | ||
| containerSizesObject, | ||
| } from '../../schema/validators/common'; | ||
| import {filteredArray} from '../../schema/validators/utils'; | ||
|
|
||
| export const FilterTagProps = { | ||
| additionalProperties: false, | ||
| required: ['id', 'label'], | ||
| properties: { | ||
| id: { | ||
| type: 'string', | ||
| }, | ||
| label: { | ||
| type: 'string', | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| export const FilterItemProps = { | ||
| additionalProperties: false, | ||
| required: ['tags', 'card'], | ||
| properties: { | ||
| tags: { | ||
| type: 'array', | ||
| items: { | ||
| type: 'string', | ||
| }, | ||
| }, | ||
| card: {$ref: 'self#/definitions/card'}, | ||
| }, | ||
| }; | ||
|
|
||
| export const FilterProps = { | ||
| additionalProperties: false, | ||
| required: ['filterTags', 'block'], | ||
| properties: { | ||
| ...BlockBaseProps, | ||
| ...AnimatableProps, | ||
| ...BlockHeaderProps, | ||
| allTag: {oneOf: [{type: 'boolean'}, {type: 'string'}]}, | ||
| colSizes: containerSizesObject, | ||
| tags: filteredArray(FilterTagProps), | ||
| items: filteredArray(FilterItemProps), | ||
| tagButtonSize: { | ||
| type: 'string', | ||
| enum: ['s', 'm', 'l', 'xl'], | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| export const FilterBlock = { | ||
| 'filterable-block': FilterProps, | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
allButton ?? : []There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately cannot change this line as suggested, because we merge here two arrays to omit nulls in final array. Replacing ternary operator with the ?? operator because in this case type of result wont be an array.