@tager/admin-blog 0.3.23 → 0.3.29

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.
Files changed (64) hide show
  1. package/dist/constants/scopes.d.ts +11 -0
  2. package/dist/dev.d.ts +0 -2
  3. package/dist/hooks/index.d.ts +7 -3
  4. package/dist/hooks/{useBlogCategoryList.d.ts → useFetchCategories.d.ts} +2 -2
  5. package/dist/hooks/useFetchCategory.d.ts +8 -0
  6. package/dist/hooks/{useModuleConfig.d.ts → useFetchModuleConfig.d.ts} +2 -2
  7. package/dist/hooks/{useBlogPostList.d.ts → useFetchPosts.d.ts} +2 -2
  8. package/dist/hooks/useStore.d.ts +4 -0
  9. package/dist/hooks/useUserPermission.d.ts +3 -0
  10. package/dist/hooks/useUserProfile.d.ts +3 -0
  11. package/dist/index.esm.js +1245 -432
  12. package/dist/index.umd.js +1253 -438
  13. package/dist/locales/en.d.ts +10 -1
  14. package/dist/router/index.d.ts +4 -0
  15. package/dist/services/requests.d.ts +13 -22
  16. package/dist/store/index.d.ts +6 -0
  17. package/dist/store/user.d.ts +14 -0
  18. package/dist/typings/common.d.ts +1 -0
  19. package/dist/typings/model.d.ts +45 -1
  20. package/dist/utils/common.d.ts +4 -0
  21. package/dist/views/Categories/CategoryForm/CategoryForm.helpers.d.ts +3 -14
  22. package/dist/views/Categories/CategoryForm/CategoryForm.vue.d.ts +45 -22
  23. package/dist/views/Categories/CategoryList/CategoryList.helpers.d.ts +2 -2
  24. package/dist/views/Categories/CategoryList/CategoryList.vue.d.ts +6 -4
  25. package/dist/views/Categories/CategoryList/hooks/useAdvancedSearch.d.ts +1 -1
  26. package/dist/views/Posts/PostForm/PostForm.helpers.d.ts +2 -2
  27. package/dist/views/Posts/PostForm/PostForm.vue.d.ts +2 -0
  28. package/dist/views/Posts/PostList/PostList.helpers.d.ts +2 -2
  29. package/dist/views/Posts/PostList/PostList.vue.d.ts +8 -2
  30. package/dist/views/Posts/PostList/hooks/useAdvancedSearch.d.ts +1 -1
  31. package/package.json +9 -5
  32. package/src/constants/scopes.ts +11 -0
  33. package/src/dev.ts +11 -42
  34. package/src/hooks/index.ts +7 -3
  35. package/src/hooks/useFetchCategories.ts +25 -0
  36. package/src/hooks/useFetchCategory.ts +41 -0
  37. package/src/hooks/{useModuleConfig.ts → useFetchModuleConfig.ts} +7 -5
  38. package/src/hooks/useFetchPosts.ts +23 -0
  39. package/src/hooks/useStore.ts +8 -0
  40. package/src/hooks/useUserPermission.ts +16 -0
  41. package/src/hooks/useUserProfile.ts +13 -0
  42. package/src/locales/en.ts +13 -1
  43. package/src/locales/ru.ts +14 -2
  44. package/src/router/index.ts +39 -0
  45. package/src/services/requests.ts +30 -26
  46. package/src/store/index.ts +21 -0
  47. package/src/store/user.ts +109 -0
  48. package/src/typings/common.ts +1 -0
  49. package/src/typings/model.ts +54 -1
  50. package/src/utils/common.ts +23 -0
  51. package/src/views/App.vue +10 -2
  52. package/src/views/Categories/CategoryForm/CategoryForm.helpers.ts +26 -35
  53. package/src/views/Categories/CategoryForm/CategoryForm.vue +180 -113
  54. package/src/views/Categories/CategoryList/CategoryList.helpers.ts +41 -36
  55. package/src/views/Categories/CategoryList/CategoryList.vue +42 -25
  56. package/src/views/Categories/CategoryList/hooks/useAdvancedSearch.ts +1 -1
  57. package/src/views/Home.vue +1 -1
  58. package/src/views/Posts/PostForm/PostForm.helpers.ts +7 -6
  59. package/src/views/Posts/PostForm/PostForm.vue +57 -22
  60. package/src/views/Posts/PostList/PostList.helpers.ts +13 -15
  61. package/src/views/Posts/PostList/PostList.vue +96 -34
  62. package/src/views/Posts/PostList/hooks/useAdvancedSearch.ts +6 -5
  63. package/src/hooks/useBlogCategoryList.ts +0 -23
  64. package/src/hooks/useBlogPostList.ts +0 -23
@@ -1,21 +1,7 @@
1
1
  import { createId, Nullable } from '@tager/admin-services';
2
- import { OptionType, SingleFileInputValueType } from '@tager/admin-ui';
2
+ import { OptionType } from '@tager/admin-ui';
3
3
 
4
- import { Category } from '../../../typings/model';
5
- import {
6
- BlogCategoryCreationPayload,
7
- BlogCategoryUpdatePayload,
8
- } from '../../../services/requests';
9
-
10
- export type CategoryFormValues = {
11
- name: string;
12
- isDefault: boolean;
13
- pageTitle: string;
14
- pageDescription: string;
15
- openGraphImage: Nullable<SingleFileInputValueType>;
16
- urlAlias: string;
17
- language: Nullable<OptionType>;
18
- };
4
+ import { Category, CategoryFormValues, CategoryPayload } from '@/typings/model';
19
5
 
20
6
  export function convertCategoryToFormValues(
21
7
  category: Nullable<Category>,
@@ -27,46 +13,51 @@ export function convertCategoryToFormValues(
27
13
 
28
14
  if (!category) {
29
15
  return {
16
+ language: currentLangOption ?? null,
30
17
  name: '',
31
- isDefault: false,
18
+ parent: null,
19
+ urlAlias: '',
20
+
32
21
  pageTitle: '',
33
22
  pageDescription: '',
23
+ pageKeywords: '',
24
+ openGraphTitle: '',
25
+ openGraphDescription: '',
34
26
  openGraphImage: null,
35
- urlAlias: '',
36
- language: currentLangOption ?? null,
37
27
  };
38
28
  }
29
+
39
30
  return {
31
+ language: currentLangOption ?? null,
40
32
  name: category.name,
41
- isDefault: category.isDefault,
33
+ parent: category.parent
34
+ ? { value: category.parent.id, label: category.parent.name }
35
+ : null,
36
+ urlAlias: category.urlAlias,
37
+
42
38
  pageTitle: category.pageTitle ?? '',
43
39
  pageDescription: category.pageDescription ?? '',
40
+ pageKeywords: category.pageKeywords ?? '',
41
+ openGraphTitle: category.openGraphTitle ?? '',
42
+ openGraphDescription: category.openGraphDescription ?? '',
44
43
  openGraphImage: category.openGraphImage
45
44
  ? { id: createId(), file: category.openGraphImage }
46
45
  : null,
47
- urlAlias: category.urlAlias,
48
- language: currentLangOption ?? null,
49
46
  };
50
47
  }
51
48
 
52
- export function convertCategoryFormValuesToCreationPayload(
49
+ export function convertCategoryFormValuesToPayload(
53
50
  values: CategoryFormValues
54
- ): BlogCategoryCreationPayload {
51
+ ): CategoryPayload {
55
52
  return {
53
+ language: values.language?.value ?? null,
56
54
  name: values.name,
57
- isDefault: values.isDefault,
55
+ parent: values.parent?.value ?? null,
56
+ urlAlias: values.urlAlias,
57
+ isDefault: false,
58
+
58
59
  pageTitle: values.pageTitle,
59
60
  pageDescription: values.pageDescription,
60
61
  openGraphImage: values.openGraphImage?.file.id ?? null,
61
- language: values.language?.value ?? null,
62
- };
63
- }
64
-
65
- export function convertCategoryFormValuesToUpdatePayload(
66
- values: CategoryFormValues
67
- ): BlogCategoryUpdatePayload {
68
- return {
69
- ...convertCategoryFormValuesToCreationPayload(values),
70
- urlAlias: values.urlAlias,
71
62
  };
72
63
  }
@@ -7,63 +7,99 @@
7
7
  >
8
8
  <template v-slot:content>
9
9
  <form novalidate @submit.prevent="submitForm">
10
- <form-field-select
11
- v-if="isCreation && isLangSpecific"
12
- v-model="values.language"
13
- name="language"
14
- :label="$t('blog:language')"
15
- :options="languageOptionList"
16
- :error="errors.language"
17
- />
18
-
19
- <form-field
20
- v-model="values.name"
21
- name="name"
22
- :label="$t('blog:name')"
23
- :error="errors.name"
24
- />
25
-
26
- <form-field-checkbox
27
- v-model="values.isDefault"
28
- name="isDefault"
29
- :label="$t('blog:defaultCategory')"
30
- :error="errors.isDefault"
31
- />
32
-
33
- <form-field-url-alias-input
34
- v-if="!isCreation"
35
- id="urlAlias"
36
- v-model="values.urlAlias"
37
- name="urlAlias"
38
- :label="$t('blog:URLAlias')"
39
- :url-template="pagePath"
40
- :error="errors.urlAlias"
41
- />
42
-
43
- <form-field
44
- v-model="values.pageTitle"
45
- name="pageTitle"
46
- :label="$t('blog:pageTitle')"
47
- :error="errors.pageTitle"
48
- />
49
-
50
- <form-field
51
- v-model="values.pageDescription"
52
- name="pageDescription"
53
- :label="$t('blog:pageDescription')"
54
- type="textarea"
55
- :error="errors.pageDescription"
56
- />
57
-
58
- <form-field-file-input
59
- v-model="values.openGraphImage"
60
- :label="$t('blog:openGraphImage')"
61
- name="openGraphImage"
62
- file-type="image"
63
- :image-scenario="
64
- moduleConfig ? moduleConfig.fileScenarios.openGraph : null
65
- "
66
- />
10
+ <template v-if="isCreation">
11
+ <form-field-select
12
+ v-if="isLangSpecific"
13
+ v-model="values.language"
14
+ name="language"
15
+ :label="$t('blog:language')"
16
+ :options="languageOptionList"
17
+ :error="errors.language"
18
+ />
19
+
20
+ <form-field
21
+ v-model="values.name"
22
+ name="name"
23
+ :label="$t('blog:name')"
24
+ :error="errors.name"
25
+ />
26
+
27
+ <form-field-select
28
+ v-model="values.parent"
29
+ name="parentCategory"
30
+ :label="$t('blog:parentCategory')"
31
+ :options="categoryOptionList"
32
+ :error="errors.parent"
33
+ />
34
+
35
+ <form-field-url-alias-input
36
+ id="urlAlias"
37
+ v-model="values.urlAlias"
38
+ name="urlAlias"
39
+ :label="$t('blog:link')"
40
+ :url-template="pagePath"
41
+ :error="errors.urlAlias"
42
+ />
43
+ </template>
44
+
45
+ <template v-else>
46
+ <tab-list
47
+ :tab-list="tabList"
48
+ :selected-tab-id="selectedTabId"
49
+ @tab:update="selectedTabId = $event.tabId"
50
+ />
51
+
52
+ <template v-if="selectedTabId === 'common'">
53
+ <form-field-select
54
+ v-if="isLangSpecific"
55
+ v-model="values.language"
56
+ name="language"
57
+ :label="$t('blog:language')"
58
+ :options="languageOptionList"
59
+ :error="errors.language"
60
+ />
61
+
62
+ <form-field
63
+ v-model="values.name"
64
+ name="name"
65
+ :label="$t('blog:name')"
66
+ :error="errors.name"
67
+ />
68
+
69
+ <form-field-select
70
+ v-model="values.parent"
71
+ name="parentCategory"
72
+ :label="$t('blog:parentCategory')"
73
+ :options="categoryOptionList"
74
+ :error="errors.parent"
75
+ />
76
+
77
+ <form-field-url-alias-input
78
+ id="urlAlias"
79
+ v-model="values.urlAlias"
80
+ name="urlAlias"
81
+ :label="$t('blog:link')"
82
+ :url-template="pagePath"
83
+ :error="errors.urlAlias"
84
+ />
85
+ </template>
86
+
87
+ <template v-if="selectedTabId === 'seo'">
88
+ <seo-field-group
89
+ :title="values.pageTitle"
90
+ :title-error-message="errors.pageTitle"
91
+ :description="values.pageDescription"
92
+ :description-error-message="errors.pageDescription"
93
+ :should-display-keywords="false"
94
+ :keywords="values.pageKeywords"
95
+ :keywords-error-message="errors.pageKeywords"
96
+ :image="values.openGraphImage"
97
+ :image-scenario="imageScenario"
98
+ :image-error-message="errors.openGraphImage"
99
+ @change="handleSeoFieldGroupChange"
100
+ />
101
+ </template>
102
+ </template>
67
103
  </form>
68
104
  </template>
69
105
 
@@ -80,54 +116,55 @@
80
116
  </template>
81
117
 
82
118
  <script lang="ts">
83
- import {
84
- computed,
85
- defineComponent,
86
- onMounted,
87
- ref,
88
- watch,
89
- } from '@vue/composition-api';
119
+ import { computed, defineComponent, ref, watch } from '@vue/composition-api';
90
120
 
91
121
  import {
92
122
  convertRequestErrorToMap,
93
- Nullable,
94
- useResource,
123
+ getWebsiteOrigin,
95
124
  } from '@tager/admin-services';
96
- import { OptionType, FormFooter, TagerFormSubmitEvent } from '@tager/admin-ui';
97
-
98
125
  import {
99
- createBlogCategory,
100
- getBlogCategory,
101
- updateBlogCategory,
102
- } from '../../../services/requests';
103
- import { Category, Language } from '../../../typings/model';
126
+ OptionType,
127
+ FormFooter,
128
+ TagerFormSubmitEvent,
129
+ SeoChangeEvent,
130
+ TabType,
131
+ useTranslation,
132
+ } from '@tager/admin-ui';
133
+
134
+ import { createCategory, updateCategory } from '@/services/requests';
135
+ import { CategoryFormValues, Language } from '@/typings/model';
104
136
  import {
105
137
  getBlogCategoryFormUrl,
106
138
  getBlogCategoryListUrl,
107
- } from '../../../constants/paths';
108
- import { useModuleConfig } from '../../../hooks';
139
+ } from '@/constants/paths';
140
+ import {
141
+ useFetchCategories,
142
+ useFetchCategory,
143
+ useFetchModuleConfig,
144
+ } from '@/hooks';
145
+ import { convertCategoryListToOptions } from '@/views/Posts/PostForm/PostForm.helpers';
109
146
 
110
147
  import {
111
- CategoryFormValues,
112
- convertCategoryFormValuesToCreationPayload,
113
- convertCategoryFormValuesToUpdatePayload,
148
+ convertCategoryFormValuesToPayload,
114
149
  convertCategoryToFormValues,
115
150
  } from './CategoryForm.helpers';
116
151
 
117
152
  export default defineComponent({
118
- name: 'BlogCategoryForm',
153
+ name: 'CategoryForm',
119
154
  components: { FormFooter },
120
155
  setup(props, context) {
156
+ const { t } = useTranslation(context);
121
157
  const categoryId = computed<string>(
122
158
  () => context.root.$route.params.categoryId
123
159
  );
124
160
  const isCreation = computed<boolean>(() => categoryId.value === 'create');
125
161
 
126
162
  /** Fetch module config */
163
+
127
164
  const {
128
165
  data: moduleConfig,
129
166
  loading: isModuleConfigLoading,
130
- } = useModuleConfig({ context });
167
+ } = useFetchModuleConfig({ context });
131
168
 
132
169
  const languageList = computed<Language[]>(
133
170
  () => moduleConfig.value?.languages ?? []
@@ -141,36 +178,31 @@ export default defineComponent({
141
178
  () => languageOptionList.value.length > 0
142
179
  );
143
180
 
144
- /** Fetch Category */
145
-
146
- const [
147
- fetchCategory,
148
- { data: category, loading: isCategoryLoading },
149
- ] = useResource<Nullable<Category>>({
150
- fetchResource: () => getBlogCategory(categoryId.value),
151
- initialValue: null,
152
- context,
153
- resourceName: 'Category',
154
- });
181
+ const imageScenario = computed<string | null>(
182
+ () => moduleConfig.value?.fileScenarios?.openGraph ?? null
183
+ );
155
184
 
156
- onMounted(() => {
157
- if (isCreation.value) return;
185
+ /** Fetch Categories */
158
186
 
159
- fetchCategory();
160
- });
187
+ const {
188
+ data: categories,
189
+ loading: isCategoriesLoading,
190
+ } = useFetchCategories({ context });
161
191
 
162
- watch(categoryId, () => {
163
- if (isCreation.value) return;
192
+ /** Fetch Category */
164
193
 
165
- fetchCategory();
194
+ const { data: category, loading: isCategoryLoading } = useFetchCategory({
195
+ context,
196
+ categoryId,
197
+ isCreation,
166
198
  });
167
199
 
168
200
  /** Form State */
201
+ const isSubmitting = ref<boolean>(false);
169
202
  const values = ref<CategoryFormValues>(
170
203
  convertCategoryToFormValues(category.value, languageOptionList.value)
171
204
  );
172
205
  const errors = ref<Record<string, string>>({});
173
- const isSubmitting = ref<boolean>(false);
174
206
 
175
207
  watch(category, () => {
176
208
  values.value = convertCategoryToFormValues(
@@ -182,19 +214,11 @@ export default defineComponent({
182
214
  function submitForm(event: TagerFormSubmitEvent) {
183
215
  isSubmitting.value = true;
184
216
 
185
- const creationBody = convertCategoryFormValuesToCreationPayload(
186
- values.value
187
- );
188
- const updateBody = convertCategoryFormValuesToUpdatePayload(values.value);
189
- if (!isCreation.value) {
190
- updateBody.language = category.value?.language
191
- ? category.value?.language
192
- : null;
193
- }
217
+ const body = convertCategoryFormValuesToPayload(values.value);
194
218
 
195
219
  const requestPromise = isCreation.value
196
- ? createBlogCategory(creationBody)
197
- : updateBlogCategory(categoryId.value, updateBody);
220
+ ? createCategory(body)
221
+ : updateCategory(categoryId.value, body);
198
222
 
199
223
  requestPromise
200
224
  .then(({ data }) => {
@@ -241,17 +265,51 @@ export default defineComponent({
241
265
  });
242
266
  }
243
267
 
268
+ /** Category Options */
269
+
270
+ const categoryOptionList = computed<OptionType<number>[]>(() =>
271
+ convertCategoryListToOptions(
272
+ categories.value,
273
+ values.value.language?.value ?? null
274
+ )
275
+ );
276
+
244
277
  /** Misc */
245
278
 
246
279
  const isContentLoading = computed<boolean>(
247
- () => isCategoryLoading.value || isModuleConfigLoading.value
280
+ () =>
281
+ isCategoryLoading.value ||
282
+ isModuleConfigLoading.value ||
283
+ isCategoriesLoading.value
248
284
  );
249
285
 
250
286
  const categoryPagePath = computed<string>(() => {
251
- const origin = process.env.VUE_APP_WEBSITE_URL || window.location.origin;
252
- return origin + (category.value?.urlTemplate ?? '');
287
+ return `${getWebsiteOrigin()}${category.value?.urlTemplate ?? ''}`;
253
288
  });
254
289
 
290
+ /** SEO **/
291
+
292
+ function handleSeoFieldGroupChange({
293
+ title,
294
+ description,
295
+ keywords,
296
+ image,
297
+ }: SeoChangeEvent) {
298
+ values.value.pageTitle = title;
299
+ values.value.pageDescription = description;
300
+ values.value.pageKeywords = keywords;
301
+ values.value.openGraphImage = image;
302
+ }
303
+
304
+ /** Tabs */
305
+
306
+ const tabList = computed<TabType[]>(() => [
307
+ { id: 'common', label: t('blog:tabs.common') },
308
+ { id: 'seo', label: t('blog:tabs.seo') },
309
+ ]);
310
+
311
+ const selectedTabId = ref<string>(tabList.value[0].id);
312
+
255
313
  return {
256
314
  isCreation,
257
315
  isContentLoading: isContentLoading,
@@ -264,6 +322,15 @@ export default defineComponent({
264
322
  submitForm,
265
323
  isSubmitting,
266
324
  moduleConfig,
325
+ categoryOptionList,
326
+
327
+ // SEO
328
+ imageScenario,
329
+ handleSeoFieldGroupChange,
330
+
331
+ // Tabs
332
+ tabList,
333
+ selectedTabId,
267
334
  };
268
335
  },
269
336
  });
@@ -1,13 +1,11 @@
1
1
  import { TFunction } from 'i18next';
2
2
 
3
- import { notEmpty, Nullable } from '@tager/admin-services';
3
+ import { getWebsiteOrigin, notEmpty, Nullable } from '@tager/admin-services';
4
4
  import { ColumnDefinition } from '@tager/admin-ui';
5
5
 
6
- import { Category, Language, ModuleConfig } from '../../../typings/model';
7
- import {
8
- getBlogCategoryFormUrl,
9
- getBlogPostListUrl,
10
- } from '../../../constants/paths';
6
+ import { Category, Language, ModuleConfig } from '@/typings/model';
7
+ import { getBlogCategoryFormUrl, getBlogPostListUrl } from '@/constants/paths';
8
+ import { getNameWithDepth } from '@/utils/common';
11
9
 
12
10
  export function convertCategoryList(
13
11
  categoryList: Category[],
@@ -31,54 +29,61 @@ export function convertCategoryList(
31
29
 
32
30
  export function getCategoryTableColumnDefs(
33
31
  moduleConfig: Nullable<ModuleConfig>,
34
- t: TFunction
32
+ t: TFunction,
33
+ canViewAdministrators: boolean
35
34
  ): ColumnDefinition<Category>[] {
36
35
  const isLangSpecific = moduleConfig
37
36
  ? moduleConfig.languages.length > 0
38
37
  : false;
39
38
 
40
- const COLUMN_DEFS: Array<ColumnDefinition<Category> | null> = [
39
+ const COLUMN_DEFS: (ColumnDefinition<Category> | null)[] = [
41
40
  {
42
- id: 2,
41
+ id: 1,
43
42
  name: t('blog:name'),
44
43
  field: 'name',
45
44
  type: 'name',
46
- format: ({ row }) => {
47
- return {
48
- adminLink: {
49
- url: getBlogCategoryFormUrl({ categoryId: row.id }),
50
- text: row.name,
51
- },
52
- websiteLink: {
53
- url: [
54
- process.env.VUE_APP_WEBSITE_URL || window.location.origin,
55
- row.url,
56
- ].join(''),
57
- text: row.url,
58
- },
59
- paramList: row.isDefault
60
- ? [{ name: t('blog:defaultCategory'), value: t('blog:yes') }]
61
- : [],
62
- };
45
+ format: ({ row }) => ({
46
+ adminLink: {
47
+ url: getBlogCategoryFormUrl({ categoryId: row.id }),
48
+ text: getNameWithDepth(row.name, row.depth),
49
+ },
50
+ paramList: row.isDefault
51
+ ? [{ name: t('blog:defaultCategory'), value: t('blog:yes') }]
52
+ : [],
53
+ }),
54
+ },
55
+ {
56
+ id: 2,
57
+ name: t('blog:link'),
58
+ field: 'path',
59
+ type: 'link',
60
+ format: ({ row }) => ({
61
+ url: `${getWebsiteOrigin()}${row.url}`,
62
+ text: row.url,
63
+ }),
64
+ options: {
65
+ shouldOpenNewTab: true,
63
66
  },
64
67
  },
65
68
  isLangSpecific
66
- ? { id: 4, name: t('blog:language'), field: 'language' }
69
+ ? { id: 3, name: t('blog:language'), field: 'language' }
67
70
  : null,
68
71
  {
69
- id: 3,
72
+ id: 4,
70
73
  name: t('blog:posts'),
71
74
  field: 'linkToPosts',
72
75
  style: { whiteSpace: 'nowrap', width: '130px' },
73
76
  },
74
- {
75
- id: 4,
76
- name: t('blog:actions'),
77
- field: 'actions',
78
- style: { whiteSpace: 'nowrap', width: '205px' },
79
- headStyle: { whiteSpace: 'nowrap', width: '205px' },
80
- class: 'actions-cell',
81
- },
77
+ canViewAdministrators
78
+ ? {
79
+ id: 5,
80
+ name: t('blog:actions'),
81
+ field: 'actions',
82
+ style: { whiteSpace: 'nowrap', width: '170px' },
83
+ headStyle: { whiteSpace: 'nowrap', width: '170px' },
84
+ class: 'actions-cell',
85
+ }
86
+ : null,
82
87
  ];
83
88
 
84
89
  return COLUMN_DEFS.filter(notEmpty);