Skip to content

Commit dedec02

Browse files
committed
feat: add tag-linked document dialog with linked/unlinked tabs, search, status filter, pagination, and batch link/unlink support via new docs-tag delete API
1 parent 4b45af4 commit dedec02

File tree

14 files changed

+581
-4
lines changed

14 files changed

+581
-4
lines changed

apps/knowledge/api/tag.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from common.mixins.api_mixin import APIMixin
55
from common.result import DefaultResultSerializer
6+
from knowledge.serializers.common import BatchSerializer
67
from knowledge.serializers.tag import TagCreateSerializer, TagEditSerializer
78

89

@@ -71,6 +72,42 @@ def get_response():
7172
return DefaultResultSerializer
7273

7374

75+
class DocsTagDeleteAPI(APIMixin):
76+
@staticmethod
77+
def get_parameters():
78+
return [
79+
OpenApiParameter(
80+
name="workspace_id",
81+
description="工作空间id",
82+
type=OpenApiTypes.STR,
83+
location='path',
84+
required=True,
85+
),
86+
OpenApiParameter(
87+
name="knowledge_id",
88+
description="知识库id",
89+
type=OpenApiTypes.STR,
90+
location='path',
91+
required=True,
92+
),
93+
OpenApiParameter(
94+
name="tag_id",
95+
description="标签id",
96+
type=OpenApiTypes.STR,
97+
location='path',
98+
required=True,
99+
),
100+
]
101+
102+
@staticmethod
103+
def get_request():
104+
return BatchSerializer
105+
106+
@staticmethod
107+
def get_response():
108+
return DefaultResultSerializer
109+
110+
74111
class TagEditAPI(APIMixin):
75112
@staticmethod
76113
def get_parameters():

apps/knowledge/serializers/document.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,13 +395,17 @@ class Query(serializers.Serializer):
395395
tag_ids = serializers.ListField(child=serializers.UUIDField(), allow_null=True, required=False,
396396
allow_empty=True)
397397
no_tag = serializers.BooleanField(required=False, default=False, allow_null=True)
398+
tag_ids = serializers.ListField(child=serializers.UUIDField(),allow_null=True,required=False,allow_empty=True)
399+
no_tag = serializers.BooleanField(required=False,default=False, allow_null=True)
400+
tag_exclude = serializers.BooleanField(required=False,default=False, allow_null=True)
398401

399402
def get_query_set(self):
400403
query_set = QuerySet(model=Document)
401404
query_set = query_set.filter(**{'knowledge_id': self.data.get("knowledge_id")})
402405

403406
tag_ids = self.data.get('tag_ids')
404407
no_tag = self.data.get('no_tag')
408+
tag_exclude = self.data.get('tag_exclude')
405409
if 'name' in self.data and self.data.get('name') is not None:
406410
query_set = query_set.filter(**{'name__icontains': self.data.get('name')})
407411
if 'hit_handling_method' in self.data and self.data.get('hit_handling_method') not in [None, '']:
@@ -419,7 +423,10 @@ def get_query_set(self):
419423
query_set = query_set.exclude(id__in=tagged_doc_ids)
420424
elif tag_ids:
421425
matched_doc_ids = QuerySet(DocumentTag).filter(tag_id__in=tag_ids).values_list('document_id', flat=True)
422-
query_set = query_set.filter(id__in=matched_doc_ids)
426+
if tag_exclude:
427+
query_set = query_set.exclude(id__in=matched_doc_ids)
428+
else:
429+
query_set = query_set.filter(id__in=matched_doc_ids)
423430

424431
if 'status' in self.data and self.data.get('status') is not None:
425432
task_type = self.data.get('task_type')
@@ -1605,6 +1612,40 @@ def delete_tags(self):
16051612
tag_id__in=tag_ids
16061613
).delete()
16071614

1615+
class DeleteDocsTag(serializers.Serializer):
1616+
workspace_id = serializers.CharField(required=True, label=_('workspace id'))
1617+
knowledge_id = serializers.UUIDField(required=True, label=_('knowledge id'))
1618+
tag_id = serializers.UUIDField(required=True, label=_('tag id'))
1619+
1620+
def is_valid(self, *, raise_exception=False):
1621+
super().is_valid(raise_exception=True)
1622+
workspace_id = self.data.get('workspace_id')
1623+
query_set = QuerySet(Knowledge).filter(id=self.data.get('knowledge_id'))
1624+
if workspace_id and workspace_id != 'None':
1625+
query_set = query_set.filter(workspace_id=workspace_id)
1626+
if not query_set.exists():
1627+
raise AppApiException(500, _('Knowledge id does not exist'))
1628+
if not QuerySet(Tag).filter(
1629+
id=self.data.get('tag_id'),
1630+
knowledge_id=self.data.get('knowledge_id')
1631+
).exists():
1632+
raise AppApiException(500, _('Tag id does not exist'))
1633+
1634+
def batch_delete_docs_tag(self, instance,with_valid=True):
1635+
if with_valid:
1636+
BatchSerializer(data=instance).is_valid(model=Document, raise_exception=True)
1637+
self.is_valid(raise_exception=True)
1638+
knowledge_id = self.data.get('knowledge_id')
1639+
tag_id=self.data.get('tag_id')
1640+
doc_id_list = instance.get("id_list")
1641+
1642+
valid_doc_count = Document.objects.filter(id__in=doc_id_list, knowledge_id=knowledge_id).count()
1643+
if valid_doc_count != len(doc_id_list):
1644+
raise AppApiException(500, _('Document id does not belong to current knowledge'))
1645+
1646+
DocumentTag.objects.filter(document_id__in=doc_id_list,tag_id=tag_id).delete()
1647+
1648+
return True
16081649
class ReplaceSourceFile(serializers.Serializer):
16091650
workspace_id = serializers.CharField(required=True, label=_('workspace id'))
16101651
knowledge_id = serializers.UUIDField(required=True, label=_('knowledge id'))

apps/knowledge/serializers/tag.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import uuid_utils.compat as uuid
1313
from django.db import transaction
1414
from django.db.models import QuerySet
15+
from django.db.models.aggregates import Count
1516
from django.db.models.query_utils import Q
1617
from django.utils.translation import gettext_lazy as _
1718
from rest_framework import serializers
@@ -225,12 +226,20 @@ def list(self):
225226
knowledge_id=self.data.get('knowledge_id')
226227
).values('key', 'value', 'id', 'create_time', 'update_time').order_by('create_time', 'key', 'value')
227228

229+
tag_ids = [tag['id'] for tag in tags]
230+
231+
tag_doc_count_map = {row['tag_id']: row['doc_count'] for row in
232+
QuerySet(DocumentTag).filter(tag_id__in=tag_ids)
233+
.values('tag_id').annotate(doc_count=Count('document_id'))
234+
}
235+
228236
# 按key分组
229237
grouped_tags = defaultdict(list)
230238
for tag in tags:
231239
grouped_tags[tag['key']].append({
232240
'id': tag['id'],
233241
'value': tag['value'],
242+
'doc_count': tag_doc_count_map.get(tag['id'],0),
234243
'create_time': tag['create_time'],
235244
'update_time': tag['update_time']
236245
})

apps/knowledge/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/document/<str:document_id>/replace_source_file', views.DocumentView.ReplaceSourceFile.as_view()),
5858
path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/document/<str:document_id>/tags', views.DocumentView.Tags.as_view()),
5959
path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/document/<str:document_id>/tags/batch_delete', views.DocumentView.Tags.BatchDelete.as_view()),
60+
path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/tag/<str:tag_id>/docs_delete', views.DocumentView.Tags.BatchDeleteDocsTag.as_view()),
6061
path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/document/<str:document_id>/paragraph', views.ParagraphView.as_view()),
6162
path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/document/<str:document_id>/paragraph/batch_delete', views.ParagraphView.BatchDelete.as_view()),
6263
path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/document/<str:document_id>/paragraph/batch_generate_related', views.ParagraphView.BatchGenerateRelated.as_view()),

apps/knowledge/views/document.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
WebDocumentCreateAPI, CancelTaskAPI, BatchCancelTaskAPI, SyncWebAPI, RefreshAPI, BatchEditHitHandlingAPI, \
1515
DocumentTreeReadAPI, DocumentSplitPatternAPI, BatchRefreshAPI, BatchGenerateRelatedAPI, TemplateExportAPI, \
1616
DocumentExportAPI, DocumentMigrateAPI, DocumentDownloadSourceAPI, DocumentTagsAPI
17+
from knowledge.api.tag import DocsTagDeleteAPI
1718
from knowledge.serializers.common import get_knowledge_operation_object
1819
from knowledge.serializers.document import DocumentSerializers
1920
from knowledge.views.common import get_knowledge_document_operation_object, get_document_operation_object_batch, \
@@ -648,6 +649,7 @@ def get(self, request: Request, workspace_id: str, knowledge_id: str, current_pa
648649
'folder_id': request.query_params.get('folder_id'),
649650
'name': request.query_params.get('name'),
650651
'tag': request.query_params.get('tag'),
652+
'tag_exclude': request.query_params.get('tag_exclude'),
651653
'tag_ids': [tag for tag in raw_tags if tag != 'NO_TAG'],
652654
'no_tag': 'NO_TAG' in raw_tags,
653655
'desc': request.query_params.get("desc"),
@@ -844,6 +846,38 @@ def put(self, request: Request, workspace_id: str, knowledge_id: str, document_i
844846
'tag_ids': request.data
845847
}).delete_tags())
846848

849+
class BatchDeleteDocsTag(APIView):
850+
authentication_classes = [TokenAuth]
851+
852+
@extend_schema(
853+
summary=_("Batch Delete Documents Tag"),
854+
description=_("Batch Delete Documents Tag"),
855+
parameters=DocsTagDeleteAPI.get_parameters(),
856+
request=DocsTagDeleteAPI.get_request(),
857+
responses=DocsTagDeleteAPI.get_response(),
858+
tags=[_('Knowledge Base/Tag')] # type: ignore
859+
)
860+
@has_permissions(
861+
PermissionConstants.KNOWLEDGE_DOCUMENT_TAG.get_workspace_knowledge_permission(),
862+
PermissionConstants.KNOWLEDGE_DOCUMENT_TAG.get_workspace_permission_workspace_manage_role(),
863+
RoleConstants.WORKSPACE_MANAGE.get_workspace_role(),
864+
ViewPermission([RoleConstants.USER.get_workspace_role()],
865+
[PermissionConstants.KNOWLEDGE.get_workspace_knowledge_permission()],
866+
CompareConstants.AND),
867+
)
868+
@log(
869+
menu='tag', operate="Batch Delete Documents Tag",
870+
get_operation_object=lambda r, keywords: get_knowledge_document_operation_object(
871+
get_knowledge_operation_object(keywords.get('knowledge_id')),
872+
get_document_operation_object_batch(r.data.get('id_list'))),
873+
)
874+
def put(self, request: Request, workspace_id: str, knowledge_id: str, tag_id: str):
875+
return result.success(DocumentSerializers.DeleteDocsTag(data={
876+
'workspace_id': workspace_id,
877+
'knowledge_id': knowledge_id,
878+
'tag_id': tag_id,
879+
}).batch_delete_docs_tag(request.data))
880+
847881
class Migrate(APIView):
848882
authentication_classes = [TokenAuth]
849883

ui/src/api/knowledge/document.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,16 @@ const delMulDocumentTag: (
646646
return put(`${prefix.value}/${knowledge_id}/document/${document_id}/tags/batch_delete`, tags, null, loading)
647647
}
648648

649+
const delDocsTag: (
650+
knowledge_id: string,
651+
tag_id: string,
652+
data: any,
653+
loading?: Ref<boolean>,
654+
) => Promise<Result<boolean>> = (knowledge_id, tag_id, data, loading) => {
655+
return put(`${prefix.value}/${knowledge_id}/tag/${tag_id}/docs_delete`, {id_list: data}, null, loading)
656+
}
657+
658+
649659
export default {
650660
getDocumentList,
651661
getDocumentPage,
@@ -683,5 +693,6 @@ export default {
683693
getDocumentTags,
684694
postDocumentTags,
685695
postMulDocumentTags,
686-
delMulDocumentTag
696+
delMulDocumentTag,
697+
delDocsTag
687698
}

ui/src/api/system-resource-management/document.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,15 @@ const delMulDocumentTag: (
606606
return put(`${prefix}/${knowledge_id}/document/${document_id}/tags/batch_delete`, tags, null, loading)
607607
}
608608

609+
const delDocsTag: (
610+
knowledge_id: string,
611+
tag_id: string,
612+
data: any,
613+
loading?: Ref<boolean>,
614+
) => Promise<Result<boolean>> = (knowledge_id, tag_id, data, loading) => {
615+
return put(`${prefix}/${knowledge_id}/tag/${tag_id}/docs_delete`, {id_list: data}, null, loading)
616+
}
617+
609618
export default {
610619
getDocumentList,
611620
getDocumentPage,
@@ -643,5 +652,6 @@ export default {
643652
getDocumentTags,
644653
postDocumentTags,
645654
postMulDocumentTags,
646-
delMulDocumentTag
655+
delMulDocumentTag,
656+
delDocsTag
647657
}

ui/src/api/system-shared/document.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,16 @@ const delMulDocumentTag: (
607607
return put(`${prefix}/${knowledge_id}/document/${document_id}/tags/batch_delete`, tags, null, loading)
608608
}
609609

610+
const delDocsTag: (
611+
knowledge_id: string,
612+
tag_id: string,
613+
data: any,
614+
loading?: Ref<boolean>,
615+
) => Promise<Result<boolean>> = (knowledge_id, tag_id, data, loading) => {
616+
return put(`${prefix}/${knowledge_id}/tag/${tag_id}/docs_delete`, {id_list: data}, null, loading)
617+
}
618+
619+
610620
export default {
611621
getDocumentList,
612622
getDocumentPage,
@@ -644,5 +654,6 @@ export default {
644654
getDocumentTags,
645655
postDocumentTags,
646656
postMulDocumentTags,
647-
delMulDocumentTag
657+
delMulDocumentTag,
658+
delDocsTag
648659
}

ui/src/components/app-icon/index.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,54 @@ export const iconMap: any = {
579579
])
580580
},
581581
},
582+
'app-unlink': {
583+
iconReader: () => {
584+
return h('i', [
585+
h(
586+
'svg',
587+
{
588+
style: { height: '100%', width: '100%' },
589+
viewBox: '0 0 16 16',
590+
fill: 'none',
591+
xmlns: 'http://www.w3.org/2000/svg',
592+
},
593+
[
594+
h('g', { 'clip-path': 'url(#clip0_10754_9765)' }, [
595+
h('path', {
596+
d: 'M1.23567 0.764126L0.76429 1.23549C0.634122 1.36565 0.634122 1.57668 0.76429 1.70685L13.9629 14.905C14.0931 15.0351 14.3042 15.0351 14.4343 14.905L14.9057 14.4336C15.0359 14.3034 15.0359 14.0924 14.9057 13.9622L1.70705 0.764126C1.57688 0.633963 1.36584 0.633963 1.23567 0.764126Z',
597+
fill: '#3370FF',
598+
}),
599+
h('path', {
600+
d: 'M9.77756 6.94871V3.33311C9.77756 3.22403 9.69895 3.1333 9.59528 3.11448L9.55534 3.1109H5.93959L4.60626 1.77762H9.55534C10.3858 1.77762 11.0643 2.42839 11.1086 3.24777L11.1109 3.33311V8.28199L9.77756 6.94871Z',
601+
fill: '#3370FF',
602+
}),
603+
h('path', {
604+
d: 'M0.888669 3.71681V8.66623L0.890971 8.75157C0.93528 9.57095 1.61375 10.2217 2.44422 10.2217H4.17756C4.32483 10.2217 4.44422 10.1023 4.44422 9.95506V9.15509C4.44422 9.00782 4.32483 8.88844 4.17756 8.88844H2.44422L2.40428 8.88486C2.30061 8.86604 2.222 8.77531 2.222 8.66623V5.05009L0.888669 3.71681Z',
605+
fill: '#3370FF',
606+
}),
607+
h('path', {
608+
d: 'M5.33311 8.16107V12.6661L5.33542 12.7514C5.37972 13.5708 6.0582 14.2216 6.88867 14.2216H11.3938L10.0605 12.8883H6.88867L6.84872 12.8847C6.74506 12.8659 6.66645 12.7751 6.66645 12.6661V9.49435L5.33311 8.16107Z',
609+
fill: '#3370FF',
610+
}),
611+
h('path', {
612+
d: 'M8.60626 5.77746L8.88867 6.05986V6.04411C8.88867 5.89684 8.76928 5.77746 8.622 5.77746H8.60626Z',
613+
fill: '#3370FF',
614+
}),
615+
h('path', {
616+
d: 'M15.5542 12.7251L14.222 11.393V7.33295C14.222 7.22386 14.1434 7.13313 14.0397 7.11431L13.9998 7.11073H12.2664C12.1192 7.11073 11.9998 6.99135 11.9998 6.84408V6.04411C11.9998 5.89684 12.1192 5.77746 12.2664 5.77746H13.9998C14.8303 5.77746 15.5087 6.42822 15.553 7.2476L15.5553 7.33295V12.6661C15.5553 12.6858 15.555 12.7055 15.5542 12.7251Z',
617+
fill: '#3370FF',
618+
}),
619+
]),
620+
h('defs', [
621+
h('clipPath', { id: 'clip0_10754_9765' }, [
622+
h('rect', { width: '16', height: '15.9993', fill: 'white' }),
623+
]),
624+
]),
625+
],
626+
),
627+
])
628+
},
629+
},
582630
// 动态加载的图标
583631
...dynamicIcons,
584632
}

ui/src/locales/lang/en-US/views/document.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ export default {
105105
value: 'Value',
106106
addTag: 'Add Tag',
107107
noTag: 'No Tag',
108+
relate: 'Link',
109+
unrelate: 'Unlink',
110+
relatedDoc: 'Linked documents',
111+
unrelatedDoc: 'Unlinked documents',
112+
tagLinkTitle: 'Tag: Tag Value',
108113
setting: 'Tag Settings',
109114
create: 'Create Tag',
110115
createValue: 'Create Tag Value',

0 commit comments

Comments
 (0)