diff --git a/src-ui/src/app/components/manage/saved-views/saved-views.component.spec.ts b/src-ui/src/app/components/manage/saved-views/saved-views.component.spec.ts
index fbcdbe8c3..05d950366 100644
--- a/src-ui/src/app/components/manage/saved-views/saved-views.component.spec.ts
+++ b/src-ui/src/app/components/manage/saved-views/saved-views.component.spec.ts
@@ -26,8 +26,20 @@ import { PageHeaderComponent } from '../../common/page-header/page-header.compon
import { SavedViewsComponent } from './saved-views.component'
const savedViews = [
- { id: 1, name: 'view1', show_in_sidebar: true, show_on_dashboard: true },
- { id: 2, name: 'view2', show_in_sidebar: false, show_on_dashboard: false },
+ {
+ id: 1,
+ name: 'view1',
+ icon: 'archive',
+ show_in_sidebar: true,
+ show_on_dashboard: true,
+ },
+ {
+ id: 2,
+ name: 'view2',
+ icon: 'funnel',
+ show_in_sidebar: false,
+ show_on_dashboard: false,
+ },
]
describe('SavedViewsComponent', () => {
@@ -158,6 +170,24 @@ describe('SavedViewsComponent', () => {
expect(patchBody.show_in_sidebar).toBeUndefined()
})
+ it('should persist a changed icon', () => {
+ const patchSpy = jest.spyOn(savedViewService, 'patchMany')
+ const view = savedViews[0]
+ const iconControl = component.savedViewsForm
+ .get('savedViews')
+ .get(view.id.toString())
+ .get('icon')
+
+ iconControl.setValue('bell')
+ iconControl.markAsDirty()
+ component.save()
+
+ expect(patchSpy.mock.calls[0][0][0]).toMatchObject({
+ id: view.id,
+ icon: 'bell',
+ })
+ })
+
it('should persist visibility changes to user settings', () => {
const patchSpy = jest.spyOn(savedViewService, 'patchMany')
const updateVisibilitySpy = jest
diff --git a/src-ui/src/app/components/manage/saved-views/saved-views.component.ts b/src-ui/src/app/components/manage/saved-views/saved-views.component.ts
index 3813e666f..0622fc399 100644
--- a/src-ui/src/app/components/manage/saved-views/saved-views.component.ts
+++ b/src-ui/src/app/components/manage/saved-views/saved-views.component.ts
@@ -20,6 +20,10 @@ import { BehaviorSubject, Observable, of, switchMap, takeUntil } from 'rxjs'
import { PermissionsDialogComponent } from 'src/app/components/common/permissions-dialog/permissions-dialog.component'
import { DisplayMode } from 'src/app/data/document'
import { SavedView } from 'src/app/data/saved-view'
+import {
+ DEFAULT_SAVED_VIEW_ICON,
+ SAVED_VIEW_ICONS,
+} from 'src/app/data/saved-view-icons'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import {
PermissionAction,
@@ -32,6 +36,7 @@ import { ToastService } from 'src/app/services/toast.service'
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
import { DragDropSelectComponent } from '../../common/input/drag-drop-select/drag-drop-select.component'
import { NumberComponent } from '../../common/input/number/number.component'
+import { SelectComponent } from '../../common/input/select/select.component'
import { TextComponent } from '../../common/input/text/text.component'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
@@ -43,6 +48,7 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
PageHeaderComponent,
ConfirmButtonComponent,
NumberComponent,
+ SelectComponent,
TextComponent,
IfPermissionsDirective,
DragDropSelectComponent,
@@ -64,6 +70,7 @@ export class SavedViewsComponent
private readonly modalService = inject(NgbModal)
DisplayMode = DisplayMode
+ readonly savedViewIcons = SAVED_VIEW_ICONS
readonly savedViews = signal(undefined)
readonly page = signal(1)
@@ -128,6 +135,7 @@ export class SavedViewsComponent
storeData.savedViews[view.id.toString()] = {
id: view.id,
name: view.name,
+ icon: view.icon ?? DEFAULT_SAVED_VIEW_ICON,
show_on_dashboard: view.show_on_dashboard,
show_in_sidebar: view.show_in_sidebar,
page_size: view.page_size,
@@ -140,6 +148,7 @@ export class SavedViewsComponent
new FormGroup({
id: new FormControl({ value: null, disabled: !canEdit }),
name: new FormControl({ value: null, disabled: !canEdit }),
+ icon: new FormControl({ value: null, disabled: !canEdit }),
show_on_dashboard: new FormControl({
value: null,
disabled: false,
@@ -218,6 +227,7 @@ export class SavedViewsComponent
const modelFieldsChanged =
group.get('name')?.dirty ||
+ group.get('icon')?.dirty ||
group.get('page_size')?.dirty ||
group.get('display_mode')?.dirty ||
group.get('display_fields')?.dirty
diff --git a/src-ui/src/app/data/saved-view-icons.ts b/src-ui/src/app/data/saved-view-icons.ts
new file mode 100644
index 000000000..b1c1307c1
--- /dev/null
+++ b/src-ui/src/app/data/saved-view-icons.ts
@@ -0,0 +1,89 @@
+export const DEFAULT_SAVED_VIEW_ICON = 'funnel'
+
+export const SAVED_VIEW_ICONS = [
+ { id: 'archive', name: $localize`Archive`, icon: 'archive' },
+ { id: 'bank', name: $localize`Bank`, icon: 'bank' },
+ { id: 'basket', name: $localize`Basket`, icon: 'basket' },
+ { id: 'bell', name: $localize`Bell`, icon: 'bell' },
+ { id: 'bookmark', name: $localize`Bookmark`, icon: 'bookmark' },
+ { id: 'boxes', name: $localize`Boxes`, icon: 'boxes' },
+ { id: 'briefcase', name: $localize`Briefcase`, icon: 'briefcase' },
+ { id: 'building', name: $localize`Building`, icon: 'building' },
+ { id: 'calculator', name: $localize`Calculator`, icon: 'calculator' },
+ { id: 'calendar', name: $localize`Calendar`, icon: 'calendar' },
+ { id: 'camera', name: $localize`Camera`, icon: 'camera' },
+ {
+ id: 'card-checklist',
+ name: $localize`Checklist`,
+ icon: 'card-checklist',
+ },
+ { id: 'cash', name: $localize`Cash`, icon: 'cash' },
+ { id: 'chat-left-text', name: $localize`Chat`, icon: 'chat-left-text' },
+ { id: 'check-circle', name: $localize`Check`, icon: 'check-circle' },
+ { id: 'clipboard', name: $localize`Clipboard`, icon: 'clipboard' },
+ { id: 'clock-history', name: $localize`Clock`, icon: 'clock-history' },
+ { id: 'credit-card', name: $localize`Credit card`, icon: 'credit-card' },
+ { id: 'download', name: $localize`Download`, icon: 'download' },
+ { id: 'envelope', name: $localize`Envelope`, icon: 'envelope' },
+ {
+ id: 'exclamation-triangle',
+ name: $localize`Warning`,
+ icon: 'exclamation-triangle',
+ },
+ { id: 'file-earmark', name: $localize`File`, icon: 'file-earmark' },
+ {
+ id: 'file-earmark-check',
+ name: $localize`Checked file`,
+ icon: 'file-earmark-check',
+ },
+ {
+ id: 'file-earmark-lock',
+ name: $localize`Locked file`,
+ icon: 'file-earmark-lock',
+ },
+ {
+ id: 'file-earmark-medical',
+ name: $localize`Medical file`,
+ icon: 'file-earmark-medical',
+ },
+ {
+ id: 'file-earmark-person',
+ name: $localize`Person file`,
+ icon: 'file-earmark-person',
+ },
+ {
+ id: 'file-earmark-spreadsheet',
+ name: $localize`Spreadsheet`,
+ icon: 'file-earmark-spreadsheet',
+ },
+ { id: 'file-text', name: $localize`Text file`, icon: 'file-text' },
+ { id: 'files', name: $localize`Files`, icon: 'files' },
+ { id: 'folder', name: $localize`Folder`, icon: 'folder' },
+ { id: 'funnel', name: $localize`Filter`, icon: 'funnel' },
+ { id: 'gear', name: $localize`Gear`, icon: 'gear' },
+ { id: 'globe2', name: $localize`Globe`, icon: 'globe2' },
+ { id: 'hash', name: $localize`Hash`, icon: 'hash' },
+ { id: 'heart', name: $localize`Heart`, icon: 'heart' },
+ { id: 'house', name: $localize`House`, icon: 'house' },
+ { id: 'inbox', name: $localize`Inbox`, icon: 'inbox' },
+ { id: 'journals', name: $localize`Journals`, icon: 'journals' },
+ { id: 'list-task', name: $localize`Task list`, icon: 'list-task' },
+ { id: 'newspaper', name: $localize`Newspaper`, icon: 'newspaper' },
+ { id: 'paperclip', name: $localize`Attachment`, icon: 'paperclip' },
+ { id: 'people', name: $localize`People`, icon: 'people' },
+ { id: 'person', name: $localize`Person`, icon: 'person' },
+ { id: 'printer', name: $localize`Printer`, icon: 'printer' },
+ { id: 'receipt', name: $localize`Receipt`, icon: 'receipt' },
+ { id: 'safe', name: $localize`Safe`, icon: 'safe' },
+ { id: 'search', name: $localize`Search`, icon: 'search' },
+ { id: 'send', name: $localize`Send`, icon: 'send' },
+ { id: 'shop', name: $localize`Shop`, icon: 'shop' },
+ { id: 'stack', name: $localize`Stack`, icon: 'stack' },
+ { id: 'stars', name: $localize`Stars`, icon: 'stars' },
+ { id: 'tag', name: $localize`Tag`, icon: 'tag' },
+ { id: 'tags', name: $localize`Tags`, icon: 'tags' },
+ { id: 'telephone', name: $localize`Telephone`, icon: 'telephone' },
+ { id: 'truck', name: $localize`Truck`, icon: 'truck' },
+ { id: 'upc-scan', name: $localize`Barcode`, icon: 'upc-scan' },
+ { id: 'wallet2', name: $localize`Wallet`, icon: 'wallet2' },
+]
diff --git a/src-ui/src/app/data/saved-view.ts b/src-ui/src/app/data/saved-view.ts
index 15959b8ad..997cedd6c 100644
--- a/src-ui/src/app/data/saved-view.ts
+++ b/src-ui/src/app/data/saved-view.ts
@@ -5,6 +5,8 @@ import { ObjectWithPermissions } from './object-with-permissions'
export interface SavedView extends ObjectWithPermissions {
name?: string
+ icon?: string
+
show_on_dashboard?: boolean
show_in_sidebar?: boolean
diff --git a/src-ui/src/main.ts b/src-ui/src/main.ts
index 60c23ed59..886516e5e 100644
--- a/src-ui/src/main.ts
+++ b/src-ui/src/main.ts
@@ -35,19 +35,27 @@ import {
arrowRightShort,
arrowUpRight,
asterisk,
+ bank,
+ basket,
bell,
bodyText,
+ bookmark,
boxArrowUp,
boxArrowUpRight,
boxes,
braces,
+ briefcase,
+ building,
+ calculator,
calendar,
calendarEvent,
calendarEventFill,
+ camera,
cardChecklist,
cardHeading,
caretDown,
caretUp,
+ cash,
chatLeftText,
chatSquareDots,
check,
@@ -65,6 +73,7 @@ import {
clipboardCheckFill,
clipboardFill,
clockHistory,
+ creditCard,
dash,
dashCircle,
diagram3,
@@ -83,9 +92,12 @@ import {
fileEarmarkDiff,
fileEarmarkFill,
fileEarmarkLock,
+ fileEarmarkMedical,
fileEarmarkMinus,
+ fileEarmarkPerson,
fileEarmarkPlus,
fileEarmarkRichtext,
+ fileEarmarkSpreadsheet,
fileText,
files,
filter,
@@ -93,12 +105,15 @@ import {
folderFill,
funnel,
gear,
+ globe2,
google,
grid,
gripVertical,
hash,
hddStack,
+ heart,
house,
+ inbox,
infoCircle,
journals,
link,
@@ -106,7 +121,9 @@ import {
listTask,
listUl,
microsoft,
+ newspaper,
nodePlus,
+ paperclip,
pencil,
people,
peopleFill,
@@ -121,9 +138,12 @@ import {
plusCircle,
printer,
questionCircle,
+ receipt,
+ safe,
scissors,
search,
send,
+ shop,
slashCircle,
sliders2Vertical,
sortAlphaDown,
@@ -133,14 +153,17 @@ import {
tag,
tagFill,
tags,
+ telephone,
textIndentLeft,
textLeft,
threeDots,
threeDotsVertical,
trash,
+ truck,
uiRadios,
unlock,
upcScan,
+ wallet2,
windowStack,
x,
xCircle,
@@ -258,15 +281,22 @@ const icons = {
arrowRightShort,
arrowUpRight,
asterisk,
+ bank,
+ basket,
bell,
braces,
bodyText,
+ bookmark,
boxArrowUp,
boxArrowUpRight,
boxes,
+ briefcase,
+ building,
+ calculator,
calendar,
calendarEvent,
calendarEventFill,
+ camera,
cardChecklist,
cardHeading,
caretDown,
@@ -288,6 +318,8 @@ const icons = {
clipboardCheckFill,
clipboardFill,
clockHistory,
+ cash,
+ creditCard,
dash,
dashCircle,
diagram3,
@@ -306,9 +338,12 @@ const icons = {
fileEarmarkDiff,
fileEarmarkFill,
fileEarmarkLock,
+ fileEarmarkMedical,
fileEarmarkMinus,
+ fileEarmarkPerson,
fileEarmarkPlus,
fileEarmarkRichtext,
+ fileEarmarkSpreadsheet,
files,
fileText,
filter,
@@ -316,12 +351,15 @@ const icons = {
folderFill,
funnel,
gear,
+ globe2,
google,
grid,
gripVertical,
hash,
hddStack,
+ heart,
house,
+ inbox,
infoCircle,
journals,
link,
@@ -329,8 +367,10 @@ const icons = {
listTask,
listUl,
microsoft,
+ newspaper,
nodePlus,
pencil,
+ paperclip,
people,
peopleFill,
person,
@@ -344,10 +384,13 @@ const icons = {
plusCircle,
printer,
questionCircle,
+ receipt,
+ safe,
scissors,
search,
send,
slashCircle,
+ shop,
sliders2Vertical,
sortAlphaDown,
sortAlphaUpAlt,
@@ -358,12 +401,15 @@ const icons = {
tags,
textIndentLeft,
textLeft,
+ telephone,
threeDots,
threeDotsVertical,
trash,
+ truck,
uiRadios,
unlock,
upcScan,
+ wallet2,
windowStack,
x,
xCircle,
diff --git a/src/documents/migrations/0023_savedview_icon.py b/src/documents/migrations/0023_savedview_icon.py
new file mode 100644
index 000000000..0c055d388
--- /dev/null
+++ b/src/documents/migrations/0023_savedview_icon.py
@@ -0,0 +1,79 @@
+from django.db import migrations
+from django.db import models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("documents", "0022_add_perf_indexes"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="savedview",
+ name="icon",
+ field=models.CharField(
+ choices=[
+ ("archive", "Archive"),
+ ("bank", "Bank"),
+ ("basket", "Basket"),
+ ("bell", "Bell"),
+ ("bookmark", "Bookmark"),
+ ("boxes", "Boxes"),
+ ("briefcase", "Briefcase"),
+ ("building", "Building"),
+ ("calculator", "Calculator"),
+ ("calendar", "Calendar"),
+ ("camera", "Camera"),
+ ("card-checklist", "Checklist"),
+ ("cash", "Cash"),
+ ("chat-left-text", "Chat"),
+ ("check-circle", "Check"),
+ ("clipboard", "Clipboard"),
+ ("clock-history", "Clock"),
+ ("credit-card", "Credit card"),
+ ("download", "Download"),
+ ("envelope", "Envelope"),
+ ("exclamation-triangle", "Warning"),
+ ("file-earmark", "File"),
+ ("file-earmark-check", "Checked file"),
+ ("file-earmark-lock", "Locked file"),
+ ("file-earmark-medical", "Medical file"),
+ ("file-earmark-person", "Person file"),
+ ("file-earmark-spreadsheet", "Spreadsheet"),
+ ("file-text", "Text file"),
+ ("files", "Files"),
+ ("folder", "Folder"),
+ ("funnel", "Filter"),
+ ("gear", "Gear"),
+ ("globe2", "Globe"),
+ ("hash", "Hash"),
+ ("heart", "Heart"),
+ ("house", "House"),
+ ("inbox", "Inbox"),
+ ("journals", "Journals"),
+ ("list-task", "Task list"),
+ ("newspaper", "Newspaper"),
+ ("paperclip", "Attachment"),
+ ("people", "People"),
+ ("person", "Person"),
+ ("printer", "Printer"),
+ ("receipt", "Receipt"),
+ ("safe", "Safe"),
+ ("search", "Search"),
+ ("send", "Send"),
+ ("shop", "Shop"),
+ ("stack", "Stack"),
+ ("stars", "Stars"),
+ ("tag", "Tag"),
+ ("tags", "Tags"),
+ ("telephone", "Telephone"),
+ ("truck", "Truck"),
+ ("upc-scan", "Barcode"),
+ ("wallet2", "Wallet"),
+ ],
+ default="funnel",
+ max_length=64,
+ verbose_name="icon",
+ ),
+ ),
+ ]
diff --git a/src/documents/models.py b/src/documents/models.py
index c9b7109bc..e11a64d0f 100644
--- a/src/documents/models.py
+++ b/src/documents/models.py
@@ -519,6 +519,68 @@ class Document(SoftDeleteModel, ModelWithOwner): # type: ignore[django-manager-
class SavedView(ModelWithOwner):
+ class Icon(models.TextChoices):
+ ARCHIVE = ("archive", _("Archive"))
+ BANK = ("bank", _("Bank"))
+ BASKET = ("basket", _("Basket"))
+ BELL = ("bell", _("Bell"))
+ BOOKMARK = ("bookmark", _("Bookmark"))
+ BOXES = ("boxes", _("Boxes"))
+ BRIEFCASE = ("briefcase", _("Briefcase"))
+ BUILDING = ("building", _("Building"))
+ CALCULATOR = ("calculator", _("Calculator"))
+ CALENDAR = ("calendar", _("Calendar"))
+ CAMERA = ("camera", _("Camera"))
+ CARD_CHECKLIST = ("card-checklist", _("Checklist"))
+ CASH = ("cash", _("Cash"))
+ CHAT_LEFT_TEXT = ("chat-left-text", _("Chat"))
+ CHECK_CIRCLE = ("check-circle", _("Check"))
+ CLIPBOARD = ("clipboard", _("Clipboard"))
+ CLOCK_HISTORY = ("clock-history", _("Clock"))
+ CREDIT_CARD = ("credit-card", _("Credit card"))
+ DOWNLOAD = ("download", _("Download"))
+ ENVELOPE = ("envelope", _("Envelope"))
+ EXCLAMATION_TRIANGLE = ("exclamation-triangle", _("Warning"))
+ FILE_EARMARK = ("file-earmark", _("File"))
+ FILE_EARMARK_CHECK = ("file-earmark-check", _("Checked file"))
+ FILE_EARMARK_LOCK = ("file-earmark-lock", _("Locked file"))
+ FILE_EARMARK_MEDICAL = ("file-earmark-medical", _("Medical file"))
+ FILE_EARMARK_PERSON = ("file-earmark-person", _("Person file"))
+ FILE_EARMARK_SPREADSHEET = (
+ "file-earmark-spreadsheet",
+ _("Spreadsheet"),
+ )
+ FILE_TEXT = ("file-text", _("Text file"))
+ FILES = ("files", _("Files"))
+ FOLDER = ("folder", _("Folder"))
+ FUNNEL = ("funnel", _("Filter"))
+ GEAR = ("gear", _("Gear"))
+ GLOBE = ("globe2", _("Globe"))
+ HASH = ("hash", _("Hash"))
+ HEART = ("heart", _("Heart"))
+ HOUSE = ("house", _("House"))
+ INBOX = ("inbox", _("Inbox"))
+ JOURNALS = ("journals", _("Journals"))
+ LIST_TASK = ("list-task", _("Task list"))
+ NEWSPAPER = ("newspaper", _("Newspaper"))
+ PAPERCLIP = ("paperclip", _("Attachment"))
+ PEOPLE = ("people", _("People"))
+ PERSON = ("person", _("Person"))
+ PRINTER = ("printer", _("Printer"))
+ RECEIPT = ("receipt", _("Receipt"))
+ SAFE = ("safe", _("Safe"))
+ SEARCH = ("search", _("Search"))
+ SEND = ("send", _("Send"))
+ SHOP = ("shop", _("Shop"))
+ STACK = ("stack", _("Stack"))
+ STARS = ("stars", _("Stars"))
+ TAG = ("tag", _("Tag"))
+ TAGS = ("tags", _("Tags"))
+ TELEPHONE = ("telephone", _("Telephone"))
+ TRUCK = ("truck", _("Truck"))
+ UPC_SCAN = ("upc-scan", _("Barcode"))
+ WALLET = ("wallet2", _("Wallet"))
+
class DisplayMode(models.TextChoices):
TABLE = ("table", _("Table"))
SMALL_CARDS = ("smallCards", _("Small Cards"))
@@ -541,6 +603,13 @@ class SavedView(ModelWithOwner):
name = models.CharField(_("name"), max_length=128)
+ icon = models.CharField(
+ _("icon"),
+ max_length=64,
+ choices=Icon.choices,
+ default=Icon.FUNNEL,
+ )
+
sort_field = models.CharField(
_("sort field"),
max_length=128,
diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py
index e47f42e09..c81640b0d 100644
--- a/src/documents/serialisers.py
+++ b/src/documents/serialisers.py
@@ -1383,6 +1383,7 @@ class SavedViewSerializer(OwnedObjectSerializer):
fields = [
"id",
"name",
+ "icon",
"sort_field",
"sort_reverse",
"filter_rules",
diff --git a/src/documents/tests/test_api_documents.py b/src/documents/tests/test_api_documents.py
index 1009ad311..0acd5e937 100644
--- a/src/documents/tests/test_api_documents.py
+++ b/src/documents/tests/test_api_documents.py
@@ -2905,18 +2905,20 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
v1 = SavedView.objects.get(name="test")
self.assertEqual(v1.sort_field, "created2")
+ self.assertEqual(v1.icon, SavedView.Icon.FUNNEL)
self.assertEqual(v1.filter_rules.count(), 1)
self.assertEqual(v1.owner, self.user)
response = self.client.patch(
f"/api/saved_views/{v1.id}/",
- {"sort_reverse": True},
+ {"sort_reverse": True, "icon": SavedView.Icon.RECEIPT},
format="json",
)
v1 = SavedView.objects.get(id=v1.id)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(v1.sort_reverse)
+ self.assertEqual(v1.icon, SavedView.Icon.RECEIPT)
self.assertEqual(v1.filter_rules.count(), 1)
view["filter_rules"] = [{"rule_type": 12, "value": "secret"}]
@@ -2936,6 +2938,13 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
v1 = SavedView.objects.get(id=v1.id)
self.assertEqual(v1.filter_rules.count(), 0)
+ response = self.client.patch(
+ f"/api/saved_views/{v1.id}/",
+ {"icon": "not-an-icon"},
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
def test_saved_view_display_options(self) -> None:
"""
GIVEN: