From d24c4bde0a48d429936334260c1071800e27bb20 Mon Sep 17 00:00:00 2001 From: Nathaniel Waldschmidt <77284592+NateWaldschmidt@users.noreply.github.com> Date: Thu, 11 Apr 2024 12:56:43 -0500 Subject: [PATCH] SELF-243: Add Steps Component (#472) * feat: add icon button * feat: add steps component * refactor: Ken PR review --- CHANGELOG.md | 12 +- package-lock.json | 4 +- package.json | 2 +- src/components/ConditionalWrapper.vue | 12 -- src/components/Icon/svgs/Back.svg | 3 + .../Icon/svgs/CampaignAutomated.svg | 3 + src/components/Icon/svgs/CampaignOneTime.svg | 3 + src/components/Icon/svgs/Next.svg | 3 + src/components/Icon/svgs/Success.svg | 3 + src/components/Icon/types.ts | 5 + src/components/IconButton/IconButton.vue | 2 +- .../IconButton/__tests__/IconButton.spec.ts | 24 +++- src/components/Steps/Steps.mdx | 10 ++ src/components/Steps/Steps.stories.ts | 26 ++++ src/components/Steps/Steps.vue | 119 ++++++++++++++++++ src/components/Steps/__tests__/Steps.spec.ts | 42 +++++++ src/components/Steps/constants.ts | 4 + src/components/Steps/index.ts | 3 + src/components/Steps/types.ts | 6 + src/main.ts | 5 +- src/utils/ConditionalWrapper.vue | 16 +++ .../__tests__/ConditionalWrapper.spec.ts | 39 ++++++ ...ormatBytes.spec.js => formatBytes.spec.ts} | 0 .../{object.spec.js => object.spec.ts} | 4 +- src/utils/index.js | 1 - src/utils/stringDiff.js | 75 ----------- vite.config.js | 6 +- 27 files changed, 329 insertions(+), 103 deletions(-) delete mode 100644 src/components/ConditionalWrapper.vue create mode 100644 src/components/Icon/svgs/Back.svg create mode 100644 src/components/Icon/svgs/CampaignAutomated.svg create mode 100644 src/components/Icon/svgs/CampaignOneTime.svg create mode 100644 src/components/Icon/svgs/Next.svg create mode 100644 src/components/Icon/svgs/Success.svg create mode 100644 src/components/Steps/Steps.mdx create mode 100644 src/components/Steps/Steps.stories.ts create mode 100644 src/components/Steps/Steps.vue create mode 100644 src/components/Steps/__tests__/Steps.spec.ts create mode 100644 src/components/Steps/constants.ts create mode 100644 src/components/Steps/index.ts create mode 100644 src/components/Steps/types.ts create mode 100644 src/utils/ConditionalWrapper.vue create mode 100644 src/utils/__tests__/ConditionalWrapper.spec.ts rename src/utils/__tests__/{formatBytes.spec.js => formatBytes.spec.ts} (100%) rename src/utils/__tests__/{object.spec.js => object.spec.ts} (93%) delete mode 100644 src/utils/stringDiff.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 824a8289b..f674c57c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ ## v2.0.32 +- Add `` component +- Adds the icons: + - `Back` + - `CampaignAutomated` + - `CampaignOneTime` + - `Next` + - `Success` + +## v2.0.32 + - Add `` component ## v2.0.31 @@ -25,7 +35,7 @@ ## v2.0.29 -- Make `PrimeVue` and external dependency +- Make `PrimeVue` an external dependency ## v2.0.28 diff --git a/package-lock.json b/package-lock.json index 68356b1db..34318447b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@lob/ui-components", - "version": "2.0.32", + "version": "2.0.33", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@lob/ui-components", - "version": "2.0.32", + "version": "2.0.33", "dependencies": { "date-fns": "^2.29.3", "date-fns-holiday-us": "^0.3.1", diff --git a/package.json b/package.json index 3adf39c1f..5da2f8bd3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lob/ui-components", - "version": "2.0.32", + "version": "2.0.33", "engines": { "node": ">=20.2.0", "npm": ">=10.2.0" diff --git a/src/components/ConditionalWrapper.vue b/src/components/ConditionalWrapper.vue deleted file mode 100644 index a3fc99457..000000000 --- a/src/components/ConditionalWrapper.vue +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/Icon/svgs/Back.svg b/src/components/Icon/svgs/Back.svg new file mode 100644 index 000000000..927c7c622 --- /dev/null +++ b/src/components/Icon/svgs/Back.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Icon/svgs/CampaignAutomated.svg b/src/components/Icon/svgs/CampaignAutomated.svg new file mode 100644 index 000000000..fa96ed142 --- /dev/null +++ b/src/components/Icon/svgs/CampaignAutomated.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Icon/svgs/CampaignOneTime.svg b/src/components/Icon/svgs/CampaignOneTime.svg new file mode 100644 index 000000000..63e0f27e4 --- /dev/null +++ b/src/components/Icon/svgs/CampaignOneTime.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Icon/svgs/Next.svg b/src/components/Icon/svgs/Next.svg new file mode 100644 index 000000000..36f7e8bfe --- /dev/null +++ b/src/components/Icon/svgs/Next.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Icon/svgs/Success.svg b/src/components/Icon/svgs/Success.svg new file mode 100644 index 000000000..a33f502ec --- /dev/null +++ b/src/components/Icon/svgs/Success.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Icon/types.ts b/src/components/Icon/types.ts index c95648f4a..f112b82be 100644 --- a/src/components/Icon/types.ts +++ b/src/components/Icon/types.ts @@ -13,10 +13,13 @@ export const IconName = { APP_WINDOWS: 'AppWindows', ARROW_DOWN_TO_LINE: 'ArrowDownToLine', ARROW_TREND_UP: 'ArrowTrendUp', + BACK: 'Back', BANK_ACCOUNT: 'BankAccount', BARS: 'Bars', BELL: 'Bell', BULLHORN: 'Bullhorn', + CAMPAIGN_AUTOMATED: 'CampaignAutomated', + CAMPAIGN_ONE_TIME: 'CampaignOneTime', CAR: 'Car', CIRCLE_CHECK: 'CircleCheck', CIRCLE_CLOSE: 'CircleClose', @@ -41,6 +44,7 @@ export const IconName = { LIGHTNING: 'Lightning', LOCATION_PIN: 'LocationPin', MONEY_BILL: 'MoneyBill', + NEXT: 'Next', OPEN_BOOK: 'OpenBook', PIE_CHART_SLICE: 'PieChartSlice', PLAN_BUSINESS: 'PlanBusiness', @@ -51,6 +55,7 @@ export const IconName = { PLAN_STARTUP: 'PlanStartup', PLUS: 'Plus', SIGNAL: 'Signal', + SUCCESS: 'Success', TRIANGLE_EXCLAMATION: 'TriangleExclamation', USER: 'User', USERS: 'Users', diff --git a/src/components/IconButton/IconButton.vue b/src/components/IconButton/IconButton.vue index 98c9bd010..618cb60f2 100644 --- a/src/components/IconButton/IconButton.vue +++ b/src/components/IconButton/IconButton.vue @@ -22,7 +22,7 @@ import Icon from '@/components/Icon/Icon.vue'; import { IconName } from '@/components/Icon/types'; import { Size } from '@/types'; -import ConditionalWrapper from '@/components/ConditionalWrapper.vue'; +import ConditionalWrapper from '@/utils/ConditionalWrapper.vue'; import Button from 'primevue/button'; import { AnchorHTMLAttributes, computed, defineOptions } from 'vue'; diff --git a/src/components/IconButton/__tests__/IconButton.spec.ts b/src/components/IconButton/__tests__/IconButton.spec.ts index 4df7a8167..3374d6a40 100644 --- a/src/components/IconButton/__tests__/IconButton.spec.ts +++ b/src/components/IconButton/__tests__/IconButton.spec.ts @@ -1,13 +1,31 @@ import '@testing-library/jest-dom'; -import { IconName } from '@/main'; +import { + IconButtonColor, + IconButtonSize, + IconButtonVariant, + IconName +} from '@/main'; import { render } from '@testing-library/vue'; import IconButton from '../IconButton.vue'; describe('IconButton', () => { - it('renders', () => { + it.each([ + { icon: IconName.APP_WINDOWS, size: IconButtonSize.SM }, + { icon: IconName.APP_WINDOWS, size: IconButtonSize.MD }, + { icon: IconName.APP_WINDOWS, size: IconButtonSize.LG }, + { icon: IconName.APP_WINDOWS, size: IconButtonSize.XL }, + { icon: IconName.APP_WINDOWS, color: IconButtonColor.ERROR }, + { icon: IconName.APP_WINDOWS, color: IconButtonColor.INFO }, + { icon: IconName.APP_WINDOWS, color: IconButtonColor.NEUTRAL }, + { icon: IconName.APP_WINDOWS, color: IconButtonColor.SUCCESS }, + { icon: IconName.APP_WINDOWS, color: IconButtonColor.UPGRADE }, + { icon: IconName.APP_WINDOWS, color: IconButtonColor.WARNING }, + { icon: IconName.APP_WINDOWS, variant: IconButtonVariant.OUTLINED }, + { icon: IconName.APP_WINDOWS, variant: IconButtonVariant.PRIMARY } + ])('renders', (props) => { const { getByTestId } = render(IconButton, { - props: { icon: IconName.APP_WINDOWS } + props }); expect(getByTestId('uic-icon-button')).toBeVisible(); }); diff --git a/src/components/Steps/Steps.mdx b/src/components/Steps/Steps.mdx new file mode 100644 index 000000000..2a3808cf5 --- /dev/null +++ b/src/components/Steps/Steps.mdx @@ -0,0 +1,10 @@ +import { Canvas, ArgTypes, PRIMARY_STORY } from '@storybook/addon-docs'; +import { Primary } from './Steps.stories'; + +# Steps + + + +## Props + + diff --git a/src/components/Steps/Steps.stories.ts b/src/components/Steps/Steps.stories.ts new file mode 100644 index 000000000..9a4c732c9 --- /dev/null +++ b/src/components/Steps/Steps.stories.ts @@ -0,0 +1,26 @@ +import { Meta, StoryObj } from '@storybook/vue3'; +import mdx from './Steps.mdx'; +// @ts-ignore No types from Vue file +import Steps from './Steps.vue'; + +const meta: Meta = { + title: 'Components/Steps', + component: Steps, + parameters: { + docs: { + page: mdx + } + } +}; + +export default meta; + +export const Primary: StoryObj = { + args: { + items: [ + { label: 'Step 1', status: 'success' }, + { label: 'Step 2' }, + { label: 'Step 3' } + ] + } +}; diff --git a/src/components/Steps/Steps.vue b/src/components/Steps/Steps.vue new file mode 100644 index 000000000..f60c6ae10 --- /dev/null +++ b/src/components/Steps/Steps.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/src/components/Steps/__tests__/Steps.spec.ts b/src/components/Steps/__tests__/Steps.spec.ts new file mode 100644 index 000000000..acfeb4c6b --- /dev/null +++ b/src/components/Steps/__tests__/Steps.spec.ts @@ -0,0 +1,42 @@ +import '@testing-library/jest-dom'; +import { render, within } from '@testing-library/vue'; + +import Steps from '../Steps.vue'; + +type PropsType = InstanceType['$props']; + +const DEFAULT_PROPS: PropsType = { + items: [{ label: 'Step 1' }, { label: 'Step 2' }, { label: 'Step 3' }] +}; + +describe('Steps', () => { + let props: PropsType; + + beforeEach(() => { + props = { ...DEFAULT_PROPS }; + }); + + it('renders', () => { + const { getByTestId } = render(Steps, { props }); + + const steps = getByTestId('uic-steps'); + expect(steps).toBeVisible(); + const { getByText: stepsGetByText } = within(steps); + expect(stepsGetByText('Step 1')).toBeVisible(); + expect(stepsGetByText('Step 2')).toBeVisible(); + expect(stepsGetByText('Step 3')).toBeVisible(); + }); + + it('renders icons with statuses', async () => { + props = { + ...props, + items: [ + { label: 'Step 1', status: 'success' }, + { label: 'Step 2' }, + { label: 'Step 3' } + ] + }; + const { findByTestId } = render(Steps, { props }); + expect(await findByTestId('uic-steps-icon')).toBeVisible(); + }); +}); diff --git a/src/components/Steps/constants.ts b/src/components/Steps/constants.ts new file mode 100644 index 000000000..66f6af3bc --- /dev/null +++ b/src/components/Steps/constants.ts @@ -0,0 +1,4 @@ +export const StepsStatus = { + SUCCESS: 'success' +} as const; +export type StepsStatus = (typeof StepsStatus)[keyof typeof StepsStatus]; diff --git a/src/components/Steps/index.ts b/src/components/Steps/index.ts new file mode 100644 index 000000000..7a2201b20 --- /dev/null +++ b/src/components/Steps/index.ts @@ -0,0 +1,3 @@ +export * from './constants'; +export { default as Steps } from './Steps.vue'; +export * from './types'; diff --git a/src/components/Steps/types.ts b/src/components/Steps/types.ts new file mode 100644 index 000000000..2551de9df --- /dev/null +++ b/src/components/Steps/types.ts @@ -0,0 +1,6 @@ +import { StepsStatus } from './constants'; + +export interface StepItem { + label: string; + status?: StepsStatus; +} diff --git a/src/main.ts b/src/main.ts index e2e655275..1a0bc5c2c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -25,11 +25,12 @@ const ComponentLibrary = { // TODO Utilize the components export but first we will have to remove global usage. export * from './components/Badge'; +export * from './components/ColorPicker'; export * from './components/Icon'; export * from './components/IconButton'; -export * from './components/Modal'; export * from './components/ImageFileUpload'; -export * from './components/ColorPicker'; +export * from './components/Modal'; +export * from './components/Steps'; export default ComponentLibrary; diff --git a/src/utils/ConditionalWrapper.vue b/src/utils/ConditionalWrapper.vue new file mode 100644 index 000000000..a2182b577 --- /dev/null +++ b/src/utils/ConditionalWrapper.vue @@ -0,0 +1,16 @@ + + + diff --git a/src/utils/__tests__/ConditionalWrapper.spec.ts b/src/utils/__tests__/ConditionalWrapper.spec.ts new file mode 100644 index 000000000..488fa79ea --- /dev/null +++ b/src/utils/__tests__/ConditionalWrapper.spec.ts @@ -0,0 +1,39 @@ +import '@testing-library/jest-dom'; +import ConditionalWrapper from '../ConditionalWrapper.vue'; +import { render } from '@testing-library/vue'; + +type PropsType = InstanceType['$props'] & + Record; + +const DEFAULT_SLOT = '

' as const; + +describe('ConditionalWrapper', () => { + let props: PropsType; + + beforeEach(() => { + props = {}; + }); + + it('renders wrapper when provided', () => { + props = { + tag: 'h2', + 'data-testid': 'conditional-wrapper' + }; + const { getByTestId } = render(ConditionalWrapper, { + props, + slots: { default: DEFAULT_SLOT } + }); + expect(getByTestId('conditional-wrapper')).toBeVisible(); + expect(getByTestId('conditional-wrapper').tagName).toBe('H2'); + expect(getByTestId('conditional-wrapper-slot')).toBeVisible(); + }); + + it('does not render wrapper when not provided', () => { + const { getByTestId, queryByRole } = render(ConditionalWrapper, { + props, + slots: { default: DEFAULT_SLOT } + }); + expect(queryByRole('h2')).toBeFalsy(); + expect(getByTestId('conditional-wrapper-slot')).toBeVisible(); + }); +}); diff --git a/src/utils/__tests__/formatBytes.spec.js b/src/utils/__tests__/formatBytes.spec.ts similarity index 100% rename from src/utils/__tests__/formatBytes.spec.js rename to src/utils/__tests__/formatBytes.spec.ts diff --git a/src/utils/__tests__/object.spec.js b/src/utils/__tests__/object.spec.ts similarity index 93% rename from src/utils/__tests__/object.spec.js rename to src/utils/__tests__/object.spec.ts index 785daae9a..64baa52c4 100644 --- a/src/utils/__tests__/object.spec.js +++ b/src/utils/__tests__/object.spec.ts @@ -2,8 +2,8 @@ import { shallowEquals } from '../object'; describe('object utils', () => { describe('shallowEquals', () => { - let obj1; - let obj2; + let obj1: Record; + let obj2: Record | null; it('throws an error when either obj is null or undefined', () => { obj1 = { a: 1, b: 2 }; diff --git a/src/utils/index.js b/src/utils/index.js index c8f6f9402..7a38a723e 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -4,4 +4,3 @@ export * from './keyboard'; export * from './object'; export * from './formatBytes'; export * from './escapeRegExp'; -export * from './stringDiff'; diff --git a/src/utils/stringDiff.js b/src/utils/stringDiff.js deleted file mode 100644 index 4842c6f77..000000000 --- a/src/utils/stringDiff.js +++ /dev/null @@ -1,75 +0,0 @@ -/** - * @typedef {Object} StringDiff - * @property {string} type - The type of edit that was made. One of 'insertion', 'edit', 'deletion', or null if the strings are identical. - * @property {string} diff - The string that was inserted, edited, or deleted. - * @property {number} diffIndex - The index at which the edit occurred. - * @property {string} prevValue - The previous value of the string at the edit index, or null if the edit was an insertion. - */ - -/** - * - * @param {string} str1 - * @param {string} str2 - * @returns {StringDiff} - */ -export function stringDiff(str1, str2) { - const len1 = str1.length; - const len2 = str2.length; - - let startIndex = 0; - while ( - startIndex < len1 && - startIndex < len2 && - str1[startIndex] === str2[startIndex] - ) { - startIndex++; - } - - let endIndex1 = len1 - 1; - let endIndex2 = len2 - 1; - while ( - endIndex1 >= 0 && - endIndex2 >= 0 && - str1[endIndex1] === str2[endIndex2] - ) { - endIndex1--; - endIndex2--; - } - - let diff; - let type; - let editIndex; - let prevValue; - if (startIndex > endIndex1 && startIndex <= endIndex2) { - // There was an insertion - diff = str2.substring(startIndex, endIndex2 + 1); - type = 'insertion'; - editIndex = startIndex; - prevValue = null; - } else if (startIndex <= endIndex1 && startIndex > endIndex2) { - // There was a deletion - diff = str1.substring(startIndex, endIndex1 + 1); - type = 'deletion'; - editIndex = startIndex; - prevValue = null; - } else if (startIndex <= endIndex1 && startIndex <= endIndex2) { - // There was a modification - diff = str2.substring(startIndex, endIndex2 + 1); - type = 'edit'; - editIndex = startIndex; - prevValue = str1.substring(startIndex, endIndex1 + 1); - } else { - // The strings are identical - diff = ''; - type = null; - editIndex = null; - prevValue = null; - } - - return { - type, - diff, - editIndex, - prevValue - }; -} diff --git a/vite.config.js b/vite.config.js index c0374b3f7..0f5e1ce91 100644 --- a/vite.config.js +++ b/vite.config.js @@ -41,12 +41,12 @@ export default defineConfig({ globals: true, setupFiles: 'setupTests.js', coverage: { - include: ['src/**/*.{js,vue}'], + include: ['src/**/*.{js,ts,vue}'], exclude: [ 'src/**/index.{js,ts}', // No need to cover index files for exports - 'src/main.js', // No need to cover bootstrap file + 'src/main.ts', // No need to cover bootstrap file 'src/**/*.spec.{js,ts}', // No need to cover test files - 'src/**/*.stories.js', // No need to cover stories files + 'src/**/*.stories.{js,ts}', // No need to cover stories files 'src/theme/**', // No need to cover components just for showing theming 'src/components/Icons/**' // No need to cover components just for rendering svg icons ],