@citizenplane/pimp 9.7.9 → 9.7.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@citizenplane/pimp",
3
- "version": "9.7.9",
3
+ "version": "9.7.12",
4
4
  "scripts": {
5
5
  "dev": "storybook dev -p 8080",
6
6
  "build-storybook": "storybook build --output-dir ./docs",
@@ -22,7 +22,6 @@
22
22
  :suggestions="options"
23
23
  :typeahead
24
24
  @click="handleClick"
25
- @complete="handleSearch"
26
25
  @hide="handleOverlayHidden"
27
26
  @keydown.enter="toggleDropdown"
28
27
  @keydown.esc.stop
@@ -80,12 +79,13 @@
80
79
  <script setup lang="ts">
81
80
  import { absolutePosition, getOuterWidth } from '@primeuix/utils/dom'
82
81
  import AutoComplete from 'primevue/autocomplete'
83
- import { ref, computed, onMounted } from 'vue'
82
+ import { ref, computed, onMounted, watch } from 'vue'
84
83
 
85
84
  import BaseInputLabel from '@/components/BaseInputLabel.vue'
86
85
  import BaseSelectClearButton from '@/components/BaseSelectClearButton.vue'
87
86
  import TransitionExpand from '@/components/TransitionExpand.vue'
88
87
 
88
+ import { debounce } from '@/helpers/functions'
89
89
  import { isEmpty } from '@/helpers/object'
90
90
 
91
91
  interface Emits {
@@ -94,7 +94,6 @@ interface Emits {
94
94
  (e: 'update:modelValue', value: Record<string, unknown> | Record<string, unknown>[] | string[] | null): void
95
95
  (e: 'overlayShown'): void
96
96
  (e: 'overlayHidden'): void
97
- (e: 'searchChange', value: string | object): void
98
97
  }
99
98
 
100
99
  interface Props {
@@ -145,7 +144,6 @@ const selectModel = computed({
145
144
  if (typeof value === 'string') {
146
145
  return
147
146
  }
148
-
149
147
  emit('update:modelValue', value)
150
148
  },
151
149
  })
@@ -190,18 +188,20 @@ const displayClearButton = computed(() => {
190
188
  return props.isClearable && !isEmpty(selectModel.value)
191
189
  })
192
190
 
193
- const handleSearch = (event: { query: string }) => emit('search', event.query)
194
191
  const handleClear = () => (selectModel.value = null)
195
192
  const handleOverlayShown = () => emit('overlayShown')
196
193
  const handleOverlayHidden = () => emit('overlayHidden')
197
194
 
198
195
  const handleValueChange = (newValue: string | object) => {
199
196
  if (typeof newValue !== 'string') return
200
-
201
197
  searchQuery.value = newValue
202
- emit('searchChange', newValue)
203
198
  }
204
199
 
200
+ watch(
201
+ searchQuery,
202
+ debounce((newValue) => emit('search', newValue), 500),
203
+ )
204
+
205
205
  const getInputElement = () => {
206
206
  if (!multiselect.value) return null
207
207
  // @ts-expect-error '$el' does not exist on type instance of AutoComplete
@@ -210,9 +210,7 @@ const getInputElement = () => {
210
210
 
211
211
  const selectInputContent = () => {
212
212
  const inputElement = getInputElement()
213
- if (inputElement) {
214
- inputElement.select()
215
- }
213
+ if (inputElement) inputElement.select()
216
214
  }
217
215
 
218
216
  const focusAndSelectInput = () => {
@@ -245,9 +243,7 @@ const toggleDropdown = () => {
245
243
 
246
244
  const handleUpdateModelValue = (value: Record<string, unknown> | string[] | null) => {
247
245
  // Autocomplete will set the model value to the query string if not blocked
248
- if (!value || typeof value === 'string') {
249
- return
250
- }
246
+ if (!value || typeof value === 'string') return
251
247
  }
252
248
 
253
249
  const overrideAlignOverlay = () => {
@@ -17,11 +17,16 @@
17
17
  <p v-if="description" class="cpToaster__description">{{ description }}</p>
18
18
  </div>
19
19
  </div>
20
- <button class="cpToaster__close" type="button" @click="closeToaster"><cp-icon type="x" /></button>
20
+ <button class="cpToaster__close" type="button" @click="closeToaster">
21
+ <cp-icon type="x" />
22
+ </button>
21
23
  <div v-if="actionLabel" class="cpToaster__footer">
22
- <button class="cpToaster__button" type="button" @click="handleActionMethod">
24
+ <button v-if="actionIsButton" class="cpToaster__button" type="button" @click="handleActionMethod">
23
25
  {{ actionLabel }}
24
26
  </button>
27
+ <a v-else class="cpToaster__button" v-bind="actionLinkProperties">
28
+ {{ actionLabel }}
29
+ </a>
25
30
  </div>
26
31
  </div>
27
32
  </transition>
@@ -37,7 +42,9 @@ import CpIcon from '@/components/CpIcon.vue'
37
42
  import { HeadingLevels, Intent } from '@/constants'
38
43
 
39
44
  interface Props {
45
+ actionAs?: 'link' | 'button'
40
46
  actionLabel?: string
47
+ actionLinkProperties?: Record<string, unknown>
41
48
  actionMethod?: (vmProperties: Record<string, unknown>) => void
42
49
  delayBeforeCloseInMs?: number
43
50
  description?: string
@@ -55,6 +62,8 @@ const props = withDefaults(defineProps<Props>(), {
55
62
  type: 'info',
56
63
  delayBeforeCloseInMs: 5000,
57
64
  actionLabel: '',
65
+ actionLinkProperties: () => ({}),
66
+ actionAs: 'button',
58
67
  actionMethod: () => {},
59
68
  isUnique: false,
60
69
  })
@@ -78,6 +87,8 @@ const countDownInterval = ref<ReturnType<typeof setInterval>>()
78
87
 
79
88
  const instance = getCurrentInstance()
80
89
 
90
+ const actionIsButton = computed(() => props.actionAs === 'button')
91
+
81
92
  const toasterIcon = computed(() => {
82
93
  const intentValues = Object.values(Intent)
83
94
  const intent = intentValues.find((intentItem) => intentItem.value === props.type)
@@ -185,8 +196,8 @@ const closeToaster = (): void => {
185
196
  }
186
197
 
187
198
  const removeElement = (el: Element): void => {
188
- if (typeof (el as HTMLElement).remove !== 'undefined') {
189
- ;(el as HTMLElement).remove()
199
+ if (typeof el.remove !== 'undefined') {
200
+ el.remove()
190
201
  } else if (el.parentNode) {
191
202
  el.parentNode.removeChild(el)
192
203
  }
@@ -0,0 +1,12 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ // https://decipher.dev/30-seconds-of-typescript/docs/debounce/
3
+ export function debounce<T extends (...args: any[]) => any>(
4
+ fn: T,
5
+ wait: number = 300,
6
+ ): (...args: Parameters<T>) => void {
7
+ let timeout: ReturnType<typeof setTimeout>
8
+ return function (this: any, ...args: Parameters<T>) {
9
+ clearTimeout(timeout)
10
+ timeout = setTimeout(() => fn.apply(this, args), wait)
11
+ }
12
+ }
@@ -4,7 +4,14 @@ import type { Meta, StoryObj } from '@storybook/vue3'
4
4
 
5
5
  import CpMultiselect from '@/components/CpMultiselect.vue'
6
6
 
7
- const supplierOptions = [
7
+ interface IOption {
8
+ disabled?: boolean
9
+ iata_code?: string
10
+ id: number
11
+ name: string
12
+ }
13
+
14
+ const supplierOptions: IOption[] = [
8
15
  { id: 1, name: 'MGA AIRLINES' },
9
16
  { id: 2, name: 'ZENITH - EUROATLANTIC AIRWAYS' },
10
17
  { id: 3, name: 'ZENITH - SOUTHERN AIRWAYS EXPRESS MOKULELE AIRLINES AND SURF AIR MOBILITY' },
@@ -18,7 +25,7 @@ const supplierOptions = [
18
25
  { id: 11, name: 'UNITRAVEL UTAZÁSI IRODA' },
19
26
  ]
20
27
 
21
- const airlineOptions = [
28
+ const airlineOptions: IOption[] = [
22
29
  { id: 1, name: 'United Airlines', iata_code: 'UA' },
23
30
  { id: 2, name: 'Delta Airlines', iata_code: 'DL' },
24
31
  { id: 3, name: 'American Airlines', iata_code: 'AA' },
@@ -123,24 +130,21 @@ export const Single: Story = {
123
130
 
124
131
  const originalOptions = ref(args.options)
125
132
  const dynamicOptions = ref(toValue(originalOptions))
133
+ const selectedSupplier = ref(null)
126
134
 
127
135
  const handleSearch = async (query: string) => {
128
136
  isLoading.value = true
129
137
  searchQuery.value = query
130
138
 
131
- dynamicOptions.value = originalOptions.value
132
-
133
139
  await new Promise((resolve) => setTimeout(resolve, 500))
134
140
 
135
- dynamicOptions.value = dynamicOptions.value?.filter((option) => {
136
- // @ts-expect-error option is unknown
137
- return option.name.toLowerCase().includes(searchQuery.value.toLowerCase())
141
+ dynamicOptions.value = originalOptions.value?.filter((option) => {
142
+ return (option as IOption).name.toLowerCase().includes(searchQuery.value.toLowerCase())
138
143
  })
139
144
 
140
145
  isLoading.value = false
141
146
  }
142
147
 
143
- const selectedSupplier = ref(null)
144
148
  return { args, selectedSupplier, dynamicOptions, handleSearch, isLoading }
145
149
  },
146
150
  template: `
@@ -174,24 +178,21 @@ export const Multiple: Story = {
174
178
 
175
179
  const originalOptions = ref(args.options)
176
180
  const dynamicOptions = ref(toValue(originalOptions))
181
+ const selectedAirlines = ref([])
177
182
 
178
183
  const handleSearch = async (query: string) => {
179
184
  isLoading.value = true
180
185
  searchQuery.value = query
181
186
 
182
- if (!searchQuery.value) return (dynamicOptions.value = originalOptions.value)
183
-
184
187
  await new Promise((resolve) => setTimeout(resolve, 500))
185
188
 
186
- dynamicOptions.value = dynamicOptions.value?.filter((option) => {
187
- // @ts-expect-error option is unknown
188
- return option.name.toLowerCase().includes(searchQuery.value.toLowerCase())
189
+ dynamicOptions.value = originalOptions.value?.filter((option) => {
190
+ return (option as IOption).name.toLowerCase().includes(searchQuery.value.toLowerCase())
189
191
  })
190
192
 
191
193
  isLoading.value = false
192
194
  }
193
195
 
194
- const selectedAirlines = ref([])
195
196
  return { args, selectedAirlines, dynamicOptions, handleSearch, isLoading }
196
197
  },
197
198
  template: `
@@ -6,6 +6,15 @@ const meta = {
6
6
  title: 'CpToaster',
7
7
  component: CpToaster,
8
8
  argTypes: {
9
+ actionAs: {
10
+ control: 'select',
11
+ options: ['link', 'button'],
12
+ description: 'Determines if the action is a link or a button',
13
+ },
14
+ actionLinkProperties: {
15
+ control: 'object',
16
+ description: 'Properties for the action link when actionAs is "link"',
17
+ },
9
18
  title: {
10
19
  control: 'text',
11
20
  description: 'The title of the toast',
@@ -37,15 +46,17 @@ const meta = {
37
46
  export default meta
38
47
  type Story = StoryObj<typeof meta>
39
48
 
49
+ const defaultArgs = {
50
+ title: 'Default Toast',
51
+ description: 'This is a default toast message',
52
+ type: 'info',
53
+ delayBeforeCloseInMs: 3000,
54
+ actionLabel: '',
55
+ isUnique: false,
56
+ }
57
+
40
58
  export const Default: Story = {
41
- args: {
42
- title: 'Default Toast',
43
- description: 'This is a default toast message',
44
- type: 'info',
45
- delayBeforeCloseInMs: 3000,
46
- actionLabel: '',
47
- isUnique: false,
48
- },
59
+ args: defaultArgs,
49
60
  render: (args) => ({
50
61
  template: `
51
62
  <div style="padding: 20px;">
@@ -61,6 +72,7 @@ export const Default: Story = {
61
72
  }
62
73
 
63
74
  export const DifferentTypes: Story = {
75
+ args: defaultArgs,
64
76
  render: () => ({
65
77
  template: `
66
78
  <div style="padding: 20px; display: flex; flex-direction: column; gap: 16px;">
@@ -103,7 +115,35 @@ export const DifferentTypes: Story = {
103
115
  }),
104
116
  }
105
117
 
106
- export const WithAction: Story = {
118
+ export const WithActionAsLink: Story = {
119
+ args: defaultArgs,
120
+ render: () => ({
121
+ template: `
122
+ <div style="padding: 20px;">
123
+ <cp-button @click="addLinkToaster">Show Toast with Action</cp-button>
124
+ </div>
125
+ `,
126
+ methods: {
127
+ addLinkToaster() {
128
+ this.$toaster.success({
129
+ title: 'This is a success toaster',
130
+ description: 'Description of a toaster with a link',
131
+ actionAs: 'link',
132
+ actionLabel: 'See flight information',
133
+ actionLinkProperties: {
134
+ href: 'http://app.citizenplane.com',
135
+ target: '_blank',
136
+ rel: 'noopener noreferrer',
137
+ },
138
+ isUnique: true,
139
+ })
140
+ },
141
+ },
142
+ }),
143
+ }
144
+
145
+ export const WithActionAsButton: Story = {
146
+ args: defaultArgs,
107
147
  render: () => ({
108
148
  template: `
109
149
  <div style="padding: 20px;">
@@ -117,9 +157,8 @@ export const WithAction: Story = {
117
157
  description: 'Description of a toaster with a link',
118
158
  actionLabel: 'See flight information',
119
159
  isUnique: true,
120
- actionMethod: (vm) => {
121
- vm.closeToaster()
122
- window.open('http://app.citizenplane.com', '_blank')
160
+ actionMethod: () => {
161
+ alert('Action button clicked!')
123
162
  },
124
163
  })
125
164
  },
@@ -128,6 +167,7 @@ export const WithAction: Story = {
128
167
  }
129
168
 
130
169
  export const CustomDuration: Story = {
170
+ args: defaultArgs,
131
171
  render: () => ({
132
172
  template: `
133
173
  <div style="padding: 20px;">