diff --git a/copperc/src/lib/api/openapi.ts b/copperc/src/lib/api/openapi.ts
index ed06eb04..c9f24312 100644
--- a/copperc/src/lib/api/openapi.ts
+++ b/copperc/src/lib/api/openapi.ts
@@ -66,7 +66,7 @@ export interface paths {
path?: never;
cookie?: never;
};
- /** Get class info */
+ /** List items in this class */
get: operations["list_items"];
put?: never;
post?: never;
@@ -625,6 +625,7 @@ export interface components {
class: number;
/** Format: int64 */
item: number;
+ primary_attr: components["schemas"]["PrimaryAttrData"];
/** @enum {string} */
type: "Reference";
};
@@ -762,6 +763,57 @@ export interface components {
[key: string]: components["schemas"]["NodeJson"];
};
};
+ PrimaryAttrData:
+ | {
+ /** @enum {string} */
+ type: "NotAvailable";
+ }
+ | {
+ /** Format: int64 */
+ attr: number;
+ /** @enum {string} */
+ type: "Text";
+ value: string;
+ }
+ | {
+ /** Format: int64 */
+ attr: number;
+ /** @enum {string} */
+ type: "Integer";
+ /** Format: int64 */
+ value: number;
+ }
+ | {
+ /** Format: int64 */
+ attr: number;
+ /** @enum {string} */
+ type: "Float";
+ /** Format: double */
+ value: number;
+ }
+ | {
+ /** Format: int64 */
+ attr: number;
+ /** @enum {string} */
+ type: "Boolean";
+ value: boolean;
+ }
+ | {
+ /** Format: int64 */
+ attr: number;
+ /** @enum {string} */
+ type: "Hash";
+ value: string;
+ }
+ | {
+ /** Format: int64 */
+ attr: number;
+ mime: string;
+ /** Format: int64 */
+ size?: number | null;
+ /** @enum {string} */
+ type: "Blob";
+ };
QueuedJobCounts: {
/** Format: int64 */
build_errors: number;
@@ -819,11 +871,6 @@ export interface components {
/** @enum {string} */
state: "FailedRunning";
}
- | {
- message: string;
- /** @enum {string} */
- state: "FailedTransaction";
- }
| {
/** @enum {string} */
state: "Success";
diff --git a/copperc/src/lib/attributes/impls/blob.tsx b/copperc/src/lib/attributes/impls/blob.tsx
index b95f7b25..3251cd73 100644
--- a/copperc/src/lib/attributes/impls/blob.tsx
+++ b/copperc/src/lib/attributes/impls/blob.tsx
@@ -45,59 +45,68 @@ export const _blobAttrType: attrTypeInfo<"Blob"> = {
type: "panel",
panel_body: (params) => {
- const data_url = `/api/item/${params.item_id}/attr/${params.attr_id}`;
+ return ;
+ },
+ },
+};
- let inner: ReactNode | null = (
- <_PanelBodyUnknown
- src={data_url}
- icon={}
- attr_value={params.value}
- />
- );
+export function BlobPanel(params: {
+ item_id: number;
+ attr_id: number;
+ value: {
+ mime: string;
+ size?: number | null;
+ type: "Blob";
+ };
+ inner?: boolean;
+}) {
+ const data_url = `/api/item/${params.item_id}/attr/${params.attr_id}`;
- if (params.value.mime != null && params.value.mime.startsWith("image/")) {
- inner = <_PanelBodyImage src={data_url} attr_value={params.value} />;
- } else if (
- params.value.mime != null &&
- params.value.mime.startsWith("audio/")
- ) {
- inner = <_PanelBodyAudio src={data_url} attr_value={params.value} />;
- }
+ let inner: ReactNode | null = (
+ <_PanelBodyUnknown src={data_url} icon={} attr_value={params.value} />
+ );
- return (
-
;
+ } else if (
+ params.value.mime != null &&
+ params.value.mime.startsWith("audio/")
+ ) {
+ inner = <_PanelBodyAudio src={data_url} attr_value={params.value} />;
+ }
+
+ return (
+
+
- );
- },
- },
-};
+ {inner}
+
+
+ {params.inner !== true ? (
+ <_PanelBottom attr_value={params.value} />
+ ) : null}
+
+ );
+}
// Same as basicform, but with the "unique" switch hidden.
// It has no effect on blobs.
diff --git a/copperc/src/lib/attributes/impls/reference.tsx b/copperc/src/lib/attributes/impls/reference.tsx
index e4b11bbe..05034490 100644
--- a/copperc/src/lib/attributes/impls/reference.tsx
+++ b/copperc/src/lib/attributes/impls/reference.tsx
@@ -11,6 +11,7 @@ import { useMutation, useQuery } from "@tanstack/react-query";
import { ReactElement } from "react";
import { Select } from "@mantine/core";
import { useForm, UseFormReturnType } from "@mantine/form";
+import { BlobPanel } from "./blob";
export const _referenceAttrType: attrTypeInfo<"Reference"> = {
pretty_name: "Reference",
@@ -43,8 +44,83 @@ export const _referenceAttrType: attrTypeInfo<"Reference"> = {
editor: {
type: "panel",
- panel_body: () => {
- return "todo-ref";
+ panel_body: (params) => {
+ const pa = params.value.primary_attr;
+
+ if (pa.type === "NotAvailable") {
+ return (
+
+ No attribute available
+
+ );
+ }
+
+ if (pa.type === "Blob") {
+ return (
+
+ );
+ }
+
+ let v = UNSET!
;
+ if (pa.type === "Boolean") {
+ v = pa.value ? (
+ true
+ ) : (
+ false
+ );
+ } else if (pa.type === "Float" || pa.type === "Integer") {
+ v = {pa.value};
+ } else if (pa.type === "Hash") {
+ v = {pa.value};
+ } else if (pa.type === "Text") {
+ v = {pa.value};
+ }
+
+ return (
+
+ );
},
},
};
diff --git a/copperd/bin/edged/src/api/class/items.rs b/copperd/bin/edged/src/api/class/items.rs
index be52fd18..575a84d0 100644
--- a/copperd/bin/edged/src/api/class/items.rs
+++ b/copperd/bin/edged/src/api/class/items.rs
@@ -11,18 +11,22 @@ use axum::{
use axum_extra::extract::CookieJar;
use copper_itemdb::{
client::errors::{
- class::GetClassError,
+ class::{ClassPrimaryAttributeError, GetClassError},
dataset::GetDatasetError,
- item::{CountItemsError, ListItemsError},
+ item::{CountItemsError, GetItemError, ListItemsError},
},
AttrData, AttributeId, ClassId, ItemId,
};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
-use sqlx::Acquire;
+use sqlx::{Acquire, Transaction};
use tracing::error;
use utoipa::{IntoParams, ToSchema};
+//
+// MARK: itemattrdata
+//
+
/// Attribute data returned to the user
#[derive(Debug, Clone, Serialize, ToSchema)]
#[serde(tag = "type")]
@@ -58,6 +62,8 @@ pub(super) enum ItemAttrData {
#[schema(value_type = i64)]
item: ItemId,
+
+ primary_attr: PrimaryAttrData,
},
}
@@ -65,8 +71,27 @@ impl ItemAttrData {
async fn from_attr_data(
state: &RouterState,
value: AttrData,
+ trans: &mut Transaction<'_, sqlx::Postgres>,
) -> Result {
Ok(match value {
+ //
+ // Easy
+ //
+ AttrData::Integer { value, .. } => Self::Integer { value },
+ AttrData::Float { value, .. } => Self::Float { value },
+ AttrData::Boolean { value } => Self::Boolean { value },
+
+ AttrData::Text { value } => Self::Text {
+ value: value.into(),
+ },
+
+ AttrData::Hash { data, .. } => Self::Hash {
+ value: data.into_iter().map(|x| format!("{x:02X?}")).join(""),
+ },
+
+ //
+ // MARK: blob
+ //
AttrData::Blob { bucket, key } => {
let meta = match state
.s3_client
@@ -89,18 +114,176 @@ impl ItemAttrData {
size: meta.size,
}
}
- AttrData::Integer { value, .. } => ItemAttrData::Integer { value },
- AttrData::Float { value, .. } => ItemAttrData::Float { value },
- AttrData::Boolean { value } => ItemAttrData::Boolean { value },
- AttrData::Reference { class, item } => ItemAttrData::Reference { class, item },
- AttrData::Text { value } => ItemAttrData::Text {
+ //
+ // MARK: reference
+ //
+ AttrData::Reference { class, item } => Self::Reference {
+ class,
+ item,
+ primary_attr: match state.itemdb_client.class_primary_attr(trans, class).await {
+ Ok(primary_attr) => {
+ if let Some(primary_attr) = primary_attr {
+ let value = state.itemdb_client.get_item(trans, item).await;
+
+ match value {
+ Ok(value) => {
+ PrimaryAttrData::from_attr_data(
+ state,
+ primary_attr.id,
+ value
+ .attribute_values
+ .get(&primary_attr.id)
+ .unwrap()
+ .clone(),
+ )
+ .await?
+ }
+
+ Err(GetItemError::NotFound) => {
+ return Err(StatusCode::NOT_FOUND.into_response())
+ }
+
+ Err(GetItemError::DbError(error)) => {
+ error!(message = "Error in itemdb client", ?error);
+ return Err((
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json("Internal server error"),
+ )
+ .into_response());
+ }
+ }
+ } else {
+ PrimaryAttrData::NotAvailable
+ }
+ }
+
+ Err(ClassPrimaryAttributeError::NotFound) => {
+ return Err(StatusCode::NOT_FOUND.into_response());
+ }
+
+ Err(ClassPrimaryAttributeError::DbError(error)) => {
+ error!(message = "Error in itemdb client", ?error);
+ return Err((
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json("Internal server error"),
+ )
+ .into_response());
+ }
+ },
+ },
+ })
+ }
+}
+
+//
+// MARK: primaryattrdata
+//
+
+// Almost identical to [`ItemAttrData`], but excluding references.
+// Used inside the reference variant of [`ItemAttrData`].
+#[derive(Debug, Clone, Serialize, ToSchema)]
+#[serde(tag = "type")]
+pub(super) enum PrimaryAttrData {
+ NotAvailable,
+ Text {
+ #[schema(value_type = i64)]
+ attr: AttributeId,
+ value: String,
+ },
+
+ Integer {
+ #[schema(value_type = i64)]
+ attr: AttributeId,
+ value: i64,
+ },
+
+ Float {
+ #[schema(value_type = i64)]
+ attr: AttributeId,
+ value: f64,
+ },
+
+ Boolean {
+ #[schema(value_type = i64)]
+ attr: AttributeId,
+ value: bool,
+ },
+
+ Hash {
+ #[schema(value_type = i64)]
+ attr: AttributeId,
+ value: String,
+ },
+
+ Blob {
+ #[schema(value_type = i64)]
+ attr: AttributeId,
+
+ mime: String,
+ size: Option,
+ },
+}
+
+impl PrimaryAttrData {
+ async fn from_attr_data(
+ state: &RouterState,
+ attr: AttributeId,
+ value: AttrData,
+ ) -> Result {
+ Ok(match value {
+ //
+ // Easy
+ //
+ AttrData::Integer { value, .. } => Self::Integer { value, attr },
+ AttrData::Float { value, .. } => Self::Float { value, attr },
+ AttrData::Boolean { value } => Self::Boolean { value, attr },
+
+ AttrData::Text { value } => Self::Text {
value: value.into(),
+ attr,
},
- AttrData::Hash { data, .. } => ItemAttrData::Hash {
+ AttrData::Hash { data, .. } => Self::Hash {
value: data.into_iter().map(|x| format!("{x:02X?}")).join(""),
+ attr,
},
+
+ //
+ // MARK: blob
+ //
+ AttrData::Blob { bucket, key } => {
+ let meta = match state
+ .s3_client
+ .get_object_metadata(bucket.as_str(), key.as_str())
+ .await
+ {
+ Ok(x) => x,
+ Err(error) => {
+ error!(message = "Error in itemdb client", ?error);
+ return Err((
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json("Internal server error"),
+ )
+ .into_response());
+ }
+ };
+
+ Self::Blob {
+ mime: meta.mime.to_string(),
+ size: meta.size,
+ attr,
+ }
+ }
+
+ AttrData::Reference { .. } => {
+ error!(message = "Tried to put a reference in a reference");
+ return Err((
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json("Internal server error"),
+ )
+ .into_response());
+ }
})
}
}
@@ -133,6 +316,10 @@ pub(super) struct PaginateParams {
count: usize,
}
+//
+// MARK: route
+//
+
/// List items in this class
#[utoipa::path(
get,
@@ -258,7 +445,7 @@ pub(super) async fn list_items(
for i in x {
let mut attribute_values = BTreeMap::new();
for (attr_id, data) in i.attribute_values {
- match ItemAttrData::from_attr_data(&state, data).await {
+ match ItemAttrData::from_attr_data(&state, data, &mut trans).await {
Ok(x) => {
attribute_values.insert(attr_id, x);
}
diff --git a/copperd/bin/edged/src/api/class/mod.rs b/copperd/bin/edged/src/api/class/mod.rs
index 9c62deeb..efd69691 100644
--- a/copperd/bin/edged/src/api/class/mod.rs
+++ b/copperd/bin/edged/src/api/class/mod.rs
@@ -27,6 +27,7 @@ use rename::*;
NewAttributeRequest,
ItemlistItemInfo,
ItemAttrData,
+ PrimaryAttrData,
ItemListResponse
))
)]
diff --git a/copperd/lib/itemdb/src/client/client/class.rs b/copperd/lib/itemdb/src/client/client/class.rs
index 6bb28dba..57ba5fa7 100644
--- a/copperd/lib/itemdb/src/client/client/class.rs
+++ b/copperd/lib/itemdb/src/client/client/class.rs
@@ -4,13 +4,20 @@ use copper_util::names::check_name;
use sqlx::Row;
use crate::{
- client::errors::class::{AddClassError, DeleteClassError, GetClassError, RenameClassError},
- AttributeInfo, AttributeOptions, ClassId, ClassInfo, DatasetId,
+ client::errors::class::{
+ AddClassError, ClassPrimaryAttributeError, DeleteClassError, GetClassError,
+ RenameClassError,
+ },
+ AttrDataStub, AttributeInfo, AttributeOptions, ClassId, ClassInfo, DatasetId,
};
use super::ItemdbClient;
impl ItemdbClient {
+ //
+ // MARK: crud
+ //
+
pub async fn add_class(
&self,
t: &mut sqlx::Transaction<'_, sqlx::Postgres>,
@@ -142,4 +149,42 @@ impl ItemdbClient {
return Ok(());
}
+
+ //
+ // MARK: misc
+ //
+
+ /// Get the attribute we should use to represent this class.
+ /// This is used as an item preview in the `Reference` ui panel.
+ ///
+ /// If this returns `None`, the class has no valid attributes
+ pub async fn class_primary_attr(
+ &self,
+ t: &mut sqlx::Transaction<'_, sqlx::Postgres>,
+ class: ClassId,
+ ) -> Result