@tager/admin-blog 0.3.21 → 0.3.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/index.esm.js +3452 -612
  2. package/dist/index.umd.js +3454 -619
  3. package/package.json +13 -7
  4. package/src/constants/menu.ts +0 -2
  5. package/src/constants/paths.ts +0 -5
  6. package/src/constants/routes.ts +5 -18
  7. package/src/constants/scopes.ts +11 -0
  8. package/src/dev.ts +11 -44
  9. package/src/hooks/index.ts +6 -0
  10. package/src/hooks/useBlogCategoryList.ts +3 -5
  11. package/src/hooks/useBlogPostList.ts +2 -4
  12. package/src/hooks/useModuleConfig.ts +4 -8
  13. package/src/hooks/useStore.ts +8 -0
  14. package/src/hooks/useUserPermission.ts +16 -0
  15. package/src/hooks/useUserProfile.ts +13 -0
  16. package/src/locales/en.ts +17 -5
  17. package/src/locales/ru.ts +17 -4
  18. package/src/router/index.ts +39 -0
  19. package/src/services/requests.ts +33 -43
  20. package/src/store/index.ts +21 -0
  21. package/src/store/user.ts +109 -0
  22. package/src/typings/common.ts +1 -0
  23. package/src/typings/model.ts +62 -38
  24. package/src/utils/common.ts +23 -0
  25. package/src/views/App.vue +10 -4
  26. package/src/views/{BlogCategoryForm/BlogCategoryForm.helpers.ts → Categories/CategoryForm/CategoryForm.helpers.ts} +22 -7
  27. package/src/views/{BlogCategoryForm/BlogCategoryForm.vue → Categories/CategoryForm/CategoryForm.vue} +140 -63
  28. package/src/views/Categories/CategoryForm/index.ts +1 -0
  29. package/src/views/Categories/CategoryList/CategoryList.helpers.ts +91 -0
  30. package/src/views/Categories/CategoryList/CategoryList.vue +269 -0
  31. package/src/views/Categories/CategoryList/hooks/index.ts +1 -0
  32. package/src/views/Categories/CategoryList/hooks/useAdvancedSearch.ts +95 -0
  33. package/src/views/Categories/CategoryList/index.ts +1 -0
  34. package/src/views/Home.vue +2 -9
  35. package/src/views/Posts/PostForm/PostForm.helpers.ts +150 -0
  36. package/src/views/Posts/PostForm/PostForm.vue +550 -0
  37. package/src/views/{BlogPostForm → Posts/PostForm}/components/TabList/TabList.types.ts +0 -0
  38. package/src/views/{BlogPostForm → Posts/PostForm}/components/TabList/TabList.vue +1 -1
  39. package/src/views/{BlogPostForm → Posts/PostForm}/components/TabList/index.ts +0 -0
  40. package/src/views/Posts/PostForm/index.ts +1 -0
  41. package/src/views/{BlogPostList/BlogPostList.helpers.ts → Posts/PostList/PostList.helpers.ts} +10 -18
  42. package/src/views/Posts/PostList/PostList.vue +361 -0
  43. package/src/views/Posts/PostList/hooks/index.ts +1 -0
  44. package/src/views/Posts/PostList/hooks/useAdvancedSearch.ts +250 -0
  45. package/src/views/Posts/PostList/index.ts +1 -0
  46. package/dist/constants/menu.d.ts +0 -5
  47. package/dist/constants/paths.d.ts +0 -16
  48. package/dist/constants/routes.d.ts +0 -6
  49. package/dist/dev.d.ts +0 -3
  50. package/dist/hooks/useBlogCategoryList.d.ts +0 -7
  51. package/dist/hooks/useBlogPostList.d.ts +0 -7
  52. package/dist/hooks/useModuleConfig.d.ts +0 -7
  53. package/dist/locales/en.d.ts +0 -63
  54. package/dist/locales/ru.d.ts +0 -3
  55. package/dist/main.d.ts +0 -4
  56. package/dist/services/requests.d.ts +0 -68
  57. package/dist/typings/model.d.ts +0 -74
  58. package/dist/views/BlogCategoryForm/BlogCategoryForm.helpers.d.ts +0 -16
  59. package/dist/views/BlogCategoryForm/BlogCategoryForm.vue.d.ts +0 -86
  60. package/dist/views/BlogCategoryForm/index.d.ts +0 -1
  61. package/dist/views/BlogCategoryList/BlogCategoryList.helpers.d.ts +0 -6
  62. package/dist/views/BlogCategoryList/BlogCategoryList.vue.d.ts +0 -3
  63. package/dist/views/BlogCategoryList/index.d.ts +0 -1
  64. package/dist/views/BlogPostForm/BlogPostForm.helpers.d.ts +0 -27
  65. package/dist/views/BlogPostForm/BlogPostForm.vue.d.ts +0 -9320
  66. package/dist/views/BlogPostForm/components/TabList/TabList.types.d.ts +0 -5
  67. package/dist/views/BlogPostForm/components/TabList/index.d.ts +0 -2
  68. package/dist/views/BlogPostForm/index.d.ts +0 -1
  69. package/dist/views/BlogPostList/BlogPostList.helpers.d.ts +0 -6
  70. package/dist/views/BlogPostList/BlogPostList.vue.d.ts +0 -45
  71. package/dist/views/BlogPostList/index.d.ts +0 -1
  72. package/dist/views/BlogSettings/BlogSettings.helpers.d.ts +0 -6
  73. package/dist/views/BlogSettings/BlogSettings.vue.d.ts +0 -9132
  74. package/dist/views/BlogSettings/index.d.ts +0 -1
  75. package/src/views/BlogCategoryForm/index.ts +0 -1
  76. package/src/views/BlogCategoryList/BlogCategoryList.helpers.ts +0 -82
  77. package/src/views/BlogCategoryList/BlogCategoryList.vue +0 -196
  78. package/src/views/BlogCategoryList/index.ts +0 -1
  79. package/src/views/BlogPostForm/BlogPostForm.helpers.ts +0 -152
  80. package/src/views/BlogPostForm/BlogPostForm.vue +0 -534
  81. package/src/views/BlogPostForm/index.ts +0 -1
  82. package/src/views/BlogPostList/BlogPostList.vue +0 -174
  83. package/src/views/BlogPostList/index.ts +0 -1
  84. package/src/views/BlogSettings/BlogSettings.helpers.ts +0 -25
  85. package/src/views/BlogSettings/BlogSettings.vue +0 -110
  86. package/src/views/BlogSettings/index.ts +0 -1
@@ -0,0 +1,109 @@
1
+ import { GetterTree, Module } from 'vuex';
2
+
3
+ import { FETCH_STATUSES, FetchStatus } from '@tager/admin-services';
4
+
5
+ import { ScopeType, UserModel } from '@/typings/model';
6
+ import { getUserProfile } from '@/services/requests';
7
+ import { RootState } from '@/store';
8
+ import { Scope } from '@/constants/scopes';
9
+
10
+ export interface UserModuleStateModel {
11
+ profile: UserModel | null;
12
+ status: FetchStatus;
13
+ }
14
+
15
+ export const USER_MODULE_NAMESPACE = 'user';
16
+
17
+ const USER_MUTATION_TYPES = {
18
+ PROFILE_REQUEST_STARTED: 'profileRequestStarted',
19
+ PROFILE_REQUEST_FULFILLED: 'profileRequestFulfilled',
20
+ PROFILE_REQUEST_REJECTED: 'profileRequestRejected',
21
+ };
22
+
23
+ export const USER_ACTION_TYPES = {
24
+ FETCH_USER_PROFILE: 'fetchUserProfile',
25
+ } as const;
26
+
27
+ type ReturnValueMapType<
28
+ T extends { [key: string]: (...args: any[]) => any }
29
+ > = { [key in keyof T]: ReturnType<T[key]> };
30
+
31
+ interface UserGettersType extends GetterTree<UserModuleStateModel, RootState> {
32
+ userScopes: (state: UserModuleStateModel) => Array<ScopeType>;
33
+ userHasPermission: (
34
+ state: UserModuleStateModel,
35
+ getters: ReturnValueMapType<UserGettersType>
36
+ ) => (scope: ScopeType) => boolean;
37
+ }
38
+
39
+ const userGetters: UserGettersType = {
40
+ userProfile(state) {
41
+ return state.profile;
42
+ },
43
+ userScopes(state) {
44
+ const roles = state.profile ? state.profile.roles : [];
45
+ const scopes: Array<ScopeType> = [];
46
+
47
+ roles.forEach((role) => {
48
+ scopes.push(...role.scopes);
49
+ });
50
+
51
+ return scopes;
52
+ },
53
+ userHasPermission: (state, getters) => (scope: ScopeType) => {
54
+ const scopes = getters.userScopes;
55
+
56
+ if (scopes.includes(Scope.All)) return true;
57
+
58
+ return scopes.includes(scope);
59
+ },
60
+ };
61
+
62
+ const userModule: Module<UserModuleStateModel, RootState> = {
63
+ namespaced: true,
64
+ state: {
65
+ profile: null,
66
+ status: FETCH_STATUSES.IDLE,
67
+ },
68
+ mutations: {
69
+ [USER_MUTATION_TYPES.PROFILE_REQUEST_STARTED](state) {
70
+ if (state.status === FETCH_STATUSES.IDLE) {
71
+ state.status = FETCH_STATUSES.LOADING;
72
+ }
73
+ },
74
+ [USER_MUTATION_TYPES.PROFILE_REQUEST_FULFILLED](state, user: UserModel) {
75
+ if (state.status === FETCH_STATUSES.LOADING) {
76
+ state.status = FETCH_STATUSES.SUCCESS;
77
+ state.profile = user;
78
+ }
79
+ },
80
+ [USER_MUTATION_TYPES.PROFILE_REQUEST_REJECTED](state) {
81
+ if (state.status === FETCH_STATUSES.LOADING) {
82
+ state.status = FETCH_STATUSES.FAILURE;
83
+ state.profile = null;
84
+ }
85
+ },
86
+ },
87
+ actions: {
88
+ [USER_ACTION_TYPES.FETCH_USER_PROFILE](store) {
89
+ if (store.state.status !== FETCH_STATUSES.IDLE) return;
90
+
91
+ store.commit(USER_MUTATION_TYPES.PROFILE_REQUEST_STARTED);
92
+
93
+ getUserProfile()
94
+ .then((response) => {
95
+ store.commit(
96
+ USER_MUTATION_TYPES.PROFILE_REQUEST_FULFILLED,
97
+ response.data
98
+ );
99
+ })
100
+ .catch((error) => {
101
+ console.error(error);
102
+ store.commit(USER_MUTATION_TYPES.PROFILE_REQUEST_REJECTED);
103
+ });
104
+ },
105
+ },
106
+ getters: userGetters,
107
+ };
108
+
109
+ export default userModule;
@@ -0,0 +1 @@
1
+ export type Values<T> = T[keyof T];
@@ -6,13 +6,34 @@ import {
6
6
  } from '@tager/admin-dynamic-field';
7
7
  import { ShortCodeItemType } from '@tager/admin-ui';
8
8
 
9
- export type SeoInfo = {
9
+ import { Values } from '@/typings/common';
10
+ import { Scope } from '@/constants/scopes';
11
+
12
+ export type ScopeType = Values<typeof Scope>;
13
+
14
+ export interface RoleModel {
15
+ readonly id: number;
16
+ readonly name: string;
17
+ readonly scopes: Array<ScopeType>;
18
+ }
19
+
20
+ export interface UserModel {
21
+ readonly id: number;
22
+ readonly name: string;
23
+ readonly email: string;
24
+ readonly roles: Array<RoleModel>;
25
+ }
26
+
27
+ export interface SeoInfo {
10
28
  readonly pageTitle: Nullable<string>;
11
29
  readonly pageDescription: Nullable<string>;
30
+ readonly pageKeywords: Nullable<string>;
31
+ readonly openGraphTitle: Nullable<string>;
32
+ readonly openGraphDescription: Nullable<string>;
12
33
  readonly openGraphImage: Nullable<FileType>;
13
- };
34
+ }
14
35
 
15
- export type BlogCategory = {
36
+ export interface Category extends SeoInfo {
16
37
  readonly id: number;
17
38
  readonly name: string;
18
39
  readonly isDefault: boolean;
@@ -21,21 +42,23 @@ export type BlogCategory = {
21
42
  readonly urlTemplate: string;
22
43
  readonly urlAlias: string;
23
44
  readonly postsCount: string;
24
- } & SeoInfo;
45
+ readonly parent: Nullable<Pick<Category, 'id' | 'name'>>;
46
+ readonly depth: number;
47
+ }
25
48
 
26
- export type PostShort = {
49
+ export interface PostShort {
27
50
  readonly id: number;
28
51
  readonly title: string;
29
52
  readonly url: string;
30
53
  readonly excerpt: string;
31
54
  readonly status: string;
32
55
  readonly language: string;
33
- readonly categories: Array<BlogCategory>;
56
+ readonly categories: Category[];
34
57
  readonly date: string;
35
58
  readonly image: Nullable<FileType>;
36
- };
59
+ }
37
60
 
38
- export type PostFull = {
61
+ export interface PostFull extends SeoInfo {
39
62
  readonly id: number;
40
63
  readonly language: string;
41
64
  readonly title: string;
@@ -47,38 +70,39 @@ export type PostFull = {
47
70
  readonly excerpt: string;
48
71
  readonly image: Nullable<FileType>;
49
72
  readonly imageMobile: Nullable<FileType>;
50
- readonly categories: Array<BlogCategory>;
73
+ readonly categories: Category[];
51
74
  readonly body: string;
52
75
  readonly coverImage: Nullable<FileType>;
53
- readonly relatedPosts: Array<Pick<PostShort, 'id' | 'title'>>;
54
- readonly tags: Array<string>;
55
- readonly additionalFields: Array<FieldShortType<IncomingValueUnion>>;
56
- } & SeoInfo;
76
+ readonly relatedPosts: Pick<PostShort, 'id' | 'title'>[];
77
+ readonly tags: string[];
78
+ readonly additionalFields: FieldShortType<IncomingValueUnion>[];
79
+ }
57
80
 
58
- export type BlogModuleConfigType = {
81
+ export interface Language {
82
+ readonly id: string;
83
+ readonly name: string;
84
+ }
85
+
86
+ export interface DefaultCategory {
87
+ readonly id: number;
88
+ readonly name: string;
89
+ readonly language: string;
90
+ }
91
+
92
+ export interface FileScenarios {
93
+ readonly cover: Nullable<string>;
94
+ readonly image: Nullable<string>;
95
+ readonly imageMobile: Nullable<string>;
96
+ readonly content: Nullable<string>;
97
+ readonly openGraph: Nullable<string>;
98
+ }
99
+
100
+ export interface ModuleConfig {
59
101
  readonly urlPostTemplate: Nullable<string>;
60
102
  readonly urlCategoryTemplate: Nullable<string>;
61
- readonly languages: Array<{
62
- id: string;
63
- name: string;
64
- }>;
65
- defaultCategories: Array<{
66
- id: number;
67
- name: string;
68
- language: string;
69
- }>;
70
- readonly fields: Array<FieldConfigUnion>;
71
- readonly shortcodes: Array<ShortCodeItemType>;
72
- readonly fileScenarios: {
73
- cover: Nullable<string>;
74
- image: Nullable<string>;
75
- imageMobile: Nullable<string>;
76
- content: Nullable<string>;
77
- openGraph: Nullable<string>;
78
- };
79
- };
80
-
81
- export type SettingItemType = {
82
- field: FieldConfigUnion;
83
- value: IncomingValueUnion;
84
- };
103
+ readonly languages: Language[];
104
+ readonly defaultCategories: DefaultCategory[];
105
+ readonly fields: FieldConfigUnion[];
106
+ readonly shortcodes: ShortCodeItemType[];
107
+ readonly fileScenarios: FileScenarios;
108
+ }
@@ -0,0 +1,23 @@
1
+ import { USER_MODULE_NAMESPACE } from '@/store/user';
2
+
3
+ export function namespace(namespace: string, name: string): string {
4
+ return `${namespace}/${name}`;
5
+ }
6
+
7
+ export function userNamespace(name: string): string {
8
+ return namespace(USER_MODULE_NAMESPACE, name);
9
+ }
10
+
11
+ export function generateNumberArray(length: number): Array<number> {
12
+ return Array.from({ length }, (_, index) => index);
13
+ }
14
+
15
+ export function getNameWithDepth(name: string, depth: number): string {
16
+ return (
17
+ generateNumberArray(depth)
18
+ .map(() => '—')
19
+ .join(' ') +
20
+ ' ' +
21
+ name
22
+ );
23
+ }
package/src/views/App.vue CHANGED
@@ -5,20 +5,26 @@
5
5
  </template>
6
6
 
7
7
  <script lang="ts">
8
- import { defineComponent, SetupContext } from '@vue/composition-api';
8
+ import { defineComponent, onMounted, SetupContext } from '@vue/composition-api';
9
9
 
10
- import { getBlogMenuItem } from '../constants/menu';
10
+ import { getBlogMenuItem } from '@/constants/menu';
11
+ import { useStore } from '@/hooks';
12
+ import { USER_ACTION_TYPES } from '@/store/user';
13
+ import { userNamespace } from '@/utils/common';
11
14
 
12
15
  export default defineComponent({
13
16
  name: 'App',
14
17
  setup(props, context: SetupContext) {
18
+ const store = useStore(context);
15
19
  const blogMenuItem = getBlogMenuItem({ t: context.root.$t });
16
20
 
21
+ onMounted(() => {
22
+ store.dispatch(userNamespace(USER_ACTION_TYPES.FETCH_USER_PROFILE));
23
+ });
24
+
17
25
  return {
18
26
  sidebarMenuList: [blogMenuItem],
19
27
  };
20
28
  },
21
29
  });
22
30
  </script>
23
-
24
- <style scoped lang="scss"></style>
@@ -1,37 +1,47 @@
1
1
  import { createId, Nullable } from '@tager/admin-services';
2
2
  import { OptionType, SingleFileInputValueType } from '@tager/admin-ui';
3
3
 
4
- import { BlogCategory } from '../../typings/model';
4
+ import { Category } from '@/typings/model';
5
5
  import {
6
6
  BlogCategoryCreationPayload,
7
7
  BlogCategoryUpdatePayload,
8
- } from '../../services/requests';
8
+ } from '@/services/requests';
9
9
 
10
10
  export type CategoryFormValues = {
11
11
  name: string;
12
12
  isDefault: boolean;
13
- pageTitle: string;
14
- pageDescription: string;
13
+
14
+ pageTitle: Nullable<string>;
15
+ pageDescription: Nullable<string>;
16
+ pageKeywords: Nullable<string>;
17
+ openGraphTitle: Nullable<string>;
18
+ openGraphDescription: Nullable<string>;
15
19
  openGraphImage: Nullable<SingleFileInputValueType>;
20
+
16
21
  urlAlias: string;
17
22
  language: Nullable<OptionType>;
18
23
  };
19
24
 
20
25
  export function convertCategoryToFormValues(
21
- category: Nullable<BlogCategory>,
22
- languageOptionList: Array<OptionType>
26
+ category: Nullable<Category>,
27
+ languageOptionList: OptionType[]
23
28
  ): CategoryFormValues {
24
29
  const currentLangOption = languageOptionList.find(
25
- (option) => option.value === category?.language
30
+ ({ value }) => value === category?.language
26
31
  );
27
32
 
28
33
  if (!category) {
29
34
  return {
30
35
  name: '',
31
36
  isDefault: false,
37
+
32
38
  pageTitle: '',
33
39
  pageDescription: '',
40
+ openGraphTitle: '',
41
+ openGraphDescription: '',
42
+ pageKeywords: '',
34
43
  openGraphImage: null,
44
+
35
45
  urlAlias: '',
36
46
  language: currentLangOption ?? null,
37
47
  };
@@ -39,11 +49,16 @@ export function convertCategoryToFormValues(
39
49
  return {
40
50
  name: category.name,
41
51
  isDefault: category.isDefault,
52
+
42
53
  pageTitle: category.pageTitle ?? '',
43
54
  pageDescription: category.pageDescription ?? '',
55
+ pageKeywords: category.pageKeywords ?? '',
56
+ openGraphTitle: category.openGraphTitle ?? '',
57
+ openGraphDescription: category.openGraphDescription ?? '',
44
58
  openGraphImage: category.openGraphImage
45
59
  ? { id: createId(), file: category.openGraphImage }
46
60
  : null,
61
+
47
62
  urlAlias: category.urlAlias,
48
63
  language: currentLangOption ?? null,
49
64
  };
@@ -7,61 +7,88 @@
7
7
  >
8
8
  <template v-slot:content>
9
9
  <form novalidate @submit.prevent="submitForm">
10
- <form-field-select
11
- v-if="isCreation && isLangSpecific"
10
+ <template v-if="isCreation">
11
+ <form-field-select
12
+ v-if="isLangSpecific"
12
13
  v-model="values.language"
13
14
  name="language"
14
15
  :label="$t('blog:language')"
15
16
  :options="languageOptionList"
16
17
  :error="errors.language"
17
- />
18
+ />
18
19
 
19
- <form-field
20
+ <form-field
20
21
  v-model="values.name"
21
22
  name="name"
22
23
  :label="$t('blog:name')"
23
24
  :error="errors.name"
24
- />
25
+ />
25
26
 
26
- <form-field-checkbox
27
+ <form-field-checkbox
27
28
  v-model="values.isDefault"
28
29
  name="isDefault"
29
30
  :label="$t('blog:defaultCategory')"
30
31
  :error="errors.isDefault"
31
- />
32
-
33
- <form-field-url-alias-input
34
- v-if="!isCreation"
35
- id="urlAlias"
36
- v-model="values.urlAlias"
37
- name="urlAlias"
38
- :label="$t('blog:URLAlias')"
39
- :url-template="pagePath"
40
- :error="errors.urlAlias"
41
- />
42
-
43
- <form-field
44
- v-model="values.pageTitle"
45
- name="pageTitle"
46
- :label="$t('blog:pageTitle')"
47
- :error="errors.pageTitle"
48
- />
49
-
50
- <form-field
51
- v-model="values.pageDescription"
52
- name="pageDescription"
53
- :label="$t('blog:pageDescription')"
54
- type="textarea"
55
- :error="errors.pageDescription"
56
- />
57
-
58
- <form-field-file-input
59
- v-model="values.openGraphImage"
60
- :label="$t('blog:openGraphImage')"
61
- name="openGraphImage"
62
- file-type="image"
63
- :image-scenario="moduleConfig ? moduleConfig.fileScenarios.openGraph : null"
64
- />
32
+ />
33
+
34
+ <form-field-url-alias-input
35
+ id="urlAlias"
36
+ v-model="values.urlAlias"
37
+ name="urlAlias"
38
+ :label="$t('blog:URLAlias')"
39
+ :url-template="pagePath"
40
+ :error="errors.urlAlias"
41
+ />
42
+ </template>
43
+
44
+ <template v-else>
45
+ <tab-list
46
+ :tab-list="tabList"
47
+ :selected-tab-id="selectedTabId"
48
+ @tab:update="selectedTabId = $event.tabId"
49
+ />
50
+
51
+ <template v-if="selectedTabId === 'common'">
52
+ <form-field
53
+ v-model="values.name"
54
+ name="name"
55
+ :label="$t('blog:name')"
56
+ :error="errors.name"
57
+ />
58
+
59
+ <form-field-checkbox
60
+ v-model="values.isDefault"
61
+ name="isDefault"
62
+ :label="$t('blog:defaultCategory')"
63
+ :error="errors.isDefault"
64
+ />
65
+
66
+ <form-field-url-alias-input
67
+ id="urlAlias"
68
+ v-model="values.urlAlias"
69
+ name="urlAlias"
70
+ :label="$t('blog:URLAlias')"
71
+ :url-template="pagePath"
72
+ :error="errors.urlAlias"
73
+ />
74
+ </template>
75
+
76
+ <template v-if="selectedTabId === 'seo'">
77
+ <seo-field-group
78
+ :title="values.pageTitle"
79
+ :title-error-message="errors.pageTitle"
80
+ :description="values.pageDescription"
81
+ :description-error-message="errors.pageDescription"
82
+ :should-display-keywords="false"
83
+ :keywords="values.pageKeywords"
84
+ :keywords-error-message="errors.pageKeywords"
85
+ :image="values.openGraphImage"
86
+ :image-scenario="imageScenario"
87
+ :image-error-message="errors.openGraphImage"
88
+ @change="handleSeoFieldGroupChange"
89
+ />
90
+ </template>
91
+ </template>
65
92
  </form>
66
93
  </template>
67
94
 
@@ -88,34 +115,44 @@ import {
88
115
 
89
116
  import {
90
117
  convertRequestErrorToMap,
118
+ getWebsiteOrigin,
119
+ notEmpty,
91
120
  Nullable,
92
121
  useResource,
93
122
  } from '@tager/admin-services';
94
- import { OptionType, FormFooter, TagerFormSubmitEvent } from '@tager/admin-ui';
123
+ import {
124
+ OptionType,
125
+ FormFooter,
126
+ TagerFormSubmitEvent,
127
+ SeoChangeEvent,
128
+ TabType,
129
+ useTranslation,
130
+ } from '@tager/admin-ui';
95
131
 
96
132
  import {
97
133
  createBlogCategory,
98
134
  getBlogCategory,
99
135
  updateBlogCategory,
100
- } from '../../services/requests';
101
- import { BlogCategory } from '../../typings/model';
136
+ } from '@/services/requests';
137
+ import { Category, FileScenarios, Language } from '@/typings/model';
102
138
  import {
103
139
  getBlogCategoryFormUrl,
104
140
  getBlogCategoryListUrl,
105
- } from '../../constants/paths';
106
- import useModuleConfig from '../../hooks/useModuleConfig';
141
+ } from '@/constants/paths';
142
+ import { useModuleConfig } from '@/hooks';
107
143
 
108
144
  import {
109
145
  CategoryFormValues,
110
146
  convertCategoryFormValuesToCreationPayload,
111
147
  convertCategoryFormValuesToUpdatePayload,
112
148
  convertCategoryToFormValues,
113
- } from './BlogCategoryForm.helpers';
149
+ } from './CategoryForm.helpers';
114
150
 
115
151
  export default defineComponent({
116
152
  name: 'BlogCategoryForm',
117
153
  components: { FormFooter },
118
154
  setup(props, context) {
155
+ const { t } = useTranslation(context);
119
156
  const categoryId = computed<string>(
120
157
  () => context.root.$route.params.categoryId
121
158
  );
@@ -127,22 +164,28 @@ export default defineComponent({
127
164
  loading: isModuleConfigLoading,
128
165
  } = useModuleConfig({ context });
129
166
 
130
- const languageOptionList = computed<Array<OptionType>>(
131
- () =>
132
- moduleConfig.value?.languages.map((lang) => ({
133
- value: lang.id,
134
- label: lang.name,
135
- })) ?? []
167
+ const languageList = computed<Language[]>(
168
+ () => moduleConfig.value?.languages ?? []
169
+ );
170
+
171
+ const languageOptionList = computed<OptionType[]>(() =>
172
+ languageList.value.map(({ id, name }) => ({ value: id, label: name }))
173
+ );
174
+
175
+ const isLangSpecific = computed<boolean>(
176
+ () => languageOptionList.value.length > 0
136
177
  );
137
178
 
138
- const isLangSpecific = computed(() => languageOptionList.value.length > 0);
179
+ const imageScenario = computed<string | null>(
180
+ () => moduleConfig.value?.fileScenarios?.openGraph ?? null
181
+ );
139
182
 
140
183
  /** Fetch Category */
141
184
 
142
185
  const [
143
186
  fetchCategory,
144
187
  { data: category, loading: isCategoryLoading },
145
- ] = useResource<Nullable<BlogCategory>>({
188
+ ] = useResource<Nullable<Category>>({
146
189
  fetchResource: () => getBlogCategory(categoryId.value),
147
190
  initialValue: null,
148
191
  context,
@@ -150,13 +193,17 @@ export default defineComponent({
150
193
  });
151
194
 
152
195
  onMounted(() => {
153
- if (isCreation.value) return;
196
+ if (isCreation.value) {
197
+ return;
198
+ }
154
199
 
155
200
  fetchCategory();
156
201
  });
157
202
 
158
203
  watch(categoryId, () => {
159
- if (isCreation.value) return;
204
+ if (isCreation.value) {
205
+ return;
206
+ }
160
207
 
161
208
  fetchCategory();
162
209
  });
@@ -182,8 +229,10 @@ export default defineComponent({
182
229
  values.value
183
230
  );
184
231
  const updateBody = convertCategoryFormValuesToUpdatePayload(values.value);
185
- if(!isCreation.value){
186
- updateBody.language = category.value?.language ? category.value?.language : null;
232
+ if (!isCreation.value) {
233
+ updateBody.language = category.value?.language
234
+ ? category.value?.language
235
+ : null;
187
236
  }
188
237
 
189
238
  const requestPromise = isCreation.value
@@ -242,10 +291,32 @@ export default defineComponent({
242
291
  );
243
292
 
244
293
  const categoryPagePath = computed<string>(() => {
245
- const origin = process.env.VUE_APP_WEBSITE_URL || window.location.origin;
246
- return origin + (category.value?.urlTemplate ?? '');
294
+ return `${getWebsiteOrigin()}${category.value?.urlTemplate ?? ''}`;
247
295
  });
248
296
 
297
+ // SEO
298
+
299
+ function handleSeoFieldGroupChange({
300
+ title,
301
+ description,
302
+ keywords,
303
+ image,
304
+ }: SeoChangeEvent) {
305
+ values.value.pageTitle = title;
306
+ values.value.pageDescription = description;
307
+ values.value.pageKeywords = keywords;
308
+ values.value.openGraphImage = image;
309
+ }
310
+
311
+ /** Tabs */
312
+
313
+ const tabList = computed<TabType[]>(() => [
314
+ { id: 'common', label: t('blog:tabs.common') },
315
+ { id: 'seo', label: t('blog:tabs.seo') },
316
+ ]);
317
+
318
+ const selectedTabId = ref<string>(tabList.value[0].id);
319
+
249
320
  return {
250
321
  isCreation,
251
322
  isContentLoading: isContentLoading,
@@ -257,10 +328,16 @@ export default defineComponent({
257
328
  categoryListUrl: getBlogCategoryListUrl(),
258
329
  submitForm,
259
330
  isSubmitting,
260
- moduleConfig
331
+ moduleConfig,
332
+
333
+ // SEO
334
+ imageScenario,
335
+ handleSeoFieldGroupChange,
336
+
337
+ // Tabs
338
+ tabList,
339
+ selectedTabId,
261
340
  };
262
341
  },
263
342
  });
264
343
  </script>
265
-
266
- <style scoped lang="scss"></style>
@@ -0,0 +1 @@
1
+ export { default } from './CategoryForm.vue';