@tager/admin-blog 1.0.1 → 1.0.2

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.
@@ -1,5 +1,5 @@
1
1
  import { MenuItemType } from '@tager/admin-layout';
2
2
  declare const _default: import("vue").DefineComponent<{}, {
3
3
  sidebarMenuList: MenuItemType[];
4
- }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, import("vue").EmitsOptions, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{}>>, {}>;
4
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{}>>, {}>;
5
5
  export default _default;
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ import { onMounted, watch, computed, ref, defineComponent, resolveComponent, ope
3
3
  import isEqual from "lodash.isequal";
4
4
  import pick from "lodash.pick";
5
5
  import { useRoute, useRouter } from "vue-router";
6
- import { getFilterParamAsStringArray, getFilterParamAsString, getFilterParams, DeleteIcon, ContentCopyIcon, EditIcon, BaseButton, FormField, FormFieldMultiSelect, AdvancedSearch, DataTable, useDataTable, formatDateTime, SeoFieldGroup, FormFieldFileInput, FormFieldRichTextInput, FormFieldCheckbox, FormFieldUrlAliasInput, FormFieldSelect, TabList, ShortCodeConstructor, FormFooter, createTabErrorFinder, SouthIcon, NorthIcon, CountButton } from "@tager/admin-ui";
6
+ import { getFilterParamAsStringArray, getFilterParamAsString, getFilterParams, DeleteIcon, ContentCopyIcon, EditIcon, BaseButton, FormField, FormFieldMultiSelect, AdvancedSearch, DataTable, useDataTable, formatDateTime, SeoFieldGroup, FormFieldFileInput, FormFieldRichTextInput, FormFieldCheckbox, FormFieldUrlAliasInput, FormFieldSelect, TabList, ShortCodeConstructor, FormFooter, formatDate, createTabErrorFinder, SouthIcon, NorthIcon, CountButton } from "@tager/admin-ui";
7
7
  import { Page } from "@tager/admin-layout";
8
8
  import { universalFieldUtils, DynamicField } from "@tager/admin-dynamic-field";
9
9
  import { compile } from "path-to-regexp";
@@ -27,7 +27,7 @@ const EN = {
27
27
  edit: "Edit",
28
28
  remove: "Remove",
29
29
  postsByCategory: "Posts by category",
30
- name: "Name",
30
+ name: "Title",
31
31
  image: "Image",
32
32
  language: "Language",
33
33
  languages: "Languages",
@@ -93,7 +93,12 @@ const EN = {
93
93
  archiveAt: "Archive at",
94
94
  publishAt: "Publish at",
95
95
  archiveAtLabel: "Archive at",
96
- publishAtLabel: "Publish at"
96
+ publishAtLabel: "Publish at",
97
+ sortDateTimeAsc: "Date - Newest to Oldest",
98
+ sortDateTimeDesc: "Date - Oldest to Newest",
99
+ sortTitleAsc: "Title - A-Z",
100
+ sortTitleDesc: "Title - Z-A",
101
+ sortStatus: "Status"
97
102
  };
98
103
  const RU = {
99
104
  yes: "\u0414\u0430",
@@ -114,7 +119,7 @@ const RU = {
114
119
  edit: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C",
115
120
  remove: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C",
116
121
  postsByCategory: "\u041F\u043E\u0441\u0442\u044B \u043F\u043E \u043A\u0430\u0442\u0435\u0433\u043E\u0440\u0438\u044F\u043C",
117
- name: "\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435",
122
+ name: "\u0417\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A",
118
123
  image: "\u0418\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435",
119
124
  language: "\u042F\u0437\u044B\u043A",
120
125
  languages: "\u042F\u0437\u044B\u043A\u0438",
@@ -180,7 +185,12 @@ const RU = {
180
185
  archiveAt: "\u041F\u0435\u0440\u0435\u043D\u0435\u0441\u0442\u0438 \u0432 \u0430\u0440\u0445\u0438\u0432 \u0432",
181
186
  publishAt: "\u041E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u0442\u044C \u0432",
182
187
  archiveAtLabel: "\u041F\u0435\u0440\u0435\u043D\u043E\u0441 \u0432 \u0430\u0440\u0445\u0438\u0432",
183
- publishAtLabel: "\u041F\u0443\u0431\u043B\u0438\u043A\u0430\u0446\u0438\u044F"
188
+ publishAtLabel: "\u041F\u0443\u0431\u043B\u0438\u043A\u0430\u0446\u0438\u044F",
189
+ sortDateTimeAsc: "\u0414\u0430\u0442\u0435 - \u041F\u043E \u0432\u043E\u0437\u0440\u0430\u0441\u0442\u0430\u043D\u0438\u044E",
190
+ sortDateTimeDesc: "\u0414\u0430\u0442\u0435 - \u041F\u043E \u0443\u0431\u044B\u0432\u0430\u043D\u0438\u044E",
191
+ sortTitleAsc: "\u0417\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u0443 - \u0410-\u042F",
192
+ sortTitleDesc: "\u0417\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u0443 - \u042F-\u0410",
193
+ sortStatus: "\u0421\u0442\u0430\u0442\u0443\u0441\u0443"
184
194
  };
185
195
  function applyTranslations() {
186
196
  i18n.addTranslations("en", "blog", EN);
@@ -239,7 +249,7 @@ function getModuleConfig() {
239
249
  }
240
250
  function useFetchPosts() {
241
251
  const [fetchPosts, resource] = useResource({
242
- fetchResource: () => getPosts({ pageNumber: 1, pageSize: 99999 }),
252
+ fetchResource: () => getPosts({ pageNumber: 1, pageSize: 99999, sort: "status" }),
243
253
  initialValue: [],
244
254
  resourceName: "Blog posts"
245
255
  });
@@ -521,6 +531,30 @@ function getPostTableColumnDefs(moduleConfig, t) {
521
531
  ];
522
532
  return COLUMN_DEFS.filter(notEmpty);
523
533
  }
534
+ function getPostTableSortOptions(t) {
535
+ return [
536
+ {
537
+ value: "datetime_desc",
538
+ label: t("blog:sortDateTimeDesc")
539
+ },
540
+ {
541
+ value: "datetime_asc",
542
+ label: t("blog:sortDateTimeAsc")
543
+ },
544
+ {
545
+ value: "title_asc",
546
+ label: t("blog:sortTitleAsc")
547
+ },
548
+ {
549
+ value: "title_desc",
550
+ label: t("blog:sortTitleDesc")
551
+ },
552
+ {
553
+ value: "status",
554
+ label: t("blog:sortStatus")
555
+ }
556
+ ];
557
+ }
524
558
  const dateFormat = (date) => date.split("-").reverse().join(".");
525
559
  function useAdvancedSearch$1({
526
560
  t,
@@ -773,6 +807,7 @@ const _sfc_main$3 = defineComponent({
773
807
  languageList,
774
808
  statusOptionList
775
809
  });
810
+ const sortOptions = getPostTableSortOptions(t);
776
811
  const {
777
812
  fetchEntityList: fetchPostList,
778
813
  isLoading: isPostLoading,
@@ -782,24 +817,27 @@ const _sfc_main$3 = defineComponent({
782
817
  handleChange,
783
818
  pageNumber,
784
819
  pageCount,
785
- pageSize
820
+ pageSize,
821
+ sort
786
822
  } = useDataTable({
787
823
  fetchEntityList: (params) => getPosts({
788
824
  query: params.searchQuery,
789
825
  pageNumber: params.pageNumber,
790
826
  pageSize: params.pageSize,
827
+ sort: params.sort || void 0,
791
828
  ...filterParams.value
792
829
  }),
793
830
  initialValue: [],
794
831
  resourceName: "Blog post list",
795
- pageSize: 100
832
+ pageSize: 100,
833
+ defaultSort: sortOptions[0].value
796
834
  });
797
835
  watch(filterParams, () => {
798
836
  if (isModuleConfigLoading.value || isCategoryListLoading.value) {
799
837
  return;
800
838
  }
801
839
  const newQuery = {
802
- ...pick(route.query, ["query", "pageNumber"]),
840
+ ...pick(route.query, ["query", "pageNumber", "sort"]),
803
841
  ...filterParams.value
804
842
  };
805
843
  if (!isEqual(route.query, newQuery)) {
@@ -842,6 +880,8 @@ const _sfc_main$3 = defineComponent({
842
880
  t,
843
881
  formatDateTime,
844
882
  columnDefs,
883
+ sortOptions,
884
+ sortValue: sort,
845
885
  rowData: displayedPostList,
846
886
  handleResourceDelete,
847
887
  isDeleting,
@@ -909,6 +949,7 @@ function _sfc_render$3(_ctx, _cache, $props, $setup, $data, $options) {
909
949
  pageSize: _ctx.pageSize,
910
950
  disabled: _ctx.isRowDataLoading
911
951
  },
952
+ sort: { options: _ctx.sortOptions, value: _ctx.sortValue },
912
953
  "data-table": "blog-post",
913
954
  onChange: _ctx.handleChange
914
955
  }, {
@@ -1019,12 +1060,12 @@ function _sfc_render$3(_ctx, _cache, $props, $setup, $data, $options) {
1019
1060
  }, 1032, ["title", "disabled", "onClick"])
1020
1061
  ]),
1021
1062
  _: 1
1022
- }, 8, ["column-defs", "row-data", "loading", "error-message", "search-query", "pagination", "onChange"])
1063
+ }, 8, ["column-defs", "row-data", "loading", "error-message", "search-query", "pagination", "sort", "onChange"])
1023
1064
  ]),
1024
1065
  _: 1
1025
1066
  }, 8, ["title", "header-buttons"]);
1026
1067
  }
1027
- var BlogPostList = /* @__PURE__ */ _export_sfc(_sfc_main$3, [["render", _sfc_render$3], ["__scopeId", "data-v-2ee9b750"]]);
1068
+ var BlogPostList = /* @__PURE__ */ _export_sfc(_sfc_main$3, [["render", _sfc_render$3], ["__scopeId", "data-v-02162423"]]);
1028
1069
  var PostForm_vue_vue_type_style_index_0_scoped_true_lang = "";
1029
1070
  const _sfc_main$2 = defineComponent({
1030
1071
  name: "BlogPostForm",
@@ -1090,7 +1131,7 @@ const _sfc_main$2 = defineComponent({
1090
1131
  return relatedPost.id !== ((_a2 = post.value) == null ? void 0 : _a2.id);
1091
1132
  }).map((post2) => ({
1092
1133
  value: post2.id,
1093
- label: post2.title
1134
+ label: (post2.status === "ARCHIVED" ? `${t("blog:statusArchived")} - ` : post2.status === "DRAFT" ? `${t("blog:statusDraft")} - ` : post2.datetime ? formatDate(new Date(post2.datetime)) + " - " : "") + post2.title
1094
1135
  }))
1095
1136
  );
1096
1137
  const isSubmitting = ref(false);
@@ -1555,7 +1596,7 @@ function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
1555
1596
  _: 1
1556
1597
  }, 8, ["title", "is-content-loading", "header-buttons", "footer"]);
1557
1598
  }
1558
- var BlogPostForm = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["render", _sfc_render$2], ["__scopeId", "data-v-62596ad5"]]);
1599
+ var BlogPostForm = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["render", _sfc_render$2], ["__scopeId", "data-v-22282a12"]]);
1559
1600
  function convertCategoryList(categoryList, languages) {
1560
1601
  return categoryList.map((category) => {
1561
1602
  if (languages.length > 0) {
@@ -1906,7 +1947,8 @@ function _sfc_render$1(_ctx, _cache, $props, $setup, $data, $options) {
1906
1947
  ]),
1907
1948
  _: 1
1908
1949
  }, 8, ["tags", "onClick:tag"])
1909
- ])
1950
+ ]),
1951
+ key: "0"
1910
1952
  } : void 0
1911
1953
  ]), 1032, ["column-defs", "row-data", "loading", "error-message", "search-query", "pagination", "onChange"])
1912
1954
  ]),
@@ -84,4 +84,9 @@ export declare const EN: {
84
84
  publishAt: string;
85
85
  archiveAtLabel: string;
86
86
  publishAtLabel: string;
87
+ sortDateTimeAsc: string;
88
+ sortDateTimeDesc: string;
89
+ sortTitleAsc: string;
90
+ sortTitleDesc: string;
91
+ sortStatus: string;
87
92
  };
@@ -6,6 +6,7 @@ export declare function getPosts(params?: {
6
6
  query?: string;
7
7
  pageNumber?: number;
8
8
  pageSize?: number;
9
+ sort?: string;
9
10
  }): Promise<ResponseBody<PostShort[]>>;
10
11
  export declare function getBlogPostCount(): Promise<ResponseBody<{
11
12
  count: number;
package/dist/style.css CHANGED
@@ -1 +1 @@
1
- .filters[data-v-2ee9b750]{margin-bottom:1rem;width:100%;display:grid;gap:1rem;grid-template-columns:1fr 1fr}.filters[data-v-2ee9b750]:not(:first-child){margin-top:1rem}.status span[data-v-2ee9b750]:first-child{display:block}.status span[data-v-2ee9b750]:nth-child(2){display:block;margin-top:5px;font-size:.8rem}.status span:nth-child(2) i[data-v-2ee9b750]{display:block}.status[data-v-62596ad5]{display:flex;align-items:flex-end}.status-value[data-v-62596ad5]{flex-basis:50%}.status-checkbox[data-v-62596ad5]{margin-left:20px;padding-bottom:8px}.status-date[data-v-62596ad5]{margin-left:20px;flex:1 1 1px}
1
+ .filters[data-v-02162423]{margin-bottom:1rem;width:100%;display:grid;gap:1rem;grid-template-columns:1fr 1fr}.filters[data-v-02162423]:not(:first-child){margin-top:1rem}.status span[data-v-02162423]{display:block}.status span[data-v-02162423]:nth-child(2){display:block;margin-top:5px;font-size:.8rem}.status span:nth-child(2) i[data-v-02162423]{display:block}.status[data-v-22282a12]{display:flex;align-items:flex-end}.status-value[data-v-22282a12]{flex-basis:50%}.status-checkbox[data-v-22282a12]{margin-left:20px;padding-bottom:8px}.status-date[data-v-22282a12]{margin-left:20px;flex:1 1 1px}
@@ -49,5 +49,5 @@ declare const _default: import("vue").DefineComponent<{}, {
49
49
  handleSeoFieldGroupChange: ({ title, description, keywords, image, }: SeoChangeEvent) => void;
50
50
  tabList: import("vue").ComputedRef<TabType[]>;
51
51
  selectedTabId: import("vue").Ref<string>;
52
- }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, import("vue").EmitsOptions, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{}>>, {}>;
52
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{}>>, {}>;
53
53
  export default _default;
@@ -22,5 +22,5 @@ declare const _default: import("vue").DefineComponent<{}, {
22
22
  languageOptionList: import("vue").ComputedRef<import("@tager/admin-ui").OptionType<string>[]>;
23
23
  tags: import("vue").ComputedRef<import("@tager/admin-ui").FilterTagType[]>;
24
24
  tagRemovalHandler: (event: import("@tager/admin-ui").FilterTagType) => void;
25
- }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, import("vue").EmitsOptions, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{}>>, {}>;
25
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{}>>, {}>;
26
26
  export default _default;
@@ -2764,5 +2764,5 @@ declare const _default: import("vue").DefineComponent<{}, {
2764
2764
  urlAliasTemplate: import("vue").ComputedRef<string>;
2765
2765
  handleCategoriesChange: (selectedOptions: OptionType<number>[]) => void;
2766
2766
  statusOptions: import("vue").ComputedRef<OptionType<string>[]>;
2767
- }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, import("vue").EmitsOptions, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{}>>, {}>;
2767
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{}>>, {}>;
2768
2768
  export default _default;
@@ -1,7 +1,8 @@
1
1
  import { TFunction } from 'i18next';
2
2
  import { Nullable } from '@tager/admin-services';
3
- import { ColumnDefinition } from '@tager/admin-ui';
3
+ import { ColumnDefinition, OptionType } from '@tager/admin-ui';
4
4
  import { Category, ModuleConfig, Language, PostShort } from '../../../typings/model';
5
5
  export declare function convertPostList(postList: PostShort[], selectedCategory: Nullable<Category>, languages: Language[]): PostShort[];
6
6
  export declare function getStatuses(t: TFunction): Record<string, string>;
7
7
  export declare function getPostTableColumnDefs(moduleConfig: Nullable<ModuleConfig>, t: TFunction): ColumnDefinition<PostShort>[];
8
+ export declare function getPostTableSortOptions(t: TFunction): Array<OptionType>;
@@ -7,6 +7,8 @@ declare const _default: import("vue").DefineComponent<{}, {
7
7
  t: import("i18next").TFunction;
8
8
  formatDateTime: typeof formatDateTime;
9
9
  columnDefs: import("vue").ComputedRef<ColumnDefinition<PostShort>[]>;
10
+ sortOptions: OptionType<string>[];
11
+ sortValue: import("vue").Ref<Nullable<string>>;
10
12
  rowData: import("vue").ComputedRef<PostShort[]>;
11
13
  handleResourceDelete: (entityId: string | number) => void;
12
14
  isDeleting: (entityId: string | number) => boolean;
@@ -34,5 +36,5 @@ declare const _default: import("vue").DefineComponent<{}, {
34
36
  getStatuses: typeof getStatuses;
35
37
  handleResourceClone: (entityId: string | number) => void;
36
38
  isBusy: (postId: number) => boolean;
37
- }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, import("vue").EmitsOptions, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{}>>, {}>;
39
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{}>>, {}>;
38
40
  export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tager/admin-blog",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.umd.js",
6
6
  "module": "dist/index.esm.js",
@@ -34,9 +34,9 @@
34
34
  "@babel/eslint-parser": "^7.17.0",
35
35
  "@rushstack/eslint-patch": "^1.1.2",
36
36
  "@tager/admin-dynamic-field": "^1.0.0",
37
- "@tager/admin-layout": "^1.0.0",
38
- "@tager/admin-services": "^1.0.0",
39
- "@tager/admin-ui": "^1.0.1",
37
+ "@tager/admin-layout": "^1.0.7",
38
+ "@tager/admin-services": "^1.1.3",
39
+ "@tager/admin-ui": "^1.0.15",
40
40
  "@types/filesize": "^5.0.0",
41
41
  "@types/lodash.isequal": "^4.5.6",
42
42
  "@types/lodash.pick": "^4.4.7",
@@ -7,7 +7,8 @@ import { getPosts } from '../services/requests';
7
7
 
8
8
  export function useFetchPosts(): ResourceRef<PostShort[]> {
9
9
  const [fetchPosts, resource] = useResource<PostShort[]>({
10
- fetchResource: () => getPosts({ pageNumber: 1, pageSize: 99999 }),
10
+ fetchResource: () =>
11
+ getPosts({ pageNumber: 1, pageSize: 99999, sort: 'status' }),
11
12
  initialValue: [],
12
13
  resourceName: 'Blog posts',
13
14
  });
package/src/locales/en.ts CHANGED
@@ -17,7 +17,7 @@ export const EN = {
17
17
  edit: 'Edit',
18
18
  remove: 'Remove',
19
19
  postsByCategory: 'Posts by category',
20
- name: 'Name',
20
+ name: 'Title',
21
21
  image: 'Image',
22
22
  language: 'Language',
23
23
  languages: 'Languages',
@@ -89,4 +89,10 @@ export const EN = {
89
89
  publishAt: 'Publish at',
90
90
  archiveAtLabel: 'Archive at',
91
91
  publishAtLabel: 'Publish at',
92
+
93
+ sortDateTimeAsc: 'Date - Newest to Oldest',
94
+ sortDateTimeDesc: 'Date - Oldest to Newest',
95
+ sortTitleAsc: 'Title - A-Z',
96
+ sortTitleDesc: 'Title - Z-A',
97
+ sortStatus: 'Status',
92
98
  };
package/src/locales/ru.ts CHANGED
@@ -19,7 +19,7 @@ export const RU: typeof EN = {
19
19
  edit: 'Редактировать',
20
20
  remove: 'Удалить',
21
21
  postsByCategory: 'Посты по категориям',
22
- name: 'Название',
22
+ name: 'Заголовок',
23
23
  image: 'Изображение',
24
24
  language: 'Язык',
25
25
  languages: 'Языки',
@@ -91,6 +91,12 @@ export const RU: typeof EN = {
91
91
  publishAt: 'Опубликовать в',
92
92
  archiveAtLabel: 'Перенос в архив',
93
93
  publishAtLabel: 'Публикация',
94
+
95
+ sortDateTimeAsc: 'Дате - По возрастанию',
96
+ sortDateTimeDesc: 'Дате - По убыванию',
97
+ sortTitleAsc: 'Заголовку - А-Я',
98
+ sortTitleDesc: 'Заголовку - Я-А',
99
+ sortStatus: 'Статусу',
94
100
  };
95
101
 
96
102
  export default RU;
@@ -15,6 +15,7 @@ export function getPosts(params?: {
15
15
  query?: string;
16
16
  pageNumber?: number;
17
17
  pageSize?: number;
18
+ sort?: string;
18
19
  }): Promise<ResponseBody<PostShort[]>> {
19
20
  return request.get({ path: '/admin/blog/posts', params });
20
21
  }
@@ -254,7 +254,7 @@ import {
254
254
  FormFieldFileInput,
255
255
  FormFieldMultiSelect,
256
256
  SeoFieldGroup,
257
- TabList,
257
+ TabList, formatDateTime, formatDate,
258
258
  } from '@tager/admin-ui';
259
259
  import { DynamicField } from '@tager/admin-dynamic-field';
260
260
  import { Page } from '@tager/admin-layout';
@@ -365,9 +365,14 @@ export default defineComponent({
365
365
  .filter((relatedPost) => relatedPost.id !== post.value?.id)
366
366
  .map((post) => ({
367
367
  value: post.id,
368
- label: post.title,
368
+ label:
369
+ (post.status === 'ARCHIVED'
370
+ ? `${t('blog:statusArchived')} - `
371
+ : post.status === 'DRAFT'
372
+ ? `${t('blog:statusDraft')} - `
373
+ : (post.datetime ? formatDate(new Date(post.datetime)) + ' - ' : '')) + post.title,
369
374
  }))
370
- );
375
+ )
371
376
 
372
377
  /** Form State **/
373
378
  const isSubmitting = ref<boolean>(false);
@@ -1,7 +1,7 @@
1
1
  import { TFunction } from 'i18next';
2
2
 
3
3
  import { notEmpty, Nullable } from '@tager/admin-services';
4
- import { ColumnDefinition } from '@tager/admin-ui';
4
+ import { ColumnDefinition, OptionType } from '@tager/admin-ui';
5
5
 
6
6
  import {
7
7
  Category,
@@ -122,3 +122,28 @@ export function getPostTableColumnDefs(
122
122
 
123
123
  return COLUMN_DEFS.filter(notEmpty);
124
124
  }
125
+
126
+ export function getPostTableSortOptions(t: TFunction): Array<OptionType> {
127
+ return [
128
+ {
129
+ value: 'datetime_desc',
130
+ label: t('blog:sortDateTimeDesc'),
131
+ },
132
+ {
133
+ value: 'datetime_asc',
134
+ label: t('blog:sortDateTimeAsc'),
135
+ },
136
+ {
137
+ value: 'title_asc',
138
+ label: t('blog:sortTitleAsc'),
139
+ },
140
+ {
141
+ value: 'title_desc',
142
+ label: t('blog:sortTitleDesc'),
143
+ },
144
+ {
145
+ value: 'status',
146
+ label: t('blog:sortStatus'),
147
+ },
148
+ ];
149
+ }
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <Page
3
- :title="pageTitle"
4
- :header-buttons="[
3
+ :title="pageTitle"
4
+ :header-buttons="[
5
5
  {
6
6
  text: $i18n.t('blog:newPost'),
7
7
  href: getBlogPostFormUrl({ postId: 'create' }),
@@ -10,66 +10,67 @@
10
10
  >
11
11
  <template #content>
12
12
  <DataTable
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
23
  }"
24
- data-table="blog-post"
25
- @change="handleChange"
24
+ :sort="{ options: sortOptions, value: sortValue }"
25
+ data-table="blog-post"
26
+ @change="handleChange"
26
27
  >
27
28
  <template #filters>
28
29
  <AdvancedSearch :tags="tags" @click:tag="tagRemovalHandler">
29
30
  <div class="filters">
30
31
  <FormFieldMultiSelect
31
- v-model:selected-options="categoryFilter"
32
- :options="categoryOptionList"
33
- name="categoryFilter"
34
- :searchable="true"
35
- :label="$i18n.t('blog:category')"
36
- class="filter"
32
+ v-model:selected-options="categoryFilter"
33
+ :options="categoryOptionList"
34
+ name="categoryFilter"
35
+ :searchable="true"
36
+ :label="$i18n.t('blog:category')"
37
+ class="filter"
37
38
  />
38
39
 
39
40
  <FormFieldMultiSelect
40
- v-if="isLangSpecific"
41
- v-model:selected-options="languageFilter"
42
- :options="languageOptionList"
43
- name="languageFilter"
44
- :searchable="true"
45
- :label="$i18n.t('blog:language')"
46
- class="filter"
41
+ v-if="isLangSpecific"
42
+ v-model:selected-options="languageFilter"
43
+ :options="languageOptionList"
44
+ name="languageFilter"
45
+ :searchable="true"
46
+ :label="$i18n.t('blog:language')"
47
+ class="filter"
47
48
  />
48
49
  <FormFieldMultiSelect
49
- v-model:selected-options="statusFilter"
50
- :options="statusOptionList"
51
- name="statusFilter"
52
- :searchable="true"
53
- :label="$i18n.t('blog:status')"
54
- class="filter"
50
+ v-model:selected-options="statusFilter"
51
+ :options="statusOptionList"
52
+ name="statusFilter"
53
+ :searchable="true"
54
+ :label="$i18n.t('blog:status')"
55
+ class="filter"
55
56
  />
56
57
  </div>
57
58
 
58
59
  <div class="filters">
59
60
  <FormField
60
- v-model:value="fromDateFilter"
61
- :label="$i18n.t('blog:dateOfPublicationFrom')"
62
- name="fromDateFilter"
63
- type="date"
64
- :max="toDateFilter"
61
+ v-model:value="fromDateFilter"
62
+ :label="$i18n.t('blog:dateOfPublicationFrom')"
63
+ name="fromDateFilter"
64
+ type="date"
65
+ :max="toDateFilter"
65
66
  />
66
67
 
67
68
  <FormField
68
- v-model:value="toDateFilter"
69
- :label="$i18n.t('blog:dateOfPublicationTo')"
70
- name="toDateFilter"
71
- type="date"
72
- :min="fromDateFilter"
69
+ v-model:value="toDateFilter"
70
+ :label="$i18n.t('blog:dateOfPublicationTo')"
71
+ name="toDateFilter"
72
+ type="date"
73
+ :min="fromDateFilter"
73
74
  />
74
75
  </div>
75
76
  </AdvancedSearch>
@@ -78,10 +79,12 @@
78
79
  <template #cell(status)="{ row }">
79
80
  <div class="status">
80
81
  <span>{{ getStatuses($i18n.t)[row.status] }}</span>
82
+
81
83
  <span v-if="row.status === 'PUBLISHED' && row.archiveAt">
82
84
  {{ $i18n.t('blog:archiveAtLabel') }}:
83
85
  <i>{{ formatDateTime(new Date(row.archiveAt), true) }}</i>
84
86
  </span>
87
+
85
88
  <span v-if="row.status === 'DRAFT' && row.publishAt">
86
89
  {{ $i18n.t('blog:publishAtLabel') }}:
87
90
  <i>{{ formatDateTime(new Date(row.publishAt), true) }}</i>
@@ -91,30 +94,30 @@
91
94
 
92
95
  <template #cell(actions)="{ row }">
93
96
  <BaseButton
94
- variant="icon"
95
- :title="$i18n.t('blog:edit')"
96
- :disabled="isBusy(row.id)"
97
- :href="getBlogPostFormUrl({ postId: row.id })"
97
+ variant="icon"
98
+ :title="$i18n.t('blog:edit')"
99
+ :disabled="isBusy(row.id)"
100
+ :href="getBlogPostFormUrl({ postId: row.id })"
98
101
  >
99
- <EditIcon />
102
+ <EditIcon/>
100
103
  </BaseButton>
101
104
 
102
105
  <BaseButton
103
- variant="icon"
104
- :title="$i18n.t('blog:clone')"
105
- :disabled="isBusy(row.id)"
106
- @click="handleResourceClone(row.id)"
106
+ variant="icon"
107
+ :title="$i18n.t('blog:clone')"
108
+ :disabled="isBusy(row.id)"
109
+ @click="handleResourceClone(row.id)"
107
110
  >
108
- <ContentCopyIcon />
111
+ <ContentCopyIcon/>
109
112
  </BaseButton>
110
113
 
111
114
  <BaseButton
112
- variant="icon"
113
- :title="$i18n.t('blog:remove')"
114
- :disabled="isBusy(row.id)"
115
- @click="handleResourceDelete(row.id)"
115
+ variant="icon"
116
+ :title="$i18n.t('blog:remove')"
117
+ :disabled="isBusy(row.id)"
118
+ @click="handleResourceDelete(row.id)"
116
119
  >
117
- <DeleteIcon />
120
+ <DeleteIcon/>
118
121
  </BaseButton>
119
122
  </template>
120
123
  </DataTable>
@@ -123,10 +126,10 @@
123
126
  </template>
124
127
 
125
128
  <script lang="ts">
126
- import { computed, defineComponent, watch } from 'vue';
129
+ import {computed, defineComponent, watch} from 'vue';
127
130
  import isEqual from 'lodash.isequal';
128
131
  import pick from 'lodash.pick';
129
- import { useRoute, useRouter } from 'vue-router';
132
+ import {useRoute, useRouter} from 'vue-router';
130
133
 
131
134
  import {
132
135
  ColumnDefinition,
@@ -148,29 +151,30 @@ import {
148
151
  useResourceClone,
149
152
  useResourceDelete,
150
153
  } from '@tager/admin-services';
151
- import { Page } from '@tager/admin-layout';
154
+ import {Page} from '@tager/admin-layout';
152
155
 
153
- import { useFetchModuleConfig, useFetchCategories } from '../../../hooks';
156
+ import {useFetchModuleConfig, useFetchCategories} from '../../../hooks';
154
157
  import {
155
158
  clonePost,
156
159
  deleteBlogPost,
157
160
  getPosts,
158
161
  } from '../../../services/requests';
159
- import { getBlogPostFormUrl } from '../../../utils/paths';
162
+ import {getBlogPostFormUrl} from '../../../utils/paths';
160
163
  import {
161
164
  Category,
162
165
  Language,
163
166
  PostFull,
164
167
  PostShort,
165
168
  } from '../../../typings/model';
166
- import { getStatusOptions } from '../PostForm/PostForm.helpers';
169
+ import {getStatusOptions} from '../PostForm/PostForm.helpers';
167
170
 
168
171
  import {
169
172
  convertPostList,
170
173
  getPostTableColumnDefs,
174
+ getPostTableSortOptions,
171
175
  getStatuses,
172
176
  } from './PostList.helpers';
173
- import { useAdvancedSearch } from './hooks';
177
+ import {useAdvancedSearch} from './hooks';
174
178
 
175
179
  export default defineComponent({
176
180
  name: 'PostList',
@@ -186,48 +190,48 @@ export default defineComponent({
186
190
  DataTable,
187
191
  },
188
192
  setup() {
189
- const { t } = useI18n();
193
+ const {t} = useI18n();
190
194
  const route = useRoute();
191
195
  const router = useRouter();
192
196
 
193
197
  const categoryId = computed<string>(() => {
194
198
  return Array.isArray(route.query.category)
195
- ? (route.query.category[0] as string) ?? ''
196
- : (route.query.category as string);
199
+ ? (route.query.category[0] as string) ?? ''
200
+ : (route.query.category as string);
197
201
  });
198
202
 
199
203
  const statusOptionList = computed<OptionType[]>(() => getStatusOptions(t));
200
204
 
201
205
  /** Fetch module config **/
202
206
 
203
- const { data: moduleConfig, loading: isModuleConfigLoading } =
204
- useFetchModuleConfig();
207
+ const {data: moduleConfig, loading: isModuleConfigLoading} =
208
+ useFetchModuleConfig();
205
209
 
206
210
  const languageList = computed<Language[]>(
207
- () => moduleConfig.value?.languages ?? []
211
+ () => moduleConfig.value?.languages ?? []
208
212
  );
209
213
 
210
214
  /** Fetch category list **/
211
215
 
212
- const { data: categoryList, loading: isCategoryListLoading } =
213
- useFetchCategories();
216
+ const {data: categoryList, loading: isCategoryListLoading} =
217
+ useFetchCategories();
214
218
 
215
219
  const selectedCategory = computed<Nullable<Category>>(() =>
216
- categoryId.value
217
- ? categoryList.value.find(
218
- ({ id }) => String(id) === String(categoryId.value)
219
- ) ?? null
220
- : null
220
+ categoryId.value
221
+ ? categoryList.value.find(
222
+ ({id}) => String(id) === String(categoryId.value)
223
+ ) ?? null
224
+ : null
221
225
  );
222
226
 
223
227
  const pageTitle = computed<string>(() => {
224
228
  return selectedCategory.value
225
- ? `${t('blog:postsByCategory')} "${selectedCategory.value.name}"`
226
- : t('blog:posts');
229
+ ? `${t('blog:postsByCategory')} "${selectedCategory.value.name}"`
230
+ : t('blog:posts');
227
231
  });
228
232
 
229
233
  const isLangSpecific = computed<boolean>(
230
- () => languageOptionList.value.length > 1
234
+ () => languageOptionList.value.length > 1
231
235
  );
232
236
 
233
237
  /** Advanced search **/
@@ -251,6 +255,8 @@ export default defineComponent({
251
255
  statusOptionList,
252
256
  });
253
257
 
258
+ const sortOptions = getPostTableSortOptions(t);
259
+
254
260
  /** Fetch Post list **/
255
261
 
256
262
  const {
@@ -263,17 +269,20 @@ export default defineComponent({
263
269
  pageNumber,
264
270
  pageCount,
265
271
  pageSize,
272
+ sort,
266
273
  } = useDataTable<PostShort>({
267
274
  fetchEntityList: (params) =>
268
- getPosts({
269
- query: params.searchQuery,
270
- pageNumber: params.pageNumber,
271
- pageSize: params.pageSize,
272
- ...filterParams.value,
273
- }),
275
+ getPosts({
276
+ query: params.searchQuery,
277
+ pageNumber: params.pageNumber,
278
+ pageSize: params.pageSize,
279
+ sort: params.sort || undefined,
280
+ ...filterParams.value,
281
+ }),
274
282
  initialValue: [],
275
283
  resourceName: 'Blog post list',
276
284
  pageSize: 100,
285
+ defaultSort: sortOptions[0].value,
277
286
  });
278
287
 
279
288
  watch(filterParams, () => {
@@ -282,38 +291,38 @@ export default defineComponent({
282
291
  }
283
292
 
284
293
  const newQuery = {
285
- ...pick(route.query, ['query', 'pageNumber']),
294
+ ...pick(route.query, ['query', 'pageNumber', 'sort']),
286
295
  ...filterParams.value,
287
296
  };
288
297
 
289
298
  if (!isEqual(route.query, newQuery)) {
290
- router.replace({ query: newQuery });
299
+ router.replace({query: newQuery});
291
300
  fetchPostList();
292
301
  }
293
302
  });
294
303
 
295
304
  const displayedPostList = computed<PostShort[]>(() =>
296
- convertPostList(
297
- postList.value,
298
- selectedCategory.value,
299
- moduleConfig.value?.languages ?? []
300
- )
305
+ convertPostList(
306
+ postList.value,
307
+ selectedCategory.value,
308
+ moduleConfig.value?.languages ?? []
309
+ )
301
310
  );
302
311
 
303
312
  const isRowDataLoading = computed<boolean>(
304
- () =>
305
- isCategoryListLoading.value ||
306
- isPostLoading.value ||
307
- isModuleConfigLoading.value
313
+ () =>
314
+ isCategoryListLoading.value ||
315
+ isPostLoading.value ||
316
+ isModuleConfigLoading.value
308
317
  );
309
318
 
310
- const { isDeleting, handleResourceDelete } = useResourceDelete({
319
+ const {isDeleting, handleResourceDelete} = useResourceDelete({
311
320
  deleteResource: deleteBlogPost,
312
321
  resourceName: 'Post',
313
322
  onSuccess: fetchPostList,
314
323
  });
315
324
 
316
- const { isCloning, handleResourceClone } = useResourceClone({
325
+ const {isCloning, handleResourceClone} = useResourceClone({
317
326
  cloneResource: clonePost,
318
327
  confirmMessage: t('blog:cloneConfirm'),
319
328
  successMessage: t('blog:cloneSuccess'),
@@ -326,13 +335,15 @@ export default defineComponent({
326
335
  }
327
336
 
328
337
  const columnDefs = computed<ColumnDefinition<PostShort>[]>(() =>
329
- getPostTableColumnDefs(moduleConfig.value, t)
338
+ getPostTableColumnDefs(moduleConfig.value, t)
330
339
  );
331
340
 
332
341
  return {
333
342
  t,
334
343
  formatDateTime,
335
344
  columnDefs,
345
+ sortOptions,
346
+ sortValue: sort,
336
347
  rowData: displayedPostList,
337
348
  handleResourceDelete,
338
349
  isDeleting,
@@ -383,7 +394,7 @@ export default defineComponent({
383
394
  }
384
395
 
385
396
  .status {
386
- span:first-child {
397
+ span {
387
398
  display: block;
388
399
  }
389
400