@tager/admin-blog 0.3.36 → 0.3.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -1,4 +1,4 @@
1
- import { request, useResource, notEmpty, isNotNullish, useResourceDelete, useResourceClone, createId, convertRequestErrorToMap, urlTranslit, getWebsiteOrigin, i18n } from '@tager/admin-services';
1
+ import { request, useResource, createId, notEmpty, isNotNullish, useResourceDelete, useResourceClone, convertRequestErrorToMap, urlTranslit, getWebsiteOrigin, i18n } from '@tager/admin-services';
2
2
  import { onMounted, watch, computed, ref, defineComponent } from '@vue/composition-api';
3
3
  import { getFilterParamAsStringArray, getFilterParamAsString, getFilterParams, useTranslation, useDataTable, ShortCodeConstructor, FormFooter, createTabErrorFinder } from '@tager/admin-ui';
4
4
  import { compile } from 'path-to-regexp';
@@ -78,6 +78,10 @@ const EN = {
78
78
  },
79
79
  parentCategory: 'Parent category',
80
80
  noParent: 'No parent',
81
+ status: 'Status',
82
+ statusPublished: 'Published',
83
+ statusDraft: 'Draft',
84
+ statusArchived: 'Archived',
81
85
  };
82
86
 
83
87
  const RU = {
@@ -154,6 +158,10 @@ const RU = {
154
158
  },
155
159
  parentCategory: 'Родительская категория',
156
160
  noParent: 'Нет родительской',
161
+ status: 'Статус',
162
+ statusPublished: 'Опубликован',
163
+ statusDraft: 'Черновик',
164
+ statusArchived: 'В архиве',
157
165
  };
158
166
 
159
167
  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
@@ -2663,6 +2671,117 @@ function getBlogCategoryFormUrl(params) {
2663
2671
  });
2664
2672
  }
2665
2673
 
2674
+ function generateNumberArray(length) {
2675
+ return Array.from({ length }, (_, index) => index);
2676
+ }
2677
+ function getNameWithDepth(name, depth) {
2678
+ return (generateNumberArray(depth)
2679
+ .map(() => '—')
2680
+ .join(' ') +
2681
+ ' ' +
2682
+ name);
2683
+ }
2684
+
2685
+ const getStatusOptions = (t) => [
2686
+ { label: t('blog:statusPublished'), value: 'PUBLISHED' },
2687
+ { label: t('blog:statusDraft'), value: 'DRAFT' },
2688
+ { label: t('blog:statusArchived'), value: 'ARCHIVED' },
2689
+ ];
2690
+ function convertPostToFormValues(post, languageOptionList, postOptionList, additionalFieldList, statusOptions) {
2691
+ var _a, _b, _c;
2692
+ if (!post) {
2693
+ return {
2694
+ title: '',
2695
+ excerpt: '',
2696
+ body: '',
2697
+ datetime: '',
2698
+ coverImage: null,
2699
+ image: null,
2700
+ imageMobile: null,
2701
+ pageTitle: '',
2702
+ pageDescription: '',
2703
+ openGraphImage: null,
2704
+ urlAlias: '',
2705
+ categories: [],
2706
+ language: null,
2707
+ relatedPosts: [],
2708
+ tags: '',
2709
+ additionalFields: additionalFieldList.map((fieldConfig) => universalFieldUtils.createFormField(fieldConfig, null)),
2710
+ status: statusOptions[0],
2711
+ };
2712
+ }
2713
+ const currentLangOption = languageOptionList.find((option) => option.value === post.language);
2714
+ const selectedPostOptionList = postOptionList.filter((option) => post.relatedPosts.some((relatedPost) => relatedPost.id === option.value));
2715
+ const foundStatus = (_a = statusOptions.find(({ value }) => value === post.status)) !== null && _a !== void 0 ? _a : statusOptions[0];
2716
+ return {
2717
+ title: post.title,
2718
+ excerpt: post.excerpt,
2719
+ body: post.body,
2720
+ datetime: post.datetime ? post.datetime.substr(0, 10) : '',
2721
+ coverImage: post.coverImage
2722
+ ? { id: createId(), file: post.coverImage }
2723
+ : null,
2724
+ image: post.image ? { id: createId(), file: post.image } : null,
2725
+ imageMobile: post.imageMobile
2726
+ ? { id: createId(), file: post.imageMobile }
2727
+ : null,
2728
+ pageTitle: (_b = post.pageTitle) !== null && _b !== void 0 ? _b : '',
2729
+ pageDescription: (_c = post.pageDescription) !== null && _c !== void 0 ? _c : '',
2730
+ openGraphImage: post.openGraphImage
2731
+ ? { id: createId(), file: post.openGraphImage }
2732
+ : null,
2733
+ urlAlias: post.urlAlias,
2734
+ categories: post.categories.map((category) => ({
2735
+ value: category.id,
2736
+ label: category.name,
2737
+ })),
2738
+ language: currentLangOption !== null && currentLangOption !== void 0 ? currentLangOption : null,
2739
+ relatedPosts: selectedPostOptionList,
2740
+ tags: post.tags.join(','),
2741
+ additionalFields: additionalFieldList.map((fieldConfig) => {
2742
+ const postAdditionalField = post.additionalFields.find(({ name }) => name === fieldConfig.name);
2743
+ return universalFieldUtils.createFormField(fieldConfig, postAdditionalField === null || postAdditionalField === void 0 ? void 0 : postAdditionalField.value);
2744
+ }),
2745
+ status: foundStatus,
2746
+ };
2747
+ }
2748
+ function convertFormValuesToCreationPayload(values) {
2749
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
2750
+ return {
2751
+ title: values.title,
2752
+ excerpt: values.excerpt,
2753
+ urlAlias: values.urlAlias,
2754
+ body: values.body,
2755
+ datetime: values.datetime,
2756
+ coverImage: (_b = (_a = values.coverImage) === null || _a === void 0 ? void 0 : _a.file.id) !== null && _b !== void 0 ? _b : null,
2757
+ image: (_d = (_c = values.image) === null || _c === void 0 ? void 0 : _c.file.id) !== null && _d !== void 0 ? _d : null,
2758
+ imageMobile: (_f = (_e = values.imageMobile) === null || _e === void 0 ? void 0 : _e.file.id) !== null && _f !== void 0 ? _f : null,
2759
+ pageTitle: values.pageTitle,
2760
+ pageDescription: values.pageDescription,
2761
+ openGraphImage: (_h = (_g = values.openGraphImage) === null || _g === void 0 ? void 0 : _g.file.id) !== null && _h !== void 0 ? _h : null,
2762
+ categories: values.categories.map((option) => option.value),
2763
+ language: (_k = (_j = values.language) === null || _j === void 0 ? void 0 : _j.value) !== null && _k !== void 0 ? _k : null,
2764
+ relatedPosts: values.relatedPosts.map((relatedPostOption) => relatedPostOption.value),
2765
+ tags: values.tags.split(',').filter((tag) => tag.trim().length > 0),
2766
+ additionalFields: values.additionalFields.map((field) => ({
2767
+ name: field.config.name,
2768
+ value: universalFieldUtils.getOutgoingValue(field),
2769
+ })),
2770
+ status: values.status.value,
2771
+ };
2772
+ }
2773
+ function convertFormValuesToUpdatePayload(values) {
2774
+ return Object.assign(Object.assign({}, convertFormValuesToCreationPayload(values)), { urlAlias: values.urlAlias });
2775
+ }
2776
+ function convertCategoryListToOptions(categoryList, languageId) {
2777
+ return categoryList
2778
+ .filter(({ language }) => (languageId ? language === languageId : true))
2779
+ .map(({ id, name, depth }) => ({
2780
+ value: id,
2781
+ label: getNameWithDepth(name, depth),
2782
+ }));
2783
+ }
2784
+
2666
2785
  function convertPostList(postList, selectedCategory, languages) {
2667
2786
  return postList
2668
2787
  .filter((post) => selectedCategory
@@ -2676,6 +2795,13 @@ function convertPostList(postList, selectedCategory, languages) {
2676
2795
  return post;
2677
2796
  });
2678
2797
  }
2798
+ function getStatuses(t) {
2799
+ return {
2800
+ PUBLISHED: t('blog:statusPublished'),
2801
+ DRAFT: t('blog:statusDraft'),
2802
+ ARCHIVED: t('blog:statusArchived'),
2803
+ };
2804
+ }
2679
2805
  function getPostTableColumnDefs(moduleConfig, t) {
2680
2806
  const isLangSpecific = moduleConfig
2681
2807
  ? moduleConfig.languages.length > 0
@@ -2688,6 +2814,14 @@ function getPostTableColumnDefs(moduleConfig, t) {
2688
2814
  style: { width: '50px', textAlign: 'center' },
2689
2815
  headStyle: { width: '50px', textAlign: 'center' },
2690
2816
  },
2817
+ {
2818
+ id: 2,
2819
+ name: t('blog:status'),
2820
+ field: 'status',
2821
+ style: { width: '120px' },
2822
+ headStyle: { width: '120px' },
2823
+ format: ({ row }) => getStatuses(t)[row.status],
2824
+ },
2691
2825
  {
2692
2826
  id: 2,
2693
2827
  name: t('blog:name'),
@@ -2718,7 +2852,13 @@ function getPostTableColumnDefs(moduleConfig, t) {
2718
2852
  isLangSpecific
2719
2853
  ? { id: 4, name: t('blog:language'), field: 'language' }
2720
2854
  : null,
2721
- { id: 5, name: t('blog:date'), field: 'date', type: 'date' },
2855
+ {
2856
+ id: 5,
2857
+ name: t('blog:date'),
2858
+ field: 'datetime',
2859
+ type: 'datetime',
2860
+ options: { ignoreStartOfDayTime: true },
2861
+ },
2722
2862
  {
2723
2863
  id: 6,
2724
2864
  name: t('blog:categories'),
@@ -2736,26 +2876,16 @@ function getPostTableColumnDefs(moduleConfig, t) {
2736
2876
  return COLUMN_DEFS.filter(notEmpty);
2737
2877
  }
2738
2878
 
2739
- function generateNumberArray(length) {
2740
- return Array.from({ length }, (_, index) => index);
2741
- }
2742
- function getNameWithDepth(name, depth) {
2743
- return (generateNumberArray(depth)
2744
- .map(() => '—')
2745
- .join(' ') +
2746
- ' ' +
2747
- name);
2748
- }
2749
-
2750
2879
  var FilterTypes;
2751
2880
  (function (FilterTypes) {
2752
2881
  FilterTypes["CATEGORY"] = "category";
2753
2882
  FilterTypes["LANGUAGE"] = "language";
2754
2883
  FilterTypes["FROM_DATE"] = "from-date";
2755
2884
  FilterTypes["TO_DATE"] = "to-date";
2885
+ FilterTypes["STATUS"] = "status";
2756
2886
  })(FilterTypes || (FilterTypes = {}));
2757
2887
  const dateFormat = (date) => date.split('-').reverse().join('.');
2758
- function useAdvancedSearch({ context, categoryList, languageList, }) {
2888
+ function useAdvancedSearch({ context, categoryList, languageList, statusOptionList, }) {
2759
2889
  /** Category **/
2760
2890
  const categoryOptionList = computed(() => categoryList.value.map(({ id, name, depth }) => ({
2761
2891
  value: String(id),
@@ -2813,11 +2943,21 @@ function useAdvancedSearch({ context, categoryList, languageList, }) {
2813
2943
  title: context.root.$t('blog:dateOfPublication'),
2814
2944
  };
2815
2945
  });
2946
+ /** Status **/
2947
+ const initialStatusFilter = computed(() => {
2948
+ const queryValue = getFilterParamAsStringArray(context.root.$route.query, FilterTypes.STATUS);
2949
+ return statusOptionList.value.filter(({ value }) => queryValue.some((selected) => selected === value));
2950
+ });
2951
+ const statusFilter = ref(initialStatusFilter.value);
2952
+ watch(initialStatusFilter, () => {
2953
+ statusFilter.value = initialStatusFilter.value;
2954
+ });
2816
2955
  /** Params **/
2817
2956
  const filterParams = computed(() => {
2818
2957
  const filters = {
2819
2958
  [FilterTypes.CATEGORY]: categoryFilter.value.map(({ value }) => value),
2820
2959
  [FilterTypes.LANGUAGE]: languageFilter.value.map(({ value }) => value),
2960
+ [FilterTypes.STATUS]: statusFilter.value.map(({ value }) => value),
2821
2961
  };
2822
2962
  if (fromDateFilter.value) {
2823
2963
  filters[FilterTypes.FROM_DATE] = fromDateFilter.value;
@@ -2839,6 +2979,9 @@ function useAdvancedSearch({ context, categoryList, languageList, }) {
2839
2979
  fromDateFilter.value = '';
2840
2980
  toDateFilter.value = '';
2841
2981
  }
2982
+ if (event.name === FilterTypes.STATUS) {
2983
+ statusFilter.value = statusFilter.value.filter(({ value }) => value !== event.value);
2984
+ }
2842
2985
  }
2843
2986
  /** Tags **/
2844
2987
  const tags = computed(() => [
@@ -2855,6 +2998,12 @@ function useAdvancedSearch({ context, categoryList, languageList, }) {
2855
2998
  title: context.root.$t('blog:language'),
2856
2999
  })),
2857
3000
  date.value,
3001
+ ...statusFilter.value.map(({ value, label }) => ({
3002
+ value,
3003
+ label,
3004
+ name: FilterTypes.STATUS,
3005
+ title: context.root.$t('blog:status'),
3006
+ })),
2858
3007
  ].filter(isNotNullish));
2859
3008
  /** Helpers **/
2860
3009
  const selectedCategoryOptionList = computed(() => {
@@ -2874,6 +3023,7 @@ function useAdvancedSearch({ context, categoryList, languageList, }) {
2874
3023
  languageOptionList,
2875
3024
  fromDateFilter,
2876
3025
  toDateFilter,
3026
+ statusFilter,
2877
3027
  tagRemovalHandler,
2878
3028
  tags: tags,
2879
3029
  filterParams,
@@ -2889,6 +3039,7 @@ var script = defineComponent({
2889
3039
  return Array.isArray(context.root.$route.query.category)
2890
3040
  ? (_a = context.root.$route.query.category[0]) !== null && _a !== void 0 ? _a : '' : context.root.$route.query.category;
2891
3041
  });
3042
+ const statusOptionList = computed(() => getStatusOptions(t));
2892
3043
  /** Fetch module config **/
2893
3044
  const { data: moduleConfig, loading: isModuleConfigLoading, } = useFetchModuleConfig({ context });
2894
3045
  const languageList = computed(() => { var _a, _b; return (_b = (_a = moduleConfig.value) === null || _a === void 0 ? void 0 : _a.languages) !== null && _b !== void 0 ? _b : []; });
@@ -2905,10 +3056,11 @@ var script = defineComponent({
2905
3056
  : context.root.$t('blog:posts');
2906
3057
  });
2907
3058
  /** Advanced search **/
2908
- const { categoryFilter, categoryOptionList, languageFilter, languageOptionList, fromDateFilter, toDateFilter, filterParams, tags, tagRemovalHandler, } = useAdvancedSearch({
3059
+ const { categoryFilter, categoryOptionList, languageFilter, languageOptionList, fromDateFilter, toDateFilter, filterParams, tags, tagRemovalHandler, statusFilter, } = useAdvancedSearch({
2909
3060
  context,
2910
3061
  categoryList,
2911
3062
  languageList,
3063
+ statusOptionList,
2912
3064
  });
2913
3065
  const isLangSpecific = computed(() => languageOptionList.value.length > 1);
2914
3066
  /** Fetch Post list **/
@@ -2977,6 +3129,8 @@ var script = defineComponent({
2977
3129
  languageOptionList,
2978
3130
  fromDateFilter,
2979
3131
  toDateFilter,
3132
+ statusFilter,
3133
+ statusOptionList,
2980
3134
  tags,
2981
3135
  tagRemovalHandler,
2982
3136
  // Clone
@@ -3253,6 +3407,23 @@ var __vue_render__ = function () {
3253
3407
  1
3254
3408
  ),
3255
3409
  ]),
3410
+ _vm._v(" "),
3411
+ _c("form-field-multi-select", {
3412
+ staticClass: "filter",
3413
+ attrs: {
3414
+ options: _vm.statusOptionList,
3415
+ name: "statusFilter",
3416
+ searchable: true,
3417
+ label: _vm.$t("blog:status"),
3418
+ },
3419
+ model: {
3420
+ value: _vm.statusFilter,
3421
+ callback: function ($$v) {
3422
+ _vm.statusFilter = $$v;
3423
+ },
3424
+ expression: "statusFilter",
3425
+ },
3426
+ }),
3256
3427
  ],
3257
3428
  1
3258
3429
  ),
@@ -3334,11 +3505,11 @@ __vue_render__._withStripped = true;
3334
3505
  /* style */
3335
3506
  const __vue_inject_styles__ = function (inject) {
3336
3507
  if (!inject) return
3337
- inject("data-v-6697ac14_0", { source: ".filters[data-v-6697ac14] {\n display: flex;\n margin: 0 -10px;\n}\n.filters[data-v-6697ac14]:not(:first-child) {\n margin-top: 10px;\n}\n.filters .filter[data-v-6697ac14] {\n padding: 10px 10px 0;\n width: 50%;\n margin: 0;\n}\n.date-label[data-v-6697ac14] {\n margin-bottom: 0.5rem;\n}\n.date-content[data-v-6697ac14] {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n column-gap: 10px;\n}\n\n/*# sourceMappingURL=PostList.vue.map */", map: {"version":3,"sources":["/home/runner/work/tager-admin-blog/tager-admin-blog/src/views/Posts/PostList/PostList.vue","PostList.vue"],"names":[],"mappings":"AAqUA;EACA,aAAA;EACA,eAAA;ACpUA;ADsUA;EACA,gBAAA;ACpUA;ADuUA;EACA,oBAAA;EACA,UAAA;EACA,SAAA;ACrUA;ADyUA;EACA,qBAAA;ACtUA;ADyUA;EACA,aAAA;EACA,qCAAA;EACA,gBAAA;ACtUA;;AAEA,uCAAuC","file":"PostList.vue","sourcesContent":["<template>\n <page\n :title=\"pageTitle\"\n :header-buttons=\"[\n {\n text: $t('blog:newPost'),\n href: getBlogPostFormUrl({ postId: 'create' }),\n },\n ]\"\n >\n <template v-slot:content>\n <data-table\n :column-defs=\"columnDefs\"\n :row-data=\"rowData\"\n :loading=\"isRowDataLoading\"\n :error-message=\"errorMessage\"\n :search-query=\"searchQuery\"\n :pagination=\"{\n pageNumber,\n pageCount,\n pageSize,\n disabled: isRowDataLoading,\n }\"\n data-table=\"blog-post\"\n @change=\"handleChange\"\n >\n <template v-slot:filters>\n <advanced-search :tags=\"tags\" @click:tag=\"tagRemovalHandler\">\n <div class=\"filters\">\n <form-field-multi-select\n v-model=\"categoryFilter\"\n :options=\"categoryOptionList\"\n name=\"categoryFilter\"\n :searchable=\"true\"\n :label=\"$t('blog:category')\"\n class=\"filter\"\n />\n\n <form-field-multi-select\n v-if=\"isLangSpecific\"\n v-model=\"languageFilter\"\n :options=\"languageOptionList\"\n name=\"languageFilter\"\n :searchable=\"true\"\n :label=\"$t('blog:language')\"\n class=\"filter\"\n />\n\n <div class=\"filter\">\n <div class=\"date-label\">\n {{ $t('blog:dateOfPublication') }}\n </div>\n\n <div class=\"date-content\">\n <form-field\n v-model=\"fromDateFilter\"\n :label=\"$t('blog:From')\"\n name=\"fromDateFilter\"\n type=\"date\"\n :max=\"toDateFilter\"\n />\n\n <form-field\n v-model=\"toDateFilter\"\n :label=\"$t('blog:To')\"\n name=\"toDateFilter\"\n type=\"date\"\n :min=\"fromDateFilter\"\n />\n </div>\n </div>\n </div>\n </advanced-search>\n </template>\n\n <template v-slot:cell(actions)=\"{ row }\">\n <base-button\n variant=\"icon\"\n :title=\"$t('blog:edit')\"\n :disabled=\"isBusy(row.id)\"\n :href=\"getBlogPostFormUrl({ postId: row.id })\"\n >\n <svg-icon name=\"edit\" />\n </base-button>\n\n <base-button\n variant=\"icon\"\n :title=\"$t('blog:clone')\"\n :disabled=\"isBusy(row.id)\"\n @click=\"handleResourceClone(row.id)\"\n >\n <svg-icon name=\"contentCopy\" />\n </base-button>\n\n <base-button\n variant=\"icon\"\n :title=\"$t('blog:remove')\"\n :disabled=\"isBusy(row.id)\"\n @click=\"handleResourceDelete(row.id)\"\n >\n <svg-icon name=\"delete\" />\n </base-button>\n </template>\n </data-table>\n </template>\n </page>\n</template>\n\n<script lang=\"ts\">\nimport { computed, defineComponent, watch } from '@vue/composition-api';\nimport isEqual from 'lodash.isequal';\nimport pick from 'lodash.pick';\n\nimport {\n ColumnDefinition,\n useDataTable,\n useTranslation,\n} from '@tager/admin-ui';\nimport {\n Nullable,\n useResourceClone,\n useResourceDelete,\n} from '@tager/admin-services';\n\nimport { useFetchModuleConfig, useFetchCategories } from '../../../hooks';\nimport {\n clonePost,\n deleteBlogPost,\n getPosts,\n} from '../../../services/requests';\nimport { getBlogPostFormUrl } from '../../../constants/paths';\nimport {\n Category,\n Language,\n PostFull,\n PostShort,\n} from '../../../typings/model';\n\nimport { convertPostList, getPostTableColumnDefs } from './PostList.helpers';\nimport { useAdvancedSearch } from './hooks';\n\nexport default defineComponent({\n name: 'Posts',\n setup(props, context) {\n const { t } = useTranslation(context);\n\n const categoryId = computed<string>(() => {\n return Array.isArray(context.root.$route.query.category)\n ? context.root.$route.query.category[0] ?? ''\n : context.root.$route.query.category;\n });\n\n /** Fetch module config **/\n\n const {\n data: moduleConfig,\n loading: isModuleConfigLoading,\n } = useFetchModuleConfig({ context });\n\n const languageList = computed<Language[]>(\n () => moduleConfig.value?.languages ?? []\n );\n\n /** Fetch category list **/\n\n const {\n data: categoryList,\n loading: isCategoryListLoading,\n } = useFetchCategories({ context });\n\n const selectedCategory = computed<Nullable<Category>>(() =>\n categoryId.value\n ? categoryList.value.find(\n ({ id }) => String(id) === String(categoryId.value)\n ) ?? null\n : null\n );\n\n const pageTitle = computed<string>(() => {\n return selectedCategory.value\n ? `${context.root.$t('blog:postsByCategory')} \"${\n selectedCategory.value.name\n }\"`\n : context.root.$t('blog:posts');\n });\n\n /** Advanced search **/\n\n const {\n categoryFilter,\n categoryOptionList,\n languageFilter,\n languageOptionList,\n fromDateFilter,\n toDateFilter,\n filterParams,\n tags,\n tagRemovalHandler,\n } = useAdvancedSearch({\n context,\n categoryList,\n languageList,\n });\n\n const isLangSpecific = computed<boolean>(\n () => languageOptionList.value.length > 1\n );\n\n /** Fetch Post list **/\n\n const {\n fetchEntityList: fetchPostList,\n isLoading: isPostLoading,\n rowData: postList,\n errorMessage,\n searchQuery,\n handleChange,\n pageNumber,\n pageCount,\n pageSize,\n } = useDataTable<PostShort>({\n fetchEntityList: (params) =>\n getPosts({\n query: params.searchQuery,\n pageNumber: params.pageNumber,\n pageSize: params.pageSize,\n ...filterParams.value,\n }),\n initialValue: [],\n context,\n resourceName: 'Blog post list',\n pageSize: 100,\n });\n\n watch(filterParams, () => {\n if (isModuleConfigLoading.value || isCategoryListLoading.value) {\n return;\n }\n\n const newQuery = {\n ...pick(context.root.$route.query, ['query', 'pageNumber']),\n ...filterParams.value,\n };\n\n if (!isEqual(context.root.$route.query, newQuery)) {\n context.root.$router.replace({ query: newQuery });\n fetchPostList();\n }\n });\n\n const displayedPostList = computed<PostShort[]>(() =>\n convertPostList(\n postList.value,\n selectedCategory.value,\n moduleConfig.value?.languages ?? []\n )\n );\n\n const isRowDataLoading = computed<boolean>(\n () =>\n isCategoryListLoading.value ||\n isPostLoading.value ||\n isModuleConfigLoading.value\n );\n\n const { isDeleting, handleResourceDelete } = useResourceDelete({\n deleteResource: deleteBlogPost,\n resourceName: 'Post',\n onSuccess: fetchPostList,\n context,\n });\n\n const { isCloning, handleResourceClone } = useResourceClone({\n cloneResource: clonePost,\n confirmMessage: t('blog:cloneConfirm'),\n successMessage: t('blog:cloneSuccess'),\n failureMessage: t('blog:cloneFailure'),\n onSuccessRedirectTo: (data: PostFull) => `/blog/posts/${data.id}`,\n context,\n });\n\n function isBusy(postId: number): boolean {\n return isDeleting(postId) || isCloning(postId) || isRowDataLoading.value;\n }\n\n const columnDefs = computed<ColumnDefinition<PostShort>[]>(() =>\n getPostTableColumnDefs(moduleConfig.value, context.root.$t)\n );\n\n return {\n columnDefs,\n rowData: displayedPostList,\n handleResourceDelete,\n isDeleting,\n categoryList,\n isRowDataLoading,\n errorMessage,\n getBlogPostFormUrl,\n pageTitle,\n searchQuery,\n handleChange,\n pageNumber,\n pageCount,\n pageSize,\n\n // Advanced search\n categoryFilter,\n categoryOptionList,\n isLangSpecific,\n languageFilter,\n languageOptionList,\n fromDateFilter,\n toDateFilter,\n tags,\n tagRemovalHandler,\n\n // Clone\n handleResourceClone,\n isBusy,\n };\n },\n});\n</script>\n\n<style scoped lang=\"scss\">\n.filters {\n display: flex;\n margin: 0 -10px;\n\n &:not(:first-child) {\n margin-top: 10px;\n }\n\n .filter {\n padding: 10px 10px 0;\n width: 50%;\n margin: 0;\n }\n}\n\n.date-label {\n margin-bottom: 0.5rem;\n}\n\n.date-content {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n column-gap: 10px;\n}\n</style>\n",".filters {\n display: flex;\n margin: 0 -10px;\n}\n.filters:not(:first-child) {\n margin-top: 10px;\n}\n.filters .filter {\n padding: 10px 10px 0;\n width: 50%;\n margin: 0;\n}\n\n.date-label {\n margin-bottom: 0.5rem;\n}\n\n.date-content {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n column-gap: 10px;\n}\n\n/*# sourceMappingURL=PostList.vue.map */"]}, media: undefined });
3508
+ inject("data-v-075b6310_0", { source: ".filters[data-v-075b6310] {\n display: flex;\n margin: 0 -10px;\n}\n.filters[data-v-075b6310]:not(:first-child) {\n margin-top: 10px;\n}\n.filters .filter[data-v-075b6310] {\n padding: 10px 10px 0;\n width: 50%;\n margin: 0;\n}\n.date-label[data-v-075b6310] {\n margin-bottom: 0.5rem;\n}\n.date-content[data-v-075b6310] {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n column-gap: 10px;\n}\n\n/*# sourceMappingURL=PostList.vue.map */", map: {"version":3,"sources":["/home/runner/work/tager-admin-blog/tager-admin-blog/src/views/Posts/PostList/PostList.vue","PostList.vue"],"names":[],"mappings":"AAsVA;EACA,aAAA;EACA,eAAA;ACrVA;ADuVA;EACA,gBAAA;ACrVA;ADwVA;EACA,oBAAA;EACA,UAAA;EACA,SAAA;ACtVA;AD0VA;EACA,qBAAA;ACvVA;AD0VA;EACA,aAAA;EACA,qCAAA;EACA,gBAAA;ACvVA;;AAEA,uCAAuC","file":"PostList.vue","sourcesContent":["<template>\n <page\n :title=\"pageTitle\"\n :header-buttons=\"[\n {\n text: $t('blog:newPost'),\n href: getBlogPostFormUrl({ postId: 'create' }),\n },\n ]\"\n >\n <template v-slot:content>\n <data-table\n :column-defs=\"columnDefs\"\n :row-data=\"rowData\"\n :loading=\"isRowDataLoading\"\n :error-message=\"errorMessage\"\n :search-query=\"searchQuery\"\n :pagination=\"{\n pageNumber,\n pageCount,\n pageSize,\n disabled: isRowDataLoading,\n }\"\n data-table=\"blog-post\"\n @change=\"handleChange\"\n >\n <template v-slot:filters>\n <advanced-search :tags=\"tags\" @click:tag=\"tagRemovalHandler\">\n <div class=\"filters\">\n <form-field-multi-select\n v-model=\"categoryFilter\"\n :options=\"categoryOptionList\"\n name=\"categoryFilter\"\n :searchable=\"true\"\n :label=\"$t('blog:category')\"\n class=\"filter\"\n />\n\n <form-field-multi-select\n v-if=\"isLangSpecific\"\n v-model=\"languageFilter\"\n :options=\"languageOptionList\"\n name=\"languageFilter\"\n :searchable=\"true\"\n :label=\"$t('blog:language')\"\n class=\"filter\"\n />\n\n <div class=\"filter\">\n <div class=\"date-label\">\n {{ $t('blog:dateOfPublication') }}\n </div>\n\n <div class=\"date-content\">\n <form-field\n v-model=\"fromDateFilter\"\n :label=\"$t('blog:From')\"\n name=\"fromDateFilter\"\n type=\"date\"\n :max=\"toDateFilter\"\n />\n\n <form-field\n v-model=\"toDateFilter\"\n :label=\"$t('blog:To')\"\n name=\"toDateFilter\"\n type=\"date\"\n :min=\"fromDateFilter\"\n />\n </div>\n </div>\n\n <form-field-multi-select\n v-model=\"statusFilter\"\n :options=\"statusOptionList\"\n name=\"statusFilter\"\n :searchable=\"true\"\n :label=\"$t('blog:status')\"\n class=\"filter\"\n />\n </div>\n </advanced-search>\n </template>\n\n <template v-slot:cell(actions)=\"{ row }\">\n <base-button\n variant=\"icon\"\n :title=\"$t('blog:edit')\"\n :disabled=\"isBusy(row.id)\"\n :href=\"getBlogPostFormUrl({ postId: row.id })\"\n >\n <svg-icon name=\"edit\" />\n </base-button>\n\n <base-button\n variant=\"icon\"\n :title=\"$t('blog:clone')\"\n :disabled=\"isBusy(row.id)\"\n @click=\"handleResourceClone(row.id)\"\n >\n <svg-icon name=\"contentCopy\" />\n </base-button>\n\n <base-button\n variant=\"icon\"\n :title=\"$t('blog:remove')\"\n :disabled=\"isBusy(row.id)\"\n @click=\"handleResourceDelete(row.id)\"\n >\n <svg-icon name=\"delete\" />\n </base-button>\n </template>\n </data-table>\n </template>\n </page>\n</template>\n\n<script lang=\"ts\">\nimport { computed, defineComponent, watch } from '@vue/composition-api';\nimport isEqual from 'lodash.isequal';\nimport pick from 'lodash.pick';\n\nimport {\n ColumnDefinition,\n OptionType,\n useDataTable,\n useTranslation,\n} from '@tager/admin-ui';\nimport {\n Nullable,\n useResourceClone,\n useResourceDelete,\n} from '@tager/admin-services';\n\nimport { useFetchModuleConfig, useFetchCategories } from '../../../hooks';\nimport {\n clonePost,\n deleteBlogPost,\n getPosts,\n} from '../../../services/requests';\nimport { getBlogPostFormUrl } from '../../../constants/paths';\nimport {\n Category,\n Language,\n PostFull,\n PostShort,\n} from '../../../typings/model';\nimport { getStatusOptions } from '../PostForm/PostForm.helpers';\n\nimport { convertPostList, getPostTableColumnDefs } from './PostList.helpers';\nimport { useAdvancedSearch } from './hooks';\n\nexport default defineComponent({\n name: 'Posts',\n setup(props, context) {\n const { t } = useTranslation(context);\n\n const categoryId = computed<string>(() => {\n return Array.isArray(context.root.$route.query.category)\n ? context.root.$route.query.category[0] ?? ''\n : context.root.$route.query.category;\n });\n\n const statusOptionList = computed<OptionType[]>(() => getStatusOptions(t));\n\n /** Fetch module config **/\n\n const {\n data: moduleConfig,\n loading: isModuleConfigLoading,\n } = useFetchModuleConfig({ context });\n\n const languageList = computed<Language[]>(\n () => moduleConfig.value?.languages ?? []\n );\n\n /** Fetch category list **/\n\n const {\n data: categoryList,\n loading: isCategoryListLoading,\n } = useFetchCategories({ context });\n\n const selectedCategory = computed<Nullable<Category>>(() =>\n categoryId.value\n ? categoryList.value.find(\n ({ id }) => String(id) === String(categoryId.value)\n ) ?? null\n : null\n );\n\n const pageTitle = computed<string>(() => {\n return selectedCategory.value\n ? `${context.root.$t('blog:postsByCategory')} \"${\n selectedCategory.value.name\n }\"`\n : context.root.$t('blog:posts');\n });\n\n /** Advanced search **/\n\n const {\n categoryFilter,\n categoryOptionList,\n languageFilter,\n languageOptionList,\n fromDateFilter,\n toDateFilter,\n filterParams,\n tags,\n tagRemovalHandler,\n statusFilter,\n } = useAdvancedSearch({\n context,\n categoryList,\n languageList,\n statusOptionList,\n });\n\n const isLangSpecific = computed<boolean>(\n () => languageOptionList.value.length > 1\n );\n\n /** Fetch Post list **/\n\n const {\n fetchEntityList: fetchPostList,\n isLoading: isPostLoading,\n rowData: postList,\n errorMessage,\n searchQuery,\n handleChange,\n pageNumber,\n pageCount,\n pageSize,\n } = useDataTable<PostShort>({\n fetchEntityList: (params) =>\n getPosts({\n query: params.searchQuery,\n pageNumber: params.pageNumber,\n pageSize: params.pageSize,\n ...filterParams.value,\n }),\n initialValue: [],\n context,\n resourceName: 'Blog post list',\n pageSize: 100,\n });\n\n watch(filterParams, () => {\n if (isModuleConfigLoading.value || isCategoryListLoading.value) {\n return;\n }\n\n const newQuery = {\n ...pick(context.root.$route.query, ['query', 'pageNumber']),\n ...filterParams.value,\n };\n\n if (!isEqual(context.root.$route.query, newQuery)) {\n context.root.$router.replace({ query: newQuery });\n fetchPostList();\n }\n });\n\n const displayedPostList = computed<PostShort[]>(() =>\n convertPostList(\n postList.value,\n selectedCategory.value,\n moduleConfig.value?.languages ?? []\n )\n );\n\n const isRowDataLoading = computed<boolean>(\n () =>\n isCategoryListLoading.value ||\n isPostLoading.value ||\n isModuleConfigLoading.value\n );\n\n const { isDeleting, handleResourceDelete } = useResourceDelete({\n deleteResource: deleteBlogPost,\n resourceName: 'Post',\n onSuccess: fetchPostList,\n context,\n });\n\n const { isCloning, handleResourceClone } = useResourceClone({\n cloneResource: clonePost,\n confirmMessage: t('blog:cloneConfirm'),\n successMessage: t('blog:cloneSuccess'),\n failureMessage: t('blog:cloneFailure'),\n onSuccessRedirectTo: (data: PostFull) => `/blog/posts/${data.id}`,\n context,\n });\n\n function isBusy(postId: number): boolean {\n return isDeleting(postId) || isCloning(postId) || isRowDataLoading.value;\n }\n\n const columnDefs = computed<ColumnDefinition<PostShort>[]>(() =>\n getPostTableColumnDefs(moduleConfig.value, context.root.$t)\n );\n\n return {\n columnDefs,\n rowData: displayedPostList,\n handleResourceDelete,\n isDeleting,\n categoryList,\n isRowDataLoading,\n errorMessage,\n getBlogPostFormUrl,\n pageTitle,\n searchQuery,\n handleChange,\n pageNumber,\n pageCount,\n pageSize,\n\n // Advanced search\n categoryFilter,\n categoryOptionList,\n isLangSpecific,\n languageFilter,\n languageOptionList,\n fromDateFilter,\n toDateFilter,\n statusFilter,\n statusOptionList,\n tags,\n tagRemovalHandler,\n\n // Clone\n handleResourceClone,\n isBusy,\n };\n },\n});\n</script>\n\n<style scoped lang=\"scss\">\n.filters {\n display: flex;\n margin: 0 -10px;\n\n &:not(:first-child) {\n margin-top: 10px;\n }\n\n .filter {\n padding: 10px 10px 0;\n width: 50%;\n margin: 0;\n }\n}\n\n.date-label {\n margin-bottom: 0.5rem;\n}\n\n.date-content {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n column-gap: 10px;\n}\n</style>\n",".filters {\n display: flex;\n margin: 0 -10px;\n}\n.filters:not(:first-child) {\n margin-top: 10px;\n}\n.filters .filter {\n padding: 10px 10px 0;\n width: 50%;\n margin: 0;\n}\n\n.date-label {\n margin-bottom: 0.5rem;\n}\n\n.date-content {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n column-gap: 10px;\n}\n\n/*# sourceMappingURL=PostList.vue.map */"]}, media: undefined });
3338
3509
 
3339
3510
  };
3340
3511
  /* scoped */
3341
- const __vue_scope_id__ = "data-v-6697ac14";
3512
+ const __vue_scope_id__ = "data-v-075b6310";
3342
3513
  /* module identifier */
3343
3514
  const __vue_module_identifier__ = undefined;
3344
3515
  /* functional template */
@@ -3362,98 +3533,6 @@ __vue_render__._withStripped = true;
3362
3533
  undefined
3363
3534
  );
3364
3535
 
3365
- function convertPostToFormValues(post, languageOptionList, postOptionList, additionalFieldList) {
3366
- var _a, _b;
3367
- if (!post) {
3368
- return {
3369
- title: '',
3370
- excerpt: '',
3371
- body: '',
3372
- date: '',
3373
- coverImage: null,
3374
- image: null,
3375
- imageMobile: null,
3376
- pageTitle: '',
3377
- pageDescription: '',
3378
- openGraphImage: null,
3379
- urlAlias: '',
3380
- categories: [],
3381
- language: null,
3382
- relatedPosts: [],
3383
- tags: '',
3384
- additionalFields: additionalFieldList.map((fieldConfig) => universalFieldUtils.createFormField(fieldConfig, null)),
3385
- };
3386
- }
3387
- const currentLangOption = languageOptionList.find((option) => option.value === post.language);
3388
- const selectedPostOptionList = postOptionList.filter((option) => post.relatedPosts.some((relatedPost) => relatedPost.id === option.value));
3389
- return {
3390
- title: post.title,
3391
- excerpt: post.excerpt,
3392
- body: post.body,
3393
- date: post.date,
3394
- coverImage: post.coverImage
3395
- ? { id: createId(), file: post.coverImage }
3396
- : null,
3397
- image: post.image ? { id: createId(), file: post.image } : null,
3398
- imageMobile: post.imageMobile
3399
- ? { id: createId(), file: post.imageMobile }
3400
- : null,
3401
- pageTitle: (_a = post.pageTitle) !== null && _a !== void 0 ? _a : '',
3402
- pageDescription: (_b = post.pageDescription) !== null && _b !== void 0 ? _b : '',
3403
- openGraphImage: post.openGraphImage
3404
- ? { id: createId(), file: post.openGraphImage }
3405
- : null,
3406
- urlAlias: post.urlAlias,
3407
- categories: post.categories.map((category) => ({
3408
- value: category.id,
3409
- label: category.name,
3410
- })),
3411
- language: currentLangOption !== null && currentLangOption !== void 0 ? currentLangOption : null,
3412
- relatedPosts: selectedPostOptionList,
3413
- tags: post.tags.join(','),
3414
- additionalFields: additionalFieldList.map((fieldConfig, index) => {
3415
- const postAdditionalField = post.additionalFields.find((item) => item.name === fieldConfig.name);
3416
- return universalFieldUtils.createFormField(fieldConfig, postAdditionalField === null || postAdditionalField === void 0 ? void 0 : postAdditionalField.value);
3417
- }),
3418
- };
3419
- }
3420
- function convertFormValuesToCreationPayload(values) {
3421
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
3422
- return {
3423
- title: values.title,
3424
- excerpt: values.excerpt,
3425
- urlAlias: values.urlAlias,
3426
- body: values.body,
3427
- date: values.date,
3428
- coverImage: (_b = (_a = values.coverImage) === null || _a === void 0 ? void 0 : _a.file.id) !== null && _b !== void 0 ? _b : null,
3429
- image: (_d = (_c = values.image) === null || _c === void 0 ? void 0 : _c.file.id) !== null && _d !== void 0 ? _d : null,
3430
- imageMobile: (_f = (_e = values.imageMobile) === null || _e === void 0 ? void 0 : _e.file.id) !== null && _f !== void 0 ? _f : null,
3431
- pageTitle: values.pageTitle,
3432
- pageDescription: values.pageDescription,
3433
- openGraphImage: (_h = (_g = values.openGraphImage) === null || _g === void 0 ? void 0 : _g.file.id) !== null && _h !== void 0 ? _h : null,
3434
- status: 'PUBLISHED',
3435
- categories: values.categories.map((option) => option.value),
3436
- language: (_k = (_j = values.language) === null || _j === void 0 ? void 0 : _j.value) !== null && _k !== void 0 ? _k : null,
3437
- relatedPosts: values.relatedPosts.map((relatedPostOption) => relatedPostOption.value),
3438
- tags: values.tags.split(',').filter((tag) => tag.trim().length > 0),
3439
- additionalFields: values.additionalFields.map((field) => ({
3440
- name: field.config.name,
3441
- value: universalFieldUtils.getOutgoingValue(field),
3442
- })),
3443
- };
3444
- }
3445
- function convertFormValuesToUpdatePayload(values) {
3446
- return Object.assign(Object.assign({}, convertFormValuesToCreationPayload(values)), { urlAlias: values.urlAlias });
3447
- }
3448
- function convertCategoryListToOptions(categoryList, languageId) {
3449
- return categoryList
3450
- .filter(({ language }) => (languageId ? language === languageId : true))
3451
- .map(({ id, name, depth }) => ({
3452
- value: id,
3453
- label: getNameWithDepth(name, depth),
3454
- }));
3455
- }
3456
-
3457
3536
  var script$1 = defineComponent({
3458
3537
  name: 'BlogPostForm',
3459
3538
  components: { DynamicField, ShortCodeConstructor, FormFooter },
@@ -3462,6 +3541,7 @@ var script$1 = defineComponent({
3462
3541
  const postId = computed(() => context.root.$route.params.postId);
3463
3542
  const isCreation = computed(() => postId.value === 'create');
3464
3543
  const urlAliasChanged = ref(false);
3544
+ const statusOptions = computed(() => getStatusOptions(context.root.$t));
3465
3545
  /** Fetch module config **/
3466
3546
  const { data: moduleConfig, loading: isModuleConfigLoading, } = useFetchModuleConfig({ context });
3467
3547
  const languageList = computed(() => { var _a, _b; return (_b = (_a = moduleConfig.value) === null || _a === void 0 ? void 0 : _a.languages) !== null && _b !== void 0 ? _b : []; });
@@ -3501,11 +3581,11 @@ var script$1 = defineComponent({
3501
3581
  })));
3502
3582
  /** Form State **/
3503
3583
  const isSubmitting = ref(false);
3504
- const values = ref(convertPostToFormValues(post.value, languageOptionList.value, postOptionList.value, (_b = (_a = moduleConfig.value) === null || _a === void 0 ? void 0 : _a.fields) !== null && _b !== void 0 ? _b : []));
3584
+ const values = ref(convertPostToFormValues(post.value, languageOptionList.value, postOptionList.value, (_b = (_a = moduleConfig.value) === null || _a === void 0 ? void 0 : _a.fields) !== null && _b !== void 0 ? _b : [], statusOptions.value));
3505
3585
  const errors = ref({});
3506
3586
  watch([post, languageOptionList, postOptionList, moduleConfig], () => {
3507
3587
  var _a, _b;
3508
- values.value = convertPostToFormValues(post.value, languageOptionList.value, postOptionList.value, (_b = (_a = moduleConfig.value) === null || _a === void 0 ? void 0 : _a.fields) !== null && _b !== void 0 ? _b : []);
3588
+ values.value = convertPostToFormValues(post.value, languageOptionList.value, postOptionList.value, (_b = (_a = moduleConfig.value) === null || _a === void 0 ? void 0 : _a.fields) !== null && _b !== void 0 ? _b : [], statusOptions.value);
3509
3589
  });
3510
3590
  function submitForm(event) {
3511
3591
  isSubmitting.value = true;
@@ -3530,7 +3610,7 @@ var script$1 = defineComponent({
3530
3610
  }
3531
3611
  }
3532
3612
  if (event.type === 'create_create-another') {
3533
- values.value = convertPostToFormValues(null, languageOptionList.value, postOptionList.value, (_b = (_a = moduleConfig.value) === null || _a === void 0 ? void 0 : _a.fields) !== null && _b !== void 0 ? _b : []);
3613
+ values.value = convertPostToFormValues(null, languageOptionList.value, postOptionList.value, (_b = (_a = moduleConfig.value) === null || _a === void 0 ? void 0 : _a.fields) !== null && _b !== void 0 ? _b : [], statusOptions.value);
3534
3614
  }
3535
3615
  context.root.$toast({
3536
3616
  variant: 'success',
@@ -3648,7 +3728,7 @@ var script$1 = defineComponent({
3648
3728
  values.value.openGraphImage = image;
3649
3729
  }
3650
3730
  function handleTitleChange(value) {
3651
- if (urlAliasChanged.value === false) {
3731
+ if (isCreation.value && urlAliasChanged.value === false) {
3652
3732
  values.value.urlAlias = urlTranslit(value);
3653
3733
  }
3654
3734
  }
@@ -3715,6 +3795,7 @@ var script$1 = defineComponent({
3715
3795
  handleAliasChange,
3716
3796
  urlAliasTemplate,
3717
3797
  handleCategoriesChange,
3798
+ statusOptions,
3718
3799
  };
3719
3800
  },
3720
3801
  });
@@ -3771,6 +3852,23 @@ var __vue_render__$1 = function () {
3771
3852
  _vm._v(" "),
3772
3853
  _vm.selectedTabId === "common"
3773
3854
  ? [
3855
+ _c("form-field-select", {
3856
+ attrs: {
3857
+ name: "status",
3858
+ error: _vm.errors.status,
3859
+ label: _vm.$t("blog:status"),
3860
+ "no-options-message": _vm.$t("blog:noTemplates"),
3861
+ options: _vm.statusOptions,
3862
+ },
3863
+ model: {
3864
+ value: _vm.values.status,
3865
+ callback: function ($$v) {
3866
+ _vm.$set(_vm.values, "status", $$v);
3867
+ },
3868
+ expression: "values.status",
3869
+ },
3870
+ }),
3871
+ _vm._v(" "),
3774
3872
  _vm.isCreation && _vm.isLangSpecific
3775
3873
  ? _c("form-field-select", {
3776
3874
  attrs: {
@@ -3866,17 +3964,17 @@ var __vue_render__$1 = function () {
3866
3964
  _vm._v(" "),
3867
3965
  _c("form-field", {
3868
3966
  attrs: {
3869
- name: "date",
3967
+ name: "datetime",
3870
3968
  label: _vm.$t("blog:date"),
3871
3969
  type: "date",
3872
- error: _vm.errors.date,
3970
+ error: _vm.errors.datetime,
3873
3971
  },
3874
3972
  model: {
3875
- value: _vm.values.date,
3973
+ value: _vm.values.datetime,
3876
3974
  callback: function ($$v) {
3877
- _vm.$set(_vm.values, "date", $$v);
3975
+ _vm.$set(_vm.values, "datetime", $$v);
3878
3976
  },
3879
- expression: "values.date",
3977
+ expression: "values.datetime",
3880
3978
  },
3881
3979
  }),
3882
3980
  ]