1 Commits

Author SHA1 Message Date
b3ba450f63 feat: deleting column from frontend 2025-10-05 14:10:25 +01:00
4 changed files with 142 additions and 49 deletions

View File

@ -147,10 +147,10 @@ func (h *StackHandler) deleteImageFromStack(w http.ResponseWriter, r *http.Reque
func (h *StackHandler) deleteImageStackSchemaItem(w http.ResponseWriter, r *http.Request) { func (h *StackHandler) deleteImageStackSchemaItem(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
stringImageID := chi.URLParam(r, "stackID") stringStackID := chi.URLParam(r, "stackID")
stringSchemaItemID := chi.URLParam(r, "schemaItemID") stringSchemaItemID := chi.URLParam(r, "schemaItemID")
imageID, err := uuid.Parse(stringImageID) stackID, err := uuid.Parse(stringStackID)
if err != nil { if err != nil {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
return return
@ -169,8 +169,9 @@ func (h *StackHandler) deleteImageStackSchemaItem(w http.ResponseWriter, r *http
return return
} }
stack, err := h.stackModel.Get(ctx, schemaItemID) stack, err := h.stackModel.Get(ctx, stackID)
if err != nil { if err != nil {
h.logger.Error("could not get stack model", "err", err)
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
return return
} }
@ -185,7 +186,7 @@ func (h *StackHandler) deleteImageStackSchemaItem(w http.ResponseWriter, r *http
// manipulations. So we could create a middleware. // manipulations. So we could create a middleware.
// If you repeat this 3 times, then organise it :) // If you repeat this 3 times, then organise it :)
err = h.stackModel.DeleteSchemaItem(ctx, schemaItemID, imageID) err = h.stackModel.DeleteSchemaItem(ctx, stackID, schemaItemID)
if err != nil { if err != nil {
h.logger.Warn("failed to delete image from list", "error", err) h.logger.Warn("failed to delete image from list", "error", err)
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)

View File

@ -7,7 +7,13 @@ import {
createResource, createResource,
useContext, useContext,
} from "solid-js"; } from "solid-js";
import { deleteImage, deleteImageFromStack, getUserImages, JustTheImageWhatAreTheseNames } from "../network"; import {
deleteImage,
deleteImageFromStack,
deleteStackItem,
getUserImages,
JustTheImageWhatAreTheseNames,
} from "../network";
export type SearchImageStore = { export type SearchImageStore = {
imagesByDate: Accessor< imagesByDate: Accessor<
@ -21,40 +27,41 @@ export type SearchImageStore = {
onRefetchImages: () => void; onRefetchImages: () => void;
onDeleteImage: (imageID: string) => void; onDeleteImage: (imageID: string) => void;
onDeleteImageFromStack: (stackID: string, imageID: string) => void; onDeleteImageFromStack: (stackID: string, imageID: string) => void;
onDeleteStackItem: (stackID: string, schemaItemID: string) => void;
}; };
const SearchImageContext = createContext<SearchImageStore>(); const SearchImageContext = createContext<SearchImageStore>();
export const SearchImageContextProvider: Component<ParentProps> = (props) => { export const SearchImageContextProvider: Component<ParentProps> = (props) => {
const [data, { refetch }] = createResource(getUserImages); const [data, { refetch }] = createResource(getUserImages);
const sortedImages = createMemo<ReturnType<SearchImageStore["imagesByDate"]>>( const sortedImages = createMemo<
() => { ReturnType<SearchImageStore["imagesByDate"]>
const d = data(); >(() => {
if (d == null) { const d = data();
return []; if (d == null) {
return [];
}
// Sorted by day. But we could potentially add more in the future.
const buckets: Record<string, JustTheImageWhatAreTheseNames> = {};
for (const image of d.UserImages) {
if (image.CreatedAt == null) {
continue;
} }
// Sorted by day. But we could potentially add more in the future. const date = new Date(image.CreatedAt).toDateString();
const buckets: Record<string, JustTheImageWhatAreTheseNames> = {}; if (!(date in buckets)) {
buckets[date] = [];
for (const image of d.UserImages) {
if (image.CreatedAt == null) {
continue;
}
const date = new Date(image.CreatedAt).toDateString();
if (!(date in buckets)) {
buckets[date] = [];
}
buckets[date].push(image);
} }
return Object.entries(buckets) buckets[date].push(image);
.map(([date, images]) => ({ date: new Date(date), images })) }
.sort((a, b) => b.date.getTime() - a.date.getTime());
}, return Object.entries(buckets)
); .map(([date, images]) => ({ date: new Date(date), images }))
.sort((a, b) => b.date.getTime() - a.date.getTime());
});
return ( return (
<SearchImageContext.Provider <SearchImageContext.Provider
@ -68,7 +75,10 @@ export const SearchImageContextProvider: Component<ParentProps> = (props) => {
}, },
onDeleteImageFromStack: (stackID: string, imageID: string) => { onDeleteImageFromStack: (stackID: string, imageID: string) => {
deleteImageFromStack(stackID, imageID).then(refetch); deleteImageFromStack(stackID, imageID).then(refetch);
} },
onDeleteStackItem: (stackID: string, schemaItemID: string) => {
deleteStackItem(stackID, schemaItemID).then(refetch);
},
}} }}
> >
{props.children} {props.children}

View File

@ -121,6 +121,18 @@ export const deleteImageFromStack = async (listID: string, imageID: string): Pro
await fetch(request); await fetch(request);
} }
export const deleteStackItem = async (
stackID: string,
schemaItemID: string,
): Promise<void> => {
const request = await getBaseAuthorizedRequest({
path: `stacks/${stackID}/${schemaItemID}`,
method: "DELETE",
});
await fetch(request);
}
export const deleteList = async (listID: string): Promise<void> => { export const deleteList = async (listID: string): Promise<void> => {
const request = await getBaseAuthorizedRequest({ const request = await getBaseAuthorizedRequest({
path: `stacks/${listID}`, path: `stacks/${listID}`,

View File

@ -57,6 +57,59 @@ const DeleteButton: Component<{ onDelete: () => void }> = (props) => {
); );
}; };
const DeleteSchemaItemButton: Component<{
onDelete: () => void;
itemName: string;
}> = (props) => {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<button
aria-label="Delete schema item"
class="text-red-600 hover:text-red-700 ml-2"
onClick={(e) => {
e.stopPropagation();
setIsOpen(true);
}}
>
×
</button>
<Dialog.Root open={isOpen()} onOpenChange={setIsOpen}>
<Dialog.Portal>
<Dialog.Overlay class="fixed inset-0 bg-black bg-opacity-50" />
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
<Dialog.Content class="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
<Dialog.Title class="text-lg font-bold mb-2">
Delete Column
</Dialog.Title>
<Dialog.Description class="mb-4">
Are you sure you want to delete the column "
{props.itemName}"? This will remove this column
and all its data from the list.
</Dialog.Description>
<div class="flex justify-end gap-2">
<Dialog.CloseButton>
<button class="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400">
Cancel
</button>
</Dialog.CloseButton>
<button
class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
onClick={props.onDelete}
>
Delete Column
</button>
</div>
</Dialog.Content>
</div>
</Dialog.Portal>
</Dialog.Root>
</>
);
};
const DeleteListButton: Component<{ onDelete: () => void }> = (props) => { const DeleteListButton: Component<{ onDelete: () => void }> = (props) => {
const [isOpen, setIsOpen] = createSignal(false); const [isOpen, setIsOpen] = createSignal(false);
@ -107,17 +160,22 @@ export const List: Component = () => {
const { listId } = useParams(); const { listId } = useParams();
const nav = useNavigate(); const nav = useNavigate();
const { stacks, onDeleteImageFromStack } = useSearchImageContext(); const { stacks, onDeleteImageFromStack, onDeleteStackItem } =
useSearchImageContext();
const [accessToken] = createResource(getAccessToken); const [accessToken] = createResource(getAccessToken);
const list = () => stacks().find((l) => l.ID === listId); const list = () => stacks().find((l) => l.ID === listId);
const handleDeleteList = async () => { const handleDeleteList = async () => {
await deleteList(listId) await deleteList(listId);
nav("/"); nav("/");
}; };
const handleDeleteSchemaItem = (schemaItemId: string) => {
onDeleteStackItem(listId, schemaItemId);
};
return ( return (
<Suspense> <Suspense>
<Show when={list()} fallback="List could not be found"> <Show when={list()} fallback="List could not be found">
@ -151,15 +209,25 @@ export const List: Component = () => {
<For each={l().SchemaItems}> <For each={l().SchemaItems}>
{(item, index) => ( {(item, index) => (
<th <th
class={`px-6 py-4 text-left text-sm font-semibold text-neutral-900 min-w-32 ${index() < class={`px-6 py-4 text-left text-sm font-semibold text-neutral-900 min-w-32 ${
l().SchemaItems index() <
.length - l().SchemaItems.length -
1 1
? "border-r border-neutral-200" ? "border-r border-neutral-200"
: "" : ""
}`} }`}
> >
{item.Item} <div class="flex items-center">
{item.Item}
<DeleteSchemaItemButton
onDelete={() =>
handleDeleteSchemaItem(
item.ID,
)
}
itemName={item.Item}
/>
</div>
</th> </th>
)} )}
</For> </For>
@ -169,10 +237,11 @@ export const List: Component = () => {
<For each={l().Images}> <For each={l().Images}>
{(image, rowIndex) => ( {(image, rowIndex) => (
<tr <tr
class={`hover:bg-neutral-50 transition-colors ${rowIndex() % 2 === 0 class={`hover:bg-neutral-50 transition-colors ${
? "bg-white" rowIndex() % 2 === 0
: "bg-neutral-25" ? "bg-white"
}`} : "bg-neutral-25"
}`}
> >
<td class="px-6 py-4 border-r border-neutral-200"> <td class="px-6 py-4 border-r border-neutral-200">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@ -199,13 +268,14 @@ export const List: Component = () => {
<For each={image.Items}> <For each={image.Items}>
{(item, colIndex) => ( {(item, colIndex) => (
<td <td
class={`px-6 py-4 text-sm text-neutral-700 ${colIndex() < class={`px-6 py-4 text-sm text-neutral-700 ${
colIndex() <
image.Items image.Items
.length - .length -
1 1
? "border-r border-neutral-200" ? "border-r border-neutral-200"
: "" : ""
}`} }`}
> >
<div <div
class="max-w-xs truncate" class="max-w-xs truncate"