@tager/admin-blog 1.0.1 → 1.0.3

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, ShortCodeConstructor, FormFooter, formatDate, createTabErrorFinder, SouthIcon, NorthIcon, CountButton, TabList } 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",
@@ -1037,7 +1078,6 @@ const _sfc_main$2 = defineComponent({
1037
1078
  FormFieldUrlAliasInput,
1038
1079
  FormField,
1039
1080
  FormFieldSelect,
1040
- TabList,
1041
1081
  Page,
1042
1082
  DynamicField,
1043
1083
  ShortCodeConstructor,
@@ -1090,7 +1130,7 @@ const _sfc_main$2 = defineComponent({
1090
1130
  return relatedPost.id !== ((_a2 = post.value) == null ? void 0 : _a2.id);
1091
1131
  }).map((post2) => ({
1092
1132
  value: post2.id,
1093
- label: post2.title
1133
+ label: (post2.status === "ARCHIVED" ? `${t("blog:statusArchived")} - ` : post2.status === "DRAFT" ? `${t("blog:statusDraft")} - ` : post2.datetime ? formatDate(new Date(post2.datetime)) + " - " : "") + post2.title
1094
1134
  }))
1095
1135
  );
1096
1136
  const isSubmitting = ref(false);
@@ -1343,7 +1383,6 @@ const _hoisted_4 = {
1343
1383
  class: "status-date"
1344
1384
  };
1345
1385
  function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
1346
- const _component_TabList = resolveComponent("TabList");
1347
1386
  const _component_FormFieldSelect = resolveComponent("FormFieldSelect");
1348
1387
  const _component_FormField = resolveComponent("FormField");
1349
1388
  const _component_FormFieldUrlAliasInput = resolveComponent("FormFieldUrlAliasInput");
@@ -1364,23 +1403,21 @@ function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
1364
1403
  backHref: _ctx.postListUrl,
1365
1404
  onSubmit: _ctx.submitForm,
1366
1405
  isSubmitting: _ctx.isSubmitting
1367
- }
1406
+ },
1407
+ "tab-id": _ctx.selectedTabId,
1408
+ "onUpdate:tab-id": _cache[16] || (_cache[16] = ($event) => _ctx.selectedTabId = $event),
1409
+ tabs: _ctx.tabList
1368
1410
  }, {
1369
1411
  content: withCtx(() => [
1370
1412
  createElementVNode("form", {
1371
1413
  novalidate: "",
1372
- onSubmit: _cache[16] || (_cache[16] = withModifiers((...args) => _ctx.submitForm && _ctx.submitForm(...args), ["prevent"]))
1414
+ onSubmit: _cache[15] || (_cache[15] = withModifiers((...args) => _ctx.submitForm && _ctx.submitForm(...args), ["prevent"]))
1373
1415
  }, [
1374
- createVNode(_component_TabList, {
1375
- "tab-id": _ctx.selectedTabId,
1376
- "onUpdate:tab-id": _cache[0] || (_cache[0] = ($event) => _ctx.selectedTabId = $event),
1377
- "tab-list": _ctx.tabList
1378
- }, null, 8, ["tab-id", "tab-list"]),
1379
1416
  _ctx.selectedTabId === "common" ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [
1380
1417
  _ctx.isCreation && _ctx.isLangSpecific ? (openBlock(), createBlock(_component_FormFieldSelect, {
1381
1418
  key: 0,
1382
1419
  value: _ctx.values.language,
1383
- "onUpdate:value": _cache[1] || (_cache[1] = ($event) => _ctx.values.language = $event),
1420
+ "onUpdate:value": _cache[0] || (_cache[0] = ($event) => _ctx.values.language = $event),
1384
1421
  name: "language",
1385
1422
  label: _ctx.$i18n.t("blog:language"),
1386
1423
  options: _ctx.languageOptionList,
@@ -1388,7 +1425,7 @@ function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
1388
1425
  }, null, 8, ["value", "label", "options", "error"])) : createCommentVNode("", true),
1389
1426
  createVNode(_component_FormField, {
1390
1427
  value: _ctx.values.title,
1391
- "onUpdate:value": _cache[2] || (_cache[2] = ($event) => _ctx.values.title = $event),
1428
+ "onUpdate:value": _cache[1] || (_cache[1] = ($event) => _ctx.values.title = $event),
1392
1429
  name: "title",
1393
1430
  label: _ctx.$i18n.t("blog:title"),
1394
1431
  error: _ctx.errors.title,
@@ -1397,7 +1434,7 @@ function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
1397
1434
  createVNode(_component_FormFieldUrlAliasInput, {
1398
1435
  id: "urlAlias",
1399
1436
  value: _ctx.values.urlAlias,
1400
- "onUpdate:value": _cache[3] || (_cache[3] = ($event) => _ctx.values.urlAlias = $event),
1437
+ "onUpdate:value": _cache[2] || (_cache[2] = ($event) => _ctx.values.urlAlias = $event),
1401
1438
  name: "urlAlias",
1402
1439
  label: _ctx.$i18n.t("blog:link"),
1403
1440
  "url-template": _ctx.urlAliasTemplate,
@@ -1408,7 +1445,7 @@ function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
1408
1445
  createElementVNode("div", _hoisted_2, [
1409
1446
  createVNode(_component_FormFieldSelect, {
1410
1447
  value: _ctx.values.status,
1411
- "onUpdate:value": _cache[4] || (_cache[4] = ($event) => _ctx.values.status = $event),
1448
+ "onUpdate:value": _cache[3] || (_cache[3] = ($event) => _ctx.values.status = $event),
1412
1449
  name: "status",
1413
1450
  error: _ctx.errors.status,
1414
1451
  label: _ctx.$i18n.t("blog:status"),
@@ -1419,7 +1456,7 @@ function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
1419
1456
  _ctx.values.status.value === "PUBLISHED" || _ctx.values.status.value === "DRAFT" ? (openBlock(), createElementBlock("div", _hoisted_3, [
1420
1457
  createVNode(_component_FormFieldCheckbox, {
1421
1458
  checked: _ctx.values.statusDateEnabled,
1422
- "onUpdate:checked": _cache[5] || (_cache[5] = ($event) => _ctx.values.statusDateEnabled = $event),
1459
+ "onUpdate:checked": _cache[4] || (_cache[4] = ($event) => _ctx.values.statusDateEnabled = $event),
1423
1460
  name: "statusDateEnabled",
1424
1461
  label: _ctx.$i18n.t(
1425
1462
  _ctx.values.status.value === "PUBLISHED" ? "blog:scheduleArchive" : "blog:schedulePublish"
@@ -1430,7 +1467,7 @@ function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
1430
1467
  _ctx.values.statusDateEnabled && (_ctx.values.status.value === "PUBLISHED" || _ctx.values.status.value === "DRAFT") ? (openBlock(), createElementBlock("div", _hoisted_4, [
1431
1468
  createVNode(_component_FormField, {
1432
1469
  value: _ctx.values.statusDate,
1433
- "onUpdate:value": _cache[6] || (_cache[6] = ($event) => _ctx.values.statusDate = $event),
1470
+ "onUpdate:value": _cache[5] || (_cache[5] = ($event) => _ctx.values.statusDate = $event),
1434
1471
  name: "statusDate",
1435
1472
  type: "date",
1436
1473
  label: _ctx.$i18n.t(
@@ -1442,7 +1479,7 @@ function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
1442
1479
  ]),
1443
1480
  createVNode(_component_FormField, {
1444
1481
  value: _ctx.values.datetime,
1445
- "onUpdate:value": _cache[7] || (_cache[7] = ($event) => _ctx.values.datetime = $event),
1482
+ "onUpdate:value": _cache[6] || (_cache[6] = ($event) => _ctx.values.datetime = $event),
1446
1483
  name: "datetime",
1447
1484
  label: _ctx.$i18n.t("blog:date"),
1448
1485
  type: "date",
@@ -1450,7 +1487,7 @@ function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
1450
1487
  }, null, 8, ["value", "label", "error"]),
1451
1488
  createVNode(_component_FormField, {
1452
1489
  value: _ctx.values.excerpt,
1453
- "onUpdate:value": _cache[8] || (_cache[8] = ($event) => _ctx.values.excerpt = $event),
1490
+ "onUpdate:value": _cache[7] || (_cache[7] = ($event) => _ctx.values.excerpt = $event),
1454
1491
  name: "excerpt",
1455
1492
  label: _ctx.$i18n.t("blog:excerpt"),
1456
1493
  type: "textarea",
@@ -1458,7 +1495,7 @@ function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
1458
1495
  }, null, 8, ["value", "label", "error"]),
1459
1496
  createVNode(_component_FormFieldRichTextInput, {
1460
1497
  value: _ctx.values.body,
1461
- "onUpdate:value": _cache[9] || (_cache[9] = ($event) => _ctx.values.body = $event),
1498
+ "onUpdate:value": _cache[8] || (_cache[8] = ($event) => _ctx.values.body = $event),
1462
1499
  name: "body",
1463
1500
  label: _ctx.$i18n.t("blog:body"),
1464
1501
  error: _ctx.errors.body,
@@ -1471,7 +1508,7 @@ function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
1471
1508
  _ctx.selectedTabId === "images" ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [
1472
1509
  createVNode(_component_FormFieldFileInput, {
1473
1510
  value: _ctx.values.coverImage,
1474
- "onUpdate:value": _cache[10] || (_cache[10] = ($event) => _ctx.values.coverImage = $event),
1511
+ "onUpdate:value": _cache[9] || (_cache[9] = ($event) => _ctx.values.coverImage = $event),
1475
1512
  label: _ctx.$i18n.t("blog:coverImage"),
1476
1513
  name: "coverImage",
1477
1514
  "file-type": "image",
@@ -1479,7 +1516,7 @@ function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
1479
1516
  }, null, 8, ["value", "label", "scenario"]),
1480
1517
  createVNode(_component_FormFieldFileInput, {
1481
1518
  value: _ctx.values.image,
1482
- "onUpdate:value": _cache[11] || (_cache[11] = ($event) => _ctx.values.image = $event),
1519
+ "onUpdate:value": _cache[10] || (_cache[10] = ($event) => _ctx.values.image = $event),
1483
1520
  name: "image",
1484
1521
  label: _ctx.$i18n.t("blog:innerImage"),
1485
1522
  "file-type": "image",
@@ -1487,7 +1524,7 @@ function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
1487
1524
  }, null, 8, ["value", "label", "scenario"]),
1488
1525
  createVNode(_component_FormFieldFileInput, {
1489
1526
  value: _ctx.values.imageMobile,
1490
- "onUpdate:value": _cache[12] || (_cache[12] = ($event) => _ctx.values.imageMobile = $event),
1527
+ "onUpdate:value": _cache[11] || (_cache[11] = ($event) => _ctx.values.imageMobile = $event),
1491
1528
  name: "imageMobile",
1492
1529
  label: _ctx.$i18n.t("blog:innerImageMobile"),
1493
1530
  "file-type": "image",
@@ -1497,14 +1534,14 @@ function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
1497
1534
  _ctx.selectedTabId === "relations" ? (openBlock(), createElementBlock(Fragment, { key: 2 }, [
1498
1535
  createVNode(_component_FormField, {
1499
1536
  value: _ctx.values.tags,
1500
- "onUpdate:value": _cache[13] || (_cache[13] = ($event) => _ctx.values.tags = $event),
1537
+ "onUpdate:value": _cache[12] || (_cache[12] = ($event) => _ctx.values.tags = $event),
1501
1538
  name: "tags",
1502
1539
  label: _ctx.$i18n.t("blog:tags"),
1503
1540
  error: _ctx.errors.tags
1504
1541
  }, null, 8, ["value", "label", "error"]),
1505
1542
  createVNode(_component_FormFieldMultiSelect, {
1506
1543
  "selected-options": _ctx.values.categories,
1507
- "onUpdate:selected-options": _cache[14] || (_cache[14] = ($event) => _ctx.values.categories = $event),
1544
+ "onUpdate:selected-options": _cache[13] || (_cache[13] = ($event) => _ctx.values.categories = $event),
1508
1545
  name: "categories",
1509
1546
  label: _ctx.$i18n.t("blog:categories"),
1510
1547
  options: _ctx.categoryOptionList,
@@ -1513,7 +1550,7 @@ function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
1513
1550
  }, null, 8, ["selected-options", "label", "options", "error", "onChange"]),
1514
1551
  createVNode(_component_FormFieldMultiSelect, {
1515
1552
  "selected-options": _ctx.values.relatedPosts,
1516
- "onUpdate:selected-options": _cache[15] || (_cache[15] = ($event) => _ctx.values.relatedPosts = $event),
1553
+ "onUpdate:selected-options": _cache[14] || (_cache[14] = ($event) => _ctx.values.relatedPosts = $event),
1517
1554
  name: "relatedPosts",
1518
1555
  label: _ctx.$i18n.t("blog:relatedPosts"),
1519
1556
  options: _ctx.postOptionList,
@@ -1553,9 +1590,9 @@ function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
1553
1590
  }, null, 8, ["back-href", "is-submitting", "is-creation", "can-create-another", "onSubmit"])
1554
1591
  ]),
1555
1592
  _: 1
1556
- }, 8, ["title", "is-content-loading", "header-buttons", "footer"]);
1593
+ }, 8, ["title", "is-content-loading", "header-buttons", "footer", "tab-id", "tabs"]);
1557
1594
  }
1558
- var BlogPostForm = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["render", _sfc_render$2], ["__scopeId", "data-v-62596ad5"]]);
1595
+ var BlogPostForm = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["render", _sfc_render$2], ["__scopeId", "data-v-15dc5eec"]]);
1559
1596
  function convertCategoryList(categoryList, languages) {
1560
1597
  return categoryList.map((category) => {
1561
1598
  if (languages.length > 0) {
@@ -1906,7 +1943,8 @@ function _sfc_render$1(_ctx, _cache, $props, $setup, $data, $options) {
1906
1943
  ]),
1907
1944
  _: 1
1908
1945
  }, 8, ["tags", "onClick:tag"])
1909
- ])
1946
+ ]),
1947
+ key: "0"
1910
1948
  } : void 0
1911
1949
  ]), 1032, ["column-defs", "row-data", "loading", "error-message", "search-query", "pagination", "onChange"])
1912
1950
  ]),
@@ -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-15dc5eec]{display:flex;align-items:flex-end}.status-value[data-v-15dc5eec]{flex-basis:50%}.status-checkbox[data-v-15dc5eec]{margin-left:20px;padding-bottom:8px}.status-date[data-v-15dc5eec]{margin-left:20px;flex:1 1 1px}
@@ -26,12 +26,12 @@ declare const _default: import("vue").DefineComponent<{}, {
26
26
  id: string;
27
27
  file: {
28
28
  id: string;
29
- url: string;
29
+ mime: string;
30
30
  name: string;
31
31
  size: number;
32
- mime: string;
32
+ url: string;
33
33
  };
34
- caption?: Nullable<string> | undefined;
34
+ caption?: import("@tager/admin-services").Nullish<string>;
35
35
  } | null;
36
36
  }>;
37
37
  errors: import("vue").Ref<Record<string, string>>;
@@ -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;
@@ -14,34 +14,34 @@ declare const _default: import("vue").DefineComponent<{}, {
14
14
  id: string;
15
15
  file: {
16
16
  id: string;
17
- url: string;
17
+ mime: string;
18
18
  name: string;
19
19
  size: number;
20
- mime: string;
20
+ url: string;
21
21
  };
22
- caption?: Nullable<string> | undefined;
22
+ caption?: import("@tager/admin-services").Nullish<string>;
23
23
  } | null;
24
24
  image: {
25
25
  id: string;
26
26
  file: {
27
27
  id: string;
28
- url: string;
28
+ mime: string;
29
29
  name: string;
30
30
  size: number;
31
- mime: string;
31
+ url: string;
32
32
  };
33
- caption?: Nullable<string> | undefined;
33
+ caption?: import("@tager/admin-services").Nullish<string>;
34
34
  } | null;
35
35
  imageMobile: {
36
36
  id: string;
37
37
  file: {
38
38
  id: string;
39
- url: string;
39
+ mime: string;
40
40
  name: string;
41
41
  size: number;
42
- mime: string;
42
+ url: string;
43
43
  };
44
- caption?: Nullable<string> | undefined;
44
+ caption?: import("@tager/admin-services").Nullish<string>;
45
45
  } | null;
46
46
  pageTitle: string;
47
47
  pageDescription: string;
@@ -49,12 +49,12 @@ declare const _default: import("vue").DefineComponent<{}, {
49
49
  id: string;
50
50
  file: {
51
51
  id: string;
52
- url: string;
52
+ mime: string;
53
53
  name: string;
54
54
  size: number;
55
- mime: string;
55
+ url: string;
56
56
  };
57
- caption?: Nullable<string> | undefined;
57
+ caption?: import("@tager/admin-services").Nullish<string>;
58
58
  } | null;
59
59
  urlAlias: string;
60
60
  categories: {
@@ -221,12 +221,12 @@ declare const _default: import("vue").DefineComponent<{}, {
221
221
  id: string;
222
222
  file: {
223
223
  id: string;
224
- url: string;
224
+ mime: string;
225
225
  name: string;
226
226
  size: number;
227
- mime: string;
227
+ url: string;
228
228
  };
229
- caption?: Nullable<string> | undefined;
229
+ caption?: import("@tager/admin-services").Nullish<string>;
230
230
  } | null;
231
231
  } | {
232
232
  readonly id: string;
@@ -251,12 +251,12 @@ declare const _default: import("vue").DefineComponent<{}, {
251
251
  id: string;
252
252
  file: {
253
253
  id: string;
254
- url: string;
254
+ mime: string;
255
255
  name: string;
256
256
  size: number;
257
- mime: string;
257
+ url: string;
258
258
  };
259
- caption?: Nullable<string> | undefined;
259
+ caption?: import("@tager/admin-services").Nullish<string>;
260
260
  }[];
261
261
  } | {
262
262
  readonly id: string;
@@ -280,12 +280,12 @@ declare const _default: import("vue").DefineComponent<{}, {
280
280
  id: string;
281
281
  file: {
282
282
  id: string;
283
- url: string;
283
+ mime: string;
284
284
  name: string;
285
285
  size: number;
286
- mime: string;
286
+ url: string;
287
287
  };
288
- caption?: Nullable<string> | undefined;
288
+ caption?: import("@tager/admin-services").Nullish<string>;
289
289
  } | null;
290
290
  } | {
291
291
  readonly id: string;
@@ -983,6 +983,8 @@ declare const _default: import("vue").DefineComponent<{}, {
983
983
  defaultIsOpen?: boolean | undefined;
984
984
  hidden?: boolean | undefined;
985
985
  maximumItemsCount?: number | undefined;
986
+ hideCount?: boolean | undefined;
987
+ addLabel?: string | undefined;
986
988
  };
987
989
  readonly name: string;
988
990
  readonly label: string;
@@ -1137,12 +1139,12 @@ declare const _default: import("vue").DefineComponent<{}, {
1137
1139
  id: string;
1138
1140
  file: {
1139
1141
  id: string;
1140
- url: string;
1142
+ mime: string;
1141
1143
  name: string;
1142
1144
  size: number;
1143
- mime: string;
1145
+ url: string;
1144
1146
  };
1145
- caption?: Nullable<string> | undefined;
1147
+ caption?: import("@tager/admin-services").Nullish<string>;
1146
1148
  } | null;
1147
1149
  } | {
1148
1150
  readonly id: string;
@@ -1167,12 +1169,12 @@ declare const _default: import("vue").DefineComponent<{}, {
1167
1169
  id: string;
1168
1170
  file: {
1169
1171
  id: string;
1170
- url: string;
1172
+ mime: string;
1171
1173
  name: string;
1172
1174
  size: number;
1173
- mime: string;
1175
+ url: string;
1174
1176
  };
1175
- caption?: Nullable<string> | undefined;
1177
+ caption?: import("@tager/admin-services").Nullish<string>;
1176
1178
  }[];
1177
1179
  } | {
1178
1180
  readonly id: string;
@@ -1196,12 +1198,12 @@ declare const _default: import("vue").DefineComponent<{}, {
1196
1198
  id: string;
1197
1199
  file: {
1198
1200
  id: string;
1199
- url: string;
1201
+ mime: string;
1200
1202
  name: string;
1201
1203
  size: number;
1202
- mime: string;
1204
+ url: string;
1203
1205
  };
1204
- caption?: Nullable<string> | undefined;
1206
+ caption?: import("@tager/admin-services").Nullish<string>;
1205
1207
  } | null;
1206
1208
  } | {
1207
1209
  readonly id: string;
@@ -1782,12 +1784,12 @@ declare const _default: import("vue").DefineComponent<{}, {
1782
1784
  id: string;
1783
1785
  file: {
1784
1786
  id: string;
1785
- url: string;
1787
+ mime: string;
1786
1788
  name: string;
1787
1789
  size: number;
1788
- mime: string;
1790
+ url: string;
1789
1791
  };
1790
- caption?: Nullable<string> | undefined;
1792
+ caption?: import("@tager/admin-services").Nullish<string>;
1791
1793
  } | null;
1792
1794
  } | {
1793
1795
  readonly id: string;
@@ -1812,12 +1814,12 @@ declare const _default: import("vue").DefineComponent<{}, {
1812
1814
  id: string;
1813
1815
  file: {
1814
1816
  id: string;
1815
- url: string;
1817
+ mime: string;
1816
1818
  name: string;
1817
1819
  size: number;
1818
- mime: string;
1820
+ url: string;
1819
1821
  };
1820
- caption?: Nullable<string> | undefined;
1822
+ caption?: import("@tager/admin-services").Nullish<string>;
1821
1823
  }[];
1822
1824
  } | {
1823
1825
  readonly id: string;
@@ -1841,12 +1843,12 @@ declare const _default: import("vue").DefineComponent<{}, {
1841
1843
  id: string;
1842
1844
  file: {
1843
1845
  id: string;
1844
- url: string;
1846
+ mime: string;
1845
1847
  name: string;
1846
1848
  size: number;
1847
- mime: string;
1849
+ url: string;
1848
1850
  };
1849
- caption?: Nullable<string> | undefined;
1851
+ caption?: import("@tager/admin-services").Nullish<string>;
1850
1852
  } | null;
1851
1853
  } | {
1852
1854
  readonly id: string;
@@ -2466,12 +2468,12 @@ declare const _default: import("vue").DefineComponent<{}, {
2466
2468
  id: string;
2467
2469
  file: {
2468
2470
  id: string;
2469
- url: string;
2471
+ mime: string;
2470
2472
  name: string;
2471
2473
  size: number;
2472
- mime: string;
2474
+ url: string;
2473
2475
  };
2474
- caption?: Nullable<string> | undefined;
2476
+ caption?: import("@tager/admin-services").Nullish<string>;
2475
2477
  } | null;
2476
2478
  } | {
2477
2479
  readonly id: string;
@@ -2496,12 +2498,12 @@ declare const _default: import("vue").DefineComponent<{}, {
2496
2498
  id: string;
2497
2499
  file: {
2498
2500
  id: string;
2499
- url: string;
2501
+ mime: string;
2500
2502
  name: string;
2501
2503
  size: number;
2502
- mime: string;
2504
+ url: string;
2503
2505
  };
2504
- caption?: Nullable<string> | undefined;
2506
+ caption?: import("@tager/admin-services").Nullish<string>;
2505
2507
  }[];
2506
2508
  } | {
2507
2509
  readonly id: string;
@@ -2525,12 +2527,12 @@ declare const _default: import("vue").DefineComponent<{}, {
2525
2527
  id: string;
2526
2528
  file: {
2527
2529
  id: string;
2528
- url: string;
2530
+ mime: string;
2529
2531
  name: string;
2530
2532
  size: number;
2531
- mime: string;
2533
+ url: string;
2532
2534
  };
2533
- caption?: Nullable<string> | undefined;
2535
+ caption?: import("@tager/admin-services").Nullish<string>;
2534
2536
  } | null;
2535
2537
  } | {
2536
2538
  readonly id: string;
@@ -2764,5 +2766,5 @@ declare const _default: import("vue").DefineComponent<{}, {
2764
2766
  urlAliasTemplate: import("vue").ComputedRef<string>;
2765
2767
  handleCategoriesChange: (selectedOptions: OptionType<number>[]) => void;
2766
2768
  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<{}>>, {}>;
2769
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{}>>, {}>;
2768
2770
  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.3",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.umd.js",
6
6
  "module": "dist/index.esm.js",
@@ -33,10 +33,10 @@
33
33
  "@babel/core": "^7.17.9",
34
34
  "@babel/eslint-parser": "^7.17.0",
35
35
  "@rushstack/eslint-patch": "^1.1.2",
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",
36
+ "@tager/admin-dynamic-field": "^1.0.2",
37
+ "@tager/admin-layout": "^1.0.11",
38
+ "@tager/admin-services": "^1.1.11",
39
+ "@tager/admin-ui": "^1.0.54",
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
  }
@@ -10,10 +10,10 @@
10
10
  onSubmit: submitForm,
11
11
  isSubmitting: isSubmitting,
12
12
  }"
13
+ v-model:tab-id="selectedTabId" :tabs="tabList"
13
14
  >
14
15
  <template #content>
15
16
  <form novalidate @submit.prevent="submitForm">
16
- <TabList v-model:tab-id="selectedTabId" :tab-list="tabList" />
17
17
 
18
18
  <template v-if="selectedTabId === 'common'">
19
19
  <FormFieldSelect
@@ -254,7 +254,7 @@ import {
254
254
  FormFieldFileInput,
255
255
  FormFieldMultiSelect,
256
256
  SeoFieldGroup,
257
- TabList,
257
+ 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';
@@ -292,7 +292,6 @@ export default defineComponent({
292
292
  FormFieldUrlAliasInput,
293
293
  FormField,
294
294
  FormFieldSelect,
295
- TabList,
296
295
  Page,
297
296
  DynamicField,
298
297
  ShortCodeConstructor,
@@ -365,9 +364,14 @@ export default defineComponent({
365
364
  .filter((relatedPost) => relatedPost.id !== post.value?.id)
366
365
  .map((post) => ({
367
366
  value: post.id,
368
- label: post.title,
367
+ label:
368
+ (post.status === 'ARCHIVED'
369
+ ? `${t('blog:statusArchived')} - `
370
+ : post.status === 'DRAFT'
371
+ ? `${t('blog:statusDraft')} - `
372
+ : (post.datetime ? formatDate(new Date(post.datetime)) + ' - ' : '')) + post.title,
369
373
  }))
370
- );
374
+ )
371
375
 
372
376
  /** Form State **/
373
377
  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