Skip to content

Commit

Permalink
Refactor pinning and rendering for browse spaces
Browse files Browse the repository at this point in the history
  • Loading branch information
jmetrikat committed Feb 8, 2025
1 parent 870561d commit 5a457e0
Show file tree
Hide file tree
Showing 6 changed files with 300 additions and 110 deletions.
26 changes: 26 additions & 0 deletions src/api/getType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { apiFetch } from "../helpers/api";
import { apiEndpoints } from "../helpers/constants";
import { Type } from "../helpers/schemas";
import { mapType } from "../mappers/types";

export async function getType(
spaceId: string,
type_id: string,
): Promise<{
type: Type | null;
}> {
const { url, method } = apiEndpoints.getType(spaceId, type_id);
try {
const response = await apiFetch<{ type: Type }>(url, { method: method });
return {
type: response ? await mapType(response.type) : null,
};
} catch (error) {
if (error instanceof Error && error.message.includes("404")) {
return {
type: null,
};
}
throw error;
}
}
2 changes: 1 addition & 1 deletion src/components/ObjectActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export default function ObjectActions({
const objectUrl = `anytype://object?objectId=${objectId}&spaceId=${spaceId}`;
const isDetailView = objectExport !== undefined;
const isType = viewType === "type";
const spaceIdForPinned = isGlobalSearch ? "all" : spaceId;
const spaceIdForPinned = isGlobalSearch ? "all" : `${spaceId}-${viewType}`;

function getContextLabel(isSingular = true) {
const labelMap: Record<string, string> = {
Expand Down
291 changes: 187 additions & 104 deletions src/components/ObjectList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useEffect, useState } from "react";
import { Member, SpaceObject, Type } from "../helpers/schemas";
import { getDateLabel, getShortDateLabel, pluralize } from "../helpers/strings";
import { useMembers } from "../hooks/useMembers";
import { usePinnedObjects } from "../hooks/usePinnedObjects";
import { usePinnedMembers, usePinnedObjects, usePinnedTypes } from "../hooks/usePinnedObjects";
import { useSearch } from "../hooks/useSearch";
import { useTypes } from "../hooks/useTypes";
import EmptyView from "./EmptyView";
Expand All @@ -15,8 +15,14 @@ type ObjectListProps = {
spaceId: string;
};

const CurrentView = {
objects: "objects",
types: "types",
members: "members",
} as const;

export default function ObjectList({ spaceId }: ObjectListProps) {
const [currentView, setCurrentView] = useState<"objects" | "types" | "members">("objects");
const [currentView, setCurrentView] = useState<keyof typeof CurrentView>("objects");
const [searchText, setSearchText] = useState("");

const { objects, objectsError, isLoadingObjects, mutateObjects, objectsPagination } = useSearch(
Expand All @@ -26,7 +32,15 @@ export default function ObjectList({ spaceId }: ObjectListProps) {
);
const { types, typesError, isLoadingTypes, mutateTypes, typesPagination } = useTypes(spaceId);
const { members, membersError, isLoadingMembers, mutateMembers, membersPagination } = useMembers(spaceId);
const { pinnedObjects, pinnedObjectsError, isLoadingPinnedObjects, mutatePinnedObjects } = usePinnedObjects(spaceId);
const { pinnedObjects, pinnedObjectsError, isLoadingPinnedObjects, mutatePinnedObjects } = usePinnedObjects(
`${spaceId}-${CurrentView.objects}`,
);
const { pinnedTypes, pinnedTypesError, isLoadingPinnedTypes, mutatePinnedTypes } = usePinnedTypes(
`${spaceId}-${CurrentView.types}`,
);
const { pinnedMembers, pinnedMembersError, isLoadingPinnedMembers, mutatePinnedMembers } = usePinnedMembers(
`${spaceId}-${CurrentView.members}`,
);
const [pagination, setPagination] = useState(objectsPagination);

useEffect(() => {
Expand All @@ -39,28 +53,24 @@ export default function ObjectList({ spaceId }: ObjectListProps) {
}, [currentView, objects, types, members]);

useEffect(() => {
if (objectsError) {
showToast(Toast.Style.Failure, "Failed to fetch objects", objectsError.message);
}
}, [objectsError]);

useEffect(() => {
if (typesError) {
showToast(Toast.Style.Failure, "Failed to fetch types", typesError.message);
}
}, [typesError]);

useEffect(() => {
if (membersError) {
showToast(Toast.Style.Failure, "Failed to fetch members", membersError.message);
if (objectsError || typesError || membersError) {
showToast(
Toast.Style.Failure,
"Failed to fetch latest data",
objectsError?.message || typesError?.message || membersError?.message,
);
}
}, [membersError]);
}, [objectsError, typesError, membersError]);

useEffect(() => {
if (pinnedObjectsError) {
showToast(Toast.Style.Failure, "Failed to fetch pinned objects", pinnedObjectsError.message);
if (pinnedObjectsError || pinnedTypesError || pinnedMembersError) {
showToast(
Toast.Style.Failure,
"Failed to fetch pinned data",
pinnedObjectsError?.message || pinnedTypesError?.message || pinnedMembersError?.message,
);
}
}, [pinnedObjectsError]);
}, [pinnedObjectsError, pinnedTypesError, pinnedMembersError]);

const filterItems = <T extends { name: string }>(items: T[], searchText: string): T[] => {
return items?.filter((item) => item.name.toLowerCase().includes(searchText.toLowerCase()));
Expand All @@ -71,105 +81,142 @@ export default function ObjectList({ spaceId }: ObjectListProps) {
};
const dateToSortAfter = getPreferenceValues().sort;

const processObject = (object: SpaceObject, isPinned: boolean) => {
const date = object.details.find((detail) => detail.id === dateToSortAfter)?.details[dateToSortAfter] as string;
const hasValidDate = date && new Date(date).getTime() !== 0;

return {
key: object.id,
spaceId: object.space_id,
objectId: object.id,
icon: {
source: object.icon,
mask:
(object.layout === "participant" || object.layout === "profile") && object.icon != Icon.Document
? Image.Mask.Circle
: Image.Mask.RoundedRectangle,
},
title: object.name,
subtitle: {
value: object.type,
tooltip: `Type: ${object.type}`,
},
accessories: [
{
date: hasValidDate ? new Date(date) : undefined,
tooltip: hasValidDate
? `${getDateLabel()}: ${format(new Date(date), "EEEE d MMMM yyyy 'at' HH:mm")}`
: `Never ${getShortDateLabel()}`,
text: hasValidDate ? undefined : "—",
},
...(isPinned ? [{ icon: Icon.Star, tooltip: "Pinned" }] : []),
],
mutate: [mutateObjects, mutatePinnedObjects as MutatePromise<SpaceObject[] | Type[] | Member[]>],
isPinned,
};
};

const processType = (type: Type, isPinned: boolean) => {
return {
key: type.id,
spaceId: spaceId,
objectId: type.id,
icon: type.icon,
title: type.name,
subtitle: { value: "", tooltip: "" },
accessories: [],
mutate: [mutateTypes, mutatePinnedTypes as MutatePromise<SpaceObject[] | Type[] | Member[]>],
isPinned,
};
};

const processMember = (member: Member, isPinned: boolean) => {
return {
key: member.id,
spaceId: spaceId,
objectId: member.id,
icon: member.icon,
title: member.name,
subtitle: { value: member.global_name, tooltip: `Global Name: ${member.global_name}` },
accessories: [
{
value: formatRole(member.role),
tooltip: `Role: ${formatRole(member.role)}`,
},
],
mutate: [mutateMembers, mutatePinnedMembers as MutatePromise<SpaceObject[] | Type[] | Member[]>],
isPinned,
};
};

const getCurrentItems = () => {
switch (currentView) {
case "objects": {
return filterItems(objects, searchText)?.map((object) => {
const date = object.details.find((detail) => detail.id === dateToSortAfter)?.details[
dateToSortAfter
] as string;
const hasValidDate = date && new Date(date).getTime() !== 0;
const processedPinned = pinnedObjects?.length
? pinnedObjects
.filter((object) => filterItems([object], searchText).length > 0)
.map((object) => processObject(object, true))
: [];

return (
<ObjectListItem
key={object.id}
spaceId={spaceId}
objectId={object.id}
icon={{
source: object.icon,
mask:
(object.layout === "participant" || object.layout === "profile") && object.icon != Icon.Document
? Image.Mask.Circle
: Image.Mask.RoundedRectangle,
}}
title={object.name}
subtitle={{
value: object.type,
tooltip: `Type: ${object.type}`,
}}
accessories={[
{
date: hasValidDate ? new Date(date) : undefined,
tooltip: hasValidDate
? `${getDateLabel()}: ${format(new Date(date), "EEEE d MMMM yyyy 'at' HH:mm")}`
: `Never ${getShortDateLabel()}`,
text: hasValidDate ? undefined : "—",
},
...(pinnedObjects?.some((pinned) => pinned.id === object.id)
? [{ icon: Icon.Star, tooltip: "Pinned" }]
: []),
]}
mutate={[mutateObjects, mutatePinnedObjects as MutatePromise<SpaceObject[] | Type[] | Member[]>]}
viewType="object"
isGlobalSearch={false}
isPinned={pinnedObjects?.some((pinned) => pinned.id === object.id) || false}
/>
);
});
const processedRegular = objects
.filter(
(object) =>
!pinnedObjects?.some((pinned) => pinned.id === object.id && pinned.space_id === object.space_id),
)
.map((object) => processObject(object, false));

return { processedPinned, processedRegular };
}

case "types": {
return filterItems(types, searchText)?.map((type) => (
<ObjectListItem
key={type.id}
spaceId={spaceId}
objectId={type.id}
icon={type.icon}
title={type.name}
mutate={[mutateTypes]}
viewType="type"
isGlobalSearch={false}
isPinned={pinnedObjects?.some((pinned) => pinned.id === type.id) || false}
/>
));
const processedPinned = pinnedTypes?.length
? pinnedTypes
.filter((type) => filterItems([type], searchText).length > 0)
.map((type) => processType(type, true))
: [];

const processedRegular = types
.filter((type) => !pinnedTypes?.some((pinned) => pinned.id === type.id))
.map((type) => processType(type, false));

return { processedPinned, processedRegular };
}

case "members": {
return filterItems(members, searchText)?.map((member) => (
<ObjectListItem
key={member.identity}
spaceId={spaceId}
objectId={member.id}
icon={{ source: member.icon, mask: Image.Mask.Circle }}
title={member.name}
subtitle={{
value: member.global_name,
tooltip: `Global Name: ${member.global_name}`,
}}
accessories={[
{
text: formatRole(member.role),
tooltip: `Role: ${formatRole(member.role)}`,
},
]}
mutate={[mutateMembers]}
viewType="member"
isGlobalSearch={false}
isPinned={pinnedObjects?.some((pinned) => pinned.id === member.id) || false}
/>
));
const processedPinned = pinnedMembers?.length
? pinnedMembers
.filter((member) => filterItems([member], searchText).length > 0)
.map((member) => processMember(member, true))
: [];

const processedRegular = members
.filter((member) => !pinnedMembers?.some((pinned) => pinned.id === member.id))
.filter((member) => filterItems([member], searchText).length > 0)
.map((member) => processMember(member, false));

return { processedPinned, processedRegular };
}

default:
return null;
return {
processedPinned: [],
processedRegular: [],
};
}
};

const currentItems = getCurrentItems();
const { processedPinned, processedRegular } = getCurrentItems();

return (
<List
isLoading={isLoadingMembers || isLoadingObjects || isLoadingTypes || isLoadingPinnedObjects}
isLoading={
isLoadingObjects ||
isLoadingTypes ||
isLoadingMembers ||
isLoadingPinnedObjects ||
isLoadingPinnedTypes ||
isLoadingPinnedMembers
}
onSearchTextChange={setSearchText}
searchBarPlaceholder={`Search ${currentView}...`}
searchBarAccessory={
Expand All @@ -185,12 +232,48 @@ export default function ObjectList({ spaceId }: ObjectListProps) {
pagination={pagination}
throttle={true}
>
{currentItems && currentItems?.length > 0 ? (
{processedPinned && processedPinned.length > 0 && (
<List.Section
title="Pinned"
subtitle={`${pluralize(processedPinned.length, currentView.slice(0, -1), { withNumber: true })}`}
>
{processedPinned.map((object) => (
<ObjectListItem
key={object.key}
spaceId={object.spaceId}
objectId={object.objectId}
icon={object.icon}
title={object.title}
subtitle={object.subtitle}
accessories={object.accessories}
mutate={object.mutate}
viewType={currentView}
isGlobalSearch={false}
isPinned={object.isPinned}
/>
))}
</List.Section>
)}
{processedRegular && processedRegular.length > 0 ? (
<List.Section
title={searchText ? "Search Results" : `All ${currentView.charAt(0).toUpperCase() + currentView.slice(1)}`}
subtitle={`${pluralize(getCurrentItems()?.length || 0, currentView.slice(0, -1), { withNumber: true })}`}
subtitle={`${pluralize(processedRegular.length, currentView.slice(0, -1), { withNumber: true })}`}
>
{getCurrentItems()}
{processedRegular.map((object) => (
<ObjectListItem
key={object.key}
spaceId={object.spaceId}
objectId={object.objectId}
icon={object.icon}
title={object.title}
subtitle={object.subtitle}
accessories={object.accessories}
mutate={object.mutate}
viewType={currentView}
isGlobalSearch={false}
isPinned={object.isPinned}
/>
))}
</List.Section>
) : (
<EmptyView title={`No ${currentView.charAt(0).toUpperCase() + currentView.slice(1)} Found`} />
Expand Down
Loading

0 comments on commit 5a457e0

Please sign in to comment.