diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2772c617..7b03591e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,6 +19,7 @@ $ pnpm cz ## Mono repository structure This mono repository contains multiple packages under the folder `packages` + - `components` contains all the vue components and is released under the name `@prestashopcorp/puik-components` - `locale` contains all the translations files for the default wording in the components, this package is bundled with the other packages when it's used and isn't released as a standalone - `puik` contains all the other packages and is released under the name `@prestashopcorp/puik` diff --git a/_templates/component/new/packages/components/my-component/src/vue.ejs.t b/_templates/component/new/packages/components/my-component/src/vue.ejs.t index c78c3e7f..d497877f 100644 --- a/_templates/component/new/packages/components/my-component/src/vue.ejs.t +++ b/_templates/component/new/packages/components/my-component/src/vue.ejs.t @@ -17,4 +17,4 @@ defineProps<<%= h.changeCase.pascal(name) %>Props>(); \ No newline at end of file + diff --git a/_templates/component/new/packages/web-components/components/ce.ejs.t b/_templates/component/new/packages/web-components/components/ce.ejs.t index 3b91ab35..e73d5bd5 100644 --- a/_templates/component/new/packages/web-components/components/ce.ejs.t +++ b/_templates/component/new/packages/web-components/components/ce.ejs.t @@ -2,10 +2,10 @@ to: packages/web-components/components/<%= h.changeCase.param(name) %>.ts --- import { defineCustomElement } from 'vue'; -import { Puik<%= h.changeCase.pascal(name) %>} from '@prestashopcorp/puik-components'; +import { Puik<%= h.changeCase.pascal(name) %> } from '@prestashopcorp/puik-components'; import type { CustomElementWithName } from '../types'; const Puik<%= h.changeCase.pascal(name) %>Ce = defineCustomElement(Puik<%= h.changeCase.pascal(name) %>) as CustomElementWithName; Puik<%= h.changeCase.pascal(name) %>Ce.ceName = 'puik-<%= h.changeCase.param(name) %>-ce'; -export default Puik<%= h.changeCase.pascal(name) %>Ce; \ No newline at end of file +export default Puik<%= h.changeCase.pascal(name) %>Ce; diff --git a/packages/components/index.ts b/packages/components/index.ts index 5eeb725f..20f2710e 100644 --- a/packages/components/index.ts +++ b/packages/components/index.ts @@ -32,3 +32,4 @@ export * from './tag'; export * from './avatar'; export * from './divider'; export * from './notification-bar'; +export * from './sortable-list'; diff --git a/packages/components/package.json b/packages/components/package.json index e83e3f95..68c501a4 100755 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -14,9 +14,12 @@ "@popperjs/core": "^2.11.8", "@prestashopcorp/puik-theme": "workspace:*", "@vueuse/core": "^10.9.0", - "radix-vue": "^1.7.4" + "radix-vue": "^1.7.4", + "sortablejs": "^1.15.2", + "sortablejs-vue3": "^1.2.11" }, "devDependencies": { + "@types/sortablejs": "^1.15.8", "vue": "^3.3.7", "vue-router": "^4.3.2", "vue-tsc": "^1.8.27" diff --git a/packages/components/sortable-list/index.ts b/packages/components/sortable-list/index.ts new file mode 100644 index 00000000..aefc8927 --- /dev/null +++ b/packages/components/sortable-list/index.ts @@ -0,0 +1,6 @@ +import SortableList from './src/sortable-list.vue'; + +export const PuikSortableList = SortableList; +export default PuikSortableList; + +export * from './src/sortable-list'; diff --git a/packages/components/sortable-list/src/sortable-list.ts b/packages/components/sortable-list/src/sortable-list.ts new file mode 100644 index 00000000..0c2e1137 --- /dev/null +++ b/packages/components/sortable-list/src/sortable-list.ts @@ -0,0 +1,63 @@ +import '@prestashopcorp/puik-components/sortable-list/style/css'; +import type SortableList from './sortable-list.vue'; +import Sortable, { SortableOptions } from 'sortablejs'; +import type { AutoScrollOptions } from 'sortablejs/plugins'; + +export enum PuikSortableListIconPosition { + Left = 'left', + Right = 'right', +} + +export enum PuikSortableListTag { + Menu = 'menu', + Ol = 'ol', + Ul = 'ul', +} + +export type SortableOptionsProp = Omit< +SortableOptions | AutoScrollOptions, +| 'onUnchoose' +| 'onChoose' +| 'onStart' +| 'onEnd' +| 'onAdd' +| 'onUpdate' +| 'onSort' +| 'onRemove' +| 'onFilter' +| 'onMove' +| 'onClone' +| 'onChange' +>; + +export type SortableEvent = Sortable.SortableEvent; +export type SortableMoveEvent = Sortable.MoveEvent; + +export interface SortableListProps { + listId: string + list: any[] + displayPositionNumbers?: boolean + iconPosition?: `${PuikSortableListIconPosition}` + itemKey: string | ((item: any) => string | number | Symbol) + tag?: `${PuikSortableListTag}` + options?: any + dataTest?: string +} + +export type SortableListEmits = { + (event: 'list-changed', evt: any[]): void + (event: 'choose', evt: Sortable.SortableEvent): void + (event: 'unchoose', evt: Sortable.SortableEvent): void + (event: 'start', evt: Sortable.SortableEvent): void + (event: 'end', evt: Sortable.SortableEvent): void + (event: 'add', evt: Sortable.SortableEvent): void + (event: 'update', evt: Sortable.SortableEvent): void + (event: 'sort', evt: Sortable.SortableEvent): void + (event: 'remove', evt: Sortable.SortableEvent): void + (event: 'filter', evt: Sortable.SortableEvent): void + (event: 'move', evt: Sortable.MoveEvent, originalEvent: Event): void + (event: 'clone', evt: Sortable.SortableEvent): void + (event: 'change', evt: Sortable.SortableEvent): void +}; + +export type SortableListInstance = InstanceType; diff --git a/packages/components/sortable-list/src/sortable-list.vue b/packages/components/sortable-list/src/sortable-list.vue new file mode 100644 index 00000000..8620485a --- /dev/null +++ b/packages/components/sortable-list/src/sortable-list.vue @@ -0,0 +1,243 @@ + + + + + + + + {{ $attrs.dataSortableId || index + 1 }} + + + + + + + {{ `${element?.title}` }} + + + {{ `${element?.description}` }} + + + + + + + + + + + + + + + diff --git a/packages/components/sortable-list/stories/sortable-list.stories.ts b/packages/components/sortable-list/stories/sortable-list.stories.ts new file mode 100644 index 00000000..655d16c4 --- /dev/null +++ b/packages/components/sortable-list/stories/sortable-list.stories.ts @@ -0,0 +1,614 @@ +import { ref } from 'vue'; +import { + PuikSortableList, + PuikSortableListIconPosition, + PuikIcon, + PuikSortableListTag +} from '@prestashopcorp/puik-components'; +import { Meta, StoryFn, Args } from '@storybook/vue3'; + +const sortableListIconPosition = Object.values(PuikSortableListIconPosition); +const sortableListIconPositionSummary = sortableListIconPosition.join('|'); + +const sortableListTag = Object.values(PuikSortableListTag); +const sortableListTagSummary = sortableListTag.join('|'); + +const itemKey = ref('id'); + +const defaultList = ref([ + { + id: 1, + title: 'list-1-item 1', + description: 'description', + imgSrc: 'https://t.ly/Ku50h', + customKey: 'A custom Value', + anotherKey: 'Another custom value' + }, + { + id: 2, + title: 'list-1-item 2', + description: 'description', + imgSrc: 'https://t.ly/hkQSL', + customKey: 'A custom Value', + anotherKey: 'Another custom value' + }, + { + id: 3, + title: 'list-1-item 3', + description: 'description', + imgSrc: 'https://t.ly/MeB5s', + customKey: 'A custom Value', + anotherKey: 'Another custom value' + }, + { + id: 4, + title: 'list-1-item 4', + description: 'description', + imgSrc: 'https://t.ly/UfeAc', + customKey: 'A custom Value', + anotherKey: 'Another custom value' + } +]); + +const sharedList1 = ref([ + { + id: 1, + title: 'list-1-item 1', + description: 'description', + imgSrc: 'https://t.ly/Ku50h', + customKey: 'A custom Value', + anotherKey: 'Another custom value' + }, + { + id: 2, + title: 'list-1-item 2', + description: 'description', + imgSrc: 'https://t.ly/hkQSL', + customKey: 'A custom Value', + anotherKey: 'Another custom value' + }, + { + id: 3, + title: 'list-1-item 3', + description: 'description', + imgSrc: 'https://t.ly/MeB5s', + customKey: 'A custom Value', + anotherKey: 'Another custom value' + }, + { + id: 4, + title: 'list-1-item 4', + description: 'description', + imgSrc: 'https://t.ly/UfeAc', + customKey: 'A custom Value', + anotherKey: 'Another custom value' + } +]); + +const sharedList2 = ref([ + { + id: 7, + title: 'list-2-item 1', + description: 'description', + imgSrc: 'https://rb.gy/3wn9bd', + customKey: 'A custom Value', + anotherKey: 'Another custom value' + }, + { + id: 8, + title: 'list-2-item 2', + description: 'description', + imgSrc: 'https://rb.gy/bvcz13', + customKey: 'A custom Value', + anotherKey: 'Another custom value' + } +]); + +const customlist = ref([ + { + customKey: 'A custom Value', + anotherKey: 'Another custom value' + }, + { + customKey: 'A custom Value', + anotherKey: 'Another custom value' + }, + { + customKey: 'A custom Value', + anotherKey: 'Another custom value' + }, + { + customKey: 'A custom Value', + anotherKey: 'Another custom value' + } +]); + +const eventsList = ref([ + { + id: 1, + title: 'list-1-item 1', + description: 'description', + imgSrc: 'https://t.ly/Ku50h', + customKey: 'A custom Value', + anotherKey: 'Another custom value' + }, + { + id: 2, + title: 'list-1-item 2', + description: 'description', + imgSrc: 'https://t.ly/hkQSL', + customKey: 'A custom Value', + anotherKey: 'Another custom value' + }, + { + id: 3, + title: 'list-1-item 3', + description: 'description', + imgSrc: 'https://t.ly/MeB5s', + customKey: 'A custom Value', + anotherKey: 'Another custom value' + }, + { + id: 4, + title: 'list-1-item 4', + description: 'description', + imgSrc: 'https://t.ly/UfeAc', + customKey: 'A custom Value', + anotherKey: 'Another custom value' + } +]); + +export default { + title: 'Components/SortableList', + component: PuikSortableList, + argTypes: { + listId: { + control: 'text', + description: 'Identifier of the sortable list', + table: { + defaultValue: { + summary: 'undefined' + } + } + }, + options: { + control: 'object', + description: + 'Options of the sortable list (see https://github.com/SortableJS/Sortable#options)', + table: { + defaultValue: { + summary: 'see details', + detail: ` +group: "name", // or { name: "...", pull: [true, false, 'clone', array], put: [true, false, array] } +sort: true, // sorting inside list +delay: 0, // time in milliseconds to define when the sorting should start +delayOnTouchOnly: false, // only delay if user is using touch +touchStartThreshold: 0, // px, how many pixels the point should move before cancelling a delayed drag event +disabled: false, // Disables the sortable if set to true. +store: null, // @see Store +animation: 150, // ms, animation speed moving items when sorting, 0 — without animation +easing: "cubic-bezier(1, 0, 0, 1)", // Easing for animation. Defaults to null. See https://easings.net/ for examples. +handle: ".my-handle", // Drag handle selector within list items +filter: ".ignore-elements", // Selectors that do not lead to dragging (String or Function) +preventOnFilter: true, // Call event.preventDefault() when triggered filter +draggable: ".item", // Specifies which items inside the element should be draggable +dataIdAttr: 'data-id', // HTML attribute that is used by the toArray() method +ghostClass: "sortable-ghost", // Class name for the drop placeholder +chosenClass: "sortable-chosen", // Class name for the chosen item +dragClass: "sortable-drag", // Class name for the dragging item +swapThreshold: 1, // Threshold of the swap zone +invertSwap: false, // Will always use inverted swap zone if set to true +invertedSwapThreshold: 1, // Threshold of the inverted swap zone (will be set to swapThreshold value by default) +direction: 'horizontal', // Direction of Sortable (will be detected automatically if not given) +forceFallback: false, // ignore the HTML5 DnD behaviour and force the fallback to kick in +fallbackClass: "sortable-fallback", // Class name for the cloned DOM Element when using forceFallback +fallbackOnBody: false, // Appends the cloned DOM Element into the Document's Body +fallbackTolerance: 0, // Specify in pixels how far the mouse should move before it's considered as a drag. +dragoverBubble: false, +removeCloneOnHide: true, // Remove the clone element when it is not showing, rather than just hiding it +emptyInsertThreshold: 5, // px, distance mouse must be from empty sortable to insert drag element into it + + ` + } + } + }, + list: { + control: 'array', + description: 'List of the sortable list', + table: { + defaultValue: { + summary: 'undefined' + }, + type: { + summary: 'ListItem[]', + detail: ` +// ListItem +{ + title?: string + description?: string + imgSrc?: string + [key: string]: any +} + +// list prop +ListItem[] + ` + } + } + }, + displayPositionNumbers: { + control: 'boolean', + description: 'Display position numbers of the sortable list', + table: { + type: { summary: 'boolean' }, + defaultValue: { summary: 'true' } + }, + defaultValue: { summary: 'true' } + }, + itemKey: { + control: 'text', + description: 'Item key of the sortable list', + table: { + type: { + summary: 'string' + }, + defaultValue: { + summary: 'undefined' + } + } + }, + tag: { + control: 'select', + options: sortableListTag, + description: 'The HTML Tag of the sortable list container', + table: { + type: { + summary: sortableListTagSummary, + detail: ` +// Import +Import type { PuikSortableListTag } from '@prestashopcorp/puik-components'; + +// Details +enum PuikSortableListTag { + Menu = 'menu', + Ol = 'ol', + Ul = 'ul', +} +` + }, + defaultValue: { summary: 'ul' } + } + }, + iconPosition: { + control: 'select', + options: sortableListIconPosition, + description: 'Drag icon position of the sortable list items', + table: { + type: { + summary: sortableListIconPositionSummary, + detail: ` +// Import +import type { PuikSortableListIconPosition } from '@prestashopcorp/puik-components'; + +// Details +enum PuikSortableListIconPosition { + Left = 'left', + Right = 'right', +} + ` + }, + defaultValue: { summary: 'right' } + } + }, + dataTest: { + control: 'text', + description: 'Set the data-test attribute on the sortable list' + } + }, + args: { + listId: 'default-list', + displayPositionNumbers: true, + iconPosition: 'right', + list: defaultList.value, + itemKey: 'id', + tag: 'ul' + } +} as Meta; + +const DefaultTemplate: StoryFn = (args: Args) => ({ + components: { + PuikSortableList, + PuikIcon + }, + setup() { + return { args }; + }, + template: '' +}); + +export const Default = { + render: DefaultTemplate, + args: { + listId: 'default-list', + list: defaultList.value + }, + parameters: { + docs: { + description: { + story: ` +A listItem is an object that can include any key/value pair. However, the keys 'title', 'description', and 'imgSrc' are specifically used to generate the default UI of the list items. + + Here is a detailed description of these keys: +- 'title': A string representing the title of the list item. It is displayed as the main text of the item. +- 'description': A string providing additional information about the list item. It is displayed below the title. +- 'imgSrc': A string URL pointing to an image. It is displayed before the content (title, description). + +Note: These keys are all optional, and you can include other keys as per your requirements by using the custom-content slot instead (see Custom Content Slot section). + ` + }, + source: { + code: ` + +const list = [ + { + id: 1, + title: 'list-1-item 1', + description: 'description', + imgSrc: 'https://t.ly/Ku50h', + customKey: 'A custom Value', + anotherKey: 'Another custom value' + }, + { + id: 2, + title: 'list-1-item 2', + description: 'description', + imgSrc: 'https://t.ly/hkQSL', + customKey: 'A custom Value', + anotherKey: 'Another custom value' + }, + { + id: 3, + title: 'list-1-item 3', + description: 'description', + imgSrc: 'https://t.ly/MeB5s', + customKey: 'A custom Value', + anotherKey: 'Another custom value' + }, + { + id: 4, + title: 'list-1-item 4', + description: 'description', + imgSrc: 'https://t.ly/UfeAc', + customKey: 'A custom Value', + anotherKey: 'Another custom value' + } +]; + + + + `, + language: 'html' + } + } + } +}; + +const CustomContentSlotTemplate: StoryFn = (args: Args) => ({ + components: { + PuikSortableList, + PuikIcon + }, + setup() { + return { args }; + }, + template: ` + + + {{ element.customKey }} + {{ element.anotherKey }} + + + ` +}); + +export const CustomContentSlot = { + render: CustomContentSlotTemplate, + args: { + listId: 'custom-list', + list: customlist.value + }, + parameters: { + docs: { + description: { + story: ` +Using the customContent slot with the named template '#custom-content' (click show code for more details) + ` + }, + source: { + code: ` + +const customlist = ref([ + { + customKey: 'A custom Value', + anotherKey: 'Another custom value' + }, + { + customKey: 'A custom Value', + anotherKey: 'Another custom value' + }, + { + customKey: 'A custom Value', + anotherKey: 'Another custom value' + }, + { + customKey: 'A custom Value', + anotherKey: 'Another custom value' + } +]); + + + + {{ element.customKey }} + {{ element.anotherKey }} + + + `, + language: 'html' + } + } + } +}; + +const SharedTemplate: StoryFn = (args: Args) => ({ + components: { + PuikSortableList, + PuikIcon + }, + setup() { + const argsList1 = { + listId: 'shared-list-1', + list: sharedList1.value, + itemKey: itemKey.value + }; + const argsList2 = { + listId: 'shared-list-2', + list: sharedList2.value, + itemKey: itemKey.value + }; + const options = { + group: 'shared' + }; + return { args, argsList1, argsList2, options }; + }, + template: ` + + + + + ` +}); + +export const Shared = { + render: SharedTemplate, + args: {}, + parameters: { + docs: { + description: { + story: ` + Using Sortable JS options : Shared option example here (for more options see https://github.com/SortableJS/Sortable#options) + ` + }, + source: { + code: ` + +let options = { + group: "shared", // Name of the group for shared sortable lists +}; + +// Using the options in the component + + + + + + + + `, + language: 'html' + } + } + } +}; + +const EventsTemplate: StoryFn = (args: Args) => ({ + components: { + PuikSortableList, + PuikIcon + }, + setup() { + const logEvent = (name: string, event: any) => { + console.log(name, event); + }; + return { args, logEvent }; + }, + template: ` + + ` +}); + +export const Events = { + render: EventsTemplate, + args: { + listId: 'events-list', + list: eventsList.value + }, + parameters: { + docs: { + description: { + story: ` +Using the sortable js events in the component (view the browser console to inspect different behaviors) + +- set of events available in the sortable library (all return an object of type CustomEvents) +See https://github.com/SortableJS/Sortable#events +- exception of the list-changed event specific to Puik and which returns the updated list + ` + }, + source: { + code: ` + +const logEvent = (name, event) => { + console.log(name, event); +}; + + + `, + language: 'html' + } + } + } +}; diff --git a/packages/components/sortable-list/style/css.ts b/packages/components/sortable-list/style/css.ts new file mode 100644 index 00000000..910752e9 --- /dev/null +++ b/packages/components/sortable-list/style/css.ts @@ -0,0 +1,2 @@ +import '@prestashopcorp/puik-components/base/style/css'; +import '@prestashopcorp/puik-theme/puik-sortable-list.css'; diff --git a/packages/components/sortable-list/style/index.ts b/packages/components/sortable-list/style/index.ts new file mode 100644 index 00000000..8df01e29 --- /dev/null +++ b/packages/components/sortable-list/style/index.ts @@ -0,0 +1,2 @@ +import '@prestashopcorp/puik-components/base/style'; +import '@prestashopcorp/puik-theme/src/puik-sortable-list.scss'; diff --git a/packages/components/sortable-list/test/sortable-list.spec.ts b/packages/components/sortable-list/test/sortable-list.spec.ts new file mode 100644 index 00000000..cabfd81b --- /dev/null +++ b/packages/components/sortable-list/test/sortable-list.spec.ts @@ -0,0 +1,201 @@ +import { mount, ComponentMountingOptions, VueWrapper } from '@vue/test-utils'; +import { describe, it, expect, vi } from 'vitest'; +import { + PuikSortableList, + SortableListProps +} from '@prestashopcorp/puik-components'; + +describe('SortableList tests', () => { + let wrapper: VueWrapper; + const factory = ( + props: SortableListProps, + options: ComponentMountingOptions = {} + ) => { + wrapper = mount(PuikSortableList, { + props, + ...options, + listeners: { + add: vi.fn(), + update: vi.fn(), + remove: vi.fn(), + choose: vi.fn(), + unchoose: vi.fn(), + start: vi.fn(), + end: vi.fn(), + sort: vi.fn(), + filter: vi.fn(), + move: vi.fn(), + clone: vi.fn() + } + }); + }; + + it('should be a Vue instance', () => { + factory({ listId: 'test', list: [], itemKey: 'id' }); + expect(wrapper).toBeTruthy(); + }); + + it('should render list items correctly', () => { + const list = [ + { id: '1', title: 'Item 1' }, + { id: '2', title: 'Item 2' } + ]; + factory({ listId: 'test', list, itemKey: 'id' }); + expect(wrapper.findAll('.draggable').length).toBe(list.length); + }); + + it('should handle keydown events correctly', async () => { + const list = [ + { id: '1', title: 'Item 1' }, + { id: '2', title: 'Item 2' } + ]; + factory({ listId: 'test', list, itemKey: 'id' }); + const draggable = wrapper.find('.draggable'); + await draggable.trigger('keydown', { key: 'ArrowUp' }); + expect(wrapper.emitted().keydown).toBeTruthy(); + expect((wrapper.emitted().keydown[0][0] as KeyboardEvent).key).toBe( + 'ArrowUp' + ); + await draggable.trigger('keydown', { key: 'ArrowDown' }); + expect(wrapper.emitted().keydown).toBeTruthy(); + expect((wrapper.emitted().keydown[1][0] as KeyboardEvent).key).toBe( + 'ArrowDown' + ); + }); + + it('should handle displayPositionNumbers prop correctly', () => { + const list = [ + { id: '1', title: 'Item 1' }, + { id: '2', title: 'Item 2' } + ]; + factory({ + listId: 'test', + list, + itemKey: 'id', + displayPositionNumbers: true + }); + expect(wrapper.findAll('.puik-sortable-list_item-index').length).toBe( + list.length + ); + }); + + it('should handle iconPosition prop correctly', () => { + const list = [ + { id: '1', title: 'Item 1' }, + { id: '2', title: 'Item 2' } + ]; + factory({ listId: 'test', list, itemKey: 'id', iconPosition: 'left' }); + expect(wrapper.findAll('.puik-icon').length).toBe(list.length); + }); + + it('should handle tag prop correctly', () => { + const list = [ + { id: '1', title: 'Item 1' }, + { id: '2', title: 'Item 2' } + ]; + factory({ listId: 'test', list, itemKey: 'id', tag: 'menu' }); + expect(wrapper.find('menu').exists()).toBe(true); + }); + + it('should handle custom-content slot correctly', () => { + const list = [ + { id: '1', title: 'Item 1', custom: 'Custom Content 1' }, + { id: '2', title: 'Item 2', custom: 'Custom Content 2' } + ]; + factory( + { listId: 'test', list, itemKey: 'id' }, + { + slots: { + 'custom-content': ` + + {{ element.custom }} + + ` + } + } + ); + expect(wrapper.findAll('.custom-content').length).toBe(list.length); + }); + + it('should emit events without errors', async () => { + const list = [ + { id: '1', title: 'Item 1' }, + { id: '2', title: 'Item 2' } + ]; + factory({ listId: 'test', list, itemKey: 'id' }); + const events = [ + 'choose', + 'unchoose', + 'start', + 'end', + 'add', + 'update', + 'sort', + 'remove', + 'filter', + 'move', + 'clone', + 'change' + ]; + + events.forEach((event) => { + const spy = vi.spyOn(wrapper.vm, '$emit'); + wrapper.vm.$emit(event, {}); + expect(spy).toHaveBeenCalledWith(event, {}); + }); + }); + + it('should handle dataTest prop correctly', () => { + const list = [ + { + id: '1', + title: 'Item 1', + description: 'Description', + imgSrc: 'https://t.ly/Ku50h', + iconPosition: 'right' + }, + { id: '2', title: 'Item 2' } + ]; + const dataTest = 'sortable-list-test'; + + factory({ + listId: 'test', + list, + itemKey: 'id', + dataTest, + iconPosition: 'right' + }); + expect(wrapper.attributes('data-test')).toBe(dataTest); + expect(wrapper.find(`[data-test="${dataTest}-item-1"]`).exists()).toBe( + true + ); + expect(wrapper.find(`[data-test="${dataTest}-item-1"]`).exists()).toBe( + true + ); + expect( + wrapper.find(`[data-test="${dataTest}-list-item-index-1"]`).exists() + ).toBe(true); + expect(wrapper.find(`[data-test="${dataTest}-img-1"]`).exists()).toBe(true); + expect(wrapper.find(`[data-test="${dataTest}-title-1"]`).exists()).toBe( + true + ); + expect( + wrapper.find(`[data-test="${dataTest}-description-1"]`).exists() + ).toBe(true); + expect( + wrapper.find(`[data-test="${dataTest}-right-icon-1"]`).exists() + ).toBe(true); + + // Test iconPosition left + factory({ + listId: 'test', + list, + itemKey: 'id', + dataTest, + iconPosition: 'left' + }); + expect(wrapper.find(`[data-test="${dataTest}-left-icon-1"]`).exists()).toBe( + true + ); + }); +}); diff --git a/packages/puik/component.ts b/packages/puik/component.ts index a485e103..57892257 100644 --- a/packages/puik/component.ts +++ b/packages/puik/component.ts @@ -1,4 +1,5 @@ import { + PuikSortableList, PuikNotificationBar, PuikAvatar, PuikDivider, @@ -54,6 +55,7 @@ import type { Component } from 'vue'; // prettier-ignore export default [ + PuikSortableList, PuikNotificationBar, PuikAvatar, PuikDivider, diff --git a/packages/puik/global.d.ts b/packages/puik/global.d.ts index ddcea344..23d45d9f 100644 --- a/packages/puik/global.d.ts +++ b/packages/puik/global.d.ts @@ -1,6 +1,7 @@ // GlobalComponents for Volar declare module '@vue/runtime-core' { export interface GlobalComponents { + PuikSortableList: typeof import('@prestashopcorp/puik-components')['PuikSortableList'] PuikNotificationBar: (typeof import('@prestashopcorp/puik-components'))['PuikNotificationBar'] PuikTag: (typeof import('@prestashopcorp/puik-components'))['PuikTag'] PuikTabNavigationGroupPanels: (typeof import('@prestashopcorp/puik-components'))['PuikTabNavigationGroupPanels'] diff --git a/packages/theme/src/index.scss b/packages/theme/src/index.scss index 6b7b234a..24a4131d 100755 --- a/packages/theme/src/index.scss +++ b/packages/theme/src/index.scss @@ -47,3 +47,4 @@ @use 'puik-avatar'; @use 'puik-divider'; @use 'puik-notification-bar'; +@use 'puik-sortable-list'; diff --git a/packages/theme/src/puik-sortable-list.scss b/packages/theme/src/puik-sortable-list.scss new file mode 100644 index 00000000..50658de1 --- /dev/null +++ b/packages/theme/src/puik-sortable-list.scss @@ -0,0 +1,26 @@ +@use './common/typography.scss'; + +.puik-sortable-list { + &_item { + @apply flex items-center cursor-move mb-2 bg-white; + &-index { + @extend .puik-body-large; + @apply p-4; + } + &-container { + @apply px-4 py-2 space-x-4 flex items-center grow border; + } + &-content{ + @apply flex flex-col grow; + &_title { + @extend .puik-body-default-bold; + } + &_subtitle { + @extend .puik-body-small; + } + } + &-img { + @apply w-[75px] h-[38px] object-cover; + } + } +} \ No newline at end of file diff --git a/packages/web-components/components/sortable-list.ts b/packages/web-components/components/sortable-list.ts new file mode 100644 index 00000000..46abfeba --- /dev/null +++ b/packages/web-components/components/sortable-list.ts @@ -0,0 +1,8 @@ +// import { defineCustomElement } from 'vue'; +// import { PuikSortableList } from '@prestashopcorp/puik-components'; +// import type { CustomElementWithName } from '../types'; + +// const PuikSortableListCe = defineCustomElement(PuikSortableList) as CustomElementWithName; +// PuikSortableListCe.ceName = 'puik-sortable-list-ce'; + +// export default PuikSortableListCe; diff --git a/packages/web-components/index.ts b/packages/web-components/index.ts index 814ac191..688caaf2 100644 --- a/packages/web-components/index.ts +++ b/packages/web-components/index.ts @@ -2,6 +2,7 @@ import type { CustomElementWithName } from './types'; import initWeb from './utils/initWeb'; import initAllWeb from './utils/initAllWeb'; +// import PuikSortableListCe from './components/sortable-list'; import PuikAccordionCe from './components/accordion'; import PuikAccordionGroupCe from './components/accordion-group'; import PuikAlertCe from './components/alert'; @@ -51,6 +52,7 @@ import PuikTextareaCe from './components/textarea'; import PuikTooltipCe from './components/tooltip'; export const components: CustomElementWithName[] = [ + // PuikSortableListCe, PuikAccordionCe, PuikAccordionGroupCe, PuikAlertCe, @@ -101,6 +103,7 @@ export const components: CustomElementWithName[] = [ ]; export { + // PuikSortableListCe, PuikAccordionCe, PuikAccordionGroupCe, PuikAlertCe, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07287065..8fa1f510 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -193,7 +193,16 @@ importers: radix-vue: specifier: ^1.7.4 version: 1.7.4(vue@3.3.10(typescript@5.4.5)) + sortablejs: + specifier: ^1.15.2 + version: 1.15.2 + sortablejs-vue3: + specifier: ^1.2.11 + version: 1.2.11(sortablejs@1.15.2)(vue@3.3.10(typescript@5.4.5)) devDependencies: + '@types/sortablejs': + specifier: ^1.15.8 + version: 1.15.8 vue: specifier: ^3.3.7 version: 3.3.10(typescript@5.4.5) @@ -2347,6 +2356,9 @@ packages: '@types/serve-static@1.15.7': resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + '@types/sortablejs@1.15.8': + resolution: {integrity: sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==} + '@types/unist@2.0.10': resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} @@ -5999,6 +6011,15 @@ packages: snake-case@2.1.0: resolution: {integrity: sha512-FMR5YoPFwOLuh4rRz92dywJjyKYZNLpMn1R5ujVpIYkbA9p01fq8RMg0FkO4M+Yobt4MjHeLTJVm5xFFBHSV2Q==} + sortablejs-vue3@1.2.11: + resolution: {integrity: sha512-oKOA6N7yu2ktmqYXPHlPTQWbe9G4v16mn5ewogb+Ybc9Bk1Y+MIURrpbgedEv7f9TS5Bptvh81HGjazh5FEyJw==} + peerDependencies: + sortablejs: ^1.15.0 + vue: ^3.2.25 + + sortablejs@1.15.2: + resolution: {integrity: sha512-FJF5jgdfvoKn1MAKSdGs33bIqLi3LmsgVTliuX6iITj834F+JRQZN90Z93yql8h0K2t0RwDPBmxwlbZfDcxNZA==} + source-map-js@1.2.0: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} @@ -9516,6 +9537,8 @@ snapshots: '@types/node': 20.12.12 '@types/send': 0.17.4 + '@types/sortablejs@1.15.8': {} + '@types/unist@2.0.10': {} '@types/uuid@9.0.8': {} @@ -13689,6 +13712,13 @@ snapshots: dependencies: no-case: 2.3.2 + sortablejs-vue3@1.2.11(sortablejs@1.15.2)(vue@3.3.10(typescript@5.4.5)): + dependencies: + sortablejs: 1.15.2 + vue: 3.3.10(typescript@5.4.5) + + sortablejs@1.15.2: {} + source-map-js@1.2.0: {} source-map-support@0.5.21:
+ {{ `${element?.title}` }} +
+ {{ `${element?.description}` }} +
{{ element.customKey }}
{{ element.anotherKey }}