mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-02 10:03:28 +00:00
Better search (#2262)
* Revert "Revert "remove fixed filters" (#2261)"
This reverts commit 5b40cefe37
.
* search sources
* only set default filters if no filters
* key saved search page on id and use raw search as searchbar default
* fix crate versions
* put media view filters in a dedicated hook
* remove ts-reset
* add comment about <Inner>
* generics!
* cleanup
* search paths/objects switch (#2278)
* feature flag target switcher
* use useZodParams in saved search route
This commit is contained in:
parent
6bb94e3507
commit
f168b5e45d
|
@ -30,17 +30,17 @@ thiserror.workspace = true
|
|||
|
||||
opener = { version = "0.6.1", features = ["reveal"] }
|
||||
tauri = { version = "=1.5.3", features = [
|
||||
"macos-private-api",
|
||||
"path-all",
|
||||
"protocol-all",
|
||||
"os-all",
|
||||
"shell-all",
|
||||
"dialog-all",
|
||||
"linux-protocol-headers",
|
||||
"updater",
|
||||
"window-all",
|
||||
"native-tls-vendored",
|
||||
"tracing",
|
||||
"macos-private-api",
|
||||
"path-all",
|
||||
"protocol-all",
|
||||
"os-all",
|
||||
"shell-all",
|
||||
"dialog-all",
|
||||
"linux-protocol-headers",
|
||||
"updater",
|
||||
"window-all",
|
||||
"native-tls-vendored",
|
||||
"tracing",
|
||||
] }
|
||||
directories = "5.0.1"
|
||||
|
||||
|
|
|
@ -25,15 +25,15 @@ sd-core-sync = { path = "./crates/sync" }
|
|||
# sd-cloud-api = { path = "../crates/cloud-api" }
|
||||
sd-file-path-helper = { path = "../crates/file-path-helper" }
|
||||
sd-crypto = { path = "../crates/crypto", features = [
|
||||
"sys",
|
||||
"tokio",
|
||||
"sys",
|
||||
"tokio",
|
||||
], optional = true }
|
||||
sd-ffmpeg = { path = "../crates/ffmpeg", optional = true }
|
||||
sd-file-ext = { path = "../crates/file-ext" }
|
||||
sd-images = { path = "../crates/images", features = [
|
||||
"rspc",
|
||||
"serde",
|
||||
"specta",
|
||||
"rspc",
|
||||
"serde",
|
||||
"specta",
|
||||
] }
|
||||
sd-media-metadata = { path = "../crates/media-metadata" }
|
||||
sd-p2p = { path = "../crates/p2p", features = ["specta"] }
|
||||
|
@ -64,12 +64,12 @@ regex = { workspace = true }
|
|||
reqwest = { workspace = true, features = ["json", "native-tls-vendored"] }
|
||||
rmp-serde = { workspace = true }
|
||||
rspc = { workspace = true, features = [
|
||||
"axum",
|
||||
"uuid",
|
||||
"chrono",
|
||||
"tracing",
|
||||
"alpha",
|
||||
"unstable",
|
||||
"axum",
|
||||
"uuid",
|
||||
"chrono",
|
||||
"tracing",
|
||||
"alpha",
|
||||
"unstable",
|
||||
] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
@ -79,12 +79,12 @@ strum_macros = { workspace = true }
|
|||
tempfile = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = [
|
||||
"sync",
|
||||
"rt-multi-thread",
|
||||
"io-util",
|
||||
"macros",
|
||||
"time",
|
||||
"process",
|
||||
"sync",
|
||||
"rt-multi-thread",
|
||||
"io-util",
|
||||
"macros",
|
||||
"time",
|
||||
"process",
|
||||
] }
|
||||
tokio-stream = { workspace = true, features = ["fs"] }
|
||||
tokio-util = { workspace = true, features = ["io"] }
|
||||
|
@ -111,7 +111,7 @@ itertools = "0.12.0"
|
|||
libc = "0.2.153"
|
||||
mini-moka = "0.10.2"
|
||||
notify = { git = "https://github.com/notify-rs/notify.git", rev = "c3929ed114fbb0bc7457a9a498260461596b00ca", default-features = false, features = [
|
||||
"macos_fsevent",
|
||||
"macos_fsevent",
|
||||
] }
|
||||
rmpv = { workspace = true }
|
||||
serde-hashkey = "0.4.5"
|
||||
|
@ -144,10 +144,10 @@ plist = "1"
|
|||
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
icrate = { version = "0.1.0", features = [
|
||||
"Foundation",
|
||||
"Foundation_NSFileManager",
|
||||
"Foundation_NSString",
|
||||
"Foundation_NSNumber",
|
||||
"Foundation",
|
||||
"Foundation_NSFileManager",
|
||||
"Foundation_NSString",
|
||||
"Foundation_NSNumber",
|
||||
] }
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -1,224 +1,224 @@
|
|||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = "file:dev.db"
|
||||
provider = "sqlite"
|
||||
url = "file:dev.db"
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "cargo prisma"
|
||||
output = "../../crates/prisma/src/prisma"
|
||||
module_path = "prisma"
|
||||
client_format = "folder"
|
||||
provider = "cargo prisma"
|
||||
output = "../../crates/prisma/src/prisma"
|
||||
module_path = "prisma"
|
||||
client_format = "folder"
|
||||
}
|
||||
|
||||
generator sync {
|
||||
provider = "cargo prisma-sync"
|
||||
output = "../../crates/prisma/src/prisma_sync"
|
||||
client_format = "folder"
|
||||
provider = "cargo prisma-sync"
|
||||
output = "../../crates/prisma/src/prisma_sync"
|
||||
client_format = "folder"
|
||||
}
|
||||
|
||||
model CRDTOperation {
|
||||
id Int @id @default(autoincrement())
|
||||
id Int @id @default(autoincrement())
|
||||
|
||||
timestamp BigInt
|
||||
model String
|
||||
timestamp BigInt
|
||||
model String
|
||||
|
||||
record_id Bytes
|
||||
// Enum: ??
|
||||
kind String
|
||||
data Bytes
|
||||
record_id Bytes
|
||||
// Enum: ??
|
||||
kind String
|
||||
data Bytes
|
||||
|
||||
instance_id Int
|
||||
instance Instance @relation(fields: [instance_id], references: [id])
|
||||
instance_id Int
|
||||
instance Instance @relation(fields: [instance_id], references: [id])
|
||||
|
||||
@@map("crdt_operation")
|
||||
@@map("crdt_operation")
|
||||
}
|
||||
|
||||
/// @deprecated: This model has to exist solely for backwards compatibility.
|
||||
model Node {
|
||||
id Int @id @default(autoincrement())
|
||||
pub_id Bytes @unique
|
||||
name String
|
||||
// Enum: sd_core::node::Platform
|
||||
platform Int
|
||||
date_created DateTime
|
||||
identity Bytes? // TODO: Change to required field in future
|
||||
id Int @id @default(autoincrement())
|
||||
pub_id Bytes @unique
|
||||
name String
|
||||
// Enum: sd_core::node::Platform
|
||||
platform Int
|
||||
date_created DateTime
|
||||
identity Bytes? // TODO: Change to required field in future
|
||||
|
||||
@@map("node")
|
||||
@@map("node")
|
||||
}
|
||||
|
||||
/// @local(id: pub_id)
|
||||
// represents a single `.db` file (SQLite DB) that is paired to the current library.
|
||||
// A `LibraryInstance` is always owned by a single `Node` but it's possible for that node to change (or two to be owned by a single node).
|
||||
model Instance {
|
||||
id Int @id @default(autoincrement()) // This is is NOT globally unique
|
||||
pub_id Bytes @unique // This UUID is meaningless and exists soley cause the `uhlc::ID` must be 16-bit. Really this should be derived from the `identity` field.
|
||||
// Enum: sd_p2p::Identity (or sd_core::p2p::IdentityOrRemoteIdentity in early versions)
|
||||
identity Bytes?
|
||||
// Enum: sd_core::node::RemoteIdentity
|
||||
remote_identity Bytes
|
||||
id Int @id @default(autoincrement()) // This is is NOT globally unique
|
||||
pub_id Bytes @unique // This UUID is meaningless and exists soley cause the `uhlc::ID` must be 16-bit. Really this should be derived from the `identity` field.
|
||||
// Enum: sd_p2p::Identity (or sd_core::p2p::IdentityOrRemoteIdentity in early versions)
|
||||
identity Bytes?
|
||||
// Enum: sd_core::node::RemoteIdentity
|
||||
remote_identity Bytes
|
||||
|
||||
node_id Bytes
|
||||
metadata Bytes? // TODO: This should not be optional
|
||||
node_id Bytes
|
||||
metadata Bytes? // TODO: This should not be optional
|
||||
|
||||
last_seen DateTime // Time core started for owner, last P2P message for P2P node
|
||||
date_created DateTime
|
||||
last_seen DateTime // Time core started for owner, last P2P message for P2P node
|
||||
date_created DateTime
|
||||
|
||||
// clock timestamp for sync
|
||||
timestamp BigInt?
|
||||
// clock timestamp for sync
|
||||
timestamp BigInt?
|
||||
|
||||
locations Location[]
|
||||
CRDTOperation CRDTOperation[]
|
||||
CloudCRDTOperation CloudCRDTOperation[]
|
||||
locations Location[]
|
||||
CRDTOperation CRDTOperation[]
|
||||
CloudCRDTOperation CloudCRDTOperation[]
|
||||
|
||||
@@map("instance")
|
||||
@@map("instance")
|
||||
}
|
||||
|
||||
model Statistics {
|
||||
id Int @id @default(autoincrement())
|
||||
date_captured DateTime @default(now())
|
||||
total_object_count Int @default(0)
|
||||
library_db_size String @default("0")
|
||||
total_bytes_used String @default("0")
|
||||
total_bytes_capacity String @default("0")
|
||||
total_unique_bytes String @default("0")
|
||||
total_bytes_free String @default("0")
|
||||
preview_media_bytes String @default("0")
|
||||
id Int @id @default(autoincrement())
|
||||
date_captured DateTime @default(now())
|
||||
total_object_count Int @default(0)
|
||||
library_db_size String @default("0")
|
||||
total_bytes_used String @default("0")
|
||||
total_bytes_capacity String @default("0")
|
||||
total_unique_bytes String @default("0")
|
||||
total_bytes_free String @default("0")
|
||||
preview_media_bytes String @default("0")
|
||||
|
||||
@@map("statistics")
|
||||
@@map("statistics")
|
||||
}
|
||||
|
||||
/// @local
|
||||
model Volume {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
mount_point String
|
||||
total_bytes_capacity String @default("0")
|
||||
total_bytes_available String @default("0")
|
||||
disk_type String?
|
||||
filesystem String?
|
||||
is_system Boolean @default(false)
|
||||
date_modified DateTime @default(now())
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
mount_point String
|
||||
total_bytes_capacity String @default("0")
|
||||
total_bytes_available String @default("0")
|
||||
disk_type String?
|
||||
filesystem String?
|
||||
is_system Boolean @default(false)
|
||||
date_modified DateTime @default(now())
|
||||
|
||||
@@unique([mount_point, name])
|
||||
@@map("volume")
|
||||
@@unique([mount_point, name])
|
||||
@@map("volume")
|
||||
}
|
||||
|
||||
/// @shared(id: pub_id)
|
||||
model Location {
|
||||
id Int @id @default(autoincrement())
|
||||
pub_id Bytes @unique
|
||||
id Int @id @default(autoincrement())
|
||||
pub_id Bytes @unique
|
||||
|
||||
name String?
|
||||
path String?
|
||||
total_capacity Int?
|
||||
available_capacity Int?
|
||||
size_in_bytes Bytes?
|
||||
is_archived Boolean?
|
||||
generate_preview_media Boolean?
|
||||
sync_preview_media Boolean?
|
||||
hidden Boolean?
|
||||
date_created DateTime?
|
||||
name String?
|
||||
path String?
|
||||
total_capacity Int?
|
||||
available_capacity Int?
|
||||
size_in_bytes Bytes?
|
||||
is_archived Boolean?
|
||||
generate_preview_media Boolean?
|
||||
sync_preview_media Boolean?
|
||||
hidden Boolean?
|
||||
date_created DateTime?
|
||||
|
||||
/// @local
|
||||
// this is just a client side cache which is annoying but oh well (@brendan)
|
||||
instance_id Int?
|
||||
instance Instance? @relation(fields: [instance_id], references: [id], onDelete: SetNull)
|
||||
/// @local
|
||||
// this is just a client side cache which is annoying but oh well (@brendan)
|
||||
instance_id Int?
|
||||
instance Instance? @relation(fields: [instance_id], references: [id], onDelete: SetNull)
|
||||
|
||||
file_paths FilePath[]
|
||||
indexer_rules IndexerRulesInLocation[]
|
||||
file_paths FilePath[]
|
||||
indexer_rules IndexerRulesInLocation[]
|
||||
|
||||
@@map("location")
|
||||
@@map("location")
|
||||
}
|
||||
|
||||
/// @shared(id: pub_id)
|
||||
model FilePath {
|
||||
id Int @id @default(autoincrement())
|
||||
pub_id Bytes @unique
|
||||
id Int @id @default(autoincrement())
|
||||
pub_id Bytes @unique
|
||||
|
||||
is_dir Boolean?
|
||||
is_dir Boolean?
|
||||
|
||||
// content addressable storage id - blake3 sampled checksum
|
||||
cas_id String?
|
||||
// full byte contents digested into blake3 checksum
|
||||
integrity_checksum String?
|
||||
// content addressable storage id - blake3 sampled checksum
|
||||
cas_id String?
|
||||
// full byte contents digested into blake3 checksum
|
||||
integrity_checksum String?
|
||||
|
||||
// location that owns this path
|
||||
location_id Int?
|
||||
location Location? @relation(fields: [location_id], references: [id], onDelete: SetNull)
|
||||
// location that owns this path
|
||||
location_id Int?
|
||||
location Location? @relation(fields: [location_id], references: [id], onDelete: SetNull)
|
||||
|
||||
// the path of the file relative to its location
|
||||
materialized_path String?
|
||||
// the path of the file relative to its location
|
||||
materialized_path String?
|
||||
|
||||
// the name and extension, MUST have 'COLLATE NOCASE' in migration
|
||||
name String?
|
||||
extension String?
|
||||
hidden Boolean?
|
||||
// the name and extension, MUST have 'COLLATE NOCASE' in migration
|
||||
name String?
|
||||
extension String?
|
||||
hidden Boolean?
|
||||
|
||||
size_in_bytes String? // deprecated
|
||||
size_in_bytes_bytes Bytes?
|
||||
size_in_bytes String? // deprecated
|
||||
size_in_bytes_bytes Bytes?
|
||||
|
||||
inode Bytes? // This is actually an unsigned 64 bit integer, but we don't have this type in SQLite
|
||||
inode Bytes? // This is actually an unsigned 64 bit integer, but we don't have this type in SQLite
|
||||
|
||||
// the unique Object for this file path
|
||||
object_id Int?
|
||||
object Object? @relation(fields: [object_id], references: [id], onDelete: SetNull)
|
||||
// the unique Object for this file path
|
||||
object_id Int?
|
||||
object Object? @relation(fields: [object_id], references: [id], onDelete: SetNull)
|
||||
|
||||
key_id Int? // replacement for encryption
|
||||
// permissions String?
|
||||
key_id Int? // replacement for encryption
|
||||
// permissions String?
|
||||
|
||||
date_created DateTime?
|
||||
date_modified DateTime?
|
||||
date_indexed DateTime?
|
||||
date_created DateTime?
|
||||
date_modified DateTime?
|
||||
date_indexed DateTime?
|
||||
|
||||
// key Key? @relation(fields: [key_id], references: [id])
|
||||
// key Key? @relation(fields: [key_id], references: [id])
|
||||
|
||||
@@unique([location_id, materialized_path, name, extension])
|
||||
@@unique([location_id, inode])
|
||||
@@index([location_id])
|
||||
@@index([location_id, materialized_path])
|
||||
@@map("file_path")
|
||||
@@unique([location_id, materialized_path, name, extension])
|
||||
@@unique([location_id, inode])
|
||||
@@index([location_id])
|
||||
@@index([location_id, materialized_path])
|
||||
@@map("file_path")
|
||||
}
|
||||
|
||||
/// @shared(id: pub_id)
|
||||
model Object {
|
||||
id Int @id @default(autoincrement())
|
||||
pub_id Bytes @unique
|
||||
// Enum: sd_file_ext::kind::ObjectKind
|
||||
kind Int?
|
||||
id Int @id @default(autoincrement())
|
||||
pub_id Bytes @unique
|
||||
// Enum: sd_file_ext::kind::ObjectKind
|
||||
kind Int?
|
||||
|
||||
key_id Int?
|
||||
// handy ways to mark an object
|
||||
hidden Boolean?
|
||||
favorite Boolean?
|
||||
important Boolean?
|
||||
// if we have generated preview media for this object on at least one Node
|
||||
// commented out for now by @brendonovich since they they're irrelevant to the sync system
|
||||
// has_thumbnail Boolean?
|
||||
// has_thumbstrip Boolean?
|
||||
// has_video_preview Boolean?
|
||||
// TODO: change above to:
|
||||
// has_generated_thumbnail Boolean @default(false)
|
||||
// has_generated_thumbstrip Boolean @default(false)
|
||||
// has_generated_video_preview Boolean @default(false)
|
||||
// integration with ipfs
|
||||
// ipfs_id String?
|
||||
// plain text note
|
||||
note String?
|
||||
// the original known creation date of this object
|
||||
date_created DateTime?
|
||||
date_accessed DateTime?
|
||||
key_id Int?
|
||||
// handy ways to mark an object
|
||||
hidden Boolean?
|
||||
favorite Boolean?
|
||||
important Boolean?
|
||||
// if we have generated preview media for this object on at least one Node
|
||||
// commented out for now by @brendonovich since they they're irrelevant to the sync system
|
||||
// has_thumbnail Boolean?
|
||||
// has_thumbstrip Boolean?
|
||||
// has_video_preview Boolean?
|
||||
// TODO: change above to:
|
||||
// has_generated_thumbnail Boolean @default(false)
|
||||
// has_generated_thumbstrip Boolean @default(false)
|
||||
// has_generated_video_preview Boolean @default(false)
|
||||
// integration with ipfs
|
||||
// ipfs_id String?
|
||||
// plain text note
|
||||
note String?
|
||||
// the original known creation date of this object
|
||||
date_created DateTime?
|
||||
date_accessed DateTime?
|
||||
|
||||
tags TagOnObject[]
|
||||
labels LabelOnObject[]
|
||||
albums ObjectInAlbum[]
|
||||
spaces ObjectInSpace[]
|
||||
file_paths FilePath[]
|
||||
// comments Comment[]
|
||||
media_data MediaData?
|
||||
tags TagOnObject[]
|
||||
labels LabelOnObject[]
|
||||
albums ObjectInAlbum[]
|
||||
spaces ObjectInSpace[]
|
||||
file_paths FilePath[]
|
||||
// comments Comment[]
|
||||
media_data MediaData?
|
||||
|
||||
// key Key? @relation(fields: [key_id], references: [id])
|
||||
// key Key? @relation(fields: [key_id], references: [id])
|
||||
|
||||
@@map("object")
|
||||
@@map("object")
|
||||
}
|
||||
|
||||
// if there is a conflicting cas_id, the conficting file should be updated to have a larger cas_id as
|
||||
|
@ -276,181 +276,181 @@ model Object {
|
|||
|
||||
/// @shared(id: object)
|
||||
model MediaData {
|
||||
id Int @id @default(autoincrement())
|
||||
id Int @id @default(autoincrement())
|
||||
|
||||
resolution Bytes?
|
||||
media_date Bytes?
|
||||
media_location Bytes?
|
||||
camera_data Bytes?
|
||||
artist String?
|
||||
description String?
|
||||
copyright String?
|
||||
exif_version String?
|
||||
resolution Bytes?
|
||||
media_date Bytes?
|
||||
media_location Bytes?
|
||||
camera_data Bytes?
|
||||
artist String?
|
||||
description String?
|
||||
copyright String?
|
||||
exif_version String?
|
||||
|
||||
// purely for sorting/ordering, never sent to the frontend as they'd be useless
|
||||
// these are also usually one-way, and not reversible
|
||||
// (e.g. we can't get `MediaDate::Utc(2023-09-26T22:04:37+01:00)` from `1695758677` as we don't store the TZ)
|
||||
epoch_time BigInt? // time since unix epoch
|
||||
// purely for sorting/ordering, never sent to the frontend as they'd be useless
|
||||
// these are also usually one-way, and not reversible
|
||||
// (e.g. we can't get `MediaDate::Utc(2023-09-26T22:04:37+01:00)` from `1695758677` as we don't store the TZ)
|
||||
epoch_time BigInt? // time since unix epoch
|
||||
|
||||
// video-specific
|
||||
// duration Int?
|
||||
// fps Int?
|
||||
// streams Int?
|
||||
// video_codec String? // eg: "h264, h265, av1"
|
||||
// audio_codec String? // eg: "opus"
|
||||
// video-specific
|
||||
// duration Int?
|
||||
// fps Int?
|
||||
// streams Int?
|
||||
// video_codec String? // eg: "h264, h265, av1"
|
||||
// audio_codec String? // eg: "opus"
|
||||
|
||||
object_id Int @unique
|
||||
object Object @relation(fields: [object_id], references: [id], onDelete: Cascade)
|
||||
object_id Int @unique
|
||||
object Object @relation(fields: [object_id], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("media_data")
|
||||
@@map("media_data")
|
||||
}
|
||||
|
||||
//// Tag ////
|
||||
|
||||
/// @shared(id: pub_id)
|
||||
model Tag {
|
||||
id Int @id @default(autoincrement())
|
||||
pub_id Bytes @unique
|
||||
name String?
|
||||
color String?
|
||||
id Int @id @default(autoincrement())
|
||||
pub_id Bytes @unique
|
||||
name String?
|
||||
color String?
|
||||
|
||||
is_hidden Boolean? // user hidden entire tag
|
||||
is_hidden Boolean? // user hidden entire tag
|
||||
|
||||
date_created DateTime?
|
||||
date_modified DateTime?
|
||||
date_created DateTime?
|
||||
date_modified DateTime?
|
||||
|
||||
tag_objects TagOnObject[]
|
||||
tag_objects TagOnObject[]
|
||||
|
||||
@@map("tag")
|
||||
@@map("tag")
|
||||
}
|
||||
|
||||
/// @relation(item: object, group: tag)
|
||||
model TagOnObject {
|
||||
object_id Int
|
||||
object Object @relation(fields: [object_id], references: [id], onDelete: Restrict)
|
||||
object_id Int
|
||||
object Object @relation(fields: [object_id], references: [id], onDelete: Restrict)
|
||||
|
||||
tag_id Int
|
||||
tag Tag @relation(fields: [tag_id], references: [id], onDelete: Restrict)
|
||||
tag_id Int
|
||||
tag Tag @relation(fields: [tag_id], references: [id], onDelete: Restrict)
|
||||
|
||||
date_created DateTime?
|
||||
date_created DateTime?
|
||||
|
||||
@@id([tag_id, object_id])
|
||||
@@map("tag_on_object")
|
||||
@@id([tag_id, object_id])
|
||||
@@map("tag_on_object")
|
||||
}
|
||||
|
||||
//// Label ////
|
||||
|
||||
/// @shared(id: name)
|
||||
model Label {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
date_created DateTime?
|
||||
date_modified DateTime?
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
date_created DateTime?
|
||||
date_modified DateTime?
|
||||
|
||||
label_objects LabelOnObject[]
|
||||
label_objects LabelOnObject[]
|
||||
|
||||
@@map("label")
|
||||
@@map("label")
|
||||
}
|
||||
|
||||
/// @relation(item: object, group: label)
|
||||
model LabelOnObject {
|
||||
date_created DateTime @default(now())
|
||||
date_created DateTime @default(now())
|
||||
|
||||
object_id Int
|
||||
object Object @relation(fields: [object_id], references: [id], onDelete: Restrict)
|
||||
object_id Int
|
||||
object Object @relation(fields: [object_id], references: [id], onDelete: Restrict)
|
||||
|
||||
label_id Int
|
||||
label Label @relation(fields: [label_id], references: [id], onDelete: Restrict)
|
||||
label_id Int
|
||||
label Label @relation(fields: [label_id], references: [id], onDelete: Restrict)
|
||||
|
||||
@@id([label_id, object_id])
|
||||
@@map("label_on_object")
|
||||
@@id([label_id, object_id])
|
||||
@@map("label_on_object")
|
||||
}
|
||||
|
||||
//// Space ////
|
||||
|
||||
model Space {
|
||||
id Int @id @default(autoincrement())
|
||||
pub_id Bytes @unique
|
||||
name String?
|
||||
description String?
|
||||
date_created DateTime?
|
||||
date_modified DateTime?
|
||||
id Int @id @default(autoincrement())
|
||||
pub_id Bytes @unique
|
||||
name String?
|
||||
description String?
|
||||
date_created DateTime?
|
||||
date_modified DateTime?
|
||||
|
||||
objects ObjectInSpace[]
|
||||
objects ObjectInSpace[]
|
||||
|
||||
@@map("space")
|
||||
@@map("space")
|
||||
}
|
||||
|
||||
model ObjectInSpace {
|
||||
space_id Int
|
||||
space Space @relation(fields: [space_id], references: [id], onDelete: Restrict)
|
||||
space_id Int
|
||||
space Space @relation(fields: [space_id], references: [id], onDelete: Restrict)
|
||||
|
||||
object_id Int
|
||||
object Object @relation(fields: [object_id], references: [id], onDelete: Restrict)
|
||||
object_id Int
|
||||
object Object @relation(fields: [object_id], references: [id], onDelete: Restrict)
|
||||
|
||||
@@id([space_id, object_id])
|
||||
@@map("object_in_space")
|
||||
@@id([space_id, object_id])
|
||||
@@map("object_in_space")
|
||||
}
|
||||
|
||||
//// Job ////
|
||||
|
||||
model Job {
|
||||
id Bytes @id
|
||||
id Bytes @id
|
||||
|
||||
name String?
|
||||
action String? // Will be composed of "{action_description}(-{children_order})*"
|
||||
name String?
|
||||
action String? // Will be composed of "{action_description}(-{children_order})*"
|
||||
|
||||
// Enum: sd_core::job::job_manager:JobStatus
|
||||
status Int? // 0 = Queued
|
||||
// Enum: sd_core::job::job_manager:JobStatus
|
||||
status Int? // 0 = Queued
|
||||
|
||||
// List of errors, separated by "\n\n" in case of failed jobs or completed with errors
|
||||
errors_text String?
|
||||
// List of errors, separated by "\n\n" in case of failed jobs or completed with errors
|
||||
errors_text String?
|
||||
|
||||
data Bytes? // Serialized data to be used on pause/resume
|
||||
metadata Bytes? // Serialized metadata field with info about the job after completion
|
||||
data Bytes? // Serialized data to be used on pause/resume
|
||||
metadata Bytes? // Serialized metadata field with info about the job after completion
|
||||
|
||||
parent_id Bytes?
|
||||
parent_id Bytes?
|
||||
|
||||
task_count Int?
|
||||
completed_task_count Int?
|
||||
date_estimated_completion DateTime? // Estimated timestamp that the job will be complete at
|
||||
task_count Int?
|
||||
completed_task_count Int?
|
||||
date_estimated_completion DateTime? // Estimated timestamp that the job will be complete at
|
||||
|
||||
date_created DateTime?
|
||||
date_started DateTime? // Started execution
|
||||
date_completed DateTime? // Finished execution
|
||||
date_created DateTime?
|
||||
date_started DateTime? // Started execution
|
||||
date_completed DateTime? // Finished execution
|
||||
|
||||
parent Job? @relation("jobs_dependency", fields: [parent_id], references: [id], onDelete: SetNull)
|
||||
children Job[] @relation("jobs_dependency")
|
||||
parent Job? @relation("jobs_dependency", fields: [parent_id], references: [id], onDelete: SetNull)
|
||||
children Job[] @relation("jobs_dependency")
|
||||
|
||||
@@map("job")
|
||||
@@map("job")
|
||||
}
|
||||
|
||||
//// Album ////
|
||||
|
||||
model Album {
|
||||
id Int @id
|
||||
pub_id Bytes @unique
|
||||
name String?
|
||||
is_hidden Boolean?
|
||||
id Int @id
|
||||
pub_id Bytes @unique
|
||||
name String?
|
||||
is_hidden Boolean?
|
||||
|
||||
date_created DateTime?
|
||||
date_modified DateTime?
|
||||
date_created DateTime?
|
||||
date_modified DateTime?
|
||||
|
||||
objects ObjectInAlbum[]
|
||||
objects ObjectInAlbum[]
|
||||
|
||||
@@map("album")
|
||||
@@map("album")
|
||||
}
|
||||
|
||||
model ObjectInAlbum {
|
||||
date_created DateTime?
|
||||
album_id Int
|
||||
album Album @relation(fields: [album_id], references: [id], onDelete: NoAction)
|
||||
date_created DateTime?
|
||||
album_id Int
|
||||
album Album @relation(fields: [album_id], references: [id], onDelete: NoAction)
|
||||
|
||||
object_id Int
|
||||
object Object @relation(fields: [object_id], references: [id], onDelete: NoAction)
|
||||
object_id Int
|
||||
object Object @relation(fields: [object_id], references: [id], onDelete: NoAction)
|
||||
|
||||
@@id([album_id, object_id])
|
||||
@@map("object_in_album")
|
||||
@@id([album_id, object_id])
|
||||
@@map("object_in_album")
|
||||
}
|
||||
|
||||
//// Comment ////
|
||||
|
@ -470,82 +470,84 @@ model ObjectInAlbum {
|
|||
//// Indexer Rules ////
|
||||
|
||||
model IndexerRule {
|
||||
id Int @id @default(autoincrement())
|
||||
pub_id Bytes @unique
|
||||
id Int @id @default(autoincrement())
|
||||
pub_id Bytes @unique
|
||||
|
||||
name String?
|
||||
default Boolean?
|
||||
rules_per_kind Bytes?
|
||||
date_created DateTime?
|
||||
date_modified DateTime?
|
||||
name String?
|
||||
default Boolean?
|
||||
rules_per_kind Bytes?
|
||||
date_created DateTime?
|
||||
date_modified DateTime?
|
||||
|
||||
locations IndexerRulesInLocation[]
|
||||
locations IndexerRulesInLocation[]
|
||||
|
||||
@@map("indexer_rule")
|
||||
@@map("indexer_rule")
|
||||
}
|
||||
|
||||
model IndexerRulesInLocation {
|
||||
location_id Int
|
||||
location Location @relation(fields: [location_id], references: [id], onDelete: Restrict)
|
||||
location_id Int
|
||||
location Location @relation(fields: [location_id], references: [id], onDelete: Restrict)
|
||||
|
||||
indexer_rule_id Int
|
||||
indexer_rule IndexerRule @relation(fields: [indexer_rule_id], references: [id], onDelete: Restrict)
|
||||
indexer_rule_id Int
|
||||
indexer_rule IndexerRule @relation(fields: [indexer_rule_id], references: [id], onDelete: Restrict)
|
||||
|
||||
@@id([location_id, indexer_rule_id])
|
||||
@@map("indexer_rule_in_location")
|
||||
@@id([location_id, indexer_rule_id])
|
||||
@@map("indexer_rule_in_location")
|
||||
}
|
||||
|
||||
/// @shared(id: key)
|
||||
model Preference {
|
||||
key String @id
|
||||
value Bytes?
|
||||
key String @id
|
||||
value Bytes?
|
||||
|
||||
@@map("preference")
|
||||
@@map("preference")
|
||||
}
|
||||
|
||||
model Notification {
|
||||
id Int @id @default(autoincrement())
|
||||
read Boolean @default(false)
|
||||
// Enum: crate::api::notifications::NotificationData
|
||||
data Bytes
|
||||
expires_at DateTime?
|
||||
id Int @id @default(autoincrement())
|
||||
read Boolean @default(false)
|
||||
// Enum: crate::api::notifications::NotificationData
|
||||
data Bytes
|
||||
expires_at DateTime?
|
||||
|
||||
@@map("notification")
|
||||
@@map("notification")
|
||||
}
|
||||
|
||||
/// @shared(id: pub_id)
|
||||
model SavedSearch {
|
||||
id Int @id @default(autoincrement())
|
||||
pub_id Bytes @unique
|
||||
id Int @id @default(autoincrement())
|
||||
pub_id Bytes @unique
|
||||
|
||||
search String?
|
||||
filters String?
|
||||
// enum: crate::api::search::saved::SearchTarget
|
||||
target String?
|
||||
search String?
|
||||
filters String?
|
||||
|
||||
name String?
|
||||
icon String?
|
||||
description String?
|
||||
// order Int? // Add this line to include ordering
|
||||
name String?
|
||||
icon String?
|
||||
description String?
|
||||
// order Int? // Add this line to include ordering
|
||||
|
||||
date_created DateTime?
|
||||
date_modified DateTime?
|
||||
date_created DateTime?
|
||||
date_modified DateTime?
|
||||
|
||||
@@map("saved_search")
|
||||
@@map("saved_search")
|
||||
}
|
||||
|
||||
/// @local(id: id)
|
||||
model CloudCRDTOperation {
|
||||
id Int @id @default(autoincrement())
|
||||
id Int @id @default(autoincrement())
|
||||
|
||||
timestamp BigInt
|
||||
model String
|
||||
timestamp BigInt
|
||||
model String
|
||||
|
||||
record_id Bytes
|
||||
// Enum: ??
|
||||
kind String
|
||||
data Bytes
|
||||
record_id Bytes
|
||||
// Enum: ??
|
||||
kind String
|
||||
data Bytes
|
||||
|
||||
instance_id Int
|
||||
instance Instance @relation(fields: [instance_id], references: [id])
|
||||
instance_id Int
|
||||
instance Instance @relation(fields: [instance_id], references: [id])
|
||||
|
||||
@@map("cloud_crdt_operation")
|
||||
@@map("cloud_crdt_operation")
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use crate::{api::utils::library, invalidate_query, library::Library};
|
||||
|
||||
use sd_prisma::{prisma::saved_search, prisma_sync};
|
||||
|
@ -6,21 +8,52 @@ use sd_utils::chain_optional_iter;
|
|||
|
||||
use chrono::{DateTime, FixedOffset, Utc};
|
||||
use rspc::alpha::AlphaRouter;
|
||||
use serde::{de::IgnoredAny, Deserialize, Serialize};
|
||||
use serde::{de::IgnoredAny, Deserialize};
|
||||
use specta::Type;
|
||||
use tracing::error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::{Ctx, R};
|
||||
|
||||
#[derive(Type, Deserialize, Clone, Debug, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
enum SearchTarget {
|
||||
#[default]
|
||||
Paths,
|
||||
Objects,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SearchTarget {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SearchTarget::Paths => write!(f, "paths"),
|
||||
SearchTarget::Objects => write!(f, "objects"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SearchTarget {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"paths" => Ok(SearchTarget::Paths),
|
||||
"objects" => Ok(SearchTarget::Objects),
|
||||
_ => Err(format!("invalid search target: {s}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
R.router()
|
||||
.procedure("create", {
|
||||
R.with2(library()).mutation({
|
||||
#[derive(Serialize, Type, Deserialize, Clone, Debug)]
|
||||
#[derive(Type, Deserialize, Clone, Debug)]
|
||||
#[specta(inline)]
|
||||
pub struct Args {
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub target: SearchTarget,
|
||||
#[specta(optional)]
|
||||
pub search: Option<String>,
|
||||
#[specta(optional)]
|
||||
|
@ -40,6 +73,7 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
[
|
||||
sync_db_entry!(date_created, saved_search::date_created),
|
||||
sync_db_entry!(args.name, saved_search::name),
|
||||
sync_db_entry!(args.target.to_string(), saved_search::target),
|
||||
],
|
||||
[
|
||||
option_sync_db_entry!(
|
||||
|
|
|
@ -11,7 +11,7 @@ import type {
|
|||
NodeState,
|
||||
Tag
|
||||
} from '@sd/client';
|
||||
import { type Ordering, type OrderingKeys } from '@sd/client';
|
||||
import { ObjectKindEnum, type Ordering, type OrderingKeys } from '@sd/client';
|
||||
|
||||
import { createDefaultExplorerSettings } from './store';
|
||||
import { uniqueId } from './util';
|
||||
|
@ -128,6 +128,12 @@ export function useExplorerSettings<TOrder extends Ordering>({
|
|||
|
||||
return {
|
||||
useSettingsSnapshot: () => useSnapshot(store),
|
||||
useLayoutSearchFilters: () => {
|
||||
const explorerSettingsSnapshot = useSnapshot(store);
|
||||
return explorerSettingsSnapshot.layoutMode === 'media'
|
||||
? [{ object: { kind: { in: [ObjectKindEnum.Image, ObjectKindEnum.Video] } } }]
|
||||
: [];
|
||||
},
|
||||
settingsStore: store,
|
||||
orderingKeys
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useMemo } from 'react';
|
||||
import { ObjectKindEnum, ObjectOrder, SearchFilterArgs, useObjectsExplorerQuery } from '@sd/client';
|
||||
import { ObjectOrder } from '@sd/client';
|
||||
import { Icon } from '~/components';
|
||||
import { useRouteTitle } from '~/hooks';
|
||||
|
||||
|
@ -9,8 +9,9 @@ import { createDefaultExplorerSettings, objectOrderingKeysSchema } from './Explo
|
|||
import { DefaultTopBarOptions } from './Explorer/TopBarOptions';
|
||||
import { useExplorer, useExplorerSettings } from './Explorer/useExplorer';
|
||||
import { EmptyNotice } from './Explorer/View/EmptyNotice';
|
||||
import { SearchContextProvider, SearchOptions, useSearch } from './search';
|
||||
import { SearchContextProvider, SearchOptions, useSearchFromSearchParams } from './search';
|
||||
import SearchBar from './search/SearchBar';
|
||||
import { useSearchExplorerQuery } from './search/useSearchExplorerQuery';
|
||||
import { TopBarPortal } from './TopBar/Portal';
|
||||
|
||||
export function Component() {
|
||||
|
@ -23,37 +24,25 @@ export function Component() {
|
|||
orderingKeys: objectOrderingKeysSchema
|
||||
});
|
||||
|
||||
const explorerSettingsSnapshot = explorerSettings.useSettingsSnapshot();
|
||||
const search = useSearchFromSearchParams();
|
||||
|
||||
const fixedFilters = useMemo<SearchFilterArgs[]>(
|
||||
() => [
|
||||
// { object: { favorite: true } },
|
||||
...(explorerSettingsSnapshot.layoutMode === 'media'
|
||||
? [{ object: { kind: { in: [ObjectKindEnum.Image, ObjectKindEnum.Video] } } }]
|
||||
: [])
|
||||
const defaultFilter = { object: { favorite: true } };
|
||||
|
||||
const items = useSearchExplorerQuery({
|
||||
search,
|
||||
explorerSettings,
|
||||
filters: [
|
||||
...search.allFilters,
|
||||
// TODO: Add filter to search options
|
||||
defaultFilter
|
||||
],
|
||||
[explorerSettingsSnapshot.layoutMode]
|
||||
);
|
||||
|
||||
const search = useSearch({
|
||||
fixedFilters
|
||||
});
|
||||
|
||||
const objects = useObjectsExplorerQuery({
|
||||
arg: {
|
||||
take: 100,
|
||||
filters: [
|
||||
...search.allFilters,
|
||||
// TODO: Add filter to search options
|
||||
{ object: { favorite: true } }
|
||||
]
|
||||
},
|
||||
order: explorerSettings.useSettingsSnapshot().order
|
||||
take: 100,
|
||||
objects: { order: explorerSettings.useSettingsSnapshot().order }
|
||||
});
|
||||
|
||||
const explorer = useExplorer({
|
||||
...objects,
|
||||
isFetchingNextPage: objects.query.isFetchingNextPage,
|
||||
...items,
|
||||
isFetchingNextPage: items.query.isFetchingNextPage,
|
||||
settings: explorerSettings
|
||||
});
|
||||
|
||||
|
@ -61,7 +50,7 @@ export function Component() {
|
|||
<ExplorerContextProvider explorer={explorer}>
|
||||
<SearchContextProvider search={search}>
|
||||
<TopBarPortal
|
||||
center={<SearchBar />}
|
||||
center={<SearchBar defaultFilters={[defaultFilter]} />}
|
||||
left={
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<span className="truncate text-sm font-medium">Favorites</span>
|
||||
|
|
|
@ -9,7 +9,12 @@ import { createDefaultExplorerSettings, objectOrderingKeysSchema } from './Explo
|
|||
import { DefaultTopBarOptions } from './Explorer/TopBarOptions';
|
||||
import { useExplorer, useExplorerSettings } from './Explorer/useExplorer';
|
||||
import { EmptyNotice } from './Explorer/View/EmptyNotice';
|
||||
import { SearchContextProvider, SearchOptions, useSearch } from './search';
|
||||
import {
|
||||
SearchContextProvider,
|
||||
SearchOptions,
|
||||
useSearch,
|
||||
useSearchFromSearchParams
|
||||
} from './search';
|
||||
import SearchBar from './search/SearchBar';
|
||||
import { TopBarPortal } from './TopBar/Portal';
|
||||
|
||||
|
@ -25,26 +30,7 @@ export function Component() {
|
|||
orderingKeys: objectOrderingKeysSchema
|
||||
});
|
||||
|
||||
// const explorerSettingsSnapshot = explorerSettings.useSettingsSnapshot();
|
||||
|
||||
// const fixedFilters = useMemo<SearchFilterArgs[]>(
|
||||
// () => [
|
||||
// ...(explorerSettingsSnapshot.layoutMode === 'media'
|
||||
// ? [{ object: { kind: { in: [ObjectKindEnum.Image, ObjectKindEnum.Video] } } }]
|
||||
// : [])
|
||||
// ],
|
||||
// [explorerSettingsSnapshot.layoutMode]
|
||||
// );
|
||||
|
||||
const search = useSearch({});
|
||||
|
||||
// const objects = useObjectsExplorerQuery({
|
||||
// arg: {
|
||||
// take: 100,
|
||||
// filters: [...search.allFilters, { object: { tags: { in: [3] } } }]
|
||||
// },
|
||||
// explorerSettings
|
||||
// });
|
||||
const search = useSearchFromSearchParams();
|
||||
|
||||
const explorer = useExplorer({
|
||||
items: labels.data || null,
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
import { ArrowClockwise, Info } from '@phosphor-icons/react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useSearchParams as useRawSearchParams } from 'react-router-dom';
|
||||
import { stringify } from 'uuid';
|
||||
import {
|
||||
arraysEqual,
|
||||
ExplorerSettings,
|
||||
FilePathOrder,
|
||||
Location,
|
||||
ObjectKindEnum,
|
||||
useCache,
|
||||
useLibraryMutation,
|
||||
useLibraryQuery,
|
||||
useLibrarySubscription,
|
||||
useNodes,
|
||||
useOnlineLocations,
|
||||
usePathsExplorerQuery,
|
||||
useRspcLibraryContext
|
||||
} from '@sd/client';
|
||||
import { Loader, Tooltip } from '@sd/ui';
|
||||
|
@ -38,11 +35,12 @@ import {
|
|||
filePathOrderingKeysSchema
|
||||
} from '../Explorer/store';
|
||||
import { DefaultTopBarOptions } from '../Explorer/TopBarOptions';
|
||||
import { useExplorer, UseExplorerSettings, useExplorerSettings } from '../Explorer/useExplorer';
|
||||
import { useExplorer, useExplorerSettings } from '../Explorer/useExplorer';
|
||||
import { useExplorerSearchParams } from '../Explorer/util';
|
||||
import { EmptyNotice } from '../Explorer/View/EmptyNotice';
|
||||
import { SearchContextProvider, SearchOptions, useSearch } from '../search';
|
||||
import { SearchContextProvider, SearchOptions, useSearchFromSearchParams } from '../search';
|
||||
import SearchBar from '../search/SearchBar';
|
||||
import { useSearchExplorerQuery } from '../search/useSearchExplorerQuery';
|
||||
import { TopBarPortal } from '../TopBar/Portal';
|
||||
import { TOP_BAR_ICON_STYLE } from '../TopBar/TopBarOptions';
|
||||
import LocationOptions from './LocationOptions';
|
||||
|
@ -71,35 +69,47 @@ const LocationExplorer = ({ location }: { location: Location; path?: string }) =
|
|||
const { layoutMode, mediaViewWithDescendants, showHiddenFiles } =
|
||||
explorerSettings.useSettingsSnapshot();
|
||||
|
||||
const search = useLocationSearch(explorerSettings, location);
|
||||
const defaultFilters = useMemo(
|
||||
() => [{ filePath: { locations: { in: [location.id] } } }],
|
||||
[location.id]
|
||||
);
|
||||
|
||||
const paths = usePathsExplorerQuery({
|
||||
arg: {
|
||||
filters: [
|
||||
...search.allFilters,
|
||||
{
|
||||
filePath: {
|
||||
path: {
|
||||
location_id: location.id,
|
||||
path: path ?? '',
|
||||
include_descendants:
|
||||
search.search !== '' ||
|
||||
search.dynamicFilters.length > 0 ||
|
||||
(layoutMode === 'media' && mediaViewWithDescendants)
|
||||
}
|
||||
const search = useSearchFromSearchParams();
|
||||
|
||||
const searchFiltersAreDefault = useMemo(
|
||||
() => JSON.stringify(defaultFilters) !== JSON.stringify(search.filters),
|
||||
[defaultFilters, search.filters]
|
||||
);
|
||||
|
||||
const items = useSearchExplorerQuery({
|
||||
search,
|
||||
explorerSettings,
|
||||
filters: [
|
||||
...(search.allFilters.length > 0 ? search.allFilters : defaultFilters),
|
||||
{
|
||||
filePath: {
|
||||
path: {
|
||||
location_id: location.id,
|
||||
path: path ?? '',
|
||||
include_descendants:
|
||||
search.search !== '' ||
|
||||
(search.filters &&
|
||||
search.filters.length > 0 &&
|
||||
searchFiltersAreDefault) ||
|
||||
(layoutMode === 'media' && mediaViewWithDescendants)
|
||||
}
|
||||
},
|
||||
!showHiddenFiles && { filePath: { hidden: false } }
|
||||
].filter(Boolean) as any,
|
||||
take
|
||||
},
|
||||
order: explorerSettings.useSettingsSnapshot().order,
|
||||
}
|
||||
},
|
||||
...(!showHiddenFiles ? [{ filePath: { hidden: false } }] : [])
|
||||
],
|
||||
take,
|
||||
paths: { order: explorerSettings.useSettingsSnapshot().order },
|
||||
onSuccess: () => explorerStore.resetNewThumbnails()
|
||||
});
|
||||
|
||||
const explorer = useExplorer({
|
||||
...paths,
|
||||
isFetchingNextPage: paths.query.isFetchingNextPage,
|
||||
...items,
|
||||
isFetchingNextPage: items.query.isFetchingNextPage,
|
||||
isLoadingPreferences: preferences.isLoading,
|
||||
settings: explorerSettings,
|
||||
parent: { type: 'Location', location }
|
||||
|
@ -134,7 +144,7 @@ const LocationExplorer = ({ location }: { location: Location; path?: string }) =
|
|||
<ExplorerContextProvider explorer={explorer}>
|
||||
<SearchContextProvider search={search}>
|
||||
<TopBarPortal
|
||||
center={<SearchBar />}
|
||||
center={<SearchBar defaultFilters={defaultFilters} />}
|
||||
left={
|
||||
<div className="flex items-center gap-2">
|
||||
<Folder size={22} className="-mt-px" />
|
||||
|
@ -267,62 +277,3 @@ function useLocationExplorerSettings(location: Location) {
|
|||
preferences
|
||||
};
|
||||
}
|
||||
|
||||
function useLocationSearch(
|
||||
explorerSettings: UseExplorerSettings<FilePathOrder>,
|
||||
location: Location
|
||||
) {
|
||||
const [searchParams, setSearchParams] = useRawSearchParams();
|
||||
const explorerSettingsSnapshot = explorerSettings.useSettingsSnapshot();
|
||||
|
||||
const fixedFilters = useMemo(
|
||||
() => [
|
||||
{ filePath: { locations: { in: [location.id] } } },
|
||||
...(explorerSettingsSnapshot.layoutMode === 'media'
|
||||
? [{ object: { kind: { in: [ObjectKindEnum.Image, ObjectKindEnum.Video] } } }]
|
||||
: [])
|
||||
],
|
||||
[location.id, explorerSettingsSnapshot.layoutMode]
|
||||
);
|
||||
|
||||
const filtersParam = searchParams.get('filters');
|
||||
const dynamicFilters = useMemo(() => JSON.parse(filtersParam ?? '[]'), [filtersParam]);
|
||||
|
||||
const searchQueryParam = searchParams.get('search');
|
||||
|
||||
const search = useSearch({
|
||||
open: !!searchQueryParam || dynamicFilters.length > 0 || undefined,
|
||||
search: searchParams.get('search') ?? undefined,
|
||||
fixedFilters,
|
||||
dynamicFilters
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setSearchParams(
|
||||
(p) => {
|
||||
if (search.dynamicFilters.length > 0)
|
||||
p.set('filters', JSON.stringify(search.dynamicFilters));
|
||||
else p.delete('filters');
|
||||
|
||||
return p;
|
||||
},
|
||||
{ replace: true }
|
||||
);
|
||||
}, [search.dynamicFilters, setSearchParams]);
|
||||
|
||||
const searchQuery = search.search;
|
||||
|
||||
useEffect(() => {
|
||||
setSearchParams(
|
||||
(p) => {
|
||||
if (searchQuery !== '') p.set('search', searchQuery);
|
||||
else p.delete('search');
|
||||
|
||||
return p;
|
||||
},
|
||||
{ replace: true }
|
||||
);
|
||||
}, [searchQuery, setSearchParams]);
|
||||
|
||||
return search;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useLocale } from '~/hooks';
|
|||
import { useRouteTitle } from '~/hooks/useRouteTitle';
|
||||
import { hardwareModelToIcon } from '~/util/hardware';
|
||||
|
||||
import { SearchContextProvider, useSearch } from '../search';
|
||||
import { SearchContextProvider, useSearch, useSearchFromSearchParams } from '../search';
|
||||
import SearchBar from '../search/SearchBar';
|
||||
import { AddLocationButton } from '../settings/library/locations/AddLocationButton';
|
||||
import { TopBarPortal } from '../TopBar/Portal';
|
||||
|
@ -25,7 +25,7 @@ export const Component = () => {
|
|||
|
||||
const { data: node } = useBridgeQuery(['nodeState']);
|
||||
|
||||
const search = useSearch();
|
||||
const search = useSearchFromSearchParams();
|
||||
|
||||
const stats = useLibraryQuery(['library.statistics']);
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useMemo } from 'react';
|
||||
import { ObjectKindEnum, ObjectOrder, SearchFilterArgs, useObjectsExplorerQuery } from '@sd/client';
|
||||
import { ObjectOrder } from '@sd/client';
|
||||
import { Icon } from '~/components';
|
||||
import { useRouteTitle } from '~/hooks';
|
||||
|
||||
|
@ -9,8 +9,9 @@ import { createDefaultExplorerSettings, objectOrderingKeysSchema } from './Explo
|
|||
import { DefaultTopBarOptions } from './Explorer/TopBarOptions';
|
||||
import { useExplorer, useExplorerSettings } from './Explorer/useExplorer';
|
||||
import { EmptyNotice } from './Explorer/View/EmptyNotice';
|
||||
import { SearchContextProvider, SearchOptions, useSearch } from './search';
|
||||
import { SearchContextProvider, SearchOptions, useSearchFromSearchParams } from './search';
|
||||
import SearchBar from './search/SearchBar';
|
||||
import { useSearchExplorerQuery } from './search/useSearchExplorerQuery';
|
||||
import { TopBarPortal } from './TopBar/Portal';
|
||||
|
||||
export function Component() {
|
||||
|
@ -23,37 +24,25 @@ export function Component() {
|
|||
orderingKeys: objectOrderingKeysSchema
|
||||
});
|
||||
|
||||
const explorerSettingsSnapshot = explorerSettings.useSettingsSnapshot();
|
||||
const search = useSearchFromSearchParams();
|
||||
|
||||
const fixedFilters = useMemo<SearchFilterArgs[]>(
|
||||
() => [
|
||||
// { object: { dateAccessed: { from: new Date(0).toISOString() } } },
|
||||
...(explorerSettingsSnapshot.layoutMode === 'media'
|
||||
? [{ object: { kind: { in: [ObjectKindEnum.Image, ObjectKindEnum.Video] } } }]
|
||||
: [])
|
||||
const defaultFilters = { object: { dateAccessed: { from: new Date(0).toISOString() } } };
|
||||
|
||||
const items = useSearchExplorerQuery({
|
||||
search,
|
||||
explorerSettings,
|
||||
filters: [
|
||||
...search.allFilters,
|
||||
// TODO: Add fil ter to search options
|
||||
defaultFilters
|
||||
],
|
||||
[explorerSettingsSnapshot.layoutMode]
|
||||
);
|
||||
|
||||
const search = useSearch({
|
||||
fixedFilters
|
||||
});
|
||||
|
||||
const objects = useObjectsExplorerQuery({
|
||||
arg: {
|
||||
take: 100,
|
||||
filters: [
|
||||
...search.allFilters,
|
||||
// TODO: Add fil ter to search options
|
||||
{ object: { dateAccessed: { from: new Date(0).toISOString() } } }
|
||||
]
|
||||
},
|
||||
order: explorerSettings.useSettingsSnapshot().order
|
||||
take: 100,
|
||||
objects: { order: explorerSettings.useSettingsSnapshot().order }
|
||||
});
|
||||
|
||||
const explorer = useExplorer({
|
||||
...objects,
|
||||
isFetchingNextPage: objects.query.isFetchingNextPage,
|
||||
...items,
|
||||
isFetchingNextPage: items.query.isFetchingNextPage,
|
||||
settings: explorerSettings
|
||||
});
|
||||
|
||||
|
@ -61,7 +50,7 @@ export function Component() {
|
|||
<ExplorerContextProvider explorer={explorer}>
|
||||
<SearchContextProvider search={search}>
|
||||
<TopBarPortal
|
||||
center={<SearchBar />}
|
||||
center={<SearchBar defaultFilters={[defaultFilters]} />}
|
||||
left={
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<span className="truncate text-sm font-medium">Recents</span>
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
import { MagnifyingGlass } from '@phosphor-icons/react';
|
||||
import { getIcon, iconNames } from '@sd/assets/util';
|
||||
import { useMemo } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import {
|
||||
FilePathOrder,
|
||||
SearchFilterArgs,
|
||||
SearchTarget,
|
||||
useLibraryMutation,
|
||||
useLibraryQuery,
|
||||
usePathsExplorerQuery
|
||||
useLibraryQuery
|
||||
} from '@sd/client';
|
||||
import { Button } from '@sd/ui';
|
||||
import { SearchIdParamsSchema } from '~/app/route-schemas';
|
||||
import { useRouteTitle, useZodRouteParams } from '~/hooks';
|
||||
import { useRouteTitle, useZodParams } from '~/hooks';
|
||||
|
||||
import Explorer from '../Explorer';
|
||||
import { ExplorerContextProvider } from '../Explorer/Context';
|
||||
|
@ -22,13 +23,25 @@ import {
|
|||
import { DefaultTopBarOptions } from '../Explorer/TopBarOptions';
|
||||
import { useExplorer, useExplorerSettings } from '../Explorer/useExplorer';
|
||||
import { EmptyNotice } from '../Explorer/View/EmptyNotice';
|
||||
import { SearchContextProvider, SearchOptions, useSearch, useSearchContext } from '../search';
|
||||
import {
|
||||
SearchContextProvider,
|
||||
SearchOptions,
|
||||
useMemorySource,
|
||||
useSearch,
|
||||
useSearchContext
|
||||
} from '../search';
|
||||
import SearchBar from '../search/SearchBar';
|
||||
import { useSearchExplorerQuery } from '../search/useSearchExplorerQuery';
|
||||
import { TopBarPortal } from '../TopBar/Portal';
|
||||
|
||||
export const Component = () => {
|
||||
const { id } = useZodRouteParams(SearchIdParamsSchema);
|
||||
const { id } = useZodParams(SearchIdParamsSchema);
|
||||
|
||||
// This forces the search to throw away all data + modified search state when id changes
|
||||
return <Inner key={id} id={id} />;
|
||||
};
|
||||
|
||||
function Inner({ id }: { id: number }) {
|
||||
const savedSearch = useLibraryQuery(['search.saved.get', id], {
|
||||
suspense: true
|
||||
});
|
||||
|
@ -46,25 +59,30 @@ export const Component = () => {
|
|||
|
||||
const rawFilters = savedSearch.data?.filters;
|
||||
|
||||
const dynamicFilters = useMemo(() => {
|
||||
const filters = useMemo(() => {
|
||||
if (rawFilters) return JSON.parse(rawFilters) as SearchFilterArgs[];
|
||||
}, [rawFilters]);
|
||||
|
||||
const search = useSearch({
|
||||
open: true,
|
||||
search: savedSearch.data?.search ?? undefined,
|
||||
dynamicFilters
|
||||
source: useMemorySource({
|
||||
initialFilters: filters ?? [],
|
||||
initialSearch: savedSearch.data?.search ?? '',
|
||||
initialTarget: (savedSearch.data?.target as SearchTarget) ?? undefined
|
||||
})
|
||||
});
|
||||
|
||||
const paths = usePathsExplorerQuery({
|
||||
arg: { filters: search.allFilters, take: 50 },
|
||||
order: explorerSettings.useSettingsSnapshot().order,
|
||||
const items = useSearchExplorerQuery({
|
||||
search,
|
||||
explorerSettings,
|
||||
filters: search.allFilters,
|
||||
take: 50,
|
||||
paths: { order: explorerSettings.useSettingsSnapshot().order },
|
||||
onSuccess: () => explorerStore.resetNewThumbnails()
|
||||
});
|
||||
|
||||
const explorer = useExplorer({
|
||||
...paths,
|
||||
isFetchingNextPage: paths.query.isFetchingNextPage,
|
||||
...items,
|
||||
isFetchingNextPage: items.query.isFetchingNextPage,
|
||||
settings: explorerSettings
|
||||
});
|
||||
|
||||
|
@ -85,7 +103,7 @@ export const Component = () => {
|
|||
>
|
||||
<hr className="w-full border-t border-sidebar-divider bg-sidebar-divider" />
|
||||
<SearchOptions>
|
||||
{(search.dynamicFilters !== dynamicFilters ||
|
||||
{(search.filters !== filters ||
|
||||
search.search !== savedSearch.data?.search) && (
|
||||
<SaveButton searchId={id} />
|
||||
)}
|
||||
|
@ -107,7 +125,7 @@ export const Component = () => {
|
|||
/>
|
||||
</ExplorerContextProvider>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function SaveButton({ searchId }: { searchId: number }) {
|
||||
const updateSavedSearch = useLibraryMutation(['search.saved.update']);
|
||||
|
@ -123,7 +141,7 @@ function SaveButton({ searchId }: { searchId: number }) {
|
|||
updateSavedSearch.mutate([
|
||||
searchId,
|
||||
{
|
||||
filters: JSON.stringify(search.dynamicFilters),
|
||||
filters: JSON.stringify(search.filters),
|
||||
search: search.search
|
||||
}
|
||||
]);
|
||||
|
|
|
@ -29,7 +29,7 @@ export const CloseTab = forwardRef<HTMLDivElement, { onClick: () => void }>(({ o
|
|||
);
|
||||
});
|
||||
|
||||
export const AppliedFilters = ({ allowRemove = true }: { allowRemove?: boolean }) => {
|
||||
export const AppliedFilters = () => {
|
||||
const search = useSearchContext();
|
||||
|
||||
return (
|
||||
|
@ -40,12 +40,12 @@ export const AppliedFilters = ({ allowRemove = true }: { allowRemove?: boolean }
|
|||
<RenderIcon className="size-4" icon={MagnifyingGlass} />
|
||||
<FilterText>{search.search}</FilterText>
|
||||
</StaticSection>
|
||||
{allowRemove && <CloseTab onClick={() => search.setSearch('')} />}
|
||||
{search.setSearch && <CloseTab onClick={() => search.setSearch?.('')} />}
|
||||
</FilterContainer>
|
||||
)}
|
||||
<div className="group w-full">
|
||||
<HorizontalScroll className="!mb-0 !pl-0">
|
||||
{search.mergedFilters.map(({ arg, removalIndex }, index) => {
|
||||
{search.mergedFilters?.map(({ arg, removalIndex }, index) => {
|
||||
const filter = filterRegistry.find((f) => f.extract(arg));
|
||||
if (!filter) return;
|
||||
return (
|
||||
|
@ -53,15 +53,13 @@ export const AppliedFilters = ({ allowRemove = true }: { allowRemove?: boolean }
|
|||
<FilterArg
|
||||
arg={arg}
|
||||
onDelete={
|
||||
removalIndex !== null && allowRemove
|
||||
removalIndex !== null && search.setFilters
|
||||
? () => {
|
||||
search.updateDynamicFilters(
|
||||
(dyanmicFilters) => {
|
||||
dyanmicFilters.splice(removalIndex, 1);
|
||||
search.setFilters?.((filters) => {
|
||||
filters?.splice(removalIndex, 1);
|
||||
|
||||
return dyanmicFilters;
|
||||
}
|
||||
);
|
||||
return filters;
|
||||
});
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
|
|
|
@ -55,13 +55,13 @@ export interface RenderSearchFilter<
|
|||
Render: (props: {
|
||||
filter: SearchFilterCRUD<TConditions>;
|
||||
options: (FilterOption & { type: string })[];
|
||||
search: UseSearch;
|
||||
search: UseSearch<any>;
|
||||
}) => JSX.Element;
|
||||
// Apply is responsible for applying the filter to the search args
|
||||
useOptions: (props: { search: string }) => FilterOption[];
|
||||
}
|
||||
|
||||
export function useToggleOptionSelected({ search }: { search: UseSearch }) {
|
||||
export function useToggleOptionSelected({ search }: { search: UseSearch<any> }) {
|
||||
return ({
|
||||
filter,
|
||||
option,
|
||||
|
@ -71,28 +71,25 @@ export function useToggleOptionSelected({ search }: { search: UseSearch }) {
|
|||
option: FilterOption;
|
||||
select: boolean;
|
||||
}) => {
|
||||
search.updateDynamicFilters((dynamicFilters) => {
|
||||
const key = getKey({ ...option, type: filter.name });
|
||||
if (search.fixedFiltersKeys?.has(key)) return dynamicFilters;
|
||||
|
||||
const rawArg = dynamicFilters.find((arg) => filter.extract(arg));
|
||||
search.setFilters?.((filters = []) => {
|
||||
const rawArg = filters.find((arg) => filter.extract(arg));
|
||||
|
||||
if (!rawArg) {
|
||||
const arg = filter.create(option.value);
|
||||
dynamicFilters.push(arg);
|
||||
filters.push(arg);
|
||||
} else {
|
||||
const rawArgIndex = dynamicFilters.findIndex((arg) => filter.extract(arg))!;
|
||||
const rawArgIndex = filters.findIndex((arg) => filter.extract(arg))!;
|
||||
|
||||
const arg = filter.extract(rawArg)!;
|
||||
|
||||
if (select) {
|
||||
if (rawArg) filter.applyAdd(arg, option);
|
||||
} else {
|
||||
if (!filter.applyRemove(arg, option)) dynamicFilters.splice(rawArgIndex, 1);
|
||||
if (!filter.applyRemove(arg, option)) filters.splice(rawArgIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
return dynamicFilters;
|
||||
return filters;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -105,7 +102,7 @@ const FilterOptionList = ({
|
|||
}: {
|
||||
filter: SearchFilterCRUD;
|
||||
options: FilterOption[];
|
||||
search: UseSearch;
|
||||
search: UseSearch<any>;
|
||||
empty?: () => JSX.Element;
|
||||
}) => {
|
||||
const { allFiltersKeys } = search;
|
||||
|
@ -143,7 +140,13 @@ const FilterOptionList = ({
|
|||
);
|
||||
};
|
||||
|
||||
const FilterOptionText = ({ filter, search }: { filter: SearchFilterCRUD; search: UseSearch }) => {
|
||||
const FilterOptionText = ({
|
||||
filter,
|
||||
search
|
||||
}: {
|
||||
filter: SearchFilterCRUD;
|
||||
search: UseSearch<any>;
|
||||
}) => {
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
const { allFiltersKeys } = search;
|
||||
|
@ -159,14 +162,14 @@ const FilterOptionText = ({ filter, search }: { filter: SearchFilterCRUD; search
|
|||
className="flex gap-1.5"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
search.updateDynamicFilters((dynamicFilters) => {
|
||||
if (allFiltersKeys.has(key)) return dynamicFilters;
|
||||
search.setFilters?.((filters) => {
|
||||
if (allFiltersKeys.has(key)) return filters;
|
||||
|
||||
const arg = filter.create(value);
|
||||
dynamicFilters.push(arg);
|
||||
filters?.push(arg);
|
||||
setValue('');
|
||||
|
||||
return dynamicFilters;
|
||||
return filters;
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
@ -189,9 +192,9 @@ const FilterOptionBoolean = ({
|
|||
search
|
||||
}: {
|
||||
filter: SearchFilterCRUD;
|
||||
search: UseSearch;
|
||||
search: UseSearch<any>;
|
||||
}) => {
|
||||
const { fixedFiltersKeys, allFiltersKeys } = search;
|
||||
const { allFiltersKeys } = search;
|
||||
|
||||
const key = getKey({
|
||||
type: filter.name,
|
||||
|
@ -204,19 +207,17 @@ const FilterOptionBoolean = ({
|
|||
icon={filter.icon}
|
||||
selected={allFiltersKeys?.has(key)}
|
||||
setSelected={() => {
|
||||
search.updateDynamicFilters((dynamicFilters) => {
|
||||
if (fixedFiltersKeys?.has(key)) return dynamicFilters;
|
||||
|
||||
const index = dynamicFilters.findIndex((f) => filter.extract(f) !== undefined);
|
||||
search.setFilters?.((filters = []) => {
|
||||
const index = filters.findIndex((f) => filter.extract(f) !== undefined);
|
||||
|
||||
if (index !== -1) {
|
||||
dynamicFilters.splice(index, 1);
|
||||
filters.splice(index, 1);
|
||||
} else {
|
||||
const arg = filter.create(true);
|
||||
dynamicFilters.push(arg);
|
||||
filters.push(arg);
|
||||
}
|
||||
|
||||
return dynamicFilters;
|
||||
return filters;
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -2,18 +2,22 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
|||
import { useNavigate } from 'react-router';
|
||||
import { createSearchParams } from 'react-router-dom';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { SearchFilterArgs } from '@sd/client';
|
||||
import { Input, ModifierKeys, Shortcut } from '@sd/ui';
|
||||
import { useOperatingSystem } from '~/hooks';
|
||||
import { keybindForOs } from '~/util/keybinds';
|
||||
|
||||
import { useSearchContext } from './context';
|
||||
import { useSearchStore } from './store';
|
||||
import { SearchTarget } from './useSearch';
|
||||
|
||||
interface Props {
|
||||
redirectToSearch?: boolean;
|
||||
defaultFilters?: SearchFilterArgs[];
|
||||
defaultTarget?: SearchTarget;
|
||||
}
|
||||
|
||||
export default ({ redirectToSearch }: Props) => {
|
||||
export default ({ redirectToSearch, defaultFilters, defaultTarget }: Props) => {
|
||||
const search = useSearchContext();
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
const navigate = useNavigate();
|
||||
|
@ -56,14 +60,14 @@ export default ({ redirectToSearch }: Props) => {
|
|||
};
|
||||
}, [blurHandler, focusHandler]);
|
||||
|
||||
const [value, setValue] = useState('');
|
||||
const [value, setValue] = useState(search.rawSearch);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(search.rawSearch);
|
||||
if (search.rawSearch !== undefined) setValue(search.rawSearch);
|
||||
}, [search.rawSearch]);
|
||||
|
||||
const updateDebounce = useDebouncedCallback((value: string) => {
|
||||
search.setSearch(value);
|
||||
search.setSearch?.(value);
|
||||
if (redirectToSearch) {
|
||||
navigate({
|
||||
pathname: '../search',
|
||||
|
@ -80,7 +84,9 @@ export default ({ redirectToSearch }: Props) => {
|
|||
}
|
||||
|
||||
function clearValue() {
|
||||
search.setSearch('');
|
||||
search.setSearch?.(undefined);
|
||||
search.setFilters?.(undefined);
|
||||
search.setTarget?.(undefined);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -99,7 +105,14 @@ export default ({ redirectToSearch }: Props) => {
|
|||
search.setSearchBarFocused(false);
|
||||
}
|
||||
}}
|
||||
onFocus={() => search.setSearchBarFocused(true)}
|
||||
onFocus={() => {
|
||||
search.setSearchBarFocused(true);
|
||||
search.setFilters?.((f) => {
|
||||
if (!f) return defaultFilters ?? [];
|
||||
else return f;
|
||||
});
|
||||
search.setTarget?.(search.target ?? defaultTarget);
|
||||
}}
|
||||
right={
|
||||
<div className="pointer-events-none flex h-7 items-center space-x-1 opacity-70 group-focus-within:hidden">
|
||||
{
|
||||
|
|
|
@ -2,7 +2,7 @@ import { FunnelSimple, Icon, Plus } from '@phosphor-icons/react';
|
|||
import { IconTypes } from '@sd/assets/util';
|
||||
import clsx from 'clsx';
|
||||
import { memo, PropsWithChildren, useDeferredValue, useMemo, useState } from 'react';
|
||||
import { useLibraryMutation } from '@sd/client';
|
||||
import { useFeatureFlag, useLibraryMutation } from '@sd/client';
|
||||
import {
|
||||
Button,
|
||||
ContextMenuDivItem,
|
||||
|
@ -15,7 +15,7 @@ import {
|
|||
} from '@sd/ui';
|
||||
import { useIsDark, useKeybind } from '~/hooks';
|
||||
|
||||
import { AppliedFilters } from './AppliedFilters';
|
||||
import { AppliedFilters, FilterContainer, InteractiveSection } from './AppliedFilters';
|
||||
import { useSearchContext } from './context';
|
||||
import { filterRegistry, SearchFilterCRUD, useToggleOptionSelected } from './Filters';
|
||||
import {
|
||||
|
@ -93,6 +93,9 @@ export const SearchOptions = ({
|
|||
}: { allowExit?: boolean } & PropsWithChildren) => {
|
||||
const search = useSearchContext();
|
||||
const isDark = useIsDark();
|
||||
|
||||
const showSearchTargets = useFeatureFlag('searchTargetSwitcher');
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => {
|
||||
|
@ -107,11 +110,26 @@ export const SearchOptions = ({
|
|||
isDark ? 'bg-black/10' : 'bg-black/5'
|
||||
)}
|
||||
>
|
||||
{/* <OptionContainer className="flex flex-row items-center">
|
||||
<FilterContainer>
|
||||
<InteractiveSection>Paths</InteractiveSection>
|
||||
</FilterContainer>
|
||||
</OptionContainer> */}
|
||||
{showSearchTargets && (
|
||||
<OptionContainer className="flex flex-row items-center overflow-hidden rounded">
|
||||
<InteractiveSection
|
||||
onClick={() => search.setTarget?.('paths')}
|
||||
className={clsx(
|
||||
search.target === 'paths' ? 'bg-app-box' : 'hover:bg-app-box/50'
|
||||
)}
|
||||
>
|
||||
Paths
|
||||
</InteractiveSection>
|
||||
<InteractiveSection
|
||||
onClick={() => search.setTarget?.('objects')}
|
||||
className={clsx(
|
||||
search.target === 'objects' ? 'bg-app-box' : 'hover:bg-app-box/50'
|
||||
)}
|
||||
>
|
||||
Objects
|
||||
</InteractiveSection>
|
||||
</OptionContainer>
|
||||
)}
|
||||
|
||||
<AddFilterButton />
|
||||
|
||||
|
@ -124,7 +142,7 @@ export const SearchOptions = ({
|
|||
|
||||
{children ?? (
|
||||
<>
|
||||
{(search.dynamicFilters.length > 0 || search.search !== '') && (
|
||||
{((search.filters && search.filters.length > 0) || search.search !== '') && (
|
||||
<SaveSearchButton />
|
||||
)}
|
||||
|
||||
|
@ -136,7 +154,7 @@ export const SearchOptions = ({
|
|||
};
|
||||
|
||||
const SearchResults = memo(
|
||||
({ searchQuery, search }: { searchQuery: string; search: UseSearch }) => {
|
||||
({ searchQuery, search }: { searchQuery: string; search: UseSearch<any> }) => {
|
||||
const { allFiltersKeys } = search;
|
||||
const searchResults = useSearchRegisteredFilters(searchQuery);
|
||||
|
||||
|
@ -275,8 +293,11 @@ function SaveSearchButton() {
|
|||
|
||||
saveSearch.mutate({
|
||||
name,
|
||||
target: search.target,
|
||||
search: search.search,
|
||||
filters: JSON.stringify(search.mergedFilters.map((f) => f.arg)),
|
||||
filters: search.mergedFilters
|
||||
? JSON.stringify(search.mergedFilters.map((f) => f.arg))
|
||||
: undefined,
|
||||
description: null,
|
||||
icon: null
|
||||
});
|
||||
|
@ -308,17 +329,17 @@ function SaveSearchButton() {
|
|||
function EscapeButton() {
|
||||
const search = useSearchContext();
|
||||
|
||||
useKeybind(['Escape'], () => {
|
||||
search.setSearch('');
|
||||
function escape() {
|
||||
search.setSearch?.(undefined);
|
||||
search.setFilters?.(undefined);
|
||||
search.setSearchBarFocused(false);
|
||||
});
|
||||
}
|
||||
|
||||
useKeybind(['Escape'], escape);
|
||||
|
||||
return (
|
||||
<kbd
|
||||
onClick={() => {
|
||||
search.setSearch('');
|
||||
search.setSearchBarFocused(false);
|
||||
}}
|
||||
onClick={escape}
|
||||
className="ml-2 rounded-lg border border-app-line bg-app-box px-2 py-1 text-[10.5px] tracking-widest shadow"
|
||||
>
|
||||
ESC
|
||||
|
|
|
@ -2,7 +2,7 @@ import { createContext, PropsWithChildren, useContext } from 'react';
|
|||
|
||||
import { UseSearch } from './useSearch';
|
||||
|
||||
const SearchContext = createContext<UseSearch | null>(null);
|
||||
const SearchContext = createContext<UseSearch<any> | null>(null);
|
||||
|
||||
export function useSearchContext() {
|
||||
const ctx = useContext(SearchContext);
|
||||
|
@ -17,6 +17,6 @@ export function useSearchContext() {
|
|||
export function SearchContextProvider({
|
||||
children,
|
||||
search
|
||||
}: { search: UseSearch } & PropsWithChildren) {
|
||||
}: { search: UseSearch<any> } & PropsWithChildren) {
|
||||
return <SearchContext.Provider value={search}>{children}</SearchContext.Provider>;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { useEffect, useMemo } from 'react';
|
||||
import { useSearchParams as useRawSearchParams } from 'react-router-dom';
|
||||
import { ObjectKindEnum, ObjectOrder, useObjectsExplorerQuery } from '@sd/client';
|
||||
import { useMemo } from 'react';
|
||||
import { ObjectOrder } from '@sd/client';
|
||||
import { Icon } from '~/components';
|
||||
import { useRouteTitle } from '~/hooks';
|
||||
|
||||
|
@ -9,10 +8,12 @@ import Explorer from '../Explorer';
|
|||
import { ExplorerContextProvider } from '../Explorer/Context';
|
||||
import { createDefaultExplorerSettings, objectOrderingKeysSchema } from '../Explorer/store';
|
||||
import { DefaultTopBarOptions } from '../Explorer/TopBarOptions';
|
||||
import { useExplorer, UseExplorerSettings, useExplorerSettings } from '../Explorer/useExplorer';
|
||||
import { useExplorer, useExplorerSettings } from '../Explorer/useExplorer';
|
||||
import { EmptyNotice } from '../Explorer/View/EmptyNotice';
|
||||
import { TopBarPortal } from '../TopBar/Portal';
|
||||
import SearchBar from './SearchBar';
|
||||
import { useSearchFromSearchParams } from './useSearch';
|
||||
import { useSearchExplorerQuery } from './useSearchExplorerQuery';
|
||||
|
||||
export * from './context';
|
||||
export * from './SearchOptions';
|
||||
|
@ -28,19 +29,19 @@ export function Component() {
|
|||
orderingKeys: objectOrderingKeysSchema
|
||||
});
|
||||
|
||||
const search = useSearchWithFilters(explorerSettings);
|
||||
const search = useSearchFromSearchParams();
|
||||
|
||||
const objects = useObjectsExplorerQuery({
|
||||
arg: {
|
||||
take: 100,
|
||||
filters: search.allFilters
|
||||
},
|
||||
order: explorerSettings.useSettingsSnapshot().order
|
||||
const items = useSearchExplorerQuery({
|
||||
search,
|
||||
explorerSettings,
|
||||
filters: search.allFilters,
|
||||
take: 100,
|
||||
objects: { order: explorerSettings.useSettingsSnapshot().order }
|
||||
});
|
||||
|
||||
const explorer = useExplorer({
|
||||
...objects,
|
||||
isFetchingNextPage: objects.query.isFetchingNextPage,
|
||||
...items,
|
||||
isFetchingNextPage: items.query.isFetchingNextPage,
|
||||
settings: explorerSettings
|
||||
});
|
||||
|
||||
|
@ -76,60 +77,3 @@ export function Component() {
|
|||
</ExplorerContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function useSearchWithFilters(explorerSettings: UseExplorerSettings<ObjectOrder>) {
|
||||
const [searchParams, setSearchParams] = useRawSearchParams();
|
||||
const explorerSettingsSnapshot = explorerSettings.useSettingsSnapshot();
|
||||
|
||||
const fixedFilters = useMemo(
|
||||
() => [
|
||||
...(explorerSettingsSnapshot.layoutMode === 'media'
|
||||
? [{ object: { kind: { in: [ObjectKindEnum.Image, ObjectKindEnum.Video] } } }]
|
||||
: [])
|
||||
],
|
||||
[explorerSettingsSnapshot.layoutMode]
|
||||
);
|
||||
|
||||
const filtersParam = searchParams.get('filters');
|
||||
const dynamicFilters = useMemo(() => JSON.parse(filtersParam ?? '[]'), [filtersParam]);
|
||||
|
||||
const searchQueryParam = searchParams.get('search');
|
||||
|
||||
const search = useSearch({
|
||||
open: !!searchQueryParam || dynamicFilters.length > 0 || undefined,
|
||||
search: searchParams.get('search') ?? undefined,
|
||||
fixedFilters,
|
||||
dynamicFilters
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setSearchParams(
|
||||
(p) => {
|
||||
if (search.dynamicFilters.length > 0)
|
||||
p.set('filters', JSON.stringify(search.dynamicFilters));
|
||||
else p.delete('filters');
|
||||
|
||||
return p;
|
||||
},
|
||||
{ replace: true }
|
||||
);
|
||||
}, [search.dynamicFilters, setSearchParams]);
|
||||
|
||||
const searchQuery = search.search;
|
||||
|
||||
useEffect(() => {
|
||||
setSearchParams(
|
||||
(p) => {
|
||||
if (searchQuery !== '') p.set('search', searchQuery);
|
||||
else p.delete('search');
|
||||
|
||||
return p;
|
||||
},
|
||||
{ replace: true }
|
||||
);
|
||||
// Do not add setSearchParams to the dependencies array, it will cause CMDK to not navigate to search page (multiple times)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchQuery]);
|
||||
|
||||
return search;
|
||||
}
|
||||
|
|
|
@ -1,42 +1,135 @@
|
|||
import { produce } from 'immer';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useSearchParams as useRawSearchParams } from 'react-router-dom';
|
||||
import { useDebouncedValue } from 'rooks';
|
||||
import { SearchFilterArgs } from '@sd/client';
|
||||
|
||||
import { filterRegistry } from './Filters';
|
||||
import { argsToOptions, getKey, useSearchStore } from './store';
|
||||
|
||||
export interface UseSearchProps {
|
||||
open?: boolean;
|
||||
export type SearchTarget = 'paths' | 'objects';
|
||||
|
||||
export interface UseSearchSource {
|
||||
target: SearchTarget;
|
||||
setTarget?: (target?: SearchTarget) => void;
|
||||
filters?: SearchFilterArgs[];
|
||||
setFilters?: (cb?: (filters?: SearchFilterArgs[]) => SearchFilterArgs[] | undefined) => void;
|
||||
search?: string;
|
||||
/**
|
||||
* Filters that cannot be removed
|
||||
*/
|
||||
fixedFilters?: SearchFilterArgs[];
|
||||
/**
|
||||
* Filters that can be removed.
|
||||
* When this value changes dynamic filters stored internally will reset.
|
||||
*/
|
||||
dynamicFilters?: SearchFilterArgs[];
|
||||
setSearch?: (search?: string) => void;
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
export function useSearch(props?: UseSearchProps) {
|
||||
export interface UseSearchProps<TSource extends UseSearchSource> {
|
||||
source: TSource;
|
||||
}
|
||||
|
||||
export function useSearchParamsSource() {
|
||||
const [searchParams, setSearchParams] = useRawSearchParams();
|
||||
|
||||
const filtersSearchParam = searchParams.get('filters');
|
||||
const filters = useMemo<SearchFilterArgs[] | undefined>(
|
||||
() => (filtersSearchParam ? JSON.parse(filtersSearchParam) : undefined),
|
||||
[filtersSearchParam]
|
||||
);
|
||||
|
||||
const searchSearchParam = searchParams.get('search');
|
||||
|
||||
const setFilters = useCallback(
|
||||
(cb?: (args?: SearchFilterArgs[]) => SearchFilterArgs[] | undefined) => {
|
||||
setSearchParams(
|
||||
(p) => {
|
||||
if (cb === undefined) p.delete('filters');
|
||||
else p.set('filters', JSON.stringify(produce(filters, cb)));
|
||||
|
||||
return p;
|
||||
},
|
||||
{ replace: true }
|
||||
);
|
||||
},
|
||||
[filters, setSearchParams]
|
||||
);
|
||||
|
||||
function setSearch(search?: string) {
|
||||
setSearchParams(
|
||||
(p) => {
|
||||
if (search && search !== '') p.set('search', search);
|
||||
else p.delete('search');
|
||||
|
||||
return p;
|
||||
},
|
||||
{ replace: true }
|
||||
);
|
||||
}
|
||||
|
||||
const target = (searchParams.get('target') ?? 'paths') as SearchTarget;
|
||||
|
||||
return {
|
||||
filters,
|
||||
setFilters,
|
||||
search: searchSearchParam ?? '',
|
||||
setSearch,
|
||||
open: searchSearchParam !== null || filtersSearchParam !== null,
|
||||
target,
|
||||
setTarget: (target) =>
|
||||
setSearchParams(
|
||||
(p) => {
|
||||
if (target) p.set('target', target);
|
||||
else p.delete('target');
|
||||
return p;
|
||||
},
|
||||
{ replace: true }
|
||||
)
|
||||
} satisfies UseSearchSource;
|
||||
}
|
||||
|
||||
export function useStaticSource(props: Pick<UseSearchSource, 'filters' | 'search' | 'target'>) {
|
||||
return props satisfies UseSearchSource;
|
||||
}
|
||||
|
||||
export function useMemorySource(props: {
|
||||
initialFilters?: SearchFilterArgs[];
|
||||
initialSearch?: string;
|
||||
initialTarget?: SearchTarget;
|
||||
}) {
|
||||
const [filters, setFilters] = useState(props.initialFilters);
|
||||
const [search, setSearch] = useState(props.initialSearch);
|
||||
const [target, setTarget] = useState(props.initialTarget ?? 'paths');
|
||||
|
||||
return {
|
||||
filters,
|
||||
setFilters: (s) => {
|
||||
if (s === undefined) setFilters(undefined);
|
||||
else setFilters((f) => produce(f, s));
|
||||
},
|
||||
search,
|
||||
setSearch,
|
||||
target,
|
||||
setTarget: (t) => setTarget(t ?? 'paths')
|
||||
} satisfies UseSearchSource;
|
||||
}
|
||||
|
||||
export function useSearch<TSource extends UseSearchSource>(props: UseSearchProps<TSource>) {
|
||||
const {
|
||||
filters,
|
||||
setFilters,
|
||||
search: rawSearch,
|
||||
setSearch,
|
||||
open,
|
||||
target,
|
||||
setTarget
|
||||
} = props.source;
|
||||
|
||||
const [searchBarFocused, setSearchBarFocused] = useState(false);
|
||||
|
||||
const searchState = useSearchStore();
|
||||
|
||||
// Filters that can't be removed
|
||||
|
||||
const fixedFilters = useMemo(() => props?.fixedFilters ?? [], [props?.fixedFilters]);
|
||||
|
||||
const fixedFiltersAsOptions = useMemo(
|
||||
() => argsToOptions(fixedFilters, searchState.filterOptions),
|
||||
[fixedFilters, searchState.filterOptions]
|
||||
const filtersAsOptions = useMemo(
|
||||
() => argsToOptions(filters ?? [], searchState.filterOptions),
|
||||
[filters, searchState.filterOptions]
|
||||
);
|
||||
|
||||
const fixedFiltersKeys: Set<string> = useMemo(() => {
|
||||
const filtersKeys: Set<string> = useMemo(() => {
|
||||
return new Set(
|
||||
fixedFiltersAsOptions.map(({ arg, filter }) =>
|
||||
filtersAsOptions.map(({ arg, filter }) =>
|
||||
getKey({
|
||||
type: filter.name,
|
||||
name: arg.name,
|
||||
|
@ -44,95 +137,19 @@ export function useSearch(props?: UseSearchProps) {
|
|||
})
|
||||
)
|
||||
);
|
||||
}, [fixedFiltersAsOptions]);
|
||||
|
||||
// Filters that can be removed
|
||||
|
||||
const [dynamicFilters, setDynamicFilters] = useState(props?.dynamicFilters ?? []);
|
||||
const [dynamicFiltersFromProps, setDynamicFiltersFromProps] = useState(props?.dynamicFilters);
|
||||
|
||||
if (dynamicFiltersFromProps !== props?.dynamicFilters) {
|
||||
setDynamicFiltersFromProps(props?.dynamicFilters);
|
||||
setDynamicFilters(props?.dynamicFilters ?? []);
|
||||
}
|
||||
|
||||
const dynamicFiltersAsOptions = useMemo(
|
||||
() => argsToOptions(dynamicFilters, searchState.filterOptions),
|
||||
[dynamicFilters, searchState.filterOptions]
|
||||
);
|
||||
|
||||
const dynamicFiltersKeys: Set<string> = useMemo(() => {
|
||||
return new Set(
|
||||
dynamicFiltersAsOptions.map(({ arg, filter }) =>
|
||||
getKey({
|
||||
type: filter.name,
|
||||
name: arg.name,
|
||||
value: arg.value
|
||||
})
|
||||
)
|
||||
);
|
||||
}, [dynamicFiltersAsOptions]);
|
||||
|
||||
const updateDynamicFilters = useCallback(
|
||||
(cb: (args: SearchFilterArgs[]) => SearchFilterArgs[]) =>
|
||||
setDynamicFilters((filters) => produce(filters, cb)),
|
||||
[]
|
||||
);
|
||||
}, [filtersAsOptions]);
|
||||
|
||||
// Merging of filters that should be ORed
|
||||
|
||||
const mergedFilters = useMemo(() => {
|
||||
const value: { arg: SearchFilterArgs; removalIndex: number | null }[] = fixedFilters.map(
|
||||
(arg) => ({
|
||||
arg,
|
||||
removalIndex: null
|
||||
})
|
||||
);
|
||||
|
||||
for (const [index, arg] of dynamicFilters.entries()) {
|
||||
const filter = filterRegistry.find((f) => f.extract(arg));
|
||||
if (!filter) continue;
|
||||
|
||||
const fixedEquivalentIndex = fixedFilters.findIndex(
|
||||
(a) => filter.extract(a) !== undefined
|
||||
);
|
||||
|
||||
if (fixedEquivalentIndex !== -1) {
|
||||
const merged = filter.merge(
|
||||
filter.extract(fixedFilters[fixedEquivalentIndex]!)! as any,
|
||||
filter.extract(arg)! as any
|
||||
);
|
||||
|
||||
value[fixedEquivalentIndex] = {
|
||||
arg: filter.create(merged),
|
||||
removalIndex: fixedEquivalentIndex
|
||||
};
|
||||
} else {
|
||||
value.push({
|
||||
arg,
|
||||
removalIndex: index
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}, [fixedFilters, dynamicFilters]);
|
||||
|
||||
// Filters generated from the search query
|
||||
|
||||
// rawSearch should only ever be read by the search input
|
||||
const [rawSearch, setRawSearch] = useState(props?.search ?? '');
|
||||
const [searchFromProps, setSearchFromProps] = useState(props?.search);
|
||||
|
||||
if (searchFromProps !== props?.search) {
|
||||
setSearchFromProps(props?.search);
|
||||
setRawSearch(props?.search ?? '');
|
||||
}
|
||||
const mergedFilters = useMemo(
|
||||
() => filters?.map((arg, removalIndex) => ({ arg, removalIndex })),
|
||||
[filters]
|
||||
);
|
||||
|
||||
const [search] = useDebouncedValue(rawSearch, 300);
|
||||
|
||||
const searchFilters = useMemo(() => {
|
||||
const [name, ext] = search.split('.') ?? [];
|
||||
const [name, ext] = search?.split('.') ?? [];
|
||||
|
||||
const filters: SearchFilterArgs[] = [];
|
||||
|
||||
|
@ -144,8 +161,8 @@ export function useSearch(props?: UseSearchProps) {
|
|||
|
||||
// All filters combined together
|
||||
const allFilters = useMemo(
|
||||
() => [...mergedFilters.map((v) => v.arg), ...searchFilters],
|
||||
[mergedFilters, searchFilters]
|
||||
() => [...(filters ?? []), ...searchFilters],
|
||||
[filters, searchFilters]
|
||||
);
|
||||
|
||||
const allFiltersAsOptions = useMemo(
|
||||
|
@ -166,22 +183,28 @@ export function useSearch(props?: UseSearchProps) {
|
|||
}, [allFiltersAsOptions]);
|
||||
|
||||
return {
|
||||
open: props?.open || searchBarFocused,
|
||||
fixedFilters,
|
||||
fixedFiltersKeys,
|
||||
open: open || searchBarFocused,
|
||||
search,
|
||||
// rawSearch should only ever be read by the search input
|
||||
rawSearch,
|
||||
setSearch: setRawSearch,
|
||||
setSearch,
|
||||
searchBarFocused,
|
||||
setSearchBarFocused,
|
||||
dynamicFilters,
|
||||
setDynamicFilters,
|
||||
updateDynamicFilters,
|
||||
dynamicFiltersKeys,
|
||||
filters,
|
||||
setFilters,
|
||||
filtersKeys,
|
||||
mergedFilters,
|
||||
allFilters,
|
||||
allFiltersKeys
|
||||
allFiltersKeys,
|
||||
target,
|
||||
setTarget
|
||||
};
|
||||
}
|
||||
|
||||
export type UseSearch = ReturnType<typeof useSearch>;
|
||||
export function useSearchFromSearchParams() {
|
||||
return useSearch({
|
||||
source: useSearchParamsSource()
|
||||
});
|
||||
}
|
||||
|
||||
export type UseSearch<TSource extends UseSearchSource> = ReturnType<typeof useSearch<TSource>>;
|
||||
|
|
36
interface/app/$libraryId/search/useSearchExplorerQuery.ts
Normal file
36
interface/app/$libraryId/search/useSearchExplorerQuery.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import {
|
||||
FilePathOrder,
|
||||
FilePathSearchArgs,
|
||||
ObjectOrder,
|
||||
ObjectSearchArgs,
|
||||
SearchFilterArgs,
|
||||
useObjectsExplorerQuery,
|
||||
usePathsExplorerQuery
|
||||
} from '@sd/client';
|
||||
|
||||
import { UseExplorerSettings } from '../Explorer/useExplorer';
|
||||
import { UseSearch, UseSearchSource } from './useSearch';
|
||||
|
||||
export function useSearchExplorerQuery<TSource extends UseSearchSource>(props: {
|
||||
search: UseSearch<TSource>;
|
||||
explorerSettings: UseExplorerSettings<any>;
|
||||
filters: SearchFilterArgs[];
|
||||
take: number;
|
||||
paths?: { arg?: Omit<FilePathSearchArgs, 'filters' | 'take'>; order?: FilePathOrder | null };
|
||||
objects?: { arg?: Omit<ObjectSearchArgs, 'filters' | 'take'>; order?: ObjectOrder | null };
|
||||
onSuccess?: () => void;
|
||||
}) {
|
||||
const filters = [...props.filters, ...props.explorerSettings.useLayoutSearchFilters()];
|
||||
|
||||
if (props.search.target === 'paths') {
|
||||
return usePathsExplorerQuery({
|
||||
arg: { ...props.paths?.arg, filters, take: props.take },
|
||||
order: null
|
||||
});
|
||||
} else {
|
||||
return useObjectsExplorerQuery({
|
||||
arg: { ...props.objects?.arg, filters, take: props.take },
|
||||
order: null
|
||||
});
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@ import {
|
|||
useZodForm
|
||||
} from '@sd/client';
|
||||
import { Button, Card, Form, InputField, Label, Tooltip, z } from '@sd/ui';
|
||||
import { SearchContextProvider, useSearch } from '~/app/$libraryId/search';
|
||||
import { SearchContextProvider, useSearch, useStaticSource } from '~/app/$libraryId/search';
|
||||
import { AppliedFilters } from '~/app/$libraryId/search/AppliedFilters';
|
||||
import { Heading } from '~/app/$libraryId/settings/Layout';
|
||||
import { useDebouncedFormWatch, useLocale } from '~/hooks';
|
||||
|
@ -82,13 +82,19 @@ function EditForm({ savedSearch, onDelete }: { savedSearch: SavedSearch; onDelet
|
|||
updateSavedSearch.mutate([savedSearch.id, { name: data.name ?? '' }]);
|
||||
});
|
||||
|
||||
const fixedFilters = useMemo(() => {
|
||||
const filters = useMemo(() => {
|
||||
if (savedSearch.filters === null) return [];
|
||||
|
||||
return JSON.parse(savedSearch.filters) as SearchFilterArgs[];
|
||||
}, [savedSearch.filters]);
|
||||
|
||||
const search = useSearch({ search: savedSearch.search ?? undefined, fixedFilters });
|
||||
const search = useSearch({
|
||||
source: useStaticSource({
|
||||
search: savedSearch.search ?? '',
|
||||
filters,
|
||||
target: 'paths'
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
|
@ -113,7 +119,7 @@ function EditForm({ savedSearch, onDelete }: { savedSearch: SavedSearch; onDelet
|
|||
<Label className="font-medium">{t('filters')}</Label>
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<SearchContextProvider search={search}>
|
||||
<AppliedFilters allowRemove={false} />
|
||||
<AppliedFilters />
|
||||
</SearchContextProvider>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,12 +1,5 @@
|
|||
import { useMemo } from 'react';
|
||||
import {
|
||||
ObjectKindEnum,
|
||||
ObjectOrder,
|
||||
useCache,
|
||||
useLibraryQuery,
|
||||
useNodes,
|
||||
useObjectsExplorerQuery
|
||||
} from '@sd/client';
|
||||
import { ObjectOrder, useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { LocationIdParamsSchema } from '~/app/route-schemas';
|
||||
import { Icon } from '~/components';
|
||||
import { useRouteTitle, useZodRouteParams } from '~/hooks';
|
||||
|
@ -17,8 +10,9 @@ import { createDefaultExplorerSettings, objectOrderingKeysSchema } from '../Expl
|
|||
import { DefaultTopBarOptions } from '../Explorer/TopBarOptions';
|
||||
import { useExplorer, useExplorerSettings } from '../Explorer/useExplorer';
|
||||
import { EmptyNotice } from '../Explorer/View/EmptyNotice';
|
||||
import { SearchContextProvider, SearchOptions, useSearch } from '../search';
|
||||
import { SearchContextProvider, SearchOptions, useSearchFromSearchParams } from '../search';
|
||||
import SearchBar from '../search/SearchBar';
|
||||
import { useSearchExplorerQuery } from '../search/useSearchExplorerQuery';
|
||||
import { TopBarPortal } from '../TopBar/Portal';
|
||||
|
||||
export function Component() {
|
||||
|
@ -36,30 +30,21 @@ export function Component() {
|
|||
orderingKeys: objectOrderingKeysSchema
|
||||
});
|
||||
|
||||
const explorerSettingsSnapshot = explorerSettings.useSettingsSnapshot();
|
||||
const search = useSearchFromSearchParams();
|
||||
|
||||
const fixedFilters = useMemo(
|
||||
() => [
|
||||
{ object: { tags: { in: [tag!.id] } } },
|
||||
...(explorerSettingsSnapshot.layoutMode === 'media'
|
||||
? [{ object: { kind: { in: [ObjectKindEnum.Image, ObjectKindEnum.Video] } } }]
|
||||
: [])
|
||||
],
|
||||
[tag, explorerSettingsSnapshot.layoutMode]
|
||||
);
|
||||
const defaultFilters = useMemo(() => [{ object: { tags: { in: [tag.id] } } }], [tag.id]);
|
||||
|
||||
const search = useSearch({
|
||||
fixedFilters
|
||||
});
|
||||
|
||||
const objects = useObjectsExplorerQuery({
|
||||
arg: { take: 100, filters: search.allFilters },
|
||||
order: explorerSettings.useSettingsSnapshot().order
|
||||
const items = useSearchExplorerQuery({
|
||||
search,
|
||||
explorerSettings,
|
||||
filters: search.allFilters.length > 0 ? search.allFilters : defaultFilters,
|
||||
take: 100,
|
||||
objects: { order: explorerSettings.useSettingsSnapshot().order }
|
||||
});
|
||||
|
||||
const explorer = useExplorer({
|
||||
...objects,
|
||||
isFetchingNextPage: objects.query.isFetchingNextPage,
|
||||
...items,
|
||||
isFetchingNextPage: items.query.isFetchingNextPage,
|
||||
settings: explorerSettings,
|
||||
parent: { type: 'Tag', tag: tag! }
|
||||
});
|
||||
|
@ -68,7 +53,7 @@ export function Component() {
|
|||
<ExplorerContextProvider explorer={explorer}>
|
||||
<SearchContextProvider search={search}>
|
||||
<TopBarPortal
|
||||
center={<SearchBar />}
|
||||
center={<SearchBar defaultFilters={defaultFilters} defaultTarget="objects" />}
|
||||
left={
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<div
|
||||
|
|
|
@ -29,3 +29,4 @@ export * from './useZodRouteParams';
|
|||
export * from './useZodSearchParams';
|
||||
export * from './usePrefersReducedMotion';
|
||||
export * from './useRandomInterval';
|
||||
export * from './useZodParams';
|
||||
|
|
10
interface/hooks/useZodParams.ts
Normal file
10
interface/hooks/useZodParams.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
export function useZodParams<Z extends z.AnyZodObject>(schema: Z): z.infer<Z> {
|
||||
// eslint-disable-next-line
|
||||
const params = useParams();
|
||||
|
||||
return useMemo(() => schema.parse(params), [schema, params]);
|
||||
}
|
|
@ -46,7 +46,7 @@ export type Procedures = {
|
|||
{ key: "search.objectsCount", input: LibraryArgs<{ filters?: SearchFilterArgs[] }>, result: number } |
|
||||
{ key: "search.paths", input: LibraryArgs<FilePathSearchArgs>, result: SearchData<ExplorerItem> } |
|
||||
{ key: "search.pathsCount", input: LibraryArgs<{ filters?: SearchFilterArgs[] }>, result: number } |
|
||||
{ key: "search.saved.get", input: LibraryArgs<number>, result: { id: number; pub_id: number[]; search: string | null; filters: string | null; name: string | null; icon: string | null; description: string | null; date_created: string | null; date_modified: string | null } | null } |
|
||||
{ key: "search.saved.get", input: LibraryArgs<number>, result: { id: number; pub_id: number[]; target: string | null; search: string | null; filters: string | null; name: string | null; icon: string | null; description: string | null; date_created: string | null; date_modified: string | null } | null } |
|
||||
{ key: "search.saved.list", input: LibraryArgs<null>, result: SavedSearch[] } |
|
||||
{ key: "sync.enabled", input: LibraryArgs<null>, result: boolean } |
|
||||
{ key: "sync.messages", input: LibraryArgs<null>, result: CRDTOperation[] } |
|
||||
|
@ -118,7 +118,7 @@ export type Procedures = {
|
|||
{ key: "p2p.debugConnect", input: RemoteIdentity, result: string } |
|
||||
{ key: "p2p.spacedrop", input: SpacedropArgs, result: string } |
|
||||
{ key: "preferences.update", input: LibraryArgs<LibraryPreferences>, result: null } |
|
||||
{ key: "search.saved.create", input: LibraryArgs<{ name: string; search?: string | null; filters?: string | null; description?: string | null; icon?: string | null }>, result: null } |
|
||||
{ key: "search.saved.create", input: LibraryArgs<{ name: string; target?: SearchTarget; search?: string | null; filters?: string | null; description?: string | null; icon?: string | null }>, result: null } |
|
||||
{ key: "search.saved.delete", input: LibraryArgs<number>, result: null } |
|
||||
{ key: "search.saved.update", input: LibraryArgs<[number, Args]>, result: null } |
|
||||
{ key: "sync.enable", input: LibraryArgs<null>, result: null } |
|
||||
|
@ -572,12 +572,14 @@ export type Response = { Start: { user_code: string; verification_url: string; v
|
|||
|
||||
export type RuleKind = "AcceptFilesByGlob" | "RejectFilesByGlob" | "AcceptIfChildrenDirectoriesArePresent" | "RejectIfChildrenDirectoriesArePresent"
|
||||
|
||||
export type SavedSearch = { id: number; pub_id: number[]; search: string | null; filters: string | null; name: string | null; icon: string | null; description: string | null; date_created: string | null; date_modified: string | null }
|
||||
export type SavedSearch = { id: number; pub_id: number[]; target: string | null; search: string | null; filters: string | null; name: string | null; icon: string | null; description: string | null; date_created: string | null; date_modified: string | null }
|
||||
|
||||
export type SearchData<T> = { cursor: number[] | null; items: Reference<T>[]; nodes: CacheNode[] }
|
||||
|
||||
export type SearchFilterArgs = { filePath: FilePathFilterArgs } | { object: ObjectFilterArgs }
|
||||
|
||||
export type SearchTarget = "paths" | "objects"
|
||||
|
||||
export type SetFavoriteArgs = { id: number; favorite: boolean }
|
||||
|
||||
export type SetNoteArgs = { id: number; note: string | null }
|
||||
|
|
|
@ -10,7 +10,8 @@ export const features = [
|
|||
'debugRoutes',
|
||||
'solidJsDemo',
|
||||
'hostedLocations',
|
||||
'debugDragAndDrop'
|
||||
'debugDragAndDrop',
|
||||
'searchTargetSwitcher'
|
||||
] as const;
|
||||
|
||||
// This defines which backend feature flags show up in the UI.
|
||||
|
|
Loading…
Reference in a new issue