pukaad-ui-lib 1.420.0 → 1.422.0

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 (40) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/card/card-reaction.d.vue.ts +2 -0
  3. package/dist/runtime/components/card/card-reaction.vue +39 -22
  4. package/dist/runtime/components/card/card-reaction.vue.d.ts +2 -0
  5. package/dist/runtime/components/display/display-video.d.vue.ts +4 -4
  6. package/dist/runtime/components/display/display-video.vue.d.ts +4 -4
  7. package/dist/runtime/components/drawer/drawer-post-content.d.vue.ts +4 -1
  8. package/dist/runtime/components/drawer/drawer-post-content.vue +20 -5
  9. package/dist/runtime/components/drawer/drawer-post-content.vue.d.ts +4 -1
  10. package/dist/runtime/components/image/image-cropper.d.vue.ts +3 -3
  11. package/dist/runtime/components/image/image-cropper.vue.d.ts +3 -3
  12. package/dist/runtime/components/input/input-address.d.vue.ts +1 -1
  13. package/dist/runtime/components/input/input-address.vue.d.ts +1 -1
  14. package/dist/runtime/components/input/input-autocomplete.d.vue.ts +1 -1
  15. package/dist/runtime/components/input/input-autocomplete.vue.d.ts +1 -1
  16. package/dist/runtime/components/input/input-combobox.d.vue.ts +1 -1
  17. package/dist/runtime/components/input/input-combobox.vue.d.ts +1 -1
  18. package/dist/runtime/components/input/input-file.d.vue.ts +1 -1
  19. package/dist/runtime/components/input/input-file.vue.d.ts +1 -1
  20. package/dist/runtime/components/input/input-localized-name.d.vue.ts +1 -1
  21. package/dist/runtime/components/input/input-localized-name.vue.d.ts +1 -1
  22. package/dist/runtime/components/input/input-password.d.vue.ts +1 -1
  23. package/dist/runtime/components/input/input-password.vue.d.ts +1 -1
  24. package/dist/runtime/components/input/input-personal-card-id.d.vue.ts +2 -2
  25. package/dist/runtime/components/input/input-personal-card-id.vue.d.ts +2 -2
  26. package/dist/runtime/components/input/input-suggest.d.vue.ts +1 -1
  27. package/dist/runtime/components/input/input-suggest.vue.d.ts +1 -1
  28. package/dist/runtime/components/input/input-tag.d.vue.ts +3 -1
  29. package/dist/runtime/components/input/input-tag.vue +83 -31
  30. package/dist/runtime/components/input/input-tag.vue.d.ts +3 -1
  31. package/dist/runtime/components/input/input-text-field.d.vue.ts +1 -1
  32. package/dist/runtime/components/input/input-text-field.vue.d.ts +1 -1
  33. package/dist/runtime/components/input/input-textarea.d.vue.ts +1 -1
  34. package/dist/runtime/components/input/input-textarea.vue.d.ts +1 -1
  35. package/dist/runtime/components/modal/modal-password-confirmed.d.vue.ts +1 -1
  36. package/dist/runtime/components/modal/modal-password-confirmed.vue.d.ts +1 -1
  37. package/dist/runtime/components/modal/modal-password-verify.d.vue.ts +1 -1
  38. package/dist/runtime/components/modal/modal-password-verify.vue.d.ts +1 -1
  39. package/dist/runtime/composables/useApi.js +4 -0
  40. package/package.json +1 -1
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pukaad-ui-lib",
3
3
  "configKey": "pukaadUI",
4
- "version": "1.420.0",
4
+ "version": "1.422.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -20,6 +20,7 @@ type __VLS_Props = {
20
20
  iconSize?: number | string;
21
21
  id?: string;
22
22
  state?: string;
23
+ loginPath?: string;
23
24
  };
24
25
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
25
26
  disabled: boolean;
@@ -32,6 +33,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
32
33
  disabledDividerBottom: boolean;
33
34
  iconSize: number | string;
34
35
  state: string;
36
+ loginPath: string;
35
37
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
36
38
  declare const _default: typeof __VLS_export;
37
39
  export default _default;
@@ -32,8 +32,14 @@
32
32
 
33
33
  <script setup>
34
34
  import { computed, reactive, ref, watch } from "vue";
35
+ import { navigateTo, useCookie, useRoute, useRuntimeConfig } from "nuxt/app";
35
36
  import { useApi } from "../../composables/useApi";
36
37
  const api = useApi();
38
+ const route = useRoute();
39
+ const config = useRuntimeConfig();
40
+ const secId = useCookie(
41
+ config.public.APP_TYPE === "OFFICE" ? "OFFICE_SEC_ID" : "SEC_ID"
42
+ );
37
43
  const isOpenModalShare = ref(false);
38
44
  const props = defineProps({
39
45
  itemCount: { type: Object, required: false, default: () => ({
@@ -54,7 +60,8 @@ const props = defineProps({
54
60
  disabled: { type: Boolean, required: false, default: false },
55
61
  iconSize: { type: [Number, String], required: false, default: 20 },
56
62
  id: { type: String, required: false, default: "" },
57
- state: { type: String, required: false, default: "" }
63
+ state: { type: String, required: false, default: "" },
64
+ loginPath: { type: String, required: false, default: "/auth/login" }
58
65
  });
59
66
  const computedPadding = computed(
60
67
  () => props.disabledPadding ? 0 : props.padding
@@ -114,37 +121,46 @@ const reactions = [
114
121
  action: toggleShared
115
122
  }
116
123
  ];
124
+ function redirectToLogin() {
125
+ return navigateTo({
126
+ path: props.loginPath,
127
+ query: { redirect: route.fullPath }
128
+ });
129
+ }
130
+ function ensureLoggedIn() {
131
+ if (secId.value) return true;
132
+ redirectToLogin();
133
+ return false;
134
+ }
135
+ function isUnauthorized(error) {
136
+ const e = error;
137
+ return (e?.status ?? e?.statusCode) === 401;
138
+ }
117
139
  async function toggleLike() {
140
+ const likeUrl = props.state === "blog" && props.id ? `/blogs/${props.id}/like` : props.state === "content" && props.id ? `/contents/${props.id}/like` : "";
141
+ if (likeUrl && !ensureLoggedIn()) return;
118
142
  const previousLiked = reaction.liked;
119
143
  reaction.liked = !previousLiked;
120
144
  counts.liked += reaction.liked ? 1 : -1;
121
- if (props.state === "blog" && props.id) {
122
- try {
123
- await api(`/blogs/${props.id}/like`, {
124
- method: reaction.liked ? "POST" : "DELETE"
125
- });
126
- } catch (error) {
127
- console.error(error);
128
- reaction.liked = previousLiked;
129
- counts.liked += previousLiked ? 1 : -1;
130
- }
131
- } else if (props.state === "content" && props.id) {
132
- try {
133
- await api(`/contents/${props.id}/like`, {
134
- method: reaction.liked ? "POST" : "DELETE"
135
- });
136
- } catch (error) {
137
- console.error(error);
138
- reaction.liked = previousLiked;
139
- counts.liked += previousLiked ? 1 : -1;
140
- }
145
+ if (!likeUrl) return;
146
+ try {
147
+ await api(likeUrl, {
148
+ method: reaction.liked ? "POST" : "DELETE"
149
+ });
150
+ } catch (error) {
151
+ console.error(error);
152
+ reaction.liked = previousLiked;
153
+ counts.liked += previousLiked ? 1 : -1;
154
+ if (isUnauthorized(error)) redirectToLogin();
141
155
  }
142
156
  }
143
157
  async function toggleBookmark() {
158
+ const canCallApi = props.state === "content" && !!props.id;
159
+ if (canCallApi && !ensureLoggedIn()) return;
144
160
  const previousBookmarked = reaction.bookmarked;
145
161
  reaction.bookmarked = !previousBookmarked;
146
162
  counts.bookmarked += reaction.bookmarked ? 1 : -1;
147
- if (props.state === "content" && props.id) {
163
+ if (canCallApi) {
148
164
  try {
149
165
  await api(`/contents/${props.id}/bookmark`, {
150
166
  method: reaction.bookmarked ? "POST" : "DELETE"
@@ -153,6 +169,7 @@ async function toggleBookmark() {
153
169
  console.error(error);
154
170
  reaction.bookmarked = previousBookmarked;
155
171
  counts.bookmarked += previousBookmarked ? 1 : -1;
172
+ if (isUnauthorized(error)) redirectToLogin();
156
173
  }
157
174
  }
158
175
  }
@@ -20,6 +20,7 @@ type __VLS_Props = {
20
20
  iconSize?: number | string;
21
21
  id?: string;
22
22
  state?: string;
23
+ loginPath?: string;
23
24
  };
24
25
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
25
26
  disabled: boolean;
@@ -32,6 +33,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
32
33
  disabledDividerBottom: boolean;
33
34
  iconSize: number | string;
34
35
  state: string;
36
+ loginPath: string;
35
37
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
36
38
  declare const _default: typeof __VLS_export;
37
39
  export default _default;
@@ -81,16 +81,16 @@ declare const __VLS_base: import("vue").DefineComponent<DisplayVideoProps, {
81
81
  toggle: () => void;
82
82
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
83
83
  close: (payload: DisplayVideoAction) => any;
84
- fullscreen: (payload: DisplayVideoAction) => any;
85
- "play-click": (payload: DisplayVideoAction) => any;
86
84
  prev: (payload: DisplayVideoAction) => any;
87
85
  next: (payload: DisplayVideoAction) => any;
86
+ fullscreen: (payload: DisplayVideoAction) => any;
87
+ "play-click": (payload: DisplayVideoAction) => any;
88
88
  }, string, import("vue").PublicProps, Readonly<DisplayVideoProps> & Readonly<{
89
89
  onClose?: ((payload: DisplayVideoAction) => any) | undefined;
90
- onFullscreen?: ((payload: DisplayVideoAction) => any) | undefined;
91
- "onPlay-click"?: ((payload: DisplayVideoAction) => any) | undefined;
92
90
  onPrev?: ((payload: DisplayVideoAction) => any) | undefined;
93
91
  onNext?: ((payload: DisplayVideoAction) => any) | undefined;
92
+ onFullscreen?: ((payload: DisplayVideoAction) => any) | undefined;
93
+ "onPlay-click"?: ((payload: DisplayVideoAction) => any) | undefined;
94
94
  }>, {
95
95
  src: string;
96
96
  fullHeight: boolean;
@@ -81,16 +81,16 @@ declare const __VLS_base: import("vue").DefineComponent<DisplayVideoProps, {
81
81
  toggle: () => void;
82
82
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
83
83
  close: (payload: DisplayVideoAction) => any;
84
- fullscreen: (payload: DisplayVideoAction) => any;
85
- "play-click": (payload: DisplayVideoAction) => any;
86
84
  prev: (payload: DisplayVideoAction) => any;
87
85
  next: (payload: DisplayVideoAction) => any;
86
+ fullscreen: (payload: DisplayVideoAction) => any;
87
+ "play-click": (payload: DisplayVideoAction) => any;
88
88
  }, string, import("vue").PublicProps, Readonly<DisplayVideoProps> & Readonly<{
89
89
  onClose?: ((payload: DisplayVideoAction) => any) | undefined;
90
- onFullscreen?: ((payload: DisplayVideoAction) => any) | undefined;
91
- "onPlay-click"?: ((payload: DisplayVideoAction) => any) | undefined;
92
90
  onPrev?: ((payload: DisplayVideoAction) => any) | undefined;
93
91
  onNext?: ((payload: DisplayVideoAction) => any) | undefined;
92
+ onFullscreen?: ((payload: DisplayVideoAction) => any) | undefined;
93
+ "onPlay-click"?: ((payload: DisplayVideoAction) => any) | undefined;
94
94
  }>, {
95
95
  src: string;
96
96
  fullHeight: boolean;
@@ -1,10 +1,11 @@
1
1
  import type { SelectPlaceItem } from "../input/input-select-place.vue.js";
2
2
  import type { MentionItem } from "../input/input-mention.vue.js";
3
- interface IFileItem {
3
+ export interface IFileItem {
4
4
  file?: File;
5
5
  url: string;
6
6
  description?: string;
7
7
  duration?: string;
8
+ mediaType?: "image" | "video";
8
9
  }
9
10
  interface SelectedTag {
10
11
  id: string;
@@ -24,6 +25,7 @@ export interface DrawerPostContentItem {
24
25
  }
25
26
  export interface DrawerPostContentProps {
26
27
  item?: DrawerPostContentItem;
28
+ state?: "personal" | "business";
27
29
  }
28
30
  type __VLS_Props = DrawerPostContentProps;
29
31
  declare const modelValue: import("vue").ModelRef<boolean | undefined, string, boolean | undefined, boolean | undefined>;
@@ -41,6 +43,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
41
43
  onSaveDraft?: ((data: unknown) => any) | undefined;
42
44
  }>, {
43
45
  item: DrawerPostContentItem;
46
+ state: "personal" | "business";
44
47
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
45
48
  declare const _default: typeof __VLS_export;
46
49
  export default _default;
@@ -105,6 +105,7 @@
105
105
  <Divider height="1" />
106
106
  <InputTag
107
107
  v-model="selectedTags"
108
+ :profile-scope="props.state"
108
109
  label="เพิ่มแท็ก"
109
110
  placeholder="กรอกชื่อแท็ก"
110
111
  />
@@ -171,7 +172,8 @@ const props = defineProps({
171
172
  tags: [],
172
173
  mentions: [],
173
174
  places: []
174
- }) }
175
+ }) },
176
+ state: { type: String, required: false, default: "personal" }
175
177
  });
176
178
  const modelValue = defineModel({ type: Boolean });
177
179
  const currentId = ref(void 0);
@@ -197,6 +199,7 @@ const hasCoverFile = computed(() => coverFiles.value.length > 0);
197
199
  const coverFileType = computed(() => {
198
200
  const first = coverFiles.value[0];
199
201
  if (!first) return null;
202
+ if (first.mediaType) return first.mediaType;
200
203
  return first.file?.type?.startsWith("video") ? "video" : "image";
201
204
  });
202
205
  const coverAccept = computed(() => {
@@ -319,6 +322,10 @@ const onClose = async () => {
319
322
  modelValue.value = false;
320
323
  };
321
324
  const onSaveDraft = async () => {
325
+ if (props.state === "business") {
326
+ await submitContent("DRAFT");
327
+ return;
328
+ }
322
329
  if (isSubmitting.value) return;
323
330
  isSubmitting.value = true;
324
331
  try {
@@ -331,13 +338,15 @@ const onSaveDraft = async () => {
331
338
  file: f.file,
332
339
  url: f.url,
333
340
  description: f.description,
334
- duration: f.duration
341
+ duration: f.duration,
342
+ mediaType: f.mediaType
335
343
  })),
336
344
  posterFiles: posterFiles.value.map((f) => ({
337
345
  file: f.file,
338
346
  url: f.url,
339
347
  description: f.description,
340
- duration: f.duration
348
+ duration: f.duration,
349
+ mediaType: f.mediaType
341
350
  })),
342
351
  description: description.value,
343
352
  tags: JSON.parse(JSON.stringify(selectedTags.value)),
@@ -373,6 +382,10 @@ const parseDurationToSeconds = (duration) => {
373
382
  if (Number.isNaN(m) || Number.isNaN(s)) return 0;
374
383
  return m * 60 + s;
375
384
  };
385
+ const endpointMap = {
386
+ personal: "/personal/contents",
387
+ business: "/business/contents"
388
+ };
376
389
  const submitContent = async (status) => {
377
390
  if (isSubmitting.value) return;
378
391
  if (!hasCoverText.value && !hasCoverFile.value) {
@@ -414,7 +427,8 @@ const submitContent = async (status) => {
414
427
  place_ids: selectedPlaces.value.map((p) => p.id),
415
428
  status
416
429
  };
417
- const endpoint = isEditMode.value ? `/personal/contents/${currentId.value}` : "/personal/contents";
430
+ const baseEndpoint = endpointMap[props.state];
431
+ const endpoint = isEditMode.value ? `${baseEndpoint}/${currentId.value}` : baseEndpoint;
418
432
  const method = isEditMode.value ? "PATCH" : "POST";
419
433
  const res = await api(
420
434
  endpoint,
@@ -426,7 +440,8 @@ const submitContent = async (status) => {
426
440
  if (currentId.value?.startsWith("draft_")) {
427
441
  await deleteDraft(currentId.value);
428
442
  }
429
- emit("success", res.data);
443
+ if (status === "DRAFT") emit("saveDraft", res.data);
444
+ else emit("success", res.data);
430
445
  modelValue.value = false;
431
446
  } catch (e) {
432
447
  const apiErr = e;
@@ -1,10 +1,11 @@
1
1
  import type { SelectPlaceItem } from "../input/input-select-place.vue.js";
2
2
  import type { MentionItem } from "../input/input-mention.vue.js";
3
- interface IFileItem {
3
+ export interface IFileItem {
4
4
  file?: File;
5
5
  url: string;
6
6
  description?: string;
7
7
  duration?: string;
8
+ mediaType?: "image" | "video";
8
9
  }
9
10
  interface SelectedTag {
10
11
  id: string;
@@ -24,6 +25,7 @@ export interface DrawerPostContentItem {
24
25
  }
25
26
  export interface DrawerPostContentProps {
26
27
  item?: DrawerPostContentItem;
28
+ state?: "personal" | "business";
27
29
  }
28
30
  type __VLS_Props = DrawerPostContentProps;
29
31
  declare const modelValue: import("vue").ModelRef<boolean | undefined, string, boolean | undefined, boolean | undefined>;
@@ -41,6 +43,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
41
43
  onSaveDraft?: ((data: unknown) => any) | undefined;
42
44
  }>, {
43
45
  item: DrawerPostContentItem;
46
+ state: "personal" | "business";
44
47
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
45
48
  declare const _default: typeof __VLS_export;
46
49
  export default _default;
@@ -64,15 +64,15 @@ declare const __VLS_export: import("vue").DefineComponent<ImageCropperProps, {
64
64
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<ImageCropperProps> & Readonly<{}>, {
65
65
  src: string;
66
66
  center: boolean;
67
- crossorigin: "" | "anonymous" | "use-credentials";
68
- background: boolean;
69
- modal: boolean;
70
67
  responsive: boolean;
71
68
  restore: boolean;
72
69
  checkCrossOrigin: boolean;
73
70
  checkOrientation: boolean;
71
+ crossorigin: "" | "anonymous" | "use-credentials";
72
+ modal: boolean;
74
73
  guides: boolean;
75
74
  highlight: boolean;
75
+ background: boolean;
76
76
  autoCrop: boolean;
77
77
  movable: boolean;
78
78
  rotatable: boolean;
@@ -64,15 +64,15 @@ declare const __VLS_export: import("vue").DefineComponent<ImageCropperProps, {
64
64
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<ImageCropperProps> & Readonly<{}>, {
65
65
  src: string;
66
66
  center: boolean;
67
- crossorigin: "" | "anonymous" | "use-credentials";
68
- background: boolean;
69
- modal: boolean;
70
67
  responsive: boolean;
71
68
  restore: boolean;
72
69
  checkCrossOrigin: boolean;
73
70
  checkOrientation: boolean;
71
+ crossorigin: "" | "anonymous" | "use-credentials";
72
+ modal: boolean;
74
73
  guides: boolean;
75
74
  highlight: boolean;
75
+ background: boolean;
76
76
  autoCrop: boolean;
77
77
  movable: boolean;
78
78
  rotatable: boolean;
@@ -50,10 +50,10 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
50
50
  name: string;
51
51
  required: boolean;
52
52
  placeholder: string;
53
+ gap: string;
53
54
  labelDetail: string;
54
55
  placeholderDetail: string;
55
56
  requiredDetail: boolean;
56
- gap: string;
57
57
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
58
58
  declare const _default: typeof __VLS_export;
59
59
  export default _default;
@@ -50,10 +50,10 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
50
50
  name: string;
51
51
  required: boolean;
52
52
  placeholder: string;
53
+ gap: string;
53
54
  labelDetail: string;
54
55
  placeholderDetail: string;
55
56
  requiredDetail: boolean;
56
- gap: string;
57
57
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
58
58
  declare const _default: typeof __VLS_export;
59
59
  export default _default;
@@ -70,9 +70,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
70
70
  required: boolean;
71
71
  description: string;
72
72
  options: AutocompleteOption[] | string[] | number[];
73
- limit: number;
74
73
  labelKey: string;
75
74
  placeholder: string;
75
+ limit: number;
76
76
  disabledErrorMessage: boolean;
77
77
  disabledBorder: boolean;
78
78
  showCounter: boolean;
@@ -70,9 +70,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
70
70
  required: boolean;
71
71
  description: string;
72
72
  options: AutocompleteOption[] | string[] | number[];
73
- limit: number;
74
73
  labelKey: string;
75
74
  placeholder: string;
75
+ limit: number;
76
76
  disabledErrorMessage: boolean;
77
77
  disabledBorder: boolean;
78
78
  showCounter: boolean;
@@ -59,8 +59,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
59
59
  }>, {
60
60
  pageSize: number;
61
61
  name: string;
62
- limit: number;
63
62
  labelKey: string;
63
+ limit: number;
64
64
  valueKey: string;
65
65
  multiple: boolean;
66
66
  placeholderSearch: string;
@@ -59,8 +59,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
59
59
  }>, {
60
60
  pageSize: number;
61
61
  name: string;
62
- limit: number;
63
62
  labelKey: string;
63
+ limit: number;
64
64
  valueKey: string;
65
65
  multiple: boolean;
66
66
  placeholderSearch: string;
@@ -35,8 +35,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
35
35
  fullHeight: boolean;
36
36
  fullWidth: boolean;
37
37
  limit: number;
38
- disabledErrorMessage: boolean;
39
38
  accept: string;
39
+ disabledErrorMessage: boolean;
40
40
  labelIcon: string;
41
41
  disabledDrop: boolean;
42
42
  column: boolean;
@@ -35,8 +35,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
35
35
  fullHeight: boolean;
36
36
  fullWidth: boolean;
37
37
  limit: number;
38
- disabledErrorMessage: boolean;
39
38
  accept: string;
39
+ disabledErrorMessage: boolean;
40
40
  labelIcon: string;
41
41
  disabledDrop: boolean;
42
42
  column: boolean;
@@ -12,8 +12,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
12
12
  name: string;
13
13
  modelValue: LocalizedNameItem[];
14
14
  options: InputSelectItem[];
15
- limit: number;
16
15
  placeholder: string;
16
+ limit: number;
17
17
  showCounter: boolean;
18
18
  addLabel: string;
19
19
  removable: boolean;
@@ -12,8 +12,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
12
12
  name: string;
13
13
  modelValue: LocalizedNameItem[];
14
14
  options: InputSelectItem[];
15
- limit: number;
16
15
  placeholder: string;
16
+ limit: number;
17
17
  showCounter: boolean;
18
18
  addLabel: string;
19
19
  removable: boolean;
@@ -22,8 +22,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
22
22
  }>, {
23
23
  id: string;
24
24
  name: string;
25
- new: boolean;
26
25
  disabledForgotPassword: boolean;
26
+ new: boolean;
27
27
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
28
28
  declare const _default: typeof __VLS_export;
29
29
  export default _default;
@@ -22,8 +22,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
22
22
  }>, {
23
23
  id: string;
24
24
  name: string;
25
- new: boolean;
26
25
  disabledForgotPassword: boolean;
26
+ new: boolean;
27
27
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
28
28
  declare const _default: typeof __VLS_export;
29
29
  export default _default;
@@ -11,12 +11,12 @@ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
11
11
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
12
12
  "update:modelValue": (value: File | null) => any;
13
13
  } & {
14
- remove: () => any;
15
14
  complete: (file: File) => any;
15
+ remove: () => any;
16
16
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
17
17
  "onUpdate:modelValue"?: ((value: File | null) => any) | undefined;
18
- onRemove?: (() => any) | undefined;
19
18
  onComplete?: ((file: File) => any) | undefined;
19
+ onRemove?: (() => any) | undefined;
20
20
  }>, {
21
21
  name: string;
22
22
  disabledErrorMessage: boolean;
@@ -11,12 +11,12 @@ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
11
11
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
12
12
  "update:modelValue": (value: File | null) => any;
13
13
  } & {
14
- remove: () => any;
15
14
  complete: (file: File) => any;
15
+ remove: () => any;
16
16
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
17
17
  "onUpdate:modelValue"?: ((value: File | null) => any) | undefined;
18
- onRemove?: (() => any) | undefined;
19
18
  onComplete?: ((file: File) => any) | undefined;
19
+ onRemove?: (() => any) | undefined;
20
20
  }>, {
21
21
  name: string;
22
22
  disabledErrorMessage: boolean;
@@ -20,9 +20,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
20
20
  name: string;
21
21
  required: boolean;
22
22
  description: string;
23
- limit: number;
24
23
  labelKey: string;
25
24
  placeholder: string;
25
+ limit: number;
26
26
  disabledErrorMessage: boolean;
27
27
  disabledBorder: boolean;
28
28
  showCounter: boolean;
@@ -20,9 +20,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
20
20
  name: string;
21
21
  required: boolean;
22
22
  description: string;
23
- limit: number;
24
23
  labelKey: string;
25
24
  placeholder: string;
25
+ limit: number;
26
26
  disabledErrorMessage: boolean;
27
27
  disabledBorder: boolean;
28
28
  showCounter: boolean;
@@ -13,6 +13,7 @@ interface InputTagProps {
13
13
  placeholder?: string;
14
14
  state?: "user" | "admin";
15
15
  ignore?: string[];
16
+ profileScope?: "personal" | "business";
16
17
  }
17
18
  type __VLS_Props = InputTagProps;
18
19
  type __VLS_ModelProps = {
@@ -26,9 +27,10 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
26
27
  }>, {
27
28
  name: string;
28
29
  state: "user" | "admin";
29
- limit: number;
30
30
  placeholder: string;
31
+ limit: number;
31
32
  ignore: string[];
33
+ profileScope: "personal" | "business";
32
34
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
33
35
  declare const _default: typeof __VLS_export;
34
36
  export default _default;
@@ -51,12 +51,15 @@
51
51
  <ShadCommand :filter="() => 1">
52
52
  <ShadCommandInput
53
53
  v-if="props.state === 'admin'"
54
+ v-model="searchQuery"
54
55
  placeholder="ค้นหาแท็ก"
55
56
  />
56
57
  <ShadTabs v-model="currentTab" class="w-full p-2">
57
58
  <ShadTabsList class="w-full grid grid-cols-2">
58
59
  <ShadTabsTrigger value="popular">ยอดนิยม</ShadTabsTrigger>
59
- <ShadTabsTrigger value="latest">ล่าสุด</ShadTabsTrigger>
60
+ <ShadTabsTrigger value="recent">{{
61
+ isOffice ? "\u0E25\u0E48\u0E32\u0E2A\u0E38\u0E14" : "\u0E43\u0E0A\u0E49\u0E07\u0E32\u0E19\u0E25\u0E48\u0E32\u0E2A\u0E38\u0E14"
62
+ }}</ShadTabsTrigger>
60
63
  </ShadTabsList>
61
64
 
62
65
  <!-- Popular Tab -->
@@ -87,7 +90,9 @@
87
90
  class="flex flex-col items-center justify-center h-full gap-2"
88
91
  >
89
92
  <Icon name="pukaad:tag-empty" class="h-[123px] w-[180px]" />
90
- <div class="text-gray font-body-medium">ไม่พบข้อมูล</div>
93
+ <div class="text-gray font-body-medium">
94
+ {{ popularState.error || "\u0E44\u0E21\u0E48\u0E1E\u0E1A\u0E02\u0E49\u0E2D\u0E21\u0E39\u0E25" }}
95
+ </div>
91
96
  </div>
92
97
  </ShadCommandEmpty>
93
98
 
@@ -104,12 +109,12 @@
104
109
  </ShadCommandList>
105
110
  </ShadTabsContent>
106
111
 
107
- <!-- Latest Tab -->
108
- <ShadTabsContent value="latest" class="m-0">
112
+ <!-- Recent / Office latest tab -->
113
+ <ShadTabsContent value="recent" class="m-0">
109
114
  <ShadCommandList class="max-h-64 overflow-y-auto">
110
115
  <ShadCommandGroup>
111
116
  <ShadCommandItem
112
- v-for="tag in filteredLatestTags"
117
+ v-for="tag in filteredRecentTags"
113
118
  :key="tag.value"
114
119
  :value="tag.value"
115
120
  class="flex w-full justify-between"
@@ -125,19 +130,23 @@
125
130
  <!-- Initital Loading / Empty State -->
126
131
  <ShadCommandEmpty
127
132
  :force-show="
128
- filteredLatestTags.length === 0 && !latestState.loading
133
+ filteredRecentTags.length === 0 && !recentState.loading
129
134
  "
130
135
  >
131
136
  <div
132
137
  class="flex flex-col items-center justify-center h-full gap-2"
133
138
  >
134
139
  <Icon name="pukaad:tag-empty" class="h-[123px] w-[180px]" />
135
- <div class="text-gray font-body-medium">ไม่พบข้อมูล</div>
140
+ <div class="text-gray font-body-medium">
141
+ {{
142
+ recentState.error || (!isOffice && !searchQuery && recentState.data.length === 0 ? "\u0E22\u0E31\u0E07\u0E44\u0E21\u0E48\u0E21\u0E35\u0E41\u0E17\u0E47\u0E01\u0E17\u0E35\u0E48\u0E43\u0E0A\u0E49\u0E07\u0E32\u0E19\u0E25\u0E48\u0E32\u0E2A\u0E38\u0E14" : "\u0E44\u0E21\u0E48\u0E1E\u0E1A\u0E02\u0E49\u0E2D\u0E21\u0E39\u0E25")
143
+ }}
144
+ </div>
136
145
  </div>
137
146
  </ShadCommandEmpty>
138
147
 
139
148
  <!-- Loading Indicator -->
140
- <div v-if="latestState.loading" class="flex justify-center p-2">
149
+ <div v-if="recentState.loading" class="flex justify-center p-2">
141
150
  <Icon
142
151
  name="lucide:loader-2"
143
152
  class="animate-spin h-5 w-5 text-primary"
@@ -145,7 +154,7 @@
145
154
  </div>
146
155
 
147
156
  <!-- Sentinel for Infinite Scroll -->
148
- <div ref="latestSentinel" class="h-1" />
157
+ <div ref="recentSentinel" class="h-1" />
149
158
  </ShadCommandList>
150
159
  </ShadTabsContent>
151
160
  </ShadTabs>
@@ -155,13 +164,21 @@
155
164
  </template>
156
165
 
157
166
  <script setup>
158
- import { ref, computed, onMounted, reactive, watch } from "vue";
159
- import { useNuxtApp } from "nuxt/app";
167
+ import { ref, computed, reactive, watch } from "vue";
168
+ import { useNuxtApp, useRuntimeConfig, useCookie } from "nuxt/app";
160
169
  import { useApi } from "../../composables/useApi";
161
170
  import { useIntersectionObserver } from "@vueuse/core";
162
171
  const { $convert } = useNuxtApp();
163
172
  const convertNumber = (val) => $convert?.convertNumber(val) || val;
164
173
  const api = useApi();
174
+ const config = useRuntimeConfig();
175
+ const isOffice = computed(
176
+ () => props.state === "admin" || config.public.APP_TYPE === "OFFICE"
177
+ );
178
+ const businessProfileId = useCookie(
179
+ "PUKAAD_BUSINESS_PROFILE_ID"
180
+ );
181
+ const sessionId = useCookie("SEC_ID");
165
182
  const props = defineProps({
166
183
  name: { type: String, required: false, default: "input-tag" },
167
184
  rules: { type: [Object, String, Function], required: false },
@@ -171,7 +188,8 @@ const props = defineProps({
171
188
  limit: { type: Number, required: false, default: 0 },
172
189
  placeholder: { type: String, required: false, default: "\u0E04\u0E49\u0E19\u0E2B\u0E32\u0E41\u0E17\u0E47\u0E01..." },
173
190
  state: { type: String, required: false, default: "user" },
174
- ignore: { type: Array, required: false, default: () => [] }
191
+ ignore: { type: Array, required: false, default: () => [] },
192
+ profileScope: { type: String, required: false, default: "personal" }
175
193
  });
176
194
  const modelValue = defineModel({ type: Array, ...{
177
195
  default: () => []
@@ -185,13 +203,15 @@ const popularState = reactive({
185
203
  data: [],
186
204
  page: 1,
187
205
  hasMore: true,
188
- loading: false
206
+ loading: false,
207
+ error: ""
189
208
  });
190
- const latestState = reactive({
209
+ const recentState = reactive({
191
210
  data: [],
192
211
  page: 1,
193
212
  hasMore: true,
194
- loading: false
213
+ loading: false,
214
+ error: ""
195
215
  });
196
216
  const mapTag = (tag) => ({
197
217
  label: tag.name,
@@ -199,14 +219,23 @@ const mapTag = (tag) => ({
199
219
  postCount: tag.usage_count
200
220
  });
201
221
  const PAGE_SIZE = 20;
222
+ let requestGeneration = 0;
202
223
  const fetchTags = async (type) => {
203
- const state = type === "popular" ? popularState : latestState;
224
+ const state = type === "popular" ? popularState : recentState;
204
225
  if (state.loading || !state.hasMore) return;
205
226
  state.loading = true;
227
+ state.error = "";
228
+ const generation = requestGeneration;
206
229
  try {
207
- const res = await api(
208
- `/tags/${type}?page=${state.page}&page_size=${PAGE_SIZE}`
209
- );
230
+ const endpoint = type === "recent" && isOffice.value ? "latest" : type;
231
+ const res = await api(`/tags/${endpoint}`, {
232
+ query: {
233
+ page: state.page,
234
+ page_size: PAGE_SIZE,
235
+ scope: props.profileScope
236
+ }
237
+ });
238
+ if (generation !== requestGeneration) return;
210
239
  if (res.data && Array.isArray(res.data)) {
211
240
  const newTags = res.data.map(mapTag);
212
241
  state.data = [...state.data, ...newTags];
@@ -216,42 +245,65 @@ const fetchTags = async (type) => {
216
245
  state.hasMore = false;
217
246
  }
218
247
  } catch (e) {
248
+ if (generation !== requestGeneration) return;
219
249
  console.error(`Failed to fetch ${type} tags:`, e);
220
- state.hasMore = false;
250
+ state.error = "\u0E42\u0E2B\u0E25\u0E14\u0E41\u0E17\u0E47\u0E01\u0E44\u0E21\u0E48\u0E2A\u0E33\u0E40\u0E23\u0E47\u0E08 \u0E01\u0E23\u0E38\u0E13\u0E32\u0E1B\u0E34\u0E14\u0E41\u0E25\u0E49\u0E27\u0E40\u0E1B\u0E34\u0E14\u0E43\u0E2B\u0E21\u0E48";
221
251
  } finally {
222
- state.loading = false;
252
+ if (generation === requestGeneration) state.loading = false;
223
253
  }
224
254
  };
225
255
  watch(popoverOpen, (isOpen) => {
226
256
  if (isOpen) {
257
+ resetLists();
227
258
  if (currentTab.value === "popular" && popularState.data.length === 0) {
228
259
  fetchTags("popular");
229
- } else if (currentTab.value === "latest" && latestState.data.length === 0) {
230
- fetchTags("latest");
260
+ } else if (currentTab.value === "recent" && recentState.data.length === 0) {
261
+ fetchTags("recent");
231
262
  }
232
263
  }
233
264
  });
234
265
  const popularSentinel = ref(null);
235
- const latestSentinel = ref(null);
266
+ const recentSentinel = ref(null);
236
267
  useIntersectionObserver(popularSentinel, (entries) => {
237
268
  const entry = entries[0];
238
269
  if (entry?.isIntersecting && currentTab.value === "popular" && popoverOpen.value) {
239
270
  fetchTags("popular");
240
271
  }
241
272
  });
242
- useIntersectionObserver(latestSentinel, (entries) => {
273
+ useIntersectionObserver(recentSentinel, (entries) => {
243
274
  const entry = entries[0];
244
- if (entry?.isIntersecting && currentTab.value === "latest" && popoverOpen.value) {
245
- fetchTags("latest");
275
+ if (entry?.isIntersecting && currentTab.value === "recent" && popoverOpen.value) {
276
+ fetchTags("recent");
246
277
  }
247
278
  });
248
279
  watch(currentTab, (newTab) => {
280
+ if (!popoverOpen.value) return;
249
281
  if (newTab === "popular" && popularState.data.length === 0) {
250
282
  fetchTags("popular");
251
- } else if (newTab === "latest" && latestState.data.length === 0) {
252
- fetchTags("latest");
283
+ } else if (newTab === "recent" && recentState.data.length === 0) {
284
+ fetchTags("recent");
253
285
  }
254
286
  });
287
+ const resetLists = () => {
288
+ requestGeneration++;
289
+ for (const state of [popularState, recentState]) {
290
+ Object.assign(state, {
291
+ data: [],
292
+ page: 1,
293
+ hasMore: true,
294
+ loading: false,
295
+ error: ""
296
+ });
297
+ }
298
+ };
299
+ watch(
300
+ [sessionId, businessProfileId, () => props.profileScope, isOffice],
301
+ () => {
302
+ resetLists();
303
+ if (popoverOpen.value)
304
+ fetchTags(currentTab.value === "popular" ? "popular" : "recent");
305
+ }
306
+ );
255
307
  const selectedTags = computed(() => modelValue.value);
256
308
  const isLimitReached = computed(() => {
257
309
  if (props.limit <= 0) return false;
@@ -286,7 +338,7 @@ const filterTags = (tags) => {
286
338
  );
287
339
  };
288
340
  const filteredPopularTags = computed(() => filterTags(popularState.data));
289
- const filteredLatestTags = computed(() => filterTags(latestState.data));
341
+ const filteredRecentTags = computed(() => filterTags(recentState.data));
290
342
  const onSelectTag = (tag) => {
291
343
  if (isSelected(tag.value)) {
292
344
  modelValue.value = modelValue.value.filter((t) => t.id !== tag.value);
@@ -318,7 +370,7 @@ const onAddCustomTag = () => {
318
370
  return;
319
371
  }
320
372
  const newValue = trimmedQuery.toLowerCase().replace(/\s+/g, "-");
321
- const existingTag = [...popularState.data, ...latestState.data].find(
373
+ const existingTag = [...popularState.data, ...recentState.data].find(
322
374
  (tag) => tag.value === newValue || tag.label === trimmedQuery
323
375
  );
324
376
  if (existingTag) {
@@ -13,6 +13,7 @@ interface InputTagProps {
13
13
  placeholder?: string;
14
14
  state?: "user" | "admin";
15
15
  ignore?: string[];
16
+ profileScope?: "personal" | "business";
16
17
  }
17
18
  type __VLS_Props = InputTagProps;
18
19
  type __VLS_ModelProps = {
@@ -26,9 +27,10 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
26
27
  }>, {
27
28
  name: string;
28
29
  state: "user" | "admin";
29
- limit: number;
30
30
  placeholder: string;
31
+ limit: number;
31
32
  ignore: string[];
33
+ profileScope: "personal" | "business";
32
34
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
33
35
  declare const _default: typeof __VLS_export;
34
36
  export default _default;
@@ -59,8 +59,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
59
59
  id: string;
60
60
  name: string;
61
61
  required: boolean;
62
- limit: number;
63
62
  readonly: boolean;
63
+ limit: number;
64
64
  disabledErrorMessage: boolean;
65
65
  disabledBorder: boolean;
66
66
  showCounter: boolean;
@@ -59,8 +59,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
59
59
  id: string;
60
60
  name: string;
61
61
  required: boolean;
62
- limit: number;
63
62
  readonly: boolean;
63
+ limit: number;
64
64
  disabledErrorMessage: boolean;
65
65
  disabledBorder: boolean;
66
66
  showCounter: boolean;
@@ -46,9 +46,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
46
46
  resize: "none" | "both" | "horizontal" | "vertical";
47
47
  fullHeight: boolean;
48
48
  fullWidth: boolean;
49
- limit: number;
50
49
  rows: number;
51
50
  readonly: boolean;
51
+ limit: number;
52
52
  disabledErrorMessage: boolean;
53
53
  disabledBorder: boolean;
54
54
  showCounter: boolean;
@@ -46,9 +46,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
46
46
  resize: "none" | "both" | "horizontal" | "vertical";
47
47
  fullHeight: boolean;
48
48
  fullWidth: boolean;
49
- limit: number;
50
49
  rows: number;
51
50
  readonly: boolean;
51
+ limit: number;
52
52
  disabledErrorMessage: boolean;
53
53
  disabledBorder: boolean;
54
54
  showCounter: boolean;
@@ -24,8 +24,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
24
24
  }) => any) | undefined;
25
25
  }>, {
26
26
  title: string;
27
- disabledForgotPassword: boolean;
28
27
  confirmText: string;
28
+ disabledForgotPassword: boolean;
29
29
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
30
30
  declare const _default: typeof __VLS_export;
31
31
  export default _default;
@@ -24,8 +24,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
24
24
  }) => any) | undefined;
25
25
  }>, {
26
26
  title: string;
27
- disabledForgotPassword: boolean;
28
27
  confirmText: string;
28
+ disabledForgotPassword: boolean;
29
29
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
30
30
  declare const _default: typeof __VLS_export;
31
31
  export default _default;
@@ -32,8 +32,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
32
32
  }>, {
33
33
  title: string;
34
34
  mode: "login" | "secure";
35
- disabledForgotPassword: boolean;
36
35
  confirmText: string;
36
+ disabledForgotPassword: boolean;
37
37
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
38
38
  declare const _default: typeof __VLS_export;
39
39
  export default _default;
@@ -32,8 +32,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
32
32
  }>, {
33
33
  title: string;
34
34
  mode: "login" | "secure";
35
- disabledForgotPassword: boolean;
36
35
  confirmText: string;
36
+ disabledForgotPassword: boolean;
37
37
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
38
38
  declare const _default: typeof __VLS_export;
39
39
  export default _default;
@@ -3,6 +3,7 @@ export const useApi = () => {
3
3
  const config = useRuntimeConfig();
4
4
  const baseURL = config.public.BASE_URL_API;
5
5
  const provinceIdState = useState("pukaad:province-id", () => null);
6
+ const businessProfileId = useCookie("PUKAAD_BUSINESS_PROFILE_ID");
6
7
  const configProvinceId = config.public.APP_PROVINCE_ID;
7
8
  const { APP_TYPE } = config.public;
8
9
  const secId = useCookie(
@@ -22,6 +23,9 @@ export const useApi = () => {
22
23
  if (pid) {
23
24
  headers.set("X-Province-ID", String(pid));
24
25
  }
26
+ if (businessProfileId.value) {
27
+ headers.set("X-Business-Profile-ID", businessProfileId.value);
28
+ }
25
29
  options.headers = headers;
26
30
  },
27
31
  onResponseError({ response }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pukaad-ui-lib",
3
- "version": "1.420.0",
3
+ "version": "1.422.0",
4
4
  "description": "pukaad-ui for MeMSG",
5
5
  "repository": {
6
6
  "type": "git",