@citizenplane/pimp 9.0.1 → 9.1.1

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.0.1",
3
+ "version": "9.1.1",
4
4
  "scripts": {
5
5
  "dev": "storybook dev -p 8080",
6
6
  "build-storybook": "storybook build --output-dir ./docs",
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <context-menu ref="menu" :model="items" :pt="passThroughConfig">
3
3
  <template #item="{ item, props }">
4
- <cp-menu-item v-bind="{ ...item, ...props.action }" />
4
+ <cp-menu-item v-bind="{ ...item, ...props.action }" @on-async-command-complete="hide" />
5
5
  </template>
6
6
  </context-menu>
7
7
  </template>
@@ -5,7 +5,7 @@
5
5
  class="cpMenuItem__button"
6
6
  :class="dynamicClass"
7
7
  :disabled="disabled"
8
- @click.stop="handleItemClick"
8
+ @click="handleItemClick"
9
9
  >
10
10
  <transition name="fade" :duration="100" mode="out-in">
11
11
  <span v-if="isLoading" class="cpMenuItem__loaderWrapper">
@@ -28,6 +28,7 @@ import { computed } from 'vue'
28
28
  import type { MenuItem } from 'primevue/menuitem'
29
29
 
30
30
  interface Props {
31
+ isAsync?: boolean
31
32
  isCritical?: boolean
32
33
  isDisabled?: boolean
33
34
  isLoading?: boolean
@@ -44,9 +45,10 @@ const props = withDefaults(defineProps<Props & Omit<MenuItem, 'class' | 'disable
44
45
  isLoading: false,
45
46
  isDisabled: false,
46
47
  isCritical: false,
48
+ isAsync: false,
47
49
  })
48
50
 
49
- const emit = defineEmits(['onItemClick'])
51
+ const emit = defineEmits(['onItemClick', 'onAsyncCommandComplete'])
50
52
 
51
53
  const dynamicClass = computed(() => ({
52
54
  'cpMenuItem__button--reverseLabel': props.reverseLabel,
@@ -55,12 +57,18 @@ const dynamicClass = computed(() => ({
55
57
 
56
58
  const disabled = computed(() => props.isLoading || props.isDisabled)
57
59
 
58
- const handleItemClick = (event: Event) => {
59
- if (props.command) {
60
- props.command({
61
- originalEvent: event,
62
- item: props,
63
- })
60
+ const handleItemClick = async (event: Event) => {
61
+ if (!props.command) return
62
+
63
+ if (props.isAsync) {
64
+ // Stop the event from bubbling up to prevent menu auto close
65
+ event.stopPropagation()
66
+
67
+ await props.command({ originalEvent: event, item: props })
68
+
69
+ emit('onAsyncCommandComplete')
70
+ } else {
71
+ props.command({ originalEvent: event, item: props })
64
72
  }
65
73
 
66
74
  emit('onItemClick')
@@ -37,7 +37,7 @@
37
37
  :class="getRowClasses(rowData, rowIndex)"
38
38
  :tabindex="getTabindex(rowData)"
39
39
  @click="handleRowClick(rowData, rowIndex)"
40
- @click.right="handleRowRightClick({ rowData, rowIndex }, $event)"
40
+ @contextmenu.prevent="handleContextMenu({ rowData, rowIndex }, $event)"
41
41
  @keydown.enter="handleRowClick(rowData, rowIndex)"
42
42
  >
43
43
  <slot name="row" :row="rowData">
@@ -65,8 +65,8 @@
65
65
  type="button"
66
66
  class="cpTable__action"
67
67
  :class="getQuickOptionClasses(option)"
68
- :disabled="option.disabled"
69
- @click.stop="option.action({ rowData, rowIndex }, $event)"
68
+ :disabled="option.isDisabled"
69
+ @click.stop="($event) => option.action(rowData, $event)"
70
70
  >
71
71
  <cp-icon :type="option.icon" size="16" />
72
72
  </button>
@@ -74,7 +74,7 @@
74
74
  <button
75
75
  type="button"
76
76
  class="cpTable__defaultAction"
77
- @click.stop="handleRowRightClick({ rowData, rowIndex }, $event)"
77
+ @click.stop="handleContextMenu({ rowData, rowIndex }, $event)"
78
78
  >
79
79
  <cp-icon type="more-vertical" size="16" />
80
80
  </button>
@@ -108,12 +108,19 @@
108
108
  </div>
109
109
  </div>
110
110
  <div class="cpTable__overlay" />
111
+ <cp-contextual-menu
112
+ v-if="hasRowOptions"
113
+ ref="contextualMenu"
114
+ :items="contextualMenuItems"
115
+ @hide="resetCurrentRowData"
116
+ />
111
117
  </div>
112
118
  </template>
113
119
 
114
120
  <script setup lang="ts">
115
121
  import { ref, computed } from 'vue'
116
122
 
123
+ import CpContextualMenu from '@/components/CpContextualMenu.vue'
117
124
  import CpTableEmptyState from '@/components/CpTableEmptyState.vue'
118
125
 
119
126
  import { camelize, decamelize } from '@/helpers/string'
@@ -123,7 +130,6 @@ import { randomString } from '@/helpers'
123
130
 
124
131
  interface Emits {
125
132
  (e: 'onRowClick', data: Record<string, unknown>): void
126
- (e: 'onRowRightClick', payload: { data: Record<string, unknown>; event: Event }): void
127
133
  (e: 'onNextClick'): void
128
134
  (e: 'onPreviousClick'): void
129
135
  }
@@ -148,11 +154,13 @@ interface Pagination {
148
154
  }
149
155
 
150
156
  interface RowOptions {
151
- action: (payload: { rowData: Record<string, unknown>; rowIndex: number }, event: Event) => void
152
- disabled?: boolean
157
+ action: (rowData: Record<string, unknown>, $event: MouseEvent) => void
153
158
  icon: string
154
159
  id: string
160
+ isAsync?: boolean
155
161
  isCritical?: boolean
162
+ isDisabled?: boolean
163
+ isLoading?: boolean
156
164
  label: string
157
165
  }
158
166
 
@@ -195,25 +203,18 @@ const LoaderColor = '#5341F9'
195
203
  const uniqueId = ref(randomString())
196
204
  const pageNumber = ref(0)
197
205
  const cpTableContainer = ref<HTMLElement | null>(null)
206
+ const contextualMenu = ref<InstanceType<typeof CpContextualMenu>>()
198
207
 
199
208
  const hasQuickOptions = computed(() => !!quickOptions.value.length)
209
+ const hasRowOptions = computed(() => props.enableRowOptions && props.rowOptions.length)
200
210
  const hasMoreQuickActionsThanLimit = computed(() => props.rowOptions.length >= props.quickOptionsLimit)
201
211
 
202
- const defaultRowOptions = computed<RowOptions>(() => {
203
- if (!props.enableRowOptions || !props.rowOptions.length) {
204
- return {
205
- id: 'default',
206
- label: 'More',
207
- icon: 'more-vertical',
208
- action: () => {},
209
- }
210
- }
212
+ const defaultRowOption = computed<RowOptions>(() => {
211
213
  return {
212
214
  id: 'more',
213
215
  label: 'More',
214
216
  icon: 'more-vertical',
215
- action: ({ rowData, rowIndex }: { rowData: Record<string, unknown>; rowIndex: number }, $event: Event) =>
216
- handleRowRightClick({ rowData, rowIndex }, $event),
217
+ action: (rowData: Record<string, unknown>, $event: MouseEvent) => showContextualMenu($event),
217
218
  }
218
219
  })
219
220
 
@@ -222,10 +223,18 @@ const quickOptions = computed(() => {
222
223
 
223
224
  if (hasMoreQuickActionsThanLimit.value) {
224
225
  const slicedOptions = props.rowOptions.slice(0, props.quickOptionsLimit)
225
- return [...slicedOptions, defaultRowOptions.value]
226
+ return [...slicedOptions, defaultRowOption.value]
226
227
  }
227
228
 
228
- return [...props.rowOptions, defaultRowOptions.value]
229
+ return [...props.rowOptions, defaultRowOption.value]
230
+ })
231
+
232
+ const currentRowData = ref<Record<string, unknown>>({})
233
+ const contextualMenuItems = computed(() => {
234
+ return props.rowOptions.map((option) => ({
235
+ ...option,
236
+ command: ({ originalEvent }: { originalEvent: MouseEvent }) => option.action(currentRowData.value, originalEvent),
237
+ }))
229
238
  })
230
239
 
231
240
  const containerDOMElement = computed(() => cpTableContainer.value)
@@ -363,7 +372,21 @@ const paginationResultsDetails = computed(() => {
363
372
  return `${formattedNumberOfResults} ${pluralizedCount}`
364
373
  })
365
374
 
366
- const getQuickOptionTooltip = (option: RowOptions) => (!option.disabled ? option.label : '')
375
+ const handleContextMenu = (
376
+ { rowData, rowIndex }: { rowData: Record<string, unknown>; rowIndex: number },
377
+ event: MouseEvent,
378
+ ) => {
379
+ if (!hasRowOptions.value || isFullWidthRow(rowData)) return
380
+ currentRowData.value = getRowPayload(rowIndex)
381
+
382
+ showContextualMenu(event)
383
+ }
384
+
385
+ const showContextualMenu = (event: MouseEvent) => contextualMenu.value?.show(event)
386
+ const hideContextualMenu = () => contextualMenu.value?.hide()
387
+ const resetCurrentRowData = () => (currentRowData.value = {})
388
+
389
+ const getQuickOptionTooltip = (option: RowOptions) => (!option.isDisabled ? option.label : '')
367
390
 
368
391
  const getQuickOptionClasses = (option: RowOptions) => {
369
392
  return { 'cpTable__action--isCritical': option.isCritical }
@@ -378,16 +401,6 @@ const handleRowClick = (rowData: Record<string, unknown>, rowIndex: number) => {
378
401
  emit('onRowClick', data)
379
402
  }
380
403
 
381
- const handleRowRightClick = (
382
- { rowData, rowIndex }: { rowData: Record<string, unknown>; rowIndex: number },
383
- event: Event,
384
- ) => {
385
- if (isFullWidthRow(rowData)) return
386
-
387
- const data = getRowPayload(rowIndex)
388
- emit('onRowRightClick', { data, event })
389
- }
390
-
391
404
  const handleNavigationClick = (isNext = true) => {
392
405
  resetScrollPosition()
393
406
 
@@ -504,7 +517,7 @@ const areRowOptionsEnabled = (rowData: Record<string, unknown>) => props.enableR
504
517
 
505
518
  const resetPagination = () => (pageNumber.value = 0)
506
519
 
507
- defineExpose({ resetPagination })
520
+ defineExpose({ hideContextualMenu, resetPagination })
508
521
  </script>
509
522
 
510
523
  <style lang="scss">
@@ -45,11 +45,16 @@ export const Default: Story = {
45
45
  label: 'Download',
46
46
  icon: 'download',
47
47
  isLoading: isLoading.value,
48
- command: () => {
48
+ isAsync: true,
49
+ command: async () => {
49
50
  isLoading.value = true
50
- setTimeout(() => (isLoading.value = false), 2000)
51
+ await new Promise((resolve) => setTimeout(resolve, 2000))
52
+ isLoading.value = false
51
53
  },
52
54
  },
55
+ {
56
+ separator: true,
57
+ },
53
58
  {
54
59
  label: 'Delete',
55
60
  icon: 'trash-2',
@@ -1,3 +1,5 @@
1
+ import { ref, computed } from 'vue'
2
+
1
3
  import type { Meta, StoryObj } from '@storybook/vue3'
2
4
 
3
5
  import CpTable from '@/components/CpTable.vue'
@@ -108,17 +110,6 @@ export const Default: Story = {
108
110
  }),
109
111
  }
110
112
 
111
- export const WithPagination: Story = {
112
- args: {
113
- ...Default.args,
114
- pagination: {
115
- enabled: true,
116
- limit: 3,
117
- format: PAGINATION_FORMATS.PAGES,
118
- },
119
- },
120
- }
121
-
122
113
  export const ClickableRows: Story = {
123
114
  args: {
124
115
  ...Default.args,
@@ -126,24 +117,28 @@ export const ClickableRows: Story = {
126
117
  },
127
118
  }
128
119
 
129
- export const WithRowOptions: Story = {
120
+ export const Loading: Story = {
130
121
  args: {
131
122
  ...Default.args,
132
- enableRowOptions: true,
123
+ isLoading: true,
133
124
  },
134
125
  }
135
126
 
136
- export const Loading: Story = {
127
+ export const Empty: Story = {
137
128
  args: {
138
129
  ...Default.args,
139
- isLoading: true,
130
+ data: [],
140
131
  },
141
132
  }
142
133
 
143
- export const Empty: Story = {
134
+ export const WithPagination: Story = {
144
135
  args: {
145
136
  ...Default.args,
146
- data: [],
137
+ pagination: {
138
+ enabled: true,
139
+ limit: 3,
140
+ format: PAGINATION_FORMATS.PAGES,
141
+ },
147
142
  },
148
143
  }
149
144
 
@@ -191,42 +186,52 @@ export const WithCustomRowOptions: Story = {
191
186
  args: {
192
187
  ...Default.args,
193
188
  enableRowOptions: true,
194
- rowOptions: [
195
- {
196
- id: 'see',
197
- label: 'see',
198
- icon: 'eye',
199
- action: () => console.log('See'),
200
- },
201
- {
202
- id: 'edit',
203
- label: 'edit',
204
- icon: 'edit-2',
205
- action: () => console.log('Edit'),
206
- },
207
- {
208
- id: 'disable',
209
- label: 'disable',
210
- icon: 'history',
211
- disabled: true,
212
- action: () => console.log('History'),
213
- },
214
- {
215
- id: 'delete',
216
- label: 'delete',
217
- icon: 'trash',
218
- isCritical: true,
219
- action: () => console.log('Delete'),
220
- },
221
- ],
222
189
  },
223
190
  render: (args) => ({
224
191
  components: { CpTable },
225
192
  setup() {
226
- return { args }
193
+ const isEditLoading = ref(false)
194
+
195
+ const rowOptions = computed(() => [
196
+ {
197
+ id: 'see',
198
+ label: 'See',
199
+ icon: 'eye',
200
+ action: () => console.log('See'),
201
+ },
202
+ {
203
+ id: 'edit',
204
+ label: 'Edit',
205
+ icon: 'edit-2',
206
+ isAsync: true,
207
+ isLoading: isEditLoading.value,
208
+ action: async (payload) => {
209
+ isEditLoading.value = true
210
+ console.log('Edit', payload)
211
+ await new Promise((resolve) => setTimeout(resolve, 2000))
212
+ isEditLoading.value = false
213
+ },
214
+ },
215
+ {
216
+ id: 'disable',
217
+ label: 'Disable',
218
+ icon: 'history',
219
+ isDisabled: true,
220
+ action: () => console.log('History'),
221
+ },
222
+ {
223
+ id: 'delete',
224
+ label: 'Delete',
225
+ icon: 'trash',
226
+ isCritical: true,
227
+ action: () => console.log('Delete'),
228
+ },
229
+ ])
230
+
231
+ return { args, isEditLoading, rowOptions }
227
232
  },
228
233
  template: `
229
- <CpTable v-bind="args">
234
+ <CpTable v-bind="args" :row-options="rowOptions">
230
235
  <template #status="{ cell }">
231
236
  <span :style="{
232
237
  padding: '4px 8px',