feat: added current user validation to apis

Signed-off-by: Varun Raj <mailme@varunraj.in>
This commit is contained in:
Varun Raj
2024-08-22 10:08:42 +05:30
parent 12500c9c6e
commit 28d3deabcd
23 changed files with 586 additions and 92 deletions
@@ -0,0 +1,110 @@
import "yet-another-react-lightbox/styles.css";
import { usePotentialAlbumContext } from "@/contexts/PotentialAlbumContext";
import { listPotentialAlbumsAssets } from "@/handlers/api/album.handler";
import { IAsset } from "@/types/asset";
import React, { useEffect, useMemo, useState } from "react";
import { Gallery } from "react-grid-gallery";
import Lightbox from "yet-another-react-lightbox";
import Captions from "yet-another-react-lightbox/plugins/captions";
import { CalendarArrowDown, CalendarArrowUp, Hourglass } from "lucide-react";
import { useMissingLocationContext } from "@/contexts/MissingLocationContext";
import { listMissingLocationAssets } from "@/handlers/api/asset.handler";
import { formatDate, parseDate } from "@/helpers/date.helper";
import { addDays } from "date-fns";
export default function MissingLocationAssets() {
const { startDate, selectedIds, assets, updateContext } = useMissingLocationContext();
const [loading, setLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [index, setIndex] = useState(-1);
const fetchAssets = async () => {
if (!startDate) return;
setLoading(true);
updateContext({
assets: [],
})
return listMissingLocationAssets({ startDate })
.then((assets) => updateContext({ assets }))
.catch(setErrorMessage)
.finally(() => setLoading(false));
};
const images = useMemo(() => {
return assets.map((p) => ({
...p,
src: p.url as string,
original: p.previewUrl as string,
width: p.exifImageWidth as number,
height: p.exifImageHeight as number,
isSelected: selectedIds.includes(p.id),
}));
}, [assets, selectedIds]);
console.log(images)
const slides = useMemo(
() =>
images.map(({ original, width, height }) => ({
src: original,
width,
height,
})),
[images]
);
const handleClick = (idx: number) => setIndex(idx);
const handleSelect = (_idx: number, asset: IAsset) => {
const isPresent =selectedIds.includes(asset.id)
if(isPresent){
updateContext({selectedIds:selectedIds.filter((id)=>id!==asset.id)})
}
else{
updateContext({selectedIds:[...selectedIds,asset.id]})
}
};
useEffect(() => {
if (startDate) fetchAssets();
}, [startDate]);
if (loading) return (
<div className="flex flex-col gap-2 h-full justify-center items-center w-full">
<Hourglass />
<p className="text-lg">Loading...</p>
</div>
)
if (!startDate) return (
<div className="flex flex-col gap-2 h-full justify-center items-center w-full">
<CalendarArrowUp />
<p className="text-lg">Please select a date</p>
<p className="text-sm">
When you select a date from the left, you will see all the orphan assets captured on that date
</p>
</div>
)
return (
<>
<Lightbox
slides={slides}
plugins={[Captions]}
open={index >= 0}
index={index}
close={() => setIndex(-1)}
/>
<div className="w-full overflow-y-auto max-h-[calc(100vh-60px)]">
<Gallery
images={images}
onClick={handleClick}
enableImageSelection={true}
onSelect={handleSelect}
/>
</div>
</>
);
}
@@ -0,0 +1,27 @@
import { useMissingLocationContext } from "@/contexts/MissingLocationContext";
import { IMissingLocationDatesResponse } from "@/handlers/api/asset.handler";
import { cn } from "@/lib/utils";
import React from "react";
interface IProps {
record: IMissingLocationDatesResponse;
onSelect: (date: string) => void;
}
export default function MissingLocationDateItem({ record, onSelect }: IProps) {
const { startDate } = useMissingLocationContext()
return (
<div
role="button"
onClick={() => onSelect(record.date)}
key={record.date}
className={
cn("flex gap-1 flex-col p-2 py-1 rounded-lg hover:dark:bg-zinc-800 border border-transparent hover:bg-zinc-100 px-4",
startDate === record.date ? "bg-zinc-100 dark:bg-zinc-800 border-gray-300 dark:border-zinc-700" : "")
}
>
<p className="font-mono text-sm">{record.date}</p>
<p className="text-xs">{record.asset_count} W/O Location</p>
</div>
);
}
@@ -0,0 +1,43 @@
import React, { use, useEffect, useState } from "react";
import { usePotentialAlbumContext } from "@/contexts/PotentialAlbumContext";
import { IMissingLocationDatesResponse, listMissingLocationDates } from "@/handlers/api/asset.handler";
import MissingLocationDateItem from "./MissingLocationDateItem";
import { useMissingLocationContext } from "@/contexts/MissingLocationContext";
export default function MissingLocationDates() {
const { updateContext } = useMissingLocationContext();
const [dateRecords, setDateRecords] = React.useState<
IMissingLocationDatesResponse[]
>([]);
const [loading, setLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const fetchData = async () => {
return listMissingLocationDates({})
.then(setDateRecords)
.catch(setErrorMessage)
.finally(() => setLoading(false));
};
const handleSelect = (date: string) => {
updateContext({ startDate: date });
};
useEffect(() => {
fetchData();
}, []);
return (
<div className="overflow-y-auto min-w-[170px] py-4 max-h-[calc(100vh-60px)] min-h-[calc(100vh-60px)] dark:bg-zinc-900 bg-gray-200 flex flex-col gap-2 px-2">
{dateRecords.map((record) => (
<MissingLocationDateItem
key={record.date}
record={record}
onSelect={handleSelect}
/>
))}
</div>
);
}
+53
View File
@@ -0,0 +1,53 @@
import { getMe } from "@/handlers/api/user.handler";
import { IUser } from "@/types/user";
import React, { useEffect, useState } from "react";
import Link from "next/link";
import { useConfig } from "@/contexts/ConfigContext";
export default function ProfileInfo() {
const { immichURL } = useConfig();
const [user, setUser] = React.useState<IUser | null>(null);
const [loading, setLoading] = React.useState(true);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const fetchData = () => {
return getMe()
.then(setUser)
.catch((error) => {
setErrorMessage(error.message);
})
.finally(() => setLoading(false));
};
useEffect(() => {
fetchData();
}, []);
if (loading) return <div>Loading...</div>;
if (errorMessage) return <div>{errorMessage}</div>;
return (
<>
<div className="flex flex-col gap-2 text-xs text-center py-2">
<p className="text-muted-foreground font-mono">Connected to</p>
<Link href={immichURL} target="_blank" className="font-mono">
{immichURL}
</Link>
<p className="text-sm">{user?.name}</p>
</div>
<div className="border text-muted-foreground text-xs text-center py-2">
<p>
Made with <span className="text-red-500">&hearts;</span> by{" "}
<Link
target="_blank"
href="https://x.com/zathvarun"
className="text-primary"
>
@zathvarun
</Link>
</p>
</div>
</>
);
}
+3 -25
View File
@@ -1,24 +1,18 @@
import Link from "next/link";
import { Bell, Camera, Package2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { sidebarNavs } from "@/config/constants/sidebarNavs";
import { cn } from "@/lib/utils";
import { useRouter } from "next/router";
import { Badge } from "../ui/badge";
import { ENV } from "@/config/environment";
import { useTheme } from "next-themes";
import dynamic from "next/dynamic";
import Image from "next/image";
import { useConfig } from "@/contexts/ConfigContext";
import ProfileInfo from "./ProfileInfo";
const ThemeSwitcher = dynamic(() => import("@/components/shared/ThemeSwitcher"), {
ssr: false,
});
export default function Sidebar() {
const { immichURL } = useConfig();
const router = useRouter();
const { pathname } = router;
@@ -58,24 +52,8 @@ export default function Sidebar() {
</nav>
</div>
<div>
<div className="flex flex-col gap-1 text-xs text-center py-2">
<p className="text-muted-foreground font-mono">Connected to</p>
<Link href={immichURL} target="_blank" className="font-mono">
{immichURL}
</Link>
</div>
<div className="border text-muted-foreground text-xs text-center py-2">
<p>
Made with <span className="text-red-500">&hearts;</span> by{" "}
<Link
target="_blank"
href="https://x.com/zathvarun"
className="text-primary"
>
@zathvarun
</Link>
</p>
</div>
<ProfileInfo />
</div>
</div>
</div>
+10 -3
View File
@@ -1,6 +1,8 @@
export const BASE_API_ENDPOINT = '/api';
export const BASE_PROXY_ENDPOINT = BASE_API_ENDPOINT + '/immich-proxy';
export const GET_ME_PATH = BASE_PROXY_ENDPOINT + "/users/me";
export const LIST_PEOPLE_PATH = BASE_API_ENDPOINT + "/people/list";
export const SEARCH_PEOPLE_PATH = BASE_PROXY_ENDPOINT + "/search/person";
export const PERSON_THUBNAIL_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/thumbnail/" + id;
@@ -13,7 +15,12 @@ export const EXIF_DISTRIBUTION_PATH = (column: string) => BASE_API_ENDPOINT + "/
// Albums
export const LIST_POTENTIAL_ALBUMS_DATES_PATH = BASE_API_ENDPOINT + "/albums/potential-albums-dates";
export const LIST_POTENTIAL_ALBUMS_ASSETS_PATH = BASE_API_ENDPOINT + "/albums/potential-albums-assets";
export const ASSET_THUMBNAIL_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/asset/thumbnail/" + id;
export const ASSET_PREVIEW_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/asset/thumbnail/" + id + "?size=preview";
export const LIST_ALBUMS_PATH = BASE_PROXY_ENDPOINT + "/albums";
export const ADD_ASSETS_ALBUMS_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/albums/" + id + "/assets";
export const ADD_ASSETS_ALBUMS_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/albums/" + id + "/assets";
// Assets
export const LIST_MISSING_LOCATION_DATES_PATH = BASE_API_ENDPOINT + "/assets/missing-location-dates";
export const LIST_MISSING_LOCATION_ASSETS_PATH = BASE_API_ENDPOINT + "/assets/missing-location-assets";
export const ASSET_THUMBNAIL_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/asset/thumbnail/" + id;
export const ASSET_PREVIEW_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/asset/thumbnail/" + id + "?size=preview";
+26
View File
@@ -0,0 +1,26 @@
import { IAsset } from "@/types/asset";
import { createContext, useContext } from "react";
export interface IMissingLocationConfig {
startDate?: string;
selectedIds: string[];
assets: IAsset[];
}
export interface MissingLocationContext extends IMissingLocationConfig {
updateContext: (newConfig: Partial<MissingLocationContext>) => void;
}
const MissingLocationContext = createContext<MissingLocationContext>({
startDate: undefined,
selectedIds: [],
assets: [],
updateContext: () => { },
});
export default MissingLocationContext;
const useMissingLocationContext = () => {
return useContext(MissingLocationContext) as MissingLocationContext;
}
export { useMissingLocationContext };
+32
View File
@@ -0,0 +1,32 @@
import {
ADD_ASSETS_ALBUMS_PATH,
LIST_ALBUMS_PATH,
LIST_MISSING_LOCATION_ASSETS_PATH,
LIST_MISSING_LOCATION_DATES_PATH,
} from "@/config/routes";
import { cleanUpAsset } from "@/helpers/asset.helper";
import API from "@/lib/api";
import { IAsset } from "@/types/asset";
interface IMissingAssetAlbumsFilters {
startDate?: string;
endDate?: string;
}
export interface IMissingLocationDatesResponse {
date: string;
asset_count: number;
}
export const listMissingLocationDates = async (
filters: IMissingAssetAlbumsFilters
): Promise<IMissingLocationDatesResponse[]> => {
return API.get(LIST_MISSING_LOCATION_DATES_PATH, filters);
};
export const listMissingLocationAssets = async (
filters: IMissingAssetAlbumsFilters
): Promise<IAsset[]> => {
return API.get(LIST_MISSING_LOCATION_ASSETS_PATH, filters).then((assets) =>
assets.map(cleanUpAsset)
);
};
+6
View File
@@ -0,0 +1,6 @@
import { GET_ME_PATH } from "@/config/routes";
import API from "@/lib/api";
export const getMe = async () => {
return API.get(GET_ME_PATH);
}
+16
View File
@@ -0,0 +1,16 @@
import { ENV } from "@/config/environment"
export const getCurrentUser = () => {
console.log(ENV.IMMICH_URL + "/api/users/me")
return fetch(ENV.IMMICH_URL + "/api/users/me", {
headers: {
'x-api-key': ENV.IMMICH_API_KEY,
Accept: "application/json",
},
}).then((res) => {
if (res.ok) {
return res.json()
}
throw new Error("Failed to fetch user")
})
}
+7 -1
View File
@@ -6,4 +6,10 @@ export const formatDate = (date: string, outputFormat?: string): string => {
export const parseDate = (date: string, inputFormat: string): Date => {
return parse(date, inputFormat, new Date());
}
}
export const addDays = (date: Date, days: number): Date => {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
}
-52
View File
@@ -3,58 +3,6 @@ import axios from 'axios'
import qs from 'qs'
// const handleErrors = (error: AxiosError | any) => {
// console.error('error_message', error.message)
// if (error.message === 'canceled') {
// const cancelledError = {
// code: 'ERR_CANCELED',
// message: 'Request canceled',
// }
// return Promise.reject(cancelledError)
// }
// let errorObject = {}
// if (error.response) {
// const { status } = error.response
// if (status === 401) {
// errorObject = {
// ...error.response.data,
// message: error.response.data.message,
// }
// if (typeof window !== 'undefined' && window.location) {
// // return resetSession().then(() => {
// // window.location.href = '/auth/signin'
// // return Promise.reject(errorObject)
// // })
// }
// } else if (status === 403) {
// errorObject = {
// ...error.response.data,
// message:
// 'You do not have access to this page. Please contact your administrator!',
// }
// if (typeof window !== 'undefined' && window.location) {
// // deleteLocalHashData()
// window.location.href = '/404'
// return Promise.reject(errorObject)
// }
// } else if (status === 422) {
// errorObject = {
// ...error.response.data,
// message: 'Unable to process this request, please try again later',
// }
// } else {
// errorObject = {
// ...error.response.data,
// }
// }
// return Promise.reject(errorObject)
// }
// errorObject = {
// message: 'Network Error. Please check your connection and try again!',
// }
// return Promise.reject(errorObject)
// }
export default class API {
+2 -1
View File
@@ -4,7 +4,8 @@ import type { AppProps } from "next/app";
import { ThemeProvider } from "next-themes";
import { ENV } from "@/config/environment";
import ConfigContext from "@/contexts/ConfigContext";
import { useRef } from "react";
import { useEffect, useRef, useState } from "react";
import { getMe } from "@/handlers/api/user.handler";
interface AppPropsWithProps extends AppProps {
props: {
@@ -1,11 +1,12 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { CHART_COLORS } from "@/config/constants/chart.constant";
import { db } from "@/config/db";
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
import { exif } from "@/schema";
import { count, desc, isNotNull, ne, sql } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next";
const SELECT_ORPHAN_PHOTOS = (date: string) =>
const SELECT_ORPHAN_PHOTOS = (date: string, ownerId: string) =>
sql.raw(`
SELECT
a."id",
@@ -35,7 +36,8 @@ const SELECT_ORPHAN_PHOTOS = (date: string) =>
ON a.id = e."assetId"
WHERE
aaa."albumsId" IS NULL
AND a."localDateTime"::date = '${date}';
AND a."ownerId" = '${ownerId}'
AND a."localDateTime"::date = '${date}'
`);
export default async function handler(
@@ -43,8 +45,9 @@ export default async function handler(
res: NextApiResponse
) {
try {
const currentUser = await getCurrentUser()
const { startDate } = req.query as { startDate: string };
const { rows } = await db.execute(SELECT_ORPHAN_PHOTOS(startDate));
const { rows } = await db.execute(SELECT_ORPHAN_PHOTOS(startDate, currentUser.id));
return res.status(200).json(rows);
} catch (error: any) {
res.status(500).json({
@@ -1,11 +1,12 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { CHART_COLORS } from "@/config/constants/chart.constant";
import { db } from "@/config/db";
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
import { exif } from "@/schema";
import { count, desc, isNotNull, ne, sql } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next";
const SELECT_ORPHAN_PHOTOS = sql`
const SELECT_ORPHAN_PHOTOS = (ownerId: string) => sql`
SELECT
DATE(a."localDateTime") AS "date",
COUNT(a."id") AS "asset_count"
@@ -15,7 +16,8 @@ LEFT JOIN
"albums_assets_assets" aaa ON a."id" = aaa."assetsId"
WHERE
aaa."albumsId" IS NULL
GROUP BY
AND a."ownerId" = ${ownerId}
GROUP BY
DATE(a."localDateTime")
ORDER BY
"asset_count" DESC;
@@ -26,7 +28,8 @@ export default async function handler(
res: NextApiResponse
) {
try {
const { rows } = await db.execute(SELECT_ORPHAN_PHOTOS);
const currentUser = await getCurrentUser()
const { rows } = await db.execute(SELECT_ORPHAN_PHOTOS(currentUser.id));
return res.status(200).json(rows);
} catch (error: any) {
res.status(500).json({
+9 -3
View File
@@ -1,8 +1,9 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { CHART_COLORS } from "@/config/constants/chart.constant";
import { db } from "@/config/db";
import { exif } from "@/schema";
import { count, desc, isNotNull, ne } from "drizzle-orm";
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
import { assets, exif } from "@/schema";
import { and, count, desc, eq, isNotNull, ne } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next";
const columnMap = {
@@ -33,12 +34,17 @@ export default async function handler(
}
try {
const currentUser = await getCurrentUser();
const dataFromDB = await db.select({
value: count(),
label: column,
})
.from(exif)
.where(isNotNull(column))
.leftJoin(assets, eq(assets.id, exif.assetId))
.where(and(
isNotNull(column),
eq(assets.ownerId, currentUser.id),
))
.limit(20)
.groupBy(column).orderBy(desc(count()));
@@ -0,0 +1,65 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { db } from "@/config/db";
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
import { parseDate } from "@/helpers/date.helper";
import { assets, exif } from "@/schema";
import { addDays } from "date-fns";
import { and, count, desc, eq, gte, isNotNull, isNull, lte, ne, sql } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { startDate } = req.query as {
startDate: string;
};
const currentUser = await getCurrentUser();
if (!startDate) {
return res.status(400).json({
error: "startDate and endDate are required",
});
}
const startDateDate = parseDate(startDate, "yyyy-MM-dd");
const endDateDate = addDays(parseDate(startDate, "yyyy-MM-dd"), 1);
try {
const rows = await db
.select({
id: assets.id,
deviceId: assets.deviceId,
type: assets.type,
originalPath: assets.originalPath,
previewPath: assets.previewPath,
isFavorite: assets.isFavorite,
duration: assets.duration,
thumbnailPath: assets.thumbnailPath,
encodedVideoPath: assets.encodedVideoPath,
originalFileName: assets.originalFileName,
sidecarPath: assets.sidecarPath,
deletedAt: assets.deletedAt,
localDateTime: assets.localDateTime,
exifImageWidth: exif.exifImageWidth,
exifImageHeight: exif.exifImageHeight,
ownerId: assets.ownerId,
})
.from(assets)
.leftJoin(exif, eq(exif.assetId, assets.id))
.where(and(
isNull(exif.latitude),
isNotNull(assets.createdAt),
gte(exif.dateTimeOriginal, startDateDate),
lte(exif.dateTimeOriginal, endDateDate),
eq(assets.ownerId, currentUser.id),
));
return res.status(200).json(rows);
} catch (error: any) {
res.status(500).json({
error: error?.message,
});
}
}
@@ -0,0 +1,35 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { CHART_COLORS } from "@/config/constants/chart.constant";
import { db } from "@/config/db";
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
import { assets, exif } from "@/schema";
import { and, count, desc, eq, isNotNull, isNull, ne, sql } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
const currentUser = await getCurrentUser();
console.log({ currentUser });
const rows = await db
.select({
asset_count: desc(count(assets.id)),
date: sql`DATE(${exif.dateTimeOriginal})`,
})
.from(assets)
.leftJoin(exif, eq(exif.assetId, assets.id))
.where(and(
isNull(exif.latitude),
eq(assets.ownerId, currentUser.id),
))
.groupBy(sql`DATE(${exif.dateTimeOriginal})`)
.orderBy(desc(count(assets.id)));
return res.status(200).json(rows);
} catch (error: any) {
res.status(500).json({
error: error?.message,
});
}
}
@@ -28,6 +28,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
})
if (!response.ok) {
console.error('HTTP error:', response)
throw new Error(`HTTP error! status: ${response.status}`)
}
+4
View File
@@ -1,6 +1,7 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { CHART_COLORS } from "@/config/constants/chart.constant";
import { db } from "@/config/db";
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
import { stringToBoolean } from "@/helpers/data.helper";
import { assetFaces, assets, exif, person } from "@/schema";
import {
@@ -43,12 +44,15 @@ export default async function handler(
sortOrder = "desc",
} = req.query as any as IQuery;
const currentUser = await getCurrentUser();
const maximumAssetCount = !maxValue || maxValue <= 0 ? 1000000 : maxValue;
const whereClause = and(
eq(person.isHidden, false),
isNull(assets.duplicateId),
eq(assets.isVisible, true),
eq(assets.isArchived, false),
eq(assets.ownerId, currentUser.id),
stringToBoolean(nameLessOnly) ? eq(person.name, "") : undefined
);
+106
View File
@@ -0,0 +1,106 @@
import AlbumSelectorDialog from "@/components/albums/AlbumSelectorDialog";
import PotentialAlbumsAssets from "@/components/albums/potential-albums/PotentialAlbumsAssets";
import PotentialAlbumsDates from "@/components/albums/potential-albums/PotentialAlbumsDates";
import MissingLocationAssets from "@/components/assets/missing-location/MissingLocationAssets";
import MissingLocationDates from "@/components/assets/missing-location/MissingLocationDates";
import PageLayout from "@/components/layouts/PageLayout";
import Header from "@/components/shared/Header";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { useToast } from "@/components/ui/use-toast";
import MissingLocationContext, {
IMissingLocationConfig,
} from "@/contexts/MissingLocationContext";
import { addAssetToAlbum } from "@/handlers/api/album.handler";
import { IAlbum } from "@/types/album";
import React from "react";
export default function MissingLocations() {
const { toast } = useToast();
const [config, setConfig] = React.useState<IMissingLocationConfig>({
startDate: undefined,
selectedIds: [],
assets: [],
});
const handleSelect = (album: IAlbum) => {
return addAssetToAlbum(album.id, config.selectedIds)
.then(() => {
setConfig({ ...config, selectedIds: [] });
setConfig({
...config,
assets: config.assets.filter(
(asset) => !config.selectedIds.includes(asset.id)
),
selectedIds: [],
});
})
.then(() => {
toast({
title: `Assets added to ${album.albumName}`,
description: `${config.selectedIds.length} assets added to album`,
});
})
.catch(() => {
toast({
title: "Error",
description: "Failed to add assets album",
variant: "destructive",
});
});
};
return (
<PageLayout className="!p-0 !mb-0">
<Header
leftComponent="Missing Location"
rightComponent={
<>
<Badge variant={"outline"}>
{config.selectedIds.length} Selected
</Badge>
{config.selectedIds.length === config.assets.length ? (
<Button
variant={"outline"}
onClick={() =>
setConfig({
...config,
selectedIds: [],
})
}
>
Unselect all
</Button>
) : (
<Button
variant={"outline"}
onClick={() =>
setConfig({
...config,
selectedIds: config.assets.map((a) => a.id),
})
}
>
Select all
</Button>
)}
</>
}
/>
<MissingLocationContext.Provider
value={{
...config,
updateContext: (newConfig: Partial<IMissingLocationConfig>) =>
setConfig({ ...config, ...newConfig }),
}}
>
<div className="flex divide-y">
<MissingLocationDates />
<MissingLocationAssets />
</div>
</MissingLocationContext.Provider>
</PageLayout>
);
}
+1 -1
View File
@@ -3,7 +3,7 @@ import { pgTable, uuid, varchar, timestamp, boolean } from 'drizzle-orm/pg-core'
export const assets = pgTable('assets', {
id: uuid('id').defaultRandom().primaryKey().notNull(),
deviceAssetId: varchar('deviceAssetId').notNull(),
// ownerId: uuid('ownerId').notNull().references(() => users.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
ownerId: uuid('ownerId').notNull(),
deviceId: varchar('deviceId').notNull(),
type: varchar('type').notNull(),
originalPath: varchar('originalPath').notNull(),
+18
View File
@@ -0,0 +1,18 @@
export interface IUser {
id: string;
email: string;
name: string;
profileImagePath: string;
avatarColor: string;
storageLabel: string;
shouldChangePassword: boolean;
isAdmin: boolean;
createdAt: Date;
deletedAt: null;
updatedAt: Date;
oauthId: string;
quotaSizeInBytes: null;
quotaUsageInBytes: number;
status: string;
license: null;
}