@finema/finework-layer 2.0.15 → 2.0.17
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/CHANGELOG.md +4 -0
- package/app/components/Bookmark/BookmarkButton.vue +267 -0
- package/app/components/Layout/Admin/index.vue +1 -0
- package/app/components/Layout/User/index.vue +1 -0
- package/app/constants/routes.ts +6 -0
- package/app/loaders/bookmarks/bookmarks.ts +53 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.0.17](https://gitlab.finema.co/finema/finework/finework-frontend-layer/compare/2.0.16...2.0.17) (2026-07-31)
|
|
4
|
+
|
|
5
|
+
## [2.0.16](https://gitlab.finema.co/finema/finework/finework-frontend-layer/compare/2.0.15...2.0.16) (2026-07-31)
|
|
6
|
+
|
|
3
7
|
## [2.0.15](https://gitlab.finema.co/finema/finework/finework-frontend-layer/compare/2.0.14...2.0.15) (2026-07-27)
|
|
4
8
|
|
|
5
9
|
## [2.0.14](https://gitlab.finema.co/finema/finework/finework-frontend-layer/compare/2.0.13...2.0.14) (2026-07-22)
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Popover
|
|
3
|
+
v-model:open="isOpen"
|
|
4
|
+
:content="{
|
|
5
|
+
align: 'end',
|
|
6
|
+
side: 'bottom',
|
|
7
|
+
sideOffset: 8,
|
|
8
|
+
}"
|
|
9
|
+
>
|
|
10
|
+
<Icon
|
|
11
|
+
name="ph:star"
|
|
12
|
+
class="size-6 cursor-pointer"
|
|
13
|
+
title="Add bookmark"
|
|
14
|
+
/>
|
|
15
|
+
|
|
16
|
+
<template #content>
|
|
17
|
+
<Card class="w-[360px]">
|
|
18
|
+
<div class="mb-3 flex items-center justify-between">
|
|
19
|
+
<h3 class="text-base font-semibold">
|
|
20
|
+
Add bookmark
|
|
21
|
+
</h3>
|
|
22
|
+
<button
|
|
23
|
+
type="button"
|
|
24
|
+
class="flex size-7 cursor-pointer items-center justify-center rounded-full text-gray-400 hover:bg-gray-100"
|
|
25
|
+
@click="isOpen = false"
|
|
26
|
+
>
|
|
27
|
+
<Icon
|
|
28
|
+
name="ph:x"
|
|
29
|
+
class="size-4"
|
|
30
|
+
/>
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="flex gap-3">
|
|
35
|
+
<div class="flex size-16 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-gray-100">
|
|
36
|
+
<Loader :loading="preview.status.value.isLoading">
|
|
37
|
+
<img
|
|
38
|
+
v-if="thumbnailUrl"
|
|
39
|
+
:src="thumbnailUrl"
|
|
40
|
+
class="size-16 object-cover"
|
|
41
|
+
/>
|
|
42
|
+
<Icon
|
|
43
|
+
v-else
|
|
44
|
+
name="ph:link"
|
|
45
|
+
class="size-6 text-gray-400"
|
|
46
|
+
/>
|
|
47
|
+
</Loader>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div class="flex flex-1 flex-col gap-2">
|
|
51
|
+
<Input
|
|
52
|
+
v-model="url"
|
|
53
|
+
placeholder="Paste the link you want to bookmark"
|
|
54
|
+
size="lg"
|
|
55
|
+
@blur="onFetchPreview"
|
|
56
|
+
@keyup.enter="onFetchPreview"
|
|
57
|
+
/>
|
|
58
|
+
<Input
|
|
59
|
+
v-model="name"
|
|
60
|
+
placeholder="Name"
|
|
61
|
+
size="lg"
|
|
62
|
+
/>
|
|
63
|
+
<div
|
|
64
|
+
v-if="!isCreatingFolder"
|
|
65
|
+
class="flex gap-2"
|
|
66
|
+
>
|
|
67
|
+
<Select
|
|
68
|
+
v-if="folderOptions.length > 0"
|
|
69
|
+
v-model="folderId"
|
|
70
|
+
:items="folderOptions"
|
|
71
|
+
placeholder="Folder"
|
|
72
|
+
size="lg"
|
|
73
|
+
class="flex-1"
|
|
74
|
+
/>
|
|
75
|
+
<Button
|
|
76
|
+
icon="ph:folder-plus"
|
|
77
|
+
variant="outline"
|
|
78
|
+
color="neutral"
|
|
79
|
+
size="lg"
|
|
80
|
+
:class="{ 'flex-1 justify-center': folderOptions.length === 0 }"
|
|
81
|
+
:label="folderOptions.length === 0 ? 'New folder' : undefined"
|
|
82
|
+
title="New folder"
|
|
83
|
+
@click="isCreatingFolder = true"
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
<div
|
|
87
|
+
v-else
|
|
88
|
+
class="flex gap-2"
|
|
89
|
+
>
|
|
90
|
+
<Input
|
|
91
|
+
v-model="newFolderName"
|
|
92
|
+
placeholder="New folder name"
|
|
93
|
+
size="lg"
|
|
94
|
+
autofocus
|
|
95
|
+
@keyup.enter="onCreateFolder"
|
|
96
|
+
/>
|
|
97
|
+
<Button
|
|
98
|
+
icon="ph:check"
|
|
99
|
+
size="lg"
|
|
100
|
+
:loading="folders.add.status.isLoading"
|
|
101
|
+
:disabled="!newFolderName"
|
|
102
|
+
@click="onCreateFolder"
|
|
103
|
+
/>
|
|
104
|
+
<Button
|
|
105
|
+
icon="ph:x"
|
|
106
|
+
size="lg"
|
|
107
|
+
variant="outline"
|
|
108
|
+
color="neutral"
|
|
109
|
+
@click="isCreatingFolder = false; newFolderName = ''"
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div class="mt-4 flex justify-end gap-2">
|
|
116
|
+
<Button
|
|
117
|
+
v-if="isBookmarked"
|
|
118
|
+
variant="outline"
|
|
119
|
+
color="neutral"
|
|
120
|
+
:loading="bookmarks.delete.status.isLoading"
|
|
121
|
+
@click="onRemove"
|
|
122
|
+
>
|
|
123
|
+
Remove
|
|
124
|
+
</Button>
|
|
125
|
+
<Button
|
|
126
|
+
:loading="bookmarks.add.status.isLoading || bookmarks.update.status.isLoading"
|
|
127
|
+
:disabled="!url || !name"
|
|
128
|
+
@click="onSave"
|
|
129
|
+
>
|
|
130
|
+
Done
|
|
131
|
+
</Button>
|
|
132
|
+
</div>
|
|
133
|
+
</Card>
|
|
134
|
+
</template>
|
|
135
|
+
</Popover>
|
|
136
|
+
</template>
|
|
137
|
+
|
|
138
|
+
<script lang="ts" setup>
|
|
139
|
+
import { useBookmarkFoldersLoader, useBookmarkPreviewLoader, useBookmarksLoader } from '#layer/app/loaders/bookmarks/bookmarks'
|
|
140
|
+
|
|
141
|
+
const isOpen = ref(false)
|
|
142
|
+
const url = ref('')
|
|
143
|
+
const name = ref('')
|
|
144
|
+
const folderId = ref<string | undefined>(undefined)
|
|
145
|
+
const thumbnailUrl = ref<string | null>(null)
|
|
146
|
+
const isCreatingFolder = ref(false)
|
|
147
|
+
const newFolderName = ref('')
|
|
148
|
+
|
|
149
|
+
const bookmarks = useBookmarksLoader()
|
|
150
|
+
const folders = useBookmarkFoldersLoader()
|
|
151
|
+
const preview = useBookmarkPreviewLoader()
|
|
152
|
+
const noti = useNotification()
|
|
153
|
+
const existing = computed(() => bookmarks.fetch.items.find((item) => item.url === url.value) || null)
|
|
154
|
+
const isBookmarked = computed(() => !!existing.value)
|
|
155
|
+
|
|
156
|
+
const folderOptions = computed(() => folders.fetch.items.map((folder) => ({
|
|
157
|
+
label: folder.name,
|
|
158
|
+
value: folder.id,
|
|
159
|
+
})))
|
|
160
|
+
|
|
161
|
+
const defaultFolderId = computed(() => folders.fetch.items[0]?.id)
|
|
162
|
+
|
|
163
|
+
onMounted(() => {
|
|
164
|
+
bookmarks.fetchAll()
|
|
165
|
+
folders.fetchAll()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
watch(isOpen, (open) => {
|
|
169
|
+
if (!open) return
|
|
170
|
+
|
|
171
|
+
isCreatingFolder.value = false
|
|
172
|
+
newFolderName.value = ''
|
|
173
|
+
url.value = ''
|
|
174
|
+
name.value = ''
|
|
175
|
+
thumbnailUrl.value = null
|
|
176
|
+
folderId.value = defaultFolderId.value
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
const onFetchPreview = () => {
|
|
180
|
+
if (!url.value) return
|
|
181
|
+
|
|
182
|
+
if (existing.value) {
|
|
183
|
+
name.value = existing.value.name
|
|
184
|
+
folderId.value = existing.value.folder_id ?? defaultFolderId.value
|
|
185
|
+
thumbnailUrl.value = existing.value.favicon_url || existing.value.image_url
|
|
186
|
+
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
preview.run({
|
|
191
|
+
params: {
|
|
192
|
+
url: url.value,
|
|
193
|
+
},
|
|
194
|
+
}).then(() => {
|
|
195
|
+
if (!preview.data.value) return
|
|
196
|
+
if (!name.value) name.value = preview.data.value.name
|
|
197
|
+
thumbnailUrl.value = preview.data.value.favicon_url || preview.data.value.image_url || null
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const onCreateFolder = async () => {
|
|
202
|
+
if (!newFolderName.value) return
|
|
203
|
+
|
|
204
|
+
await folders.addRun({
|
|
205
|
+
data: {
|
|
206
|
+
name: newFolderName.value,
|
|
207
|
+
},
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
if (folders.add.item) {
|
|
211
|
+
folderId.value = folders.add.item.id
|
|
212
|
+
folders.fetchAll()
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
newFolderName.value = ''
|
|
216
|
+
isCreatingFolder.value = false
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const onSave = async () => {
|
|
220
|
+
if (existing.value) {
|
|
221
|
+
await bookmarks.updateRun(existing.value.id, {
|
|
222
|
+
data: {
|
|
223
|
+
name: name.value,
|
|
224
|
+
folder_id: folderId.value,
|
|
225
|
+
},
|
|
226
|
+
})
|
|
227
|
+
} else {
|
|
228
|
+
await bookmarks.addRun({
|
|
229
|
+
data: {
|
|
230
|
+
url: url.value,
|
|
231
|
+
name: name.value,
|
|
232
|
+
folder_id: folderId.value,
|
|
233
|
+
},
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
isOpen.value = false
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const onRemove = async () => {
|
|
241
|
+
if (!existing.value) return
|
|
242
|
+
await bookmarks.deleteRun(existing.value.id)
|
|
243
|
+
isOpen.value = false
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
useWatchTrue(() => bookmarks.add.status.isSuccess, () => {
|
|
247
|
+
bookmarks.fetchAll()
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
useWatchTrue(() => bookmarks.add.status.isError, () => {
|
|
251
|
+
noti.error({
|
|
252
|
+
title: 'Failed to add bookmark',
|
|
253
|
+
description: StringHelper.getError(bookmarks.add.status.errorData, 'Something went wrong. Please try again later.'),
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
useWatchTrue(() => bookmarks.update.status.isSuccess, () => {
|
|
258
|
+
bookmarks.fetchAll()
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
useWatchTrue(() => bookmarks.update.status.isError, () => {
|
|
262
|
+
noti.error({
|
|
263
|
+
title: 'Failed to update bookmark',
|
|
264
|
+
description: StringHelper.getError(bookmarks.update.status.errorData, 'Something went wrong. Please try again later.'),
|
|
265
|
+
})
|
|
266
|
+
})
|
|
267
|
+
</script>
|
package/app/constants/routes.ts
CHANGED
|
@@ -38,6 +38,11 @@ export const routes = {
|
|
|
38
38
|
to: '/account/company-directory',
|
|
39
39
|
icon: 'ph:building-office',
|
|
40
40
|
},
|
|
41
|
+
bookmarks: {
|
|
42
|
+
label: 'Bookmarks',
|
|
43
|
+
to: '/account/bookmarks',
|
|
44
|
+
icon: 'ph:star',
|
|
45
|
+
},
|
|
41
46
|
},
|
|
42
47
|
|
|
43
48
|
announcements: {
|
|
@@ -276,5 +281,6 @@ export const sidebarProfile = [
|
|
|
276
281
|
routes.account.profile,
|
|
277
282
|
routes.account.myTeam,
|
|
278
283
|
routes.account.companyDirectory,
|
|
284
|
+
routes.account.bookmarks,
|
|
279
285
|
routes.logout,
|
|
280
286
|
]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export interface IBookmarkFolder {
|
|
2
|
+
id: string
|
|
3
|
+
name: string
|
|
4
|
+
created_at: string
|
|
5
|
+
updated_at: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface IBookmark {
|
|
9
|
+
id: string
|
|
10
|
+
name: string
|
|
11
|
+
url: string
|
|
12
|
+
folder_id: string | null
|
|
13
|
+
favicon_url: string | null
|
|
14
|
+
image_url: string | null
|
|
15
|
+
created_at: string
|
|
16
|
+
updated_at: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface IBookmarkPreview {
|
|
20
|
+
name: string
|
|
21
|
+
url: string
|
|
22
|
+
favicon_url: string
|
|
23
|
+
image_url: string
|
|
24
|
+
bookmark: IBookmark | null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const useBookmarkFoldersLoader = defineStore('bookmarks.folders', () => {
|
|
28
|
+
const options = useRequestOptions()
|
|
29
|
+
|
|
30
|
+
return usePageLoader<IBookmarkFolder>({
|
|
31
|
+
baseURL: '/bookmark-folders',
|
|
32
|
+
getBaseRequestOptions: options.auth,
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
export const useBookmarksLoader = defineStore('bookmarks.bookmarks', () => {
|
|
37
|
+
const options = useRequestOptions()
|
|
38
|
+
|
|
39
|
+
return usePageLoader<IBookmark>({
|
|
40
|
+
baseURL: '/bookmarks',
|
|
41
|
+
getBaseRequestOptions: options.auth,
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
export const useBookmarkPreviewLoader = defineStore('bookmarks.preview', () => {
|
|
46
|
+
const options = useRequestOptions()
|
|
47
|
+
|
|
48
|
+
return useObjectLoader<IBookmarkPreview>({
|
|
49
|
+
method: 'GET',
|
|
50
|
+
url: '/bookmarks/preview',
|
|
51
|
+
getRequestOptions: options.auth,
|
|
52
|
+
})
|
|
53
|
+
})
|