@tager/admin-blog 0.3.38 → 0.3.42

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.
@@ -7,7 +7,7 @@ import {
7
7
  Category,
8
8
  ModuleConfig,
9
9
  Language,
10
- PostShort
10
+ PostShort,
11
11
  } from '../../../typings/model';
12
12
  import { getBlogPostFormUrl } from '../../../constants/paths';
13
13
 
@@ -28,7 +28,7 @@ export function convertPostList(
28
28
 
29
29
  return {
30
30
  ...post,
31
- language: foundLanguage ? foundLanguage.name : post.language
31
+ language: foundLanguage ? foundLanguage.name : post.language,
32
32
  };
33
33
  }
34
34
 
@@ -41,7 +41,7 @@ export function getStatuses(t: TFunction): Record<string, string> {
41
41
  PUBLISHED: t('blog:statusPublished'),
42
42
  DRAFT: t('blog:statusDraft'),
43
43
  ARCHIVED: t('blog:statusArchived'),
44
- }
44
+ };
45
45
  }
46
46
 
47
47
  export function getPostTableColumnDefs(
@@ -58,7 +58,7 @@ export function getPostTableColumnDefs(
58
58
  name: 'ID',
59
59
  field: 'id',
60
60
  style: { width: '50px', textAlign: 'center' },
61
- headStyle: { width: '50px', textAlign: 'center' }
61
+ headStyle: { width: '50px', textAlign: 'center' },
62
62
  },
63
63
  {
64
64
  id: 2,
@@ -66,55 +66,58 @@ export function getPostTableColumnDefs(
66
66
  field: 'status',
67
67
  style: { width: '120px' },
68
68
  headStyle: { width: '120px' },
69
- format: ({ row }) => {
70
- return getStatuses(t)[row.status];
71
- }
72
69
  },
73
70
  {
74
- id: 2,
71
+ id: 3,
75
72
  name: t('blog:name'),
76
73
  field: 'name',
77
74
  type: 'name',
78
75
  format: ({ row }) => ({
79
76
  adminLink: {
80
77
  text: row.title,
81
- url: getBlogPostFormUrl({ postId: row.id })
78
+ url: getBlogPostFormUrl({ postId: row.id }),
82
79
  },
83
80
  websiteLink: {
84
81
  text: row.url,
85
82
  url: [
86
83
  process.env.VUE_APP_WEBSITE_URL || window.location.origin,
87
- row.url
88
- ].join('')
89
- }
90
- })
84
+ row.url,
85
+ ].join(''),
86
+ },
87
+ }),
91
88
  },
92
89
  {
93
- id: 3,
90
+ id: 4,
94
91
  name: t('blog:image'),
95
92
  field: 'image',
96
93
  type: 'image',
97
94
  style: { width: '250px' },
98
- headStyle: { width: '250px' }
95
+ headStyle: { width: '250px' },
99
96
  },
100
97
  isLangSpecific
101
- ? { id: 4, name: t('blog:language'), field: 'language' }
98
+ ? { id: 5, name: t('blog:language'), field: 'language' }
102
99
  : null,
103
- { id: 5, name: t('blog:date'), field: 'datetime', type: 'datetime', options: { ignoreStartOfDayTime: true } },
104
100
  {
105
101
  id: 6,
102
+ name: t('blog:date'),
103
+ field: 'datetime',
104
+ type: 'datetime',
105
+ options: { ignoreStartOfDayTime: true },
106
+ },
107
+ {
108
+ id: 7,
106
109
  name: t('blog:categories'),
107
110
  field: 'categories',
108
111
  format: ({ row }) =>
109
- row.categories.map((category) => category.name).join(', ')
112
+ row.categories.map((category) => category.name).join(', '),
110
113
  },
111
114
  {
112
- id: 7,
115
+ id: 8,
113
116
  name: t('blog:actions'),
114
117
  field: 'actions',
115
118
  style: { whiteSpace: 'nowrap', width: '120px' },
116
- headStyle: { whiteSpace: 'nowrap', width: '120px' }
117
- }
119
+ headStyle: { whiteSpace: 'nowrap', width: '120px' },
120
+ },
118
121
  ];
119
122
 
120
123
  return COLUMN_DEFS.filter(notEmpty);
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <page
3
- :title="pageTitle"
3
+ :title='pageTitle'
4
4
  :header-buttons="[
5
5
  {
6
6
  text: $t('blog:newPost'),
@@ -10,95 +10,116 @@
10
10
  >
11
11
  <template v-slot:content>
12
12
  <data-table
13
- :column-defs="columnDefs"
14
- :row-data="rowData"
15
- :loading="isRowDataLoading"
16
- :error-message="errorMessage"
17
- :search-query="searchQuery"
18
- :pagination="{
13
+ :column-defs='columnDefs'
14
+ :row-data='rowData'
15
+ :loading='isRowDataLoading'
16
+ :error-message='errorMessage'
17
+ :search-query='searchQuery'
18
+ :pagination='{
19
19
  pageNumber,
20
20
  pageCount,
21
21
  pageSize,
22
22
  disabled: isRowDataLoading,
23
- }"
24
- data-table="blog-post"
25
- @change="handleChange"
23
+ }'
24
+ data-table='blog-post'
25
+ @change='handleChange'
26
26
  >
27
27
  <template v-slot:filters>
28
- <advanced-search :tags="tags" @click:tag="tagRemovalHandler">
29
- <div class="filters">
28
+ <advanced-search :tags='tags' @click:tag='tagRemovalHandler'>
29
+ <div class='filters'>
30
30
  <form-field-multi-select
31
- v-model="categoryFilter"
32
- :options="categoryOptionList"
33
- name="categoryFilter"
34
- :searchable="true"
31
+ v-model='categoryFilter'
32
+ :options='categoryOptionList'
33
+ name='categoryFilter'
34
+ :searchable='true'
35
35
  :label="$t('blog:category')"
36
- class="filter"
36
+ class='filter'
37
37
  />
38
38
 
39
39
  <form-field-multi-select
40
- v-if="isLangSpecific"
41
- v-model="languageFilter"
42
- :options="languageOptionList"
43
- name="languageFilter"
44
- :searchable="true"
40
+ v-if='isLangSpecific'
41
+ v-model='languageFilter'
42
+ :options='languageOptionList'
43
+ name='languageFilter'
44
+ :searchable='true'
45
45
  :label="$t('blog:language')"
46
- class="filter"
46
+ class='filter'
47
47
  />
48
48
 
49
- <div class="filter">
50
- <div class="date-label">
49
+ <div class='filter'>
50
+ <div class='date-label'>
51
51
  {{ $t('blog:dateOfPublication') }}
52
52
  </div>
53
53
 
54
- <div class="date-content">
54
+ <div class='date-content'>
55
55
  <form-field
56
- v-model="fromDateFilter"
56
+ v-model='fromDateFilter'
57
57
  :label="$t('blog:From')"
58
- name="fromDateFilter"
59
- type="date"
60
- :max="toDateFilter"
58
+ name='fromDateFilter'
59
+ type='date'
60
+ :max='toDateFilter'
61
61
  />
62
62
 
63
63
  <form-field
64
- v-model="toDateFilter"
64
+ v-model='toDateFilter'
65
65
  :label="$t('blog:To')"
66
- name="toDateFilter"
67
- type="date"
68
- :min="fromDateFilter"
66
+ name='toDateFilter'
67
+ type='date'
68
+ :min='fromDateFilter'
69
69
  />
70
70
  </div>
71
71
  </div>
72
+
73
+ <form-field-multi-select
74
+ v-model='statusFilter'
75
+ :options='statusOptionList'
76
+ name='statusFilter'
77
+ :searchable='true'
78
+ :label="$t('blog:status')"
79
+ class='filter'
80
+ />
72
81
  </div>
73
82
  </advanced-search>
74
83
  </template>
75
84
 
76
- <template v-slot:cell(actions)="{ row }">
85
+ <template v-slot:cell(status)='{ row }'>
86
+ <div class='status'>
87
+ <span>{{ getStatuses(t)[row.status] }}</span>
88
+ <span v-if='row.status === "PUBLISHED" && row.archiveAt'>
89
+ {{ t('blog:publishAtLabel') }}: <i>{{ formatDateTime(new Date(row.archiveAt), true) }}</i>
90
+ </span>
91
+ <span v-if='row.status === "DRAFT" && row.publishAt'>
92
+ {{ t('blog:publishAtLabel') }}: <i>{{ formatDateTime(new Date(row.publishAt), true) }}</i>
93
+ </span>
94
+ </div>
95
+ </template>
96
+
97
+ <template v-slot:cell(actions)='{ row }'>
77
98
  <base-button
78
- variant="icon"
99
+ variant='icon'
79
100
  :title="$t('blog:edit')"
80
- :disabled="isBusy(row.id)"
81
- :href="getBlogPostFormUrl({ postId: row.id })"
101
+ :disabled='isBusy(row.id)'
102
+ :href='getBlogPostFormUrl({ postId: row.id })'
82
103
  >
83
- <svg-icon name="edit" />
104
+ <svg-icon name='edit' />
84
105
  </base-button>
85
106
 
86
107
  <base-button
87
- variant="icon"
108
+ variant='icon'
88
109
  :title="$t('blog:clone')"
89
- :disabled="isBusy(row.id)"
90
- @click="handleResourceClone(row.id)"
110
+ :disabled='isBusy(row.id)'
111
+ @click='handleResourceClone(row.id)'
91
112
  >
92
- <svg-icon name="contentCopy" />
113
+ <svg-icon name='contentCopy' />
93
114
  </base-button>
94
115
 
95
116
  <base-button
96
- variant="icon"
117
+ variant='icon'
97
118
  :title="$t('blog:remove')"
98
- :disabled="isBusy(row.id)"
99
- @click="handleResourceDelete(row.id)"
119
+ :disabled='isBusy(row.id)'
120
+ @click='handleResourceDelete(row.id)'
100
121
  >
101
- <svg-icon name="delete" />
122
+ <svg-icon name='delete' />
102
123
  </base-button>
103
124
  </template>
104
125
  </data-table>
@@ -106,37 +127,40 @@
106
127
  </page>
107
128
  </template>
108
129
 
109
- <script lang="ts">
130
+ <script lang='ts'>
110
131
  import { computed, defineComponent, watch } from '@vue/composition-api';
111
132
  import isEqual from 'lodash.isequal';
112
133
  import pick from 'lodash.pick';
113
134
 
114
135
  import {
115
136
  ColumnDefinition,
137
+ OptionType,
116
138
  useDataTable,
117
139
  useTranslation,
140
+ formatDateTime
118
141
  } from '@tager/admin-ui';
119
142
  import {
120
143
  Nullable,
121
144
  useResourceClone,
122
- useResourceDelete,
145
+ useResourceDelete
123
146
  } from '@tager/admin-services';
124
147
 
125
148
  import { useFetchModuleConfig, useFetchCategories } from '../../../hooks';
126
149
  import {
127
150
  clonePost,
128
151
  deleteBlogPost,
129
- getPosts,
152
+ getPosts
130
153
  } from '../../../services/requests';
131
154
  import { getBlogPostFormUrl } from '../../../constants/paths';
132
155
  import {
133
156
  Category,
134
157
  Language,
135
158
  PostFull,
136
- PostShort,
159
+ PostShort
137
160
  } from '../../../typings/model';
161
+ import { getStatusOptions } from '../PostForm/PostForm.helpers';
138
162
 
139
- import { convertPostList, getPostTableColumnDefs } from './PostList.helpers';
163
+ import { convertPostList, getPostTableColumnDefs, getStatuses } from './PostList.helpers';
140
164
  import { useAdvancedSearch } from './hooks';
141
165
 
142
166
  export default defineComponent({
@@ -150,11 +174,13 @@ export default defineComponent({
150
174
  : context.root.$route.query.category;
151
175
  });
152
176
 
177
+ const statusOptionList = computed<OptionType[]>(() => getStatusOptions(t));
178
+
153
179
  /** Fetch module config **/
154
180
 
155
181
  const {
156
182
  data: moduleConfig,
157
- loading: isModuleConfigLoading,
183
+ loading: isModuleConfigLoading
158
184
  } = useFetchModuleConfig({ context });
159
185
 
160
186
  const languageList = computed<Language[]>(
@@ -165,22 +191,22 @@ export default defineComponent({
165
191
 
166
192
  const {
167
193
  data: categoryList,
168
- loading: isCategoryListLoading,
194
+ loading: isCategoryListLoading
169
195
  } = useFetchCategories({ context });
170
196
 
171
197
  const selectedCategory = computed<Nullable<Category>>(() =>
172
198
  categoryId.value
173
199
  ? categoryList.value.find(
174
- ({ id }) => String(id) === String(categoryId.value)
175
- ) ?? null
200
+ ({ id }) => String(id) === String(categoryId.value)
201
+ ) ?? null
176
202
  : null
177
203
  );
178
204
 
179
205
  const pageTitle = computed<string>(() => {
180
206
  return selectedCategory.value
181
207
  ? `${context.root.$t('blog:postsByCategory')} "${
182
- selectedCategory.value.name
183
- }"`
208
+ selectedCategory.value.name
209
+ }"`
184
210
  : context.root.$t('blog:posts');
185
211
  });
186
212
 
@@ -196,10 +222,12 @@ export default defineComponent({
196
222
  filterParams,
197
223
  tags,
198
224
  tagRemovalHandler,
225
+ statusFilter
199
226
  } = useAdvancedSearch({
200
227
  context,
201
228
  categoryList,
202
229
  languageList,
230
+ statusOptionList
203
231
  });
204
232
 
205
233
  const isLangSpecific = computed<boolean>(
@@ -217,19 +245,19 @@ export default defineComponent({
217
245
  handleChange,
218
246
  pageNumber,
219
247
  pageCount,
220
- pageSize,
248
+ pageSize
221
249
  } = useDataTable<PostShort>({
222
250
  fetchEntityList: (params) =>
223
251
  getPosts({
224
252
  query: params.searchQuery,
225
253
  pageNumber: params.pageNumber,
226
254
  pageSize: params.pageSize,
227
- ...filterParams.value,
255
+ ...filterParams.value
228
256
  }),
229
257
  initialValue: [],
230
258
  context,
231
259
  resourceName: 'Blog post list',
232
- pageSize: 100,
260
+ pageSize: 100
233
261
  });
234
262
 
235
263
  watch(filterParams, () => {
@@ -239,7 +267,7 @@ export default defineComponent({
239
267
 
240
268
  const newQuery = {
241
269
  ...pick(context.root.$route.query, ['query', 'pageNumber']),
242
- ...filterParams.value,
270
+ ...filterParams.value
243
271
  };
244
272
 
245
273
  if (!isEqual(context.root.$route.query, newQuery)) {
@@ -267,7 +295,7 @@ export default defineComponent({
267
295
  deleteResource: deleteBlogPost,
268
296
  resourceName: 'Post',
269
297
  onSuccess: fetchPostList,
270
- context,
298
+ context
271
299
  });
272
300
 
273
301
  const { isCloning, handleResourceClone } = useResourceClone({
@@ -276,7 +304,7 @@ export default defineComponent({
276
304
  successMessage: t('blog:cloneSuccess'),
277
305
  failureMessage: t('blog:cloneFailure'),
278
306
  onSuccessRedirectTo: (data: PostFull) => `/blog/posts/${data.id}`,
279
- context,
307
+ context
280
308
  });
281
309
 
282
310
  function isBusy(postId: number): boolean {
@@ -288,6 +316,8 @@ export default defineComponent({
288
316
  );
289
317
 
290
318
  return {
319
+ t,
320
+ formatDateTime,
291
321
  columnDefs,
292
322
  rowData: displayedPostList,
293
323
  handleResourceDelete,
@@ -311,18 +341,21 @@ export default defineComponent({
311
341
  languageOptionList,
312
342
  fromDateFilter,
313
343
  toDateFilter,
344
+ statusFilter,
345
+ statusOptionList,
314
346
  tags,
315
347
  tagRemovalHandler,
348
+ getStatuses,
316
349
 
317
350
  // Clone
318
351
  handleResourceClone,
319
- isBusy,
352
+ isBusy
320
353
  };
321
- },
354
+ }
322
355
  });
323
356
  </script>
324
357
 
325
- <style scoped lang="scss">
358
+ <style scoped lang='scss'>
326
359
  .filters {
327
360
  display: flex;
328
361
  margin: 0 -10px;
@@ -347,4 +380,21 @@ export default defineComponent({
347
380
  grid-template-columns: repeat(2, 1fr);
348
381
  column-gap: 10px;
349
382
  }
383
+
384
+ .status {
385
+ span:first-child {
386
+ display: block;
387
+ }
388
+
389
+ span:nth-child(2) {
390
+ display: block;
391
+ margin-top: 5px;
392
+ font-size: 0.8rem;
393
+
394
+ i {
395
+ display: block;
396
+ }
397
+ }
398
+ ;
399
+ }
350
400
  </style>
@@ -23,6 +23,7 @@ interface Params {
23
23
  context: SetupContext;
24
24
  categoryList: ComputedRef<Category[]>;
25
25
  languageList: ComputedRef<Language[]>;
26
+ statusOptionList: ComputedRef<OptionType[]>;
26
27
  }
27
28
 
28
29
  interface State {
@@ -32,6 +33,7 @@ interface State {
32
33
  languageOptionList: ComputedRef<OptionType[]>;
33
34
  fromDateFilter: Ref<string>;
34
35
  toDateFilter: Ref<string>;
36
+ statusFilter: Ref<OptionType[]>;
35
37
  filterParams: ComputedRef<Record<string, string | string[]>>;
36
38
  tags: ComputedRef<FilterTagType[]>;
37
39
  tagRemovalHandler(event: FilterTagType): void;
@@ -42,6 +44,7 @@ enum FilterTypes {
42
44
  LANGUAGE = 'language',
43
45
  FROM_DATE = 'from-date',
44
46
  TO_DATE = 'to-date',
47
+ STATUS = 'status',
45
48
  }
46
49
 
47
50
  export const dateFormat = (date: string) => date.split('-').reverse().join('.');
@@ -50,6 +53,7 @@ export function useAdvancedSearch({
50
53
  context,
51
54
  categoryList,
52
55
  languageList,
56
+ statusOptionList,
53
57
  }: Params): State {
54
58
  /** Category **/
55
59
 
@@ -161,12 +165,31 @@ export function useAdvancedSearch({
161
165
  };
162
166
  });
163
167
 
168
+ /** Status **/
169
+
170
+ const initialStatusFilter = computed<OptionType[]>(() => {
171
+ const queryValue = getFilterParamAsStringArray(
172
+ context.root.$route.query,
173
+ FilterTypes.STATUS
174
+ );
175
+ return statusOptionList.value.filter(({ value }) =>
176
+ queryValue.some((selected) => selected === value)
177
+ );
178
+ });
179
+
180
+ const statusFilter = ref<OptionType[]>(initialStatusFilter.value);
181
+
182
+ watch(initialStatusFilter, () => {
183
+ statusFilter.value = initialStatusFilter.value;
184
+ });
185
+
164
186
  /** Params **/
165
187
 
166
188
  const filterParams = computed(() => {
167
189
  const filters: Record<string, string | string[]> = {
168
190
  [FilterTypes.CATEGORY]: categoryFilter.value.map(({ value }) => value),
169
191
  [FilterTypes.LANGUAGE]: languageFilter.value.map(({ value }) => value),
192
+ [FilterTypes.STATUS]: statusFilter.value.map(({ value }) => value),
170
193
  };
171
194
 
172
195
  if (fromDateFilter.value) {
@@ -197,6 +220,11 @@ export function useAdvancedSearch({
197
220
  fromDateFilter.value = '';
198
221
  toDateFilter.value = '';
199
222
  }
223
+ if (event.name === FilterTypes.STATUS) {
224
+ statusFilter.value = statusFilter.value.filter(
225
+ ({ value }) => value !== event.value
226
+ );
227
+ }
200
228
  }
201
229
 
202
230
  /** Tags **/
@@ -216,6 +244,12 @@ export function useAdvancedSearch({
216
244
  title: context.root.$t('blog:language'),
217
245
  })),
218
246
  date.value,
247
+ ...statusFilter.value.map(({ value, label }) => ({
248
+ value,
249
+ label,
250
+ name: FilterTypes.STATUS,
251
+ title: context.root.$t('blog:status'),
252
+ })),
219
253
  ].filter(isNotNullish)
220
254
  );
221
255
 
@@ -243,6 +277,7 @@ export function useAdvancedSearch({
243
277
  languageOptionList,
244
278
  fromDateFilter,
245
279
  toDateFilter,
280
+ statusFilter,
246
281
  tagRemovalHandler,
247
282
  tags: tags,
248
283
  filterParams,