diff --git a/extensions/anytype/CHANGELOG.md b/extensions/anytype/CHANGELOG.md
index 991298a7730..b2b3a12b9ad 100644
--- a/extensions/anytype/CHANGELOG.md
+++ b/extensions/anytype/CHANGELOG.md
@@ -1,6 +1,40 @@
# Anytype Changelog
-## [✨ AI Enhancements & Improvements] - 2025-03-XX
+## [Properties, Types & Tags] - 2025-05-07
+
+#### New Creation Options
+
+When browsing spaces, press `CMD+N` to create new objects, types, properties or tags.
+
+- Add ability to create new spaces
+- Add ability to create new types
+- Add ability to create new properties
+- Add ability to create new tags
+
+#### Edit Form Enhancements
+
+Use `CMD+E` to quickly edit the currently selected item - whether it's a space, object, type, property, or tag.
+
+- Add support for editing spaces
+- Add support for editing objects
+- Add support for editing types
+- Add support for editing properties
+- Add support for editing tags
+
+#### New Commands & Navigation
+
+- Add new command to add objects to lists
+- Pop back to list view when deleting object, with automatic refresh
+- List properties when browsing space
+- Browse tags for select/multi-select properties
+- Open bookmarks directly in browser
+
+#### Form Improvements
+
+- Allow custom properties (inherited from type) for object creation
+- Improve number and emoji validation logic in create form
+
+## [✨ AI Enhancements & Improvements] - 2025-04-22
#### AI Extension
diff --git a/extensions/anytype/assets/icons/object/action.svg b/extensions/anytype/assets/icons/object/action.svg
new file mode 100644
index 00000000000..d7ef6e1b35b
--- /dev/null
+++ b/extensions/anytype/assets/icons/object/action.svg
@@ -0,0 +1,4 @@
+
diff --git a/extensions/anytype/assets/icons/object/basic.svg b/extensions/anytype/assets/icons/object/basic.svg
new file mode 100644
index 00000000000..525ccd9378e
--- /dev/null
+++ b/extensions/anytype/assets/icons/object/basic.svg
@@ -0,0 +1,5 @@
+
diff --git a/extensions/anytype/assets/icons/object/note.svg b/extensions/anytype/assets/icons/object/note.svg
new file mode 100644
index 00000000000..b50e453c717
--- /dev/null
+++ b/extensions/anytype/assets/icons/object/note.svg
@@ -0,0 +1,6 @@
+
diff --git a/extensions/anytype/assets/icons/object/profile.svg b/extensions/anytype/assets/icons/object/profile.svg
new file mode 100644
index 00000000000..5acd840004e
--- /dev/null
+++ b/extensions/anytype/assets/icons/object/profile.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/extensions/anytype/assets/icons/property/checkbox.svg b/extensions/anytype/assets/icons/property/checkbox.svg
new file mode 100644
index 00000000000..e8e79eac89f
--- /dev/null
+++ b/extensions/anytype/assets/icons/property/checkbox.svg
@@ -0,0 +1,4 @@
+
diff --git a/extensions/anytype/assets/icons/property/files.svg b/extensions/anytype/assets/icons/property/files.svg
new file mode 100644
index 00000000000..5f07e8f48f6
--- /dev/null
+++ b/extensions/anytype/assets/icons/property/files.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/extensions/anytype/assets/icons/property/multi_select.svg b/extensions/anytype/assets/icons/property/multi_select.svg
new file mode 100644
index 00000000000..ecc7b580678
--- /dev/null
+++ b/extensions/anytype/assets/icons/property/multi_select.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/extensions/anytype/assets/icons/property/objects.svg b/extensions/anytype/assets/icons/property/objects.svg
new file mode 100644
index 00000000000..f0be3d3fd76
--- /dev/null
+++ b/extensions/anytype/assets/icons/property/objects.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/extensions/anytype/assets/icons/property/tag.svg b/extensions/anytype/assets/icons/property/tag.svg
new file mode 100644
index 00000000000..daaaa6d01c6
--- /dev/null
+++ b/extensions/anytype/assets/icons/property/tag.svg
@@ -0,0 +1,4 @@
+
diff --git a/extensions/anytype/package-lock.json b/extensions/anytype/package-lock.json
index f8f00f98b5a..63852409b88 100644
--- a/extensions/anytype/package-lock.json
+++ b/extensions/anytype/package-lock.json
@@ -10,6 +10,7 @@
"@raycast/api": "^1.93.1",
"@raycast/utils": "^1.17.0",
"date-fns": "^4.1.0",
+ "emoji-regex": "^10.4.0",
"node-fetch": "^3.3.2"
},
"devDependencies": {
@@ -2164,13 +2165,6 @@
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
- "node_modules/cli-truncate/node_modules/emoji-regex": {
- "version": "10.4.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
- "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/cli-truncate/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
@@ -2593,9 +2587,9 @@
"license": "MIT"
},
"node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
+ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
"license": "MIT"
},
"node_modules/end-of-stream": {
@@ -4077,13 +4071,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/listr2/node_modules/emoji-regex": {
- "version": "10.4.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
- "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/listr2/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
@@ -4221,13 +4208,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/log-update/node_modules/emoji-regex": {
- "version": "10.4.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
- "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/log-update/node_modules/is-fullwidth-code-point": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz",
@@ -5513,6 +5493,12 @@
"node": ">=8"
}
},
+ "node_modules/string-width/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
"node_modules/string-width/node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
diff --git a/extensions/anytype/package.json b/extensions/anytype/package.json
index 25053cd775f..5343c144f30 100644
--- a/extensions/anytype/package.json
+++ b/extensions/anytype/package.json
@@ -33,6 +33,13 @@
"subtitle": "Anytype",
"description": "Search objects globally in all spaces.",
"mode": "view"
+ },
+ {
+ "name": "add-to-list",
+ "title": "Add to List",
+ "subtitle": "Anytype",
+ "description": "Add an existing object to a list.",
+ "mode": "view"
}
],
"tools": [
@@ -114,8 +121,8 @@
"object": "type",
"name": "Task",
"id": "bafyreigs5crqbryk2sq45ig4p6uqkcbmntuyosc1djvdeqoydj23b6d1mm",
- "type_key": "ot-task",
- "recommended_layout": "todo"
+ "type_key": "task",
+ "layout": "action"
}
],
"search-space": [
@@ -173,15 +180,15 @@
"types": {
"or": [
{
- "includes": "ot-task"
+ "includes": "task"
},
{
"includes": "bafyreigs5crqbryk2sq45ig4p6uqkcbmntuyosc1djvdeqoydj23b6d1mm"
},
{
"includes": [
- "ot-collection",
- "ot-set"
+ "collection",
+ "set"
]
}
]
@@ -225,8 +232,8 @@
"object": "type",
"name": "Page",
"id": "bafyreigs5crqbryk2sq45ig4p6uqkcbmntuyosc1djvdeqoydj23b6d1pp",
- "type_key": "ot-page",
- "recommended_layout": "basic"
+ "type_key": "page",
+ "layout": "basic"
}
],
"create-object": [
@@ -238,7 +245,7 @@
"type": {
"id": "bafyreigs5crqbryk2sq45ig4p6uqkcbmntuyosc1djvdeqoydj23b6d1mm",
"name": "Page",
- "type_key": "ot-page"
+ "type_key": "page"
},
"snippet": "This is the snippet of the object."
}
@@ -264,7 +271,7 @@
"name": "create-object",
"arguments": {
"spaceId": "bafyreihliylimyqct7vbyc2jqsoanibku656jj4uwwhulx4tctb4qw346q.2lcu0r85yg10d",
- "type_key": "ot-page",
+ "type_key": "page",
"name": {
"includes": "Privacy"
}
@@ -481,8 +488,8 @@
"object": "type",
"name": "Page",
"id": "bafyreigs5crqbryk2sq45ig4p6uqkcbmntuyosc1djvdeqoydj23b6d1pp",
- "type_key": "ot-page",
- "recommended_layout": "basic"
+ "type_key": "page",
+ "layout": "basic"
}
]
},
@@ -640,7 +647,7 @@
"arguments": {
"spaceId": "bafyreihliylimyqct7vbyc2jqsoanibku656jj4uwwhulx4tctb4qw346q.2lcu0r85yg10d",
"query": "",
- "sort.property": "last_modified_date",
+ "sort.propertyKey": "last_modified_date",
"sort.direction": "desc"
}
}
@@ -684,7 +691,7 @@
"type": "dropdown",
"data": [
{
- "title": "Created Date",
+ "title": "Creation Date",
"value": "created_date"
},
{
@@ -709,10 +716,6 @@
"description": "The maximum number of objects to fetch from the Anytype API at once. Be careful when changing this, as a large number can cause performance issues.",
"type": "dropdown",
"data": [
- {
- "title": "25",
- "value": "25"
- },
{
"title": "50",
"value": "50"
@@ -720,9 +723,13 @@
{
"title": "100",
"value": "100"
+ },
+ {
+ "title": "200",
+ "value": "200"
}
],
- "default": "50",
+ "default": "100",
"required": false
},
{
@@ -748,6 +755,7 @@
"@raycast/api": "^1.93.1",
"@raycast/utils": "^1.17.0",
"date-fns": "^4.1.0",
+ "emoji-regex": "^10.4.0",
"node-fetch": "^3.3.2"
},
"devDependencies": {
diff --git a/extensions/anytype/src/add-to-list.tsx b/extensions/anytype/src/add-to-list.tsx
new file mode 100644
index 00000000000..82547908053
--- /dev/null
+++ b/extensions/anytype/src/add-to-list.tsx
@@ -0,0 +1,148 @@
+import { Action, ActionPanel, Form, popToRoot, showToast, Toast } from "@raycast/api";
+import { showFailureToast, useForm } from "@raycast/utils";
+import { useEffect, useState } from "react";
+import { addObjectsToList } from "./api";
+import { EnsureAuthenticated } from "./components/EnsureAuthenticated";
+import { useObjectsInList, useSearch, useSpaces } from "./hooks";
+import { bundledTypeKeys } from "./utils";
+
+export interface AddToListValues {
+ space: string;
+ list: string;
+ object: string;
+}
+
+export default function Command() {
+ return (
+
+
+
+ );
+}
+
+export function AddToList() {
+ const [loading, setLoading] = useState(false);
+ const [listSearchText, setListSearchText] = useState("");
+ const [objectSearchText, setObjectSearchText] = useState("");
+ const [selectedSpace, setSelectedSpace] = useState("");
+ const [selectedList, setSelectedList] = useState("");
+ const [selectedObject, setSelectedObject] = useState("");
+
+ const { spaces, spacesError, isLoadingSpaces } = useSpaces();
+ const {
+ objects: lists,
+ objectsError: listsError,
+ isLoadingObjects: isLoadingLists,
+ } = useSearch(selectedSpace, listSearchText, [bundledTypeKeys.collection]);
+ const { objects, objectsError, isLoadingObjects } = useSearch(selectedSpace, objectSearchText, []);
+ const {
+ objects: listItems,
+ objectsError: listItemsError,
+ isLoadingObjects: isLoadingListItems,
+ } = useObjectsInList(selectedSpace, selectedList, "");
+
+ useEffect(() => {
+ if (spacesError || objectsError || listsError || listItemsError) {
+ showFailureToast(spacesError || objectsError || listsError || listItemsError, {
+ title: "Failed to fetch latest data",
+ });
+ }
+ }, [spacesError, objectsError, listsError]);
+
+ const { handleSubmit, itemProps } = useForm({
+ onSubmit: async (values) => {
+ setLoading(true);
+ try {
+ await showToast(Toast.Style.Animated, "Adding object to list...");
+ const response = await addObjectsToList(values.space, values.list, [values.object]);
+ if (response.payload) {
+ await showToast(Toast.Style.Success, "Object added to list successfully", response.payload);
+ popToRoot();
+ } else {
+ await showToast(Toast.Style.Failure, "Failed to add object to list");
+ }
+ } catch (error) {
+ await showFailureToast(error, { title: "Failed to add object to list" });
+ } finally {
+ setLoading(false);
+ }
+ },
+ validation: {
+ space: (value) => {
+ if (!value) {
+ return "Space is required";
+ }
+ },
+ list: (value) => {
+ if (!value) {
+ return "List is required";
+ }
+ },
+ object: (value) => {
+ if (!value) {
+ return "Object is required";
+ }
+ },
+ },
+ });
+
+ return (
+
+ {spaces.map((space) => (
+
+ ))}
+
+
+
+ {lists.map((list) => (
+
+ ))}
+
+
+ {selectedList && (
+
+ {objects
+ .filter((object) => !listItems.some((item) => item.id === object.id) && object.id !== selectedList)
+ .map((object) => (
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/extensions/anytype/src/api/auth/validateToken.ts b/extensions/anytype/src/api/auth/validateToken.ts
index 5ddb3ec5f4f..4a7ddf2b63a 100644
--- a/extensions/anytype/src/api/auth/validateToken.ts
+++ b/extensions/anytype/src/api/auth/validateToken.ts
@@ -1,4 +1,4 @@
-import { showToast, Toast } from "@raycast/api";
+import { showFailureToast } from "@raycast/utils";
import { PaginatedResponse, RawSpace } from "../../models";
import { apiEndpoints, apiFetch, currentApiVersion, errorConnectionMessage } from "../../utils";
@@ -10,17 +10,15 @@ export async function checkApiTokenValidity(): Promise {
const apiVersion = response.headers.get("Anytype-Version");
if (!apiVersion || apiVersion < currentApiVersion) {
- showToast(
- Toast.Style.Failure,
- "App Update Required",
- `Please update the Anytype app to match the extension's API version ${currentApiVersion}.`,
- );
+ await showFailureToast({
+ title: "App Update Required",
+ message: `Please update the Anytype app to match the extension's API version ${currentApiVersion}.`,
+ });
} else if (apiVersion > currentApiVersion) {
- showToast(
- Toast.Style.Failure,
- "Extension Update Required",
- `Please update the extension to match the Anytype app's API version ${apiVersion}.`,
- );
+ await showFailureToast({
+ title: "Extension Update Required",
+ message: `Please update the extension to match the Anytype app's API version ${apiVersion}.`,
+ });
}
return true;
} catch (error) {
diff --git a/extensions/anytype/src/api/export/getExport.ts b/extensions/anytype/src/api/export/getExport.ts
deleted file mode 100644
index d484c9635bc..00000000000
--- a/extensions/anytype/src/api/export/getExport.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { Export, ExportFormat } from "../../models";
-import { apiEndpoints, apiFetch } from "../../utils";
-
-export async function getExport(spaceId: string, objectId: string, format: ExportFormat): Promise {
- const { url, method } = apiEndpoints.getExport(spaceId, objectId, format);
- const response = await apiFetch(url, { method: method });
- return response.payload;
-}
diff --git a/extensions/anytype/src/api/index.ts b/extensions/anytype/src/api/index.ts
index 0f9828ee7a6..d3d385c5df6 100644
--- a/extensions/anytype/src/api/index.ts
+++ b/extensions/anytype/src/api/index.ts
@@ -1,7 +1,6 @@
export * from "./auth/displayCode";
export * from "./auth/getToken";
export * from "./auth/validateToken";
-export * from "./export/getExport";
export * from "./lists/addObjectsToList";
export * from "./lists/getListViews";
export * from "./lists/getObjectsInList";
@@ -13,11 +12,26 @@ export * from "./objects/createObject";
export * from "./objects/deleteObject";
export * from "./objects/getObject";
export * from "./objects/getObjects";
+export * from "./objects/updateObject";
+export * from "./properties/createProperty";
+export * from "./properties/deleteProperty";
+export * from "./properties/getProperties";
+export * from "./properties/getProperty";
+export * from "./properties/updateProperty";
export * from "./search/globalSearch";
export * from "./search/search";
export * from "./spaces/createSpace";
export * from "./spaces/getSpace";
export * from "./spaces/getSpaces";
+export * from "./spaces/updateSpace";
+export * from "./tags/createTag";
+export * from "./tags/deleteTag";
+export * from "./tags/getTag";
+export * from "./tags/getTags";
+export * from "./tags/updateTag";
export * from "./templates/getTemplates";
+export * from "./types/createType";
+export * from "./types/deleteType";
export * from "./types/getType";
export * from "./types/getTypes";
+export * from "./types/updateType";
diff --git a/extensions/anytype/src/api/lists/addObjectsToList.ts b/extensions/anytype/src/api/lists/addObjectsToList.ts
index 1a24a600b97..8f43c871214 100644
--- a/extensions/anytype/src/api/lists/addObjectsToList.ts
+++ b/extensions/anytype/src/api/lists/addObjectsToList.ts
@@ -1,9 +1,14 @@
import { apiEndpoints, apiFetch } from "../../utils";
+import { ApiResponse } from "../../utils/api";
-export async function addObjectsToList(spaceId: string, listId: string, objectIds: string[]): Promise {
+export async function addObjectsToList(
+ spaceId: string,
+ listId: string,
+ objectIds: string[],
+): Promise> {
const { url, method } = apiEndpoints.addObjectsToList(spaceId, listId);
- await apiFetch(url, {
+ return await apiFetch(url, {
method: method,
body: JSON.stringify(objectIds),
});
diff --git a/extensions/anytype/src/api/members/getMember.ts b/extensions/anytype/src/api/members/getMember.ts
index 5041102b194..58be9ba91f2 100644
--- a/extensions/anytype/src/api/members/getMember.ts
+++ b/extensions/anytype/src/api/members/getMember.ts
@@ -2,15 +2,8 @@ import { mapMember } from "../../mappers/members";
import { Member, RawMember } from "../../models";
import { apiEndpoints, apiFetch } from "../../utils";
-export async function getMember(
- spaceId: string,
- objectId: string,
-): Promise<{
- member: Member | null;
-}> {
+export async function getMember(spaceId: string, objectId: string): Promise<{ member: Member }> {
const { url, method } = apiEndpoints.getMember(spaceId, objectId);
const response = await apiFetch<{ member: RawMember }>(url, { method: method });
- return {
- member: response ? await mapMember(response.payload.member) : null,
- };
+ return { member: await mapMember(response.payload.member) };
}
diff --git a/extensions/anytype/src/api/members/updateMember.ts b/extensions/anytype/src/api/members/updateMember.ts
index 696a8c20da5..21efbeea06d 100644
--- a/extensions/anytype/src/api/members/updateMember.ts
+++ b/extensions/anytype/src/api/members/updateMember.ts
@@ -7,9 +7,7 @@ export async function updateMember(
spaceId: string,
memberId: string,
data: UpdateMemberRequest,
-): Promise<{
- member: Member | null;
-}> {
+): Promise<{ member: Member }> {
const { url, method } = apiEndpoints.updateMember(spaceId, memberId);
const response = await apiFetch<{ member: RawMember }>(url, {
@@ -17,7 +15,5 @@ export async function updateMember(
body: JSON.stringify(data),
});
- return {
- member: response ? await mapMember(response.payload.member) : null,
- };
+ return { member: await mapMember(response.payload.member) };
}
diff --git a/extensions/anytype/src/api/objects/deleteObject.ts b/extensions/anytype/src/api/objects/deleteObject.ts
index a65dafb2460..4d68a92ba27 100644
--- a/extensions/anytype/src/api/objects/deleteObject.ts
+++ b/extensions/anytype/src/api/objects/deleteObject.ts
@@ -1,10 +1,13 @@
-import { RawSpaceObject } from "../../models";
+import { mapObject } from "../../mappers/objects";
+import { RawSpaceObject, SpaceObject } from "../../models";
import { apiEndpoints, apiFetch } from "../../utils";
-export async function deleteObject(spaceId: string, objectId: string): Promise {
+export async function deleteObject(spaceId: string, objectId: string): Promise {
const { url, method } = apiEndpoints.deleteObject(spaceId, objectId);
- await apiFetch(url, {
+ const response = await apiFetch<{ object: RawSpaceObject }>(url, {
method: method,
});
+
+ return await mapObject(response.payload.object);
}
diff --git a/extensions/anytype/src/api/objects/getObject.ts b/extensions/anytype/src/api/objects/getObject.ts
index 5c992c6caca..819cf442ed0 100644
--- a/extensions/anytype/src/api/objects/getObject.ts
+++ b/extensions/anytype/src/api/objects/getObject.ts
@@ -1,27 +1,35 @@
import { mapObject } from "../../mappers/objects";
import { mapType } from "../../mappers/types";
-import { RawSpaceObjectWithBlocks, SpaceObject } from "../../models";
+import { BodyFormat, RawSpaceObjectWithBody, SpaceObjectWithBody } from "../../models";
import { apiEndpoints, apiFetch, getIconWithFallback } from "../../utils";
export async function getObject(
spaceId: string,
objectId: string,
-): Promise<{
- object: SpaceObject | null;
-}> {
- const { url, method } = apiEndpoints.getObject(spaceId, objectId);
- const response = await apiFetch<{ object: RawSpaceObjectWithBlocks }>(url, { method: method });
- return {
- object: response ? await mapObject(response.payload.object) : null,
- };
+ format: BodyFormat,
+): Promise<{ object: SpaceObjectWithBody }> {
+ const { url, method } = apiEndpoints.getObject(spaceId, objectId, format);
+ const response = await apiFetch<{ object: RawSpaceObjectWithBody }>(url, { method: method });
+ return { object: (await mapObject(response.payload.object)) as SpaceObjectWithBody };
+}
+
+export async function getRawObject(
+ spaceId: string,
+ objectId: string,
+ format: BodyFormat,
+): Promise<{ object: RawSpaceObjectWithBody }> {
+ const { url, method } = apiEndpoints.getObject(spaceId, objectId, format);
+ const response = await apiFetch<{ object: RawSpaceObjectWithBody }>(url, { method });
+ return { object: response.payload.object };
}
-export async function getObjectWithoutMappedDetails(spaceId: string, objectId: string): Promise {
- const { url, method } = apiEndpoints.getObject(spaceId, objectId);
- const response = await apiFetch<{ object: RawSpaceObjectWithBlocks }>(url, { method });
- if (!response) {
- return null;
- }
+export async function getObjectWithoutMappedProperties(
+ spaceId: string,
+ objectId: string,
+ format: BodyFormat,
+): Promise {
+ const { url, method } = apiEndpoints.getObject(spaceId, objectId, format);
+ const response = await apiFetch<{ object: RawSpaceObjectWithBody }>(url, { method });
const { object } = response.payload;
const icon = await getIconWithFallback(object.icon, object.layout, object.type);
@@ -29,7 +37,9 @@ export async function getObjectWithoutMappedDetails(spaceId: string, objectId: s
return {
...object,
icon,
- name: object.name || "Untitled",
+ name: object.name?.trim() || "Untitled",
type: await mapType(object.type),
+ properties: [], // performance optimization
+ markdown: "", // performance optimization
};
}
diff --git a/extensions/anytype/src/api/objects/updateObject.ts b/extensions/anytype/src/api/objects/updateObject.ts
new file mode 100644
index 00000000000..339d5a39b45
--- /dev/null
+++ b/extensions/anytype/src/api/objects/updateObject.ts
@@ -0,0 +1,18 @@
+import { mapObject } from "../../mappers/objects";
+import { RawSpaceObject, SpaceObject, UpdateObjectRequest } from "../../models";
+import { apiEndpoints, apiFetch } from "../../utils";
+
+export async function updateObject(
+ spaceId: string,
+ objectId: string,
+ data: UpdateObjectRequest,
+): Promise<{ object: SpaceObject }> {
+ const { url, method } = apiEndpoints.updateObject(spaceId, objectId);
+
+ const response = await apiFetch<{ object: RawSpaceObject }>(url, {
+ method: method,
+ body: JSON.stringify(data),
+ });
+
+ return { object: await mapObject(response.payload.object) };
+}
diff --git a/extensions/anytype/src/api/properties/createProperty.ts b/extensions/anytype/src/api/properties/createProperty.ts
new file mode 100644
index 00000000000..cbc9bbb9924
--- /dev/null
+++ b/extensions/anytype/src/api/properties/createProperty.ts
@@ -0,0 +1,19 @@
+import { mapProperty } from "../../mappers/properties";
+import { CreatePropertyRequest, Property, RawProperty } from "../../models";
+import { apiEndpoints, apiFetch } from "../../utils";
+
+export async function createProperty(
+ spaceId: string,
+ propertyData: CreatePropertyRequest,
+): Promise<{ property: Property | null }> {
+ const { url, method } = apiEndpoints.createProperty(spaceId);
+
+ const response = await apiFetch<{ property: RawProperty }>(url, {
+ method: method,
+ body: JSON.stringify(propertyData),
+ });
+
+ return {
+ property: response ? mapProperty(response.payload.property) : null,
+ };
+}
diff --git a/extensions/anytype/src/api/properties/deleteProperty.ts b/extensions/anytype/src/api/properties/deleteProperty.ts
new file mode 100644
index 00000000000..347a62dd2f2
--- /dev/null
+++ b/extensions/anytype/src/api/properties/deleteProperty.ts
@@ -0,0 +1,13 @@
+import { mapProperty } from "../../mappers/properties";
+import { Property, RawProperty } from "../../models";
+import { apiEndpoints, apiFetch } from "../../utils";
+
+export async function deleteProperty(spaceId: string, propertyId: string): Promise {
+ const { url, method } = apiEndpoints.deleteProperty(spaceId, propertyId);
+
+ const response = await apiFetch<{ property: RawProperty }>(url, {
+ method: method,
+ });
+
+ return mapProperty(response.payload.property);
+}
diff --git a/extensions/anytype/src/api/properties/getProperties.ts b/extensions/anytype/src/api/properties/getProperties.ts
new file mode 100644
index 00000000000..c1843051571
--- /dev/null
+++ b/extensions/anytype/src/api/properties/getProperties.ts
@@ -0,0 +1,19 @@
+import { mapProperties } from "../../mappers/properties";
+import { PaginatedResponse, Pagination, Property, RawProperty } from "../../models";
+import { apiEndpoints, apiFetch } from "../../utils";
+
+export async function getProperties(
+ spaceId: string,
+ options: { offset: number; limit: number },
+): Promise<{
+ properties: Property[];
+ pagination: Pagination;
+}> {
+ const { url, method } = apiEndpoints.getProperties(spaceId, options);
+ const response = await apiFetch>(url, { method: method });
+
+ return {
+ properties: response.payload.data ? mapProperties(response.payload.data) : [],
+ pagination: response.payload.pagination,
+ };
+}
diff --git a/extensions/anytype/src/api/properties/getProperty.ts b/extensions/anytype/src/api/properties/getProperty.ts
new file mode 100644
index 00000000000..032c5fce9f9
--- /dev/null
+++ b/extensions/anytype/src/api/properties/getProperty.ts
@@ -0,0 +1,11 @@
+import { mapProperty } from "../../mappers/properties";
+import { Property, RawProperty } from "../../models";
+import { apiEndpoints, apiFetch } from "../../utils";
+
+export async function getProperty(spaceId: string, propertyId: string): Promise<{ property: Property }> {
+ const { url, method } = apiEndpoints.getProperty(spaceId, propertyId);
+ const response = await apiFetch<{ property: RawProperty }>(url, { method: method });
+ return {
+ property: mapProperty(response.payload.property),
+ };
+}
diff --git a/extensions/anytype/src/api/properties/updateProperty.ts b/extensions/anytype/src/api/properties/updateProperty.ts
new file mode 100644
index 00000000000..087c612d7bc
--- /dev/null
+++ b/extensions/anytype/src/api/properties/updateProperty.ts
@@ -0,0 +1,18 @@
+import { mapProperty } from "../../mappers/properties";
+import { Property, RawProperty, UpdatePropertyRequest } from "../../models";
+import { apiEndpoints, apiFetch } from "../../utils";
+
+export async function updateProperty(
+ spaceId: string,
+ propertyId: string,
+ data: UpdatePropertyRequest,
+): Promise<{ property: Property }> {
+ const { url, method } = apiEndpoints.updateProperty(spaceId, propertyId);
+
+ const response = await apiFetch<{ property: RawProperty }>(url, {
+ method: method,
+ body: JSON.stringify(data),
+ });
+
+ return { property: mapProperty(response.payload.property) };
+}
diff --git a/extensions/anytype/src/api/spaces/getSpace.ts b/extensions/anytype/src/api/spaces/getSpace.ts
index b8ddacf6cea..dc0637845ed 100644
--- a/extensions/anytype/src/api/spaces/getSpace.ts
+++ b/extensions/anytype/src/api/spaces/getSpace.ts
@@ -2,12 +2,8 @@ import { mapSpace } from "../../mappers/spaces";
import { RawSpace, Space } from "../../models";
import { apiEndpoints, apiFetch } from "../../utils";
-export async function getSpace(spaceId: string): Promise<{
- space: Space | null;
-}> {
+export async function getSpace(spaceId: string): Promise<{ space: Space }> {
const { url, method } = apiEndpoints.getSpace(spaceId);
const response = await apiFetch<{ space: RawSpace }>(url, { method: method });
- return {
- space: response ? await mapSpace(response.payload.space) : null,
- };
+ return { space: await mapSpace(response.payload.space) };
}
diff --git a/extensions/anytype/src/api/spaces/updateSpace.ts b/extensions/anytype/src/api/spaces/updateSpace.ts
new file mode 100644
index 00000000000..0eb94245cb7
--- /dev/null
+++ b/extensions/anytype/src/api/spaces/updateSpace.ts
@@ -0,0 +1,14 @@
+import { mapSpace } from "../../mappers/spaces";
+import { RawSpace, Space, UpdateSpaceRequest } from "../../models";
+import { apiEndpoints, apiFetch } from "../../utils";
+
+export async function updateSpace(spaceId: string, data: UpdateSpaceRequest): Promise<{ space: Space }> {
+ const { url, method } = apiEndpoints.updateSpace(spaceId);
+
+ const response = await apiFetch<{ space: RawSpace }>(url, {
+ method: method,
+ body: JSON.stringify(data),
+ });
+
+ return { space: await mapSpace(response.payload.space) };
+}
diff --git a/extensions/anytype/src/api/tags/createTag.ts b/extensions/anytype/src/api/tags/createTag.ts
new file mode 100644
index 00000000000..9adeff91a45
--- /dev/null
+++ b/extensions/anytype/src/api/tags/createTag.ts
@@ -0,0 +1,20 @@
+import { mapTag } from "../../mappers/properties";
+import { CreateTagRequest, RawTag, Tag } from "../../models";
+import { apiEndpoints, apiFetch } from "../../utils";
+
+export async function createTag(
+ spaceId: string,
+ propertyId: string,
+ tagData: CreateTagRequest,
+): Promise<{ tag: Tag | null }> {
+ const { url, method } = apiEndpoints.createTag(spaceId, propertyId);
+
+ const response = await apiFetch<{ tag: RawTag }>(url, {
+ method: method,
+ body: JSON.stringify(tagData),
+ });
+
+ return {
+ tag: response ? mapTag(response.payload.tag) : null,
+ };
+}
diff --git a/extensions/anytype/src/api/tags/deleteTag.ts b/extensions/anytype/src/api/tags/deleteTag.ts
new file mode 100644
index 00000000000..7a853ea3fe4
--- /dev/null
+++ b/extensions/anytype/src/api/tags/deleteTag.ts
@@ -0,0 +1,13 @@
+import { mapTag } from "../../mappers/properties";
+import { RawTag, Tag } from "../../models";
+import { apiEndpoints, apiFetch } from "../../utils";
+
+export async function deleteTag(spaceId: string, propertyId: string, tagId: string): Promise {
+ const { url, method } = apiEndpoints.deleteTag(spaceId, propertyId, tagId);
+
+ const response = await apiFetch<{ tag: RawTag }>(url, {
+ method: method,
+ });
+
+ return mapTag(response.payload.tag);
+}
diff --git a/extensions/anytype/src/api/tags/getTag.ts b/extensions/anytype/src/api/tags/getTag.ts
new file mode 100644
index 00000000000..ccbd5c9f0f6
--- /dev/null
+++ b/extensions/anytype/src/api/tags/getTag.ts
@@ -0,0 +1,11 @@
+import { mapTag } from "../../mappers/properties";
+import { RawTag, Tag } from "../../models";
+import { apiEndpoints, apiFetch } from "../../utils";
+
+export async function getTag(spaceId: string, propertyId: string, tagId: string): Promise<{ tag: Tag }> {
+ const { url, method } = apiEndpoints.getTag(spaceId, propertyId, tagId);
+ const response = await apiFetch<{ tag: RawTag }>(url, { method: method });
+ return {
+ tag: mapTag(response.payload.tag),
+ };
+}
diff --git a/extensions/anytype/src/api/tags/getTags.ts b/extensions/anytype/src/api/tags/getTags.ts
new file mode 100644
index 00000000000..62df7d08fdc
--- /dev/null
+++ b/extensions/anytype/src/api/tags/getTags.ts
@@ -0,0 +1,20 @@
+import { mapTags } from "../../mappers/properties";
+import { PaginatedResponse, Pagination, RawTag, Tag } from "../../models";
+import { apiEndpoints, apiFetch } from "../../utils";
+
+export async function getTags(
+ spaceId: string,
+ propertyId: string,
+ options: { offset: number; limit: number },
+): Promise<{
+ tags: Tag[];
+ pagination: Pagination;
+}> {
+ const { url, method } = apiEndpoints.getTags(spaceId, propertyId, options);
+ const response = await apiFetch>(url, { method: method });
+
+ return {
+ tags: response.payload.data ? mapTags(response.payload.data) : [],
+ pagination: response.payload.pagination,
+ };
+}
diff --git a/extensions/anytype/src/api/tags/updateTag.ts b/extensions/anytype/src/api/tags/updateTag.ts
new file mode 100644
index 00000000000..6047b32b72d
--- /dev/null
+++ b/extensions/anytype/src/api/tags/updateTag.ts
@@ -0,0 +1,19 @@
+import { mapTag } from "../../mappers/properties";
+import { RawTag, Tag, UpdateTagRequest } from "../../models";
+import { apiEndpoints, apiFetch } from "../../utils";
+
+export async function updateTag(
+ spaceId: string,
+ propertyId: string,
+ tagId: string,
+ data: UpdateTagRequest,
+): Promise<{ tag: Tag }> {
+ const { url, method } = apiEndpoints.updateTag(spaceId, propertyId, tagId);
+
+ const response = await apiFetch<{ tag: RawTag }>(url, {
+ method: method,
+ body: JSON.stringify(data),
+ });
+
+ return { tag: mapTag(response.payload.tag) };
+}
diff --git a/extensions/anytype/src/api/templates/getTemplates.ts b/extensions/anytype/src/api/templates/getTemplates.ts
index 146cc68dc94..b6680c92d93 100644
--- a/extensions/anytype/src/api/templates/getTemplates.ts
+++ b/extensions/anytype/src/api/templates/getTemplates.ts
@@ -1,5 +1,5 @@
-import { mapTemplates } from "../../mappers/templates";
-import { PaginatedResponse, Pagination, RawTemplate, Template } from "../../models";
+import { mapObjects } from "../../mappers/objects";
+import { PaginatedResponse, Pagination, RawSpaceObject, SpaceObject } from "../../models";
import { apiEndpoints, apiFetch } from "../../utils";
export async function getTemplates(
@@ -10,14 +10,14 @@ export async function getTemplates(
limit: number;
},
): Promise<{
- templates: Template[];
+ templates: SpaceObject[];
pagination: Pagination;
}> {
const { url, method } = apiEndpoints.getTemplates(spaceId, typeId, options);
- const response = await apiFetch>(url, { method: method });
+ const response = await apiFetch>(url, { method: method });
return {
- templates: response.payload.data ? await mapTemplates(response.payload.data) : [],
+ templates: response.payload.data ? await mapObjects(response.payload.data) : [],
pagination: response.payload.pagination,
};
}
diff --git a/extensions/anytype/src/api/types/createType.ts b/extensions/anytype/src/api/types/createType.ts
new file mode 100644
index 00000000000..fd3c3dcb76d
--- /dev/null
+++ b/extensions/anytype/src/api/types/createType.ts
@@ -0,0 +1,16 @@
+import { mapType } from "../../mappers/types";
+import { CreateTypeRequest, RawType, Type } from "../../models";
+import { apiEndpoints, apiFetch } from "../../utils";
+
+export async function createType(spaceId: string, typeData: CreateTypeRequest): Promise<{ type: Type | null }> {
+ const { url, method } = apiEndpoints.createType(spaceId);
+
+ const response = await apiFetch<{ type: RawType }>(url, {
+ method: method,
+ body: JSON.stringify(typeData),
+ });
+
+ return {
+ type: response ? await mapType(response.payload.type) : null,
+ };
+}
diff --git a/extensions/anytype/src/api/types/deleteType.ts b/extensions/anytype/src/api/types/deleteType.ts
new file mode 100644
index 00000000000..90e9bafda21
--- /dev/null
+++ b/extensions/anytype/src/api/types/deleteType.ts
@@ -0,0 +1,13 @@
+import { mapType } from "../../mappers/types";
+import { RawType, Type } from "../../models";
+import { apiEndpoints, apiFetch } from "../../utils";
+
+export async function deleteType(spaceId: string, typeId: string): Promise {
+ const { url, method } = apiEndpoints.deleteType(spaceId, typeId);
+
+ const response = await apiFetch<{ type: RawType }>(url, {
+ method: method,
+ });
+
+ return mapType(response.payload.type);
+}
diff --git a/extensions/anytype/src/api/types/getType.ts b/extensions/anytype/src/api/types/getType.ts
index 655a4779eed..e57abe00447 100644
--- a/extensions/anytype/src/api/types/getType.ts
+++ b/extensions/anytype/src/api/types/getType.ts
@@ -2,15 +2,14 @@ import { mapType } from "../../mappers/types";
import { RawType, Type } from "../../models";
import { apiEndpoints, apiFetch } from "../../utils";
-export async function getType(
- spaceId: string,
- typeId: string,
-): Promise<{
- type: Type | null;
-}> {
+export async function getType(spaceId: string, typeId: string): Promise<{ type: Type }> {
const { url, method } = apiEndpoints.getType(spaceId, typeId);
const response = await apiFetch<{ type: RawType }>(url, { method: method });
- return {
- type: response ? await mapType(response.payload.type) : null,
- };
+ return { type: await mapType(response.payload.type) };
+}
+
+export async function getRawType(spaceId: string, typeId: string): Promise<{ type: RawType }> {
+ const { url, method } = apiEndpoints.getType(spaceId, typeId);
+ const response = await apiFetch<{ type: RawType }>(url, { method: method });
+ return { type: response.payload.type };
}
diff --git a/extensions/anytype/src/api/types/updateType.ts b/extensions/anytype/src/api/types/updateType.ts
new file mode 100644
index 00000000000..e60fbc5ad33
--- /dev/null
+++ b/extensions/anytype/src/api/types/updateType.ts
@@ -0,0 +1,14 @@
+import { mapType } from "../../mappers/types";
+import { RawType, Type, UpdateTypeRequest } from "../../models";
+import { apiEndpoints, apiFetch } from "../../utils";
+
+export async function updateType(spaceId: string, typeId: string, data: UpdateTypeRequest): Promise<{ type: Type }> {
+ const { url, method } = apiEndpoints.updateType(spaceId, typeId);
+
+ const response = await apiFetch<{ type: RawType }>(url, {
+ method: method,
+ body: JSON.stringify(data),
+ });
+
+ return { type: await mapType(response.payload.type) };
+}
diff --git a/extensions/anytype/src/browse-spaces.tsx b/extensions/anytype/src/browse-spaces.tsx
index 21431f5f980..0cb7cd7f099 100644
--- a/extensions/anytype/src/browse-spaces.tsx
+++ b/extensions/anytype/src/browse-spaces.tsx
@@ -1,142 +1,11 @@
-import { Icon, List, Toast, showToast } from "@raycast/api";
-import { useEffect, useMemo, useState } from "react";
-import { getMembers } from "./api";
-import { EmptyViewSpace, EnsureAuthenticated, SpaceListItem } from "./components";
-import { usePinnedSpaces, useSpaces } from "./hooks";
-import { Space } from "./models";
-import { defaultTintColor, pluralize } from "./utils";
+import { EnsureAuthenticated, SpaceList } from "./components";
const searchPlaceholder = "Search spaces...";
export default function Command() {
return (
-
+
);
}
-
-function BrowseSpaces() {
- const { spaces, spacesError, mutateSpaces, isLoadingSpaces, spacesPagination } = useSpaces();
- const { pinnedSpaces, pinnedSpacesError, isLoadingPinnedSpaces, mutatePinnedSpaces } = usePinnedSpaces();
- const [searchText, setSearchText] = useState("");
- const [membersData, setMembersData] = useState<{ [spaceId: string]: number }>({});
-
- useEffect(() => {
- if (!spaces) return;
-
- const fetchMembersData = async () => {
- const newData: { [key: string]: number } = {};
-
- const spaceIdsToFetch = spaces.map((space) => space.id).filter((id) => !(id in membersData));
-
- try {
- await Promise.all(
- spaceIdsToFetch.map(async (id) => {
- const response = await getMembers(id, { offset: 0, limit: 1 });
- newData[id] = response.pagination.total;
- }),
- );
- setMembersData((prev) => ({ ...prev, ...newData }));
- } catch (error) {
- showToast(
- Toast.Style.Failure,
- "Failed to fetch members",
- error instanceof Error ? error.message : "An unknown error occurred.",
- );
- }
- };
-
- fetchMembersData();
- }, [spaces]);
-
- const isLoadingMembers = useMemo(() => {
- if (!spaces) return true;
- return Object.keys(membersData).length !== spaces.length;
- }, [spaces, membersData]);
-
- useEffect(() => {
- if (spacesError) {
- showToast(Toast.Style.Failure, "Failed to fetch spaces", spacesError.message);
- }
- }, [spacesError]);
-
- useEffect(() => {
- if (pinnedSpacesError) {
- showToast(Toast.Style.Failure, "Failed to fetch pinned spaces", pinnedSpacesError.message);
- }
- }, [pinnedSpacesError]);
-
- const filteredSpaces = spaces?.filter((space) => space.name.toLowerCase().includes(searchText.toLowerCase()));
- const pinnedFiltered = pinnedSpaces
- ?.map((pin) => filteredSpaces.find((space) => space.id === pin.id))
- .filter(Boolean) as Space[];
- const regularFiltered = filteredSpaces?.filter((space) => !pinnedFiltered?.includes(space));
-
- return (
-
- {pinnedFiltered.length > 0 && (
-
- {pinnedFiltered.map((space) => {
- const memberCount = membersData[space.id] || 0;
- return (
-
- );
- })}
-
- )}
- {regularFiltered.length > 0 ? (
-
- {regularFiltered.map((space) => {
- const memberCount = membersData[space.id] || 0;
- return (
-
- );
- })}
-
- ) : (
-
- )}
-
- );
-}
diff --git a/extensions/anytype/src/components/Actions/ListSubmenu.tsx b/extensions/anytype/src/components/Actions/ListSubmenu.tsx
new file mode 100644
index 00000000000..daa2dfee78c
--- /dev/null
+++ b/extensions/anytype/src/components/Actions/ListSubmenu.tsx
@@ -0,0 +1,44 @@
+import { Action, ActionPanel, Icon, Toast, showToast } from "@raycast/api";
+import { showFailureToast } from "@raycast/utils";
+import { useState } from "react";
+import { addObjectsToList } from "../../api";
+import { useSearch } from "../../hooks";
+import { bundledTypeKeys } from "../../utils";
+
+interface ListSubmenuProps {
+ spaceId: string;
+ objectId: string;
+}
+
+export function ListSubmenu({ spaceId, objectId }: ListSubmenuProps) {
+ const [load, setLoad] = useState(false);
+ const { objects: lists, isLoadingObjects } = useSearch(spaceId, "", [bundledTypeKeys.collection], { execute: load });
+ const filteredLists = lists.filter((list) => list.id !== objectId);
+
+ async function handleAddToList(listId: string) {
+ await showToast({ style: Toast.Style.Animated, title: `Adding to list…` });
+ try {
+ await addObjectsToList(spaceId, listId, [objectId]);
+ await showToast({ style: Toast.Style.Success, title: "Added to list" });
+ } catch (error) {
+ await showFailureToast(error, { title: "Failed to add to list" });
+ }
+ }
+
+ return (
+ setLoad(true)}
+ >
+ {filteredLists.length === 0 && isLoadingObjects ? (
+
+ ) : (
+ filteredLists.map((list) => (
+ handleAddToList(list.id)} />
+ ))
+ )}
+
+ );
+}
diff --git a/extensions/anytype/src/components/ObjectActions.tsx b/extensions/anytype/src/components/Actions/ObjectActions.tsx
similarity index 65%
rename from extensions/anytype/src/components/ObjectActions.tsx
rename to extensions/anytype/src/components/Actions/ObjectActions.tsx
index 064e930a473..8b5d300b627 100644
--- a/extensions/anytype/src/components/ObjectActions.tsx
+++ b/extensions/anytype/src/components/Actions/ObjectActions.tsx
@@ -7,41 +7,66 @@ import {
getPreferenceValues,
Icon,
Keyboard,
+ open,
showToast,
Toast,
+ useNavigation,
} from "@raycast/api";
-import { MutatePromise } from "@raycast/utils";
-import { CollectionList, ObjectDetail, TemplateList, ViewType } from ".";
-import { deleteObject } from "../api";
-import { Export, Member, Space, SpaceObject, Template, Type, View } from "../models";
+import { MutatePromise, showFailureToast } from "@raycast/utils";
+import {
+ CollectionList,
+ CreateObjectForm,
+ CreatePropertyForm,
+ CreateTypeForm,
+ ListSubmenu,
+ ObjectDetail,
+ TagList,
+ TemplateList,
+ UpdateObjectForm,
+ UpdatePropertyForm,
+ UpdateTypeForm,
+ ViewType,
+} from "..";
+import { deleteObject, deleteProperty, deleteTag, deleteType, getRawObject, getRawType } from "../../api";
+import {
+ BodyFormat,
+ Member,
+ ObjectLayout,
+ Property,
+ Space,
+ SpaceObject,
+ SpaceObjectWithBody,
+ Type,
+ View,
+} from "../../models";
import {
addPinned,
+ bundledPropKeys,
localStorageKeys,
moveDownInPinned,
moveUpInPinned,
pluralize,
removePinned,
- typeIsList,
-} from "../utils";
+} from "../../utils";
type ObjectActionsProps = {
space: Space;
objectId: string;
title: string;
- objectExport?: Export;
- mutate?: MutatePromise[];
- mutateTemplates?: MutatePromise;
- mutateObject?: MutatePromise;
- mutateExport?: MutatePromise;
+ mutate?: MutatePromise[];
+ mutateTemplates?: MutatePromise;
+ mutateObject?: MutatePromise;
mutateViews?: MutatePromise;
- layout: string;
- member?: Member | undefined;
+ layout: ObjectLayout | undefined;
+ object?: SpaceObject | SpaceObjectWithBody | Type | Property | Member;
viewType: ViewType;
isGlobalSearch: boolean;
isNoPinView: boolean;
isPinned: boolean;
+ isDetailView?: boolean;
showDetails?: boolean;
onToggleDetails?: () => void;
+ searchText?: string;
};
export function ObjectActions({
@@ -49,29 +74,38 @@ export function ObjectActions({
objectId,
title,
mutate,
- objectExport,
mutateTemplates,
mutateObject,
- mutateExport,
mutateViews,
layout,
+ object,
viewType,
isGlobalSearch,
isNoPinView,
isPinned,
+ isDetailView,
showDetails,
onToggleDetails,
+ searchText,
}: ObjectActionsProps) {
+ const { pop, push } = useNavigation();
const { primaryAction } = getPreferenceValues();
const objectUrl = `anytype://object?objectId=${objectId}&spaceId=${space?.id}`;
const pinSuffixForView = isGlobalSearch
? localStorageKeys.suffixForGlobalSearch
: localStorageKeys.suffixForViewsPerSpace(space?.id, viewType);
- const isDetailView = objectExport !== undefined;
- const isList = typeIsList(layout);
+
+ const isObject = viewType === ViewType.objects;
const isType = viewType === ViewType.types;
+ const isProperty = viewType === ViewType.properties;
+ const isTag = viewType === ViewType.tags;
const isMember = viewType === ViewType.members;
+ const isList = layout === ObjectLayout.Set || layout === ObjectLayout.Collection;
+ const isBookmark = layout === ObjectLayout.Bookmark;
+ const hasTags =
+ isProperty && ((object as Property).format === "select" || (object as Property).format === "multi_select");
+
const getContextLabel = (isSingular = true) => (isDetailView || isSingular ? viewType : pluralize(2, viewType));
async function handleCopyLink() {
@@ -91,8 +125,21 @@ export function ObjectActions({
});
if (confirm) {
+ if (isDetailView) {
+ pop(); // pop back to list view
+ }
try {
- await deleteObject(space.id, objectId);
+ if (isObject) {
+ await deleteObject(space.id, objectId);
+ } else if (isType) {
+ await deleteType(space.id, objectId);
+ } else if (isProperty) {
+ await deleteProperty(space.id, objectId);
+ } else if (isTag) {
+ await deleteTag(space.id, "", objectId); // TODO: fix property Id
+ } else {
+ await deleteObject(space.id, objectId);
+ }
if (mutate) {
for (const m of mutate) {
await m();
@@ -104,9 +151,6 @@ export function ObjectActions({
if (mutateObject) {
await mutateObject();
}
- if (mutateExport) {
- await mutateExport();
- }
if (mutateViews) {
await mutateViews();
}
@@ -116,11 +160,7 @@ export function ObjectActions({
message: `"${title}" has been deleted.`,
});
} catch (error) {
- await showToast({
- style: Toast.Style.Failure,
- title: `Failed to delete ${getContextLabel()}`,
- message: error instanceof Error ? error.message : "An unknown error occurred.",
- });
+ await showFailureToast(error, { title: `Failed to delete ${getContextLabel()}` });
}
}
}
@@ -183,20 +223,13 @@ export function ObjectActions({
if (mutateObject) {
await mutateObject();
}
- if (mutateExport) {
- await mutateExport();
- }
await showToast({
style: Toast.Style.Success,
title: `${label} refreshed`,
});
} catch (error) {
- await showToast({
- style: Toast.Style.Failure,
- title: `Failed to refresh ${label}`,
- message: error instanceof Error ? error.message : "An unknown error occurred.",
- });
+ await showFailureToast(error, { title: `Failed to refresh ${label}` });
}
}
@@ -215,11 +248,7 @@ export function ObjectActions({
// message: `${name} has been approved as ${formatMemberRole(role)}.`,
// });
// } catch (error) {
- // await showToast({
- // style: Toast.Style.Failure,
- // title: `Failed to approve member`,
- // message: error instanceof Error ? error.message : "An unknown error occurred.",
- // });
+ // await showFailureToast(error, { title: `Failed to approve member` });
// }
// }
@@ -244,11 +273,7 @@ export function ObjectActions({
// message: `${name} has been rejected.`,
// });
// } catch (error) {
- // await showToast({
- // style: Toast.Style.Failure,
- // title: `Failed to reject member`,
- // message: error instanceof Error ? error.message : "An unknown error occurred.",
- // });
+ // await showFailureToast(error, { title: `Failed to reject member` });
// }
// }
// }
@@ -274,11 +299,7 @@ export function ObjectActions({
// message: `${memberName} has been removed from ${spaceName}.`,
// });
// } catch (error) {
- // await showToast({
- // style: Toast.Style.Failure,
- // title: `Failed to remove member`,
- // message: error instanceof Error ? error.message : "An unknown error occurred.",
- // });
+ // await showFailureToast(error, { title: `Failed to remove member` });
// }
// }
// }
@@ -297,15 +318,11 @@ export function ObjectActions({
// message: `${name} has been changed to ${formatMemberRole(role)}.`,
// });
// } catch (error) {
- // await showToast({
- // style: Toast.Style.Failure,
- // title: `Failed to change member role`,
- // message: error instanceof Error ? error.message : "An unknown error occurred.",
- // });
+ // await showFailureToast(error, { title: `Failed to change member role` });
// }
// }
- const canShowDetails = !isType && !isList && !isDetailView;
+ const canShowDetails = !isType && !isProperty && !isList && !isBookmark && !isDetailView;
const showDetailsAction = canShowDetails && (
)}
+ {hasTags && (
+ } />
+ )}
+ {isBookmark && (
+ {
+ const url = (object as SpaceObject).properties.find((p) => p.key === bundledPropKeys.source)?.url;
+ if (url) {
+ open(url);
+ } else {
+ await showToast({
+ title: "Missing URL",
+ message: "Cannot open bookmark without a source URL.",
+ style: Toast.Style.Failure,
+ });
+ }
+ }}
+ />
+ )}
{secondPrimaryAction}
+
- {objectExport && (
+ {!isType && !isProperty && !isTag && !isMember && (
+ {
+ const { object } = await getRawObject(space.id, objectId, BodyFormat.Markdown);
+ push(
+ []}
+ mutateObject={mutateObject}
+ />,
+ );
+ }}
+ />
+ )}
+ {isType && (
+ {
+ const { type } = await getRawType(space.id, objectId);
+ push([]} />);
+ }}
+ />
+ )}
+ {isProperty && (
+ []}
+ />
+ }
+ />
+ )}
+ {isDetailView && (
)}
+ {!isType && !isProperty && }
- {!isMember && (
-
- )}
{!isDetailView && !isNoPinView && (
<>
)}
+ {!isMember && (
+
+ )}
+ {!isMember && (
+ {
+ if (isType) {
+ push();
+ } else if (isProperty) {
+ push();
+ } else {
+ push();
+ }
+ }}
+ />
+ )}
{isDetailView && (
[];
isPinned: boolean;
+ searchText: string;
};
-export function SpaceActions({ space, mutate, isPinned }: SpaceActionsProps) {
+export function SpaceActions({ space, mutate, isPinned, searchText }: SpaceActionsProps) {
const spaceDeeplink = anytypeSpaceDeeplink(space.id);
const pinSuffix = localStorageKeys.suffixForSpaces;
@@ -64,11 +65,7 @@ export function SpaceActions({ space, mutate, isPinned }: SpaceActionsProps) {
await Promise.all(mutate.map((mutateFunc) => mutateFunc()));
await showToast({ style: Toast.Style.Success, title: "Spaces refreshed" });
} catch (error) {
- await showToast({
- style: Toast.Style.Failure,
- title: "Failed to refresh spaces",
- message: error instanceof Error ? error.message : "An unknown error occurred.",
- });
+ await showFailureToast(error, { title: "Failed to refresh spaces" });
}
}
}
@@ -84,6 +81,13 @@ export function SpaceActions({ space, mutate, isPinned }: SpaceActionsProps) {
/>
+ }
+ />
+
+ }
+ />
([]);
+
+ const selectedTypeDef = types.find((type) => type.id === selectedTypeId);
+ const selectedTypeKey = selectedTypeDef?.key ?? "";
+ const hasselectedSpaceIdAndType = Boolean(selectedSpaceId && selectedTypeKey);
+
+ const properties = selectedTypeDef?.properties.filter((p) => !Object.values(bundledPropKeys).includes(p.key)) || [];
+ const { tagsMap } = useTagsMap(
+ selectedSpaceId,
+ properties
+ .filter((prop) => prop.format === PropertyFormat.Select || prop.format === PropertyFormat.MultiSelect)
+ .map((prop) => prop.id),
+ );
+
+ const numberFieldValidations = useMemo(() => getNumberFieldValidations(properties), [properties]);
+
+ useEffect(() => {
+ const fetchTypesForLists = async () => {
+ if (spaces) {
+ const listsTypes = await fetchTypeKeysForLists(spaces);
+ setTypeKeysForLists(listsTypes);
+ }
+ };
+ fetchTypesForLists();
+ }, [spaces]);
+
+ const { handleSubmit, itemProps } = useForm({
+ initialValues: draftValues,
+ onSubmit: async (values) => {
+ setLoading(true);
+ try {
+ await showToast({ style: Toast.Style.Animated, title: "Creating object..." });
+ const propertiesEntries: PropertyLinkWithValue[] = [];
+ properties.forEach((prop) => {
+ const raw = itemProps[prop.key]?.value;
+ if (raw !== undefined && raw !== null && raw !== "" && raw !== false) {
+ const entry: PropertyLinkWithValue = { key: prop.key, format: prop.format };
+ switch (prop.format) {
+ case PropertyFormat.Text:
+ entry.text = String(raw);
+ break;
+ case PropertyFormat.Select:
+ entry.select = String(raw);
+ break;
+ case PropertyFormat.Url:
+ entry.url = String(raw);
+ break;
+ case PropertyFormat.Email:
+ entry.email = String(raw);
+ break;
+ case PropertyFormat.Phone:
+ entry.phone = String(raw);
+ break;
+ case PropertyFormat.Number:
+ entry.number = Number(raw);
+ break;
+ case PropertyFormat.MultiSelect:
+ entry.multi_select = raw as string[];
+ break;
+ case PropertyFormat.Date:
+ {
+ const date = raw instanceof Date ? raw : new Date(String(raw));
+ if (!isNaN(date.getTime())) {
+ entry.date = formatRFC3339(date);
+ } else {
+ console.warn(`Invalid date value for property ${prop.key}:`, raw);
+ }
+ }
+ break;
+ case PropertyFormat.Checkbox:
+ entry.checkbox = Boolean(raw);
+ break;
+ case PropertyFormat.Files:
+ entry.files = Array.isArray(raw) ? (raw as string[]) : [String(raw)];
+ break;
+ case PropertyFormat.Objects:
+ entry.objects = Array.isArray(raw) ? (raw as string[]) : [String(raw)];
+ break;
+ default:
+ console.warn(`Unsupported property format: ${prop.format}`);
+ break;
+ }
+ propertiesEntries.push(entry);
+ }
+ });
+
+ const descriptionValue = itemProps[bundledPropKeys.description]?.value;
+ if (descriptionValue !== undefined && descriptionValue !== null && descriptionValue !== "") {
+ propertiesEntries.push({
+ key: bundledPropKeys.description,
+ format: PropertyFormat.Text,
+ text: String(descriptionValue),
+ });
+ }
+
+ const sourceValue = itemProps[bundledPropKeys.source]?.value;
+ if (sourceValue !== undefined && sourceValue !== null && sourceValue !== "") {
+ propertiesEntries.push({
+ key: bundledPropKeys.source,
+ format: PropertyFormat.Url,
+ url: String(sourceValue),
+ });
+ }
+
+ const objectData: CreateObjectRequest = {
+ name: values.name || "",
+ icon: { format: IconFormat.Emoji, emoji: values.icon || "" },
+ body: values.body || "",
+ template_id: values.templateId || "",
+ type_key: selectedTypeKey,
+ properties: propertiesEntries,
+ };
+
+ const response = await createObject(selectedSpaceId, objectData);
+
+ if (response.object?.id) {
+ if (selectedListId) {
+ await addObjectsToList(selectedSpaceId, selectedListId, [response.object.id]);
+ await showToast(Toast.Style.Success, "Object created and added to collection");
+ } else {
+ await showToast(Toast.Style.Success, "Object created successfully");
+ }
+ popToRoot();
+ } else {
+ await showToast(Toast.Style.Failure, "Failed to create object");
+ }
+ } catch (error) {
+ await showFailureToast(error, { title: "Failed to create object" });
+ } finally {
+ setLoading(false);
+ }
+ },
+ validation: {
+ name: (v: PropertyFieldValue) => {
+ const s = typeof v === "string" ? v.trim() : undefined;
+ if (![bundledTypeKeys.bookmark, bundledTypeKeys.note].includes(selectedTypeKey) && !s) {
+ return "Name is required";
+ }
+ },
+ icon: (v: PropertyFieldValue) => {
+ if (typeof v === "string" && v && !isEmoji(v)) {
+ return "Icon must be single emoji";
+ }
+ },
+ source: (v: PropertyFieldValue) => {
+ const s = typeof v === "string" ? v.trim() : undefined;
+ if (selectedTypeId === bundledTypeKeys.bookmark && !s) {
+ return "Source is required for Bookmarks";
+ }
+ },
+ ...numberFieldValidations,
+ },
+ });
+
+ function getQuicklink(): { name: string; link: string } {
+ const url = "raycast://extensions/any/anytype/create-object";
+
+ const defaults: Record = {
+ space: selectedSpaceId,
+ type: selectedTypeId,
+ list: selectedListId,
+ name: itemProps.name.value,
+ icon: itemProps.icon.value,
+ description: itemProps.description.value,
+ body: itemProps.body.value,
+ source: itemProps.source.value,
+ };
+
+ properties.forEach((prop) => {
+ const raw = itemProps[prop.key]?.value;
+ if (raw !== undefined && raw !== null && raw !== "" && raw !== false) {
+ defaults[prop.key] = raw;
+ }
+ });
+
+ const launchContext = { defaults };
+
+ return {
+ name: `Create ${types.find((type) => type.id === selectedTypeId)?.name} in ${spaces.find((space) => space.id === selectedSpaceId)?.name}`,
+ link: url + "?launchContext=" + encodeURIComponent(JSON.stringify(launchContext)),
+ };
+ }
+
+ const textFieldPlaceholderMap: Partial> = {
+ [PropertyFormat.Text]: "Add text",
+ [PropertyFormat.Url]: "Add URL",
+ [PropertyFormat.Email]: "Add email address",
+ [PropertyFormat.Phone]: "Add phone number",
+ };
+
+ return (
+
+ );
+}
diff --git a/extensions/anytype/src/components/CreateForm/CreatePropertyForm.tsx b/extensions/anytype/src/components/CreateForm/CreatePropertyForm.tsx
new file mode 100644
index 00000000000..60e6a6349ea
--- /dev/null
+++ b/extensions/anytype/src/components/CreateForm/CreatePropertyForm.tsx
@@ -0,0 +1,66 @@
+import { Action, ActionPanel, Form, Icon, popToRoot, showToast, Toast } from "@raycast/api";
+import { showFailureToast, useForm } from "@raycast/utils";
+import { createProperty } from "../../api";
+import { PropertyFormat } from "../../models";
+
+export interface CreatePropertyFormValues {
+ name: string;
+ format?: string;
+}
+
+interface CreatePropertyFormProps {
+ spaceId: string;
+ draftValues: CreatePropertyFormValues;
+}
+
+export function CreatePropertyForm({ spaceId, draftValues }: CreatePropertyFormProps) {
+ const { handleSubmit, itemProps } = useForm({
+ initialValues: { ...draftValues, format: draftValues.format as PropertyFormat },
+ onSubmit: async (values) => {
+ try {
+ await showToast({ style: Toast.Style.Animated, title: "Creating property..." });
+
+ await createProperty(spaceId, {
+ name: values.name || "",
+ format: values.format as PropertyFormat,
+ });
+
+ showToast(Toast.Style.Success, "Property created successfully");
+ popToRoot();
+ } catch (error) {
+ await showFailureToast(error, { title: "Failed to create property" });
+ }
+ },
+ validation: {
+ name: (v) => (!v ? "Name is required" : undefined),
+ },
+ });
+
+ const propertyFormatKeys = Object.keys(PropertyFormat) as Array;
+
+ return (
+
+
+ {propertyFormatKeys.map((key) => {
+ const value = PropertyFormat[key];
+ return (
+
+ );
+ })}
+
+
+ );
+}
diff --git a/extensions/anytype/src/components/CreateSpaceForm.tsx b/extensions/anytype/src/components/CreateForm/CreateSpaceForm.tsx
similarity index 82%
rename from extensions/anytype/src/components/CreateSpaceForm.tsx
rename to extensions/anytype/src/components/CreateForm/CreateSpaceForm.tsx
index cfad4636541..f196484f297 100644
--- a/extensions/anytype/src/components/CreateSpaceForm.tsx
+++ b/extensions/anytype/src/components/CreateForm/CreateSpaceForm.tsx
@@ -1,6 +1,6 @@
import { Action, ActionPanel, Form, Icon, popToRoot, showToast, Toast } from "@raycast/api";
-import { useForm } from "@raycast/utils";
-import { createSpace } from "../api";
+import { showFailureToast, useForm } from "@raycast/utils";
+import { createSpace } from "../../api";
export interface CreateSpaceFormValues {
name?: string;
@@ -25,11 +25,7 @@ export function CreateSpaceForm({ draftValues }: CreateSpaceFormProps) {
showToast(Toast.Style.Success, "Space created successfully");
popToRoot();
} catch (error) {
- if (error instanceof Error) {
- showToast(Toast.Style.Failure, "Failed to create space", error.message);
- } else {
- showToast(Toast.Style.Failure, "Failed to create space", "Unknown error");
- }
+ await showFailureToast(error, { title: "Failed to create space" });
}
},
validation: {
diff --git a/extensions/anytype/src/components/CreateForm/CreateTagForm.tsx b/extensions/anytype/src/components/CreateForm/CreateTagForm.tsx
new file mode 100644
index 00000000000..7f9d72064bc
--- /dev/null
+++ b/extensions/anytype/src/components/CreateForm/CreateTagForm.tsx
@@ -0,0 +1,77 @@
+import { Action, ActionPanel, Form, Icon, popToRoot, showToast, Toast } from "@raycast/api";
+import { showFailureToast, useForm } from "@raycast/utils";
+import { createTag } from "../../api";
+import { Color } from "../../models";
+import { colorToHex } from "../../utils";
+
+export interface CreateTagFormValues {
+ name: string;
+ color?: string;
+}
+
+interface CreateTagFormProps {
+ spaceId: string;
+ propertyId: string;
+ draftValues: CreateTagFormValues;
+}
+
+export function CreateTagForm({ spaceId, propertyId, draftValues }: CreateTagFormProps) {
+ const { handleSubmit, itemProps } = useForm({
+ initialValues: { ...draftValues, name: draftValues.name, color: draftValues.color as Color },
+ onSubmit: async (values) => {
+ try {
+ await showToast({ style: Toast.Style.Animated, title: "Creating tag..." });
+
+ await createTag(spaceId, propertyId, {
+ name: values.name || "",
+ color: values.color as Color,
+ });
+
+ showToast(Toast.Style.Success, "Tag created successfully");
+ popToRoot();
+ } catch (error) {
+ await showFailureToast(error, { title: "Failed to create tag" });
+ }
+ },
+ validation: {
+ name: (value) => {
+ if (!value) {
+ return "Name is required";
+ }
+ },
+ color: (value) => {
+ if (!value) {
+ return "Color is required";
+ }
+ },
+ },
+ });
+
+ const tagColorKeys = Object.keys(Color) as Array;
+
+ return (
+
+
+ {tagColorKeys.map((key) => {
+ const value = Color[key];
+ return (
+
+ );
+ })}
+
+
+ );
+}
diff --git a/extensions/anytype/src/components/CreateForm/CreateTypeForm.tsx b/extensions/anytype/src/components/CreateForm/CreateTypeForm.tsx
new file mode 100644
index 00000000000..deea61f4411
--- /dev/null
+++ b/extensions/anytype/src/components/CreateForm/CreateTypeForm.tsx
@@ -0,0 +1,102 @@
+import { Action, ActionPanel, Form, Icon, popToRoot, showToast, Toast } from "@raycast/api";
+import { showFailureToast, useForm } from "@raycast/utils";
+import { useState } from "react";
+import { createType } from "../../api";
+import { useProperties, useSpaces } from "../../hooks";
+import { CreateTypeRequest, IconFormat, PropertyLink, TypeLayout } from "../../models";
+import { isEmoji } from "../../utils";
+
+export interface CreateTypeFormValues {
+ space: string;
+ name: string;
+ plural_name?: string;
+ icon?: string;
+ layout?: TypeLayout;
+ properties?: string[];
+}
+
+export interface CreateTypeFormProps {
+ draftValues: CreateTypeFormValues;
+}
+
+export function CreateTypeForm({ draftValues }: CreateTypeFormProps) {
+ const [loading, setLoading] = useState(false);
+ const [selectedSpace, setSelectedSpace] = useState(draftValues.space);
+
+ const { spaces } = useSpaces();
+ const { properties } = useProperties(selectedSpace);
+
+ const { handleSubmit, itemProps } = useForm({
+ initialValues: draftValues,
+ onSubmit: async (values) => {
+ setLoading(true);
+ try {
+ await showToast({ style: Toast.Style.Animated, title: "Creating type..." });
+
+ const propertyLinks: PropertyLink[] =
+ values.properties?.map((key) => {
+ const prop = properties.find((p) => p.key === key)!;
+ return { key: prop.key, format: prop.format, name: prop.name };
+ }) || [];
+
+ const request: CreateTypeRequest = {
+ name: values.name || "",
+ plural_name: values.plural_name || "",
+ icon: { format: IconFormat.Emoji, emoji: values.icon || "" },
+ Layout: values.layout || TypeLayout.Basic,
+ Properties: propertyLinks,
+ };
+ const response = await createType(selectedSpace, request);
+ if (response.type?.key) {
+ await showToast(Toast.Style.Success, "Type created successfully");
+ popToRoot();
+ } else {
+ await showToast(Toast.Style.Failure, "Failed to create type");
+ }
+ } catch (error) {
+ await showFailureToast(error, { title: "Failed to create type" });
+ } finally {
+ setLoading(false);
+ }
+ },
+ validation: {
+ name: (v) => (!v ? "Name is required" : undefined),
+ plural_name: (v) => (!v ? "Plural name is required" : undefined),
+ icon: (v) => (typeof v === "string" && v && !isEmoji(v) ? "Icon must be a single emoji" : undefined),
+ },
+ });
+
+ const layoutKeys = Object.keys(TypeLayout) as Array;
+
+ return (
+
+ {spaces.map((space) => (
+
+ ))}
+
+
+
+
+
+ {layoutKeys.map((layout) => {
+ const value = TypeLayout[layout];
+ return ;
+ })}
+
+
+ {properties.map((prop) => (
+
+ ))}
+
+
+ );
+}
diff --git a/extensions/anytype/src/components/CreateObjectForm.tsx b/extensions/anytype/src/components/CreateObjectForm.tsx
deleted file mode 100644
index cf44dcb1c28..00000000000
--- a/extensions/anytype/src/components/CreateObjectForm.tsx
+++ /dev/null
@@ -1,270 +0,0 @@
-import { Action, ActionPanel, Form, Icon, popToRoot, showToast, Toast } from "@raycast/api";
-import { useForm } from "@raycast/utils";
-import { useEffect, useState } from "react";
-import { addObjectsToList, createObject } from "../api";
-import { CreateObjectFormValues } from "../create-object";
-import { IconFormat, Space, SpaceObject, Template, Type } from "../models";
-import { fetchTypeKeysForLists } from "../utils";
-
-interface CreateObjectFormProps {
- spaces: Space[];
- types: Type[];
- templates: Template[];
- lists: SpaceObject[];
- selectedSpace: string;
- setSelectedSpace: (spaceId: string) => void;
- selectedType: string;
- setSelectedType: (type: string) => void;
- selectedTemplate: string;
- setSelectedTemplate: (templateId: string) => void;
- selectedList: string;
- setSelectedList: (listId: string) => void;
- listSearchText: string;
- setListSearchText: (searchText: string) => void;
- isLoading: boolean;
- draftValues: CreateObjectFormValues;
- enableDrafts: boolean;
-}
-
-export function CreateObjectForm({
- spaces,
- types,
- templates,
- lists,
- selectedSpace,
- setSelectedSpace,
- selectedType,
- setSelectedType,
- selectedTemplate,
- setSelectedTemplate,
- selectedList,
- setSelectedList,
- listSearchText,
- setListSearchText,
- isLoading,
- draftValues,
- enableDrafts,
-}: CreateObjectFormProps) {
- const [loading, setLoading] = useState(false);
- const [typeKeysForLists, setTypeKeysForLists] = useState([]);
- const hasSelectedSpaceAndType = selectedSpace && selectedType;
- const selectedTypeUniqueKey = types.reduce((acc, type) => (type.id === selectedType ? type.key : acc), "");
-
- useEffect(() => {
- const fetchTypesForLists = async () => {
- if (spaces) {
- const listsTypes = await fetchTypeKeysForLists(spaces);
- setTypeKeysForLists(listsTypes);
- }
- };
- fetchTypesForLists();
- }, [spaces]);
-
- const { handleSubmit, itemProps } = useForm({
- initialValues: draftValues,
- onSubmit: async (values) => {
- setLoading(true);
- try {
- await showToast({ style: Toast.Style.Animated, title: "Creating object..." });
-
- const response = await createObject(selectedSpace, {
- name: values.name || "",
- icon: { format: IconFormat.Emoji, emoji: values.icon || "" },
- description: values.description || "",
- body: values.body || "",
- source: values.source || "",
- template_id: values.template || "",
- type_key: selectedTypeUniqueKey,
- });
-
- if (response.object?.id) {
- if (selectedList) {
- await addObjectsToList(selectedSpace, selectedList, [response.object.id]);
- await showToast(Toast.Style.Success, "Object created and added to collection");
- } else {
- await showToast(Toast.Style.Success, "Object created successfully");
- }
- popToRoot();
- } else {
- await showToast(Toast.Style.Failure, "Failed to create object");
- }
- } catch (error) {
- await showToast(Toast.Style.Failure, "Failed to create object", String(error));
- } finally {
- setLoading(false);
- }
- },
- validation: {
- name: (value) => {
- if (!["ot-bookmark", "ot-note"].includes(selectedTypeUniqueKey) && !value) {
- return "Name is required";
- }
- },
- icon: (value) => {
- if (value && value.length > 2) {
- return "Icon must be a single character";
- }
- },
- source: (value) => {
- if (selectedTypeUniqueKey === "ot-bookmark" && !value) {
- return "Source is required for Bookmarks";
- }
- },
- },
- });
-
- function getQuicklink(): { name: string; link: string } {
- const url = "raycast://extensions/any/anytype/create-object";
-
- const launchContext = {
- defaults: {
- space: selectedSpace,
- type: selectedType,
- list: selectedList,
- name: itemProps.name.value,
- icon: itemProps.icon.value,
- description: itemProps.description.value,
- body: itemProps.body.value,
- source: itemProps.source.value,
- },
- };
-
- return {
- name: `Create ${types.find((type) => type.key === selectedTypeUniqueKey)?.name} in ${spaces.find((space) => space.id === selectedSpace)?.name}`,
- link: url + "?launchContext=" + encodeURIComponent(JSON.stringify(launchContext)),
- };
- }
-
- return (
-
- );
-}
diff --git a/extensions/anytype/src/components/EmptyView/EmptyViewObject.tsx b/extensions/anytype/src/components/EmptyView/EmptyViewObject.tsx
new file mode 100644
index 00000000000..b39fc7b57ec
--- /dev/null
+++ b/extensions/anytype/src/components/EmptyView/EmptyViewObject.tsx
@@ -0,0 +1,37 @@
+import { Action, ActionPanel, Icon, List } from "@raycast/api";
+import { CreateObjectForm } from "..";
+import { CreateObjectFormValues } from "../../create-object";
+
+type EmptyViewObjectProps = {
+ title: string;
+ contextValues: CreateObjectFormValues;
+};
+
+export function EmptyViewObject({ title, contextValues }: EmptyViewObjectProps) {
+ const draftValues: CreateObjectFormValues = {
+ spaceId: contextValues.spaceId,
+ typeId: contextValues.typeId,
+ listId: contextValues.listId,
+ name: contextValues.name,
+ icon: contextValues.icon,
+ description: contextValues.description,
+ body: contextValues.body,
+ source: contextValues.source,
+ };
+
+ return (
+
+ }
+ icon={Icon.Plus}
+ />
+
+ }
+ />
+ );
+}
diff --git a/extensions/anytype/src/components/EmptyView/EmptyViewProperty.tsx b/extensions/anytype/src/components/EmptyView/EmptyViewProperty.tsx
new file mode 100644
index 00000000000..f560ad7bd53
--- /dev/null
+++ b/extensions/anytype/src/components/EmptyView/EmptyViewProperty.tsx
@@ -0,0 +1,26 @@
+import { Action, ActionPanel, Icon, List } from "@raycast/api";
+import { CreatePropertyForm, CreatePropertyFormValues } from "..";
+
+type EmptyViewPropertyProps = {
+ title: string;
+ spaceId: string;
+ contextValues: CreatePropertyFormValues;
+};
+
+export function EmptyViewProperty({ title, spaceId, contextValues }: EmptyViewPropertyProps) {
+ return (
+
+ }
+ icon={Icon.Plus}
+ />
+
+ }
+ />
+ );
+}
diff --git a/extensions/anytype/src/components/EmptyViewSpace.tsx b/extensions/anytype/src/components/EmptyView/EmptyViewSpace.tsx
similarity index 89%
rename from extensions/anytype/src/components/EmptyViewSpace.tsx
rename to extensions/anytype/src/components/EmptyView/EmptyViewSpace.tsx
index 9f0c75fa70b..ffa202f4836 100644
--- a/extensions/anytype/src/components/EmptyViewSpace.tsx
+++ b/extensions/anytype/src/components/EmptyView/EmptyViewSpace.tsx
@@ -1,5 +1,5 @@
import { Action, ActionPanel, Icon, List } from "@raycast/api";
-import { CreateSpaceForm, CreateSpaceFormValues } from ".";
+import { CreateSpaceForm, CreateSpaceFormValues } from "..";
type EmptyViewSpaceProps = {
title: string;
diff --git a/extensions/anytype/src/components/EmptyView/EmptyViewTag.tsx b/extensions/anytype/src/components/EmptyView/EmptyViewTag.tsx
new file mode 100644
index 00000000000..f55f2237ee7
--- /dev/null
+++ b/extensions/anytype/src/components/EmptyView/EmptyViewTag.tsx
@@ -0,0 +1,27 @@
+import { Action, ActionPanel, Icon, List } from "@raycast/api";
+import { CreateTagForm, CreateTagFormValues } from "..";
+
+type EmptyViewTagProps = {
+ title: string;
+ spaceId: string;
+ propertyId: string;
+ contextValues: CreateTagFormValues;
+};
+
+export function EmptyViewTag({ title, spaceId, propertyId, contextValues }: EmptyViewTagProps) {
+ return (
+
+ }
+ icon={Icon.Plus}
+ />
+
+ }
+ />
+ );
+}
diff --git a/extensions/anytype/src/components/EmptyView/EmptyViewType.tsx b/extensions/anytype/src/components/EmptyView/EmptyViewType.tsx
new file mode 100644
index 00000000000..ef77d0b28ec
--- /dev/null
+++ b/extensions/anytype/src/components/EmptyView/EmptyViewType.tsx
@@ -0,0 +1,21 @@
+import { Action, ActionPanel, Icon, List } from "@raycast/api";
+import { CreateTypeForm, CreateTypeFormValues } from "..";
+
+type EmptyViewTypeProps = {
+ title: string;
+ contextValues: CreateTypeFormValues;
+};
+
+export function EmptyViewType({ title, contextValues }: EmptyViewTypeProps) {
+ return (
+
+ } icon={Icon.Plus} />
+
+ }
+ />
+ );
+}
diff --git a/extensions/anytype/src/components/EmptyViewObject.tsx b/extensions/anytype/src/components/EmptyViewObject.tsx
deleted file mode 100644
index bb9ab1931a0..00000000000
--- a/extensions/anytype/src/components/EmptyViewObject.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import { Action, ActionPanel, Icon, List } from "@raycast/api";
-import { useEffect } from "react";
-import { CreateObjectForm } from ".";
-import { CreateObjectFormValues } from "../create-object";
-import { useCreateObjectData } from "../hooks";
-
-type EmptyViewObjectProps = {
- title: string;
- contextValues: CreateObjectFormValues;
-};
-
-export function EmptyViewObject({ title, contextValues }: EmptyViewObjectProps) {
- const draftValues: CreateObjectFormValues = {
- space: contextValues.space,
- type: contextValues.type,
- list: contextValues.list,
- name: contextValues.name,
- icon: contextValues.icon,
- description: contextValues.description,
- body: contextValues.body,
- source: contextValues.source,
- };
-
- const {
- spaces,
- types,
- templates,
- lists,
- selectedSpace,
- setSelectedSpace,
- selectedType,
- setSelectedType,
- selectedTemplate,
- setSelectedTemplate,
- selectedList,
- setSelectedList,
- listSearchText,
- setListSearchText,
- isLoading,
- } = useCreateObjectData(draftValues);
-
- useEffect(() => {
- if (spaces.length > 0 && !selectedSpace) {
- setSelectedSpace(spaces[0].id);
- }
- }, [spaces]);
-
- useEffect(() => {
- if (types.length > 0 && !selectedType) {
- setSelectedType(types[0].id);
- }
- }, [types]);
-
- return (
-
-
- }
- icon={Icon.Plus}
- />
-
- }
- />
- );
-}
diff --git a/extensions/anytype/src/components/EnsureAuthenticated.tsx b/extensions/anytype/src/components/EnsureAuthenticated.tsx
index 9d5de80d077..f0d51a8351d 100644
--- a/extensions/anytype/src/components/EnsureAuthenticated.tsx
+++ b/extensions/anytype/src/components/EnsureAuthenticated.tsx
@@ -11,7 +11,7 @@ import {
showToast,
Toast,
} from "@raycast/api";
-import { useForm } from "@raycast/utils";
+import { showFailureToast, useForm } from "@raycast/utils";
import { useEffect, useState } from "react";
import { checkApiTokenValidity, displayCode, getToken } from "../api";
import { apiAppName, downloadUrl, localStorageKeys } from "../utils";
@@ -31,8 +31,7 @@ export function EnsureAuthenticated({ placeholder, viewType, children }: EnsureA
const { handleSubmit, itemProps } = useForm<{ userCode: string }>({
onSubmit: async (values) => {
if (!challengeId) {
- showToast({
- style: Toast.Style.Failure,
+ await showFailureToast({
title: "Pairing not started",
message: "Start the pairing before submitting the code.",
});
@@ -47,11 +46,7 @@ export function EnsureAuthenticated({ placeholder, viewType, children }: EnsureA
setHasToken(true);
setTokenIsValid(true);
} catch (error) {
- showToast({
- style: Toast.Style.Failure,
- title: "Failed to pair",
- message: String(error),
- });
+ await showFailureToast(error, { title: "Failed to pair" });
} finally {
setIsLoading(false);
}
diff --git a/extensions/anytype/src/components/CollectionList.tsx b/extensions/anytype/src/components/Lists/CollectionList.tsx
similarity index 75%
rename from extensions/anytype/src/components/CollectionList.tsx
rename to extensions/anytype/src/components/Lists/CollectionList.tsx
index 17ee3d6ca53..0c0b5766f39 100644
--- a/extensions/anytype/src/components/CollectionList.tsx
+++ b/extensions/anytype/src/components/Lists/CollectionList.tsx
@@ -1,11 +1,12 @@
-import { Icon, List, showToast, Toast } from "@raycast/api";
+import { Icon, List } from "@raycast/api";
+import { showFailureToast } from "@raycast/utils";
import { useEffect, useState } from "react";
-import { EmptyViewObject, ObjectListItem, ViewType } from ".";
-import { useObjectsInList } from "../hooks";
-import { useListViews } from "../hooks/useListViews";
-import { Space, ViewLayout } from "../models";
-import { pluralize, processObject } from "../utils";
-import { defaultTintColor } from "../utils/constant";
+import { EmptyViewObject, ObjectListItem, ViewType } from "..";
+import { useObjectsInList } from "../../hooks";
+import { useListViews } from "../../hooks/useListViews";
+import { Space, ViewLayout } from "../../models";
+import { isEmoji, pluralize, processObject } from "../../utils";
+import { defaultTintColor } from "../../utils/constant";
type CollectionListProps = {
space: Space;
@@ -25,7 +26,7 @@ export function CollectionList({ space, listId, listName }: CollectionListProps)
useEffect(() => {
if (viewsError || objectsError) {
- showToast(Toast.Style.Failure, "Failed to fetch objects", viewsError?.message || objectsError?.message);
+ showFailureToast(viewsError || objectsError, { title: "Failed to fetch latest data" });
}
}, [viewsError, objectsError]);
@@ -35,8 +36,8 @@ export function CollectionList({ space, listId, listName }: CollectionListProps)
return processObject(object, false, mutateObjects);
});
- const resolveLayoutIcon = (layout: string) => {
- switch (layout.toLowerCase()) {
+ const resolveLayoutIcon = (layout: ViewLayout) => {
+ switch (layout) {
case ViewLayout.Grid:
return { source: "icons/dataview/grid.svg" };
case ViewLayout.List:
@@ -65,7 +66,12 @@ export function CollectionList({ space, listId, listName }: CollectionListProps)
searchBarAccessory={
setViewId(newValue)}>
{views.map((view) => (
-
+
))}
}
@@ -85,12 +91,14 @@ export function CollectionList({ space, listId, listName }: CollectionListProps)
subtitle={object.subtitle}
accessories={object.accessories}
mutate={object.mutate}
+ object={object.object}
mutateViews={mutateViews}
layout={object.layout}
viewType={ViewType.objects}
isGlobalSearch={false}
isNoPinView={true}
isPinned={object.isPinned}
+ searchText={searchText}
/>
))}
@@ -98,8 +106,8 @@ export function CollectionList({ space, listId, listName }: CollectionListProps)
diff --git a/extensions/anytype/src/components/Lists/SpaceList.tsx b/extensions/anytype/src/components/Lists/SpaceList.tsx
new file mode 100644
index 00000000000..7dfd7e114aa
--- /dev/null
+++ b/extensions/anytype/src/components/Lists/SpaceList.tsx
@@ -0,0 +1,135 @@
+import { Icon, List } from "@raycast/api";
+import { showFailureToast } from "@raycast/utils";
+import { useEffect, useMemo, useState } from "react";
+import { getMembers } from "../../api";
+import { EmptyViewSpace, SpaceListItem } from "../../components";
+import { usePinnedSpaces, useSpaces } from "../../hooks";
+import { Space } from "../../models";
+import { defaultTintColor, pluralize } from "../../utils";
+
+type SpacesListProps = {
+ searchPlaceholder: string;
+};
+
+export function SpaceList({ searchPlaceholder }: SpacesListProps) {
+ const { spaces, spacesError, mutateSpaces, isLoadingSpaces, spacesPagination } = useSpaces();
+ const { pinnedSpaces, pinnedSpacesError, isLoadingPinnedSpaces, mutatePinnedSpaces } = usePinnedSpaces();
+ const [searchText, setSearchText] = useState("");
+ const [membersData, setMembersData] = useState<{ [spaceId: string]: number }>({});
+
+ useEffect(() => {
+ if (!spaces) return;
+
+ const fetchMembersData = async () => {
+ const newData: { [key: string]: number } = {};
+ const spaceIdsToFetch = spaces.map((space) => space.id).filter((id) => !(id in membersData));
+
+ try {
+ await Promise.all(
+ spaceIdsToFetch.map(async (id) => {
+ const response = await getMembers(id, { offset: 0, limit: 1 });
+ newData[id] = response.pagination.total;
+ }),
+ );
+ setMembersData((prev) => ({ ...prev, ...newData }));
+ } catch (error) {
+ await showFailureToast(error, { title: "Failed to fetch members" });
+ }
+ };
+
+ fetchMembersData();
+ }, [spaces]);
+
+ const isLoadingMembers = useMemo(() => {
+ if (!spaces) return true;
+ return Object.keys(membersData).length !== spaces.length;
+ }, [spaces, membersData]);
+
+ useEffect(() => {
+ if (spacesError) {
+ showFailureToast(spacesError, { title: "Failed to fetch spaces" });
+ }
+ }, [spacesError]);
+
+ useEffect(() => {
+ if (pinnedSpacesError) {
+ showFailureToast(pinnedSpacesError, { title: "Failed to fetch pinned spaces" });
+ }
+ }, [pinnedSpacesError]);
+
+ const filteredSpaces = spaces?.filter((space) => space.name.toLowerCase().includes(searchText.toLowerCase()));
+ const pinnedFiltered = pinnedSpaces
+ ?.map((pin) => filteredSpaces.find((space) => space.id === pin.id))
+ .filter(Boolean) as Space[];
+ const regularFiltered = filteredSpaces?.filter((space) => !pinnedFiltered?.includes(space));
+
+ return (
+
+ {pinnedFiltered.length > 0 && (
+
+ {pinnedFiltered.map((space) => {
+ const memberCount = membersData[space.id] || 0;
+ return (
+
+ );
+ })}
+
+ )}
+ {regularFiltered.length > 0 ? (
+
+ {regularFiltered.map((space) => {
+ const memberCount = membersData[space.id] || 0;
+ return (
+
+ );
+ })}
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/extensions/anytype/src/components/Lists/TagList.tsx b/extensions/anytype/src/components/Lists/TagList.tsx
new file mode 100644
index 00000000000..d9bfb140004
--- /dev/null
+++ b/extensions/anytype/src/components/Lists/TagList.tsx
@@ -0,0 +1,101 @@
+import { Action, ActionPanel, Icon, Keyboard, List, showToast, Toast } from "@raycast/api";
+import { showFailureToast } from "@raycast/utils";
+import { useEffect, useState } from "react";
+import { useTags } from "../../hooks/useTags";
+import { Space } from "../../models";
+import { hexToColor } from "../../utils";
+import { CreateTagForm } from "../CreateForm/CreateTagForm";
+import { EmptyViewTag } from "../EmptyView/EmptyViewTag";
+import { UpdateTagForm } from "../UpdateForm/UpdateTagForm";
+
+interface TagListProps {
+ space: Space;
+ propertyId: string;
+}
+
+export function TagList({ space, propertyId }: TagListProps) {
+ const [searchText, setSearchText] = useState("");
+ const { tags, isLoadingTags, tagsError, mutateTags, tagsPagination } = useTags(space.id, propertyId);
+
+ useEffect(() => {
+ if (tagsError) {
+ showFailureToast(tagsError, { title: "Failed to fetch tags" });
+ }
+ }, [tagsError]);
+
+ const handleRefresh = async () => {
+ await showToast({
+ style: Toast.Style.Animated,
+ title: "Refreshing tags...",
+ });
+ try {
+ await mutateTags();
+ await showToast({
+ style: Toast.Style.Success,
+ title: "Tags refreshed",
+ });
+ } catch (error) {
+ await showFailureToast(error, { title: "Failed to refresh tags" });
+ }
+ };
+
+ const filteredTags = tags?.filter((tag) => tag.name.toLowerCase().includes(searchText.toLowerCase()));
+
+ return (
+
+ {filteredTags && filteredTags.length > 0 ? (
+ filteredTags.map((tag) => (
+
+
+ }
+ />
+
+
+ }
+ />
+
+
+
+ }
+ />
+ ))
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/extensions/anytype/src/components/TemplateList.tsx b/extensions/anytype/src/components/Lists/TemplateList.tsx
similarity index 81%
rename from extensions/anytype/src/components/TemplateList.tsx
rename to extensions/anytype/src/components/Lists/TemplateList.tsx
index b62872ce11f..4ca4d051c06 100644
--- a/extensions/anytype/src/components/TemplateList.tsx
+++ b/extensions/anytype/src/components/Lists/TemplateList.tsx
@@ -1,9 +1,10 @@
-import { List, showToast, Toast } from "@raycast/api";
+import { List } from "@raycast/api";
+import { showFailureToast } from "@raycast/utils";
import { useEffect, useState } from "react";
-import { EmptyViewObject, ObjectActions, ObjectListItem, ViewType } from ".";
-import { useSearch, useTemplates } from "../hooks";
-import { Space, Template } from "../models";
-import { pluralize, processObject } from "../utils";
+import { EmptyViewObject, ObjectActions, ObjectListItem, ViewType } from "..";
+import { useSearch, useTemplates } from "../../hooks";
+import { Space, SpaceObject } from "../../models";
+import { pluralize, processObject } from "../../utils";
type TemplatesListProps = {
space: Space;
@@ -26,17 +27,17 @@ export function TemplateList({ space, typeId, isGlobalSearch, isPinned }: Templa
useEffect(() => {
if (templatesError) {
- showToast(Toast.Style.Failure, "Failed to fetch templates", templatesError.message);
+ showFailureToast(templatesError, { title: "Failed to fetch templates" });
}
}, [templatesError]);
useEffect(() => {
if (objectsError) {
- showToast(Toast.Style.Failure, "Failed to fetch objects", objectsError.message);
+ showFailureToast(objectsError, { title: "Failed to fetch objects" });
}
}, [objectsError]);
- const filteredTemplates = templates?.filter((template: Template) =>
+ const filteredTemplates = templates?.filter((template: SpaceObject) =>
template.name.toLowerCase().includes(searchText.toLowerCase()),
);
@@ -60,7 +61,7 @@ export function TemplateList({ space, typeId, isGlobalSearch, isPinned }: Templa
title={searchText ? "Search Results" : "Templates"}
subtitle={`${pluralize(filteredTemplates.length, "template", { withNumber: true })}`}
>
- {filteredTemplates.map((template: Template) => (
+ {filteredTemplates.map((template: SpaceObject) => (
}
/>
@@ -98,11 +100,13 @@ export function TemplateList({ space, typeId, isGlobalSearch, isPinned }: Templa
subtitle={object.subtitle}
accessories={object.accessories}
mutate={object.mutate}
+ object={object.object}
layout={object.layout}
viewType={ViewType.objects}
isGlobalSearch={isGlobalSearch}
isNoPinView={true}
isPinned={object.isPinned}
+ searchText={searchText}
/>
))}
@@ -112,8 +116,8 @@ export function TemplateList({ space, typeId, isGlobalSearch, isPinned }: Templa
diff --git a/extensions/anytype/src/components/ObjectDetail.tsx b/extensions/anytype/src/components/ObjectDetail.tsx
index f0cda0296a1..576e8462011 100644
--- a/extensions/anytype/src/components/ObjectDetail.tsx
+++ b/extensions/anytype/src/components/ObjectDetail.tsx
@@ -1,16 +1,28 @@
-import { Color, Detail, getPreferenceValues, showToast, Toast, useNavigation } from "@raycast/api";
+import { Color, Detail, getPreferenceValues, useNavigation } from "@raycast/api";
+import { MutatePromise, showFailureToast } from "@raycast/utils";
import { format } from "date-fns";
import { useEffect, useState } from "react";
import { ObjectActions, TemplateList, ViewType } from ".";
-import { useExport, useObject } from "../hooks";
-import { ExportFormat, Property, Space } from "../models";
-import { injectEmojiIntoHeading } from "../utils";
+import { useObject } from "../hooks";
+import {
+ BodyFormat,
+ Member,
+ ObjectLayout,
+ Property,
+ PropertyFormat,
+ PropertyWithValue,
+ Space,
+ SpaceObject,
+ Type,
+} from "../models";
+import { bundledPropKeys, injectEmojiIntoHeading } from "../utils";
type ObjectDetailProps = {
space: Space;
objectId: string;
title: string;
- layout: string;
+ mutate?: MutatePromise[];
+ layout: ObjectLayout | undefined;
viewType: ViewType;
isGlobalSearch: boolean;
isPinned: boolean;
@@ -20,6 +32,7 @@ export function ObjectDetail({
space,
objectId,
title,
+ mutate,
layout,
viewType,
isGlobalSearch,
@@ -27,30 +40,25 @@ export function ObjectDetail({
}: ObjectDetailProps) {
const { push } = useNavigation();
const { linkDisplay } = getPreferenceValues();
- const { object, objectError, isLoadingObject, mutateObject } = useObject(space.id, objectId);
- const { objectExport, objectExportError, isLoadingObjectExport, mutateObjectExport } = useExport(
- space.id,
- objectId,
- ExportFormat.Markdown,
- );
+ const { object, objectError, isLoadingObject, mutateObject } = useObject(space.id, objectId, BodyFormat.Markdown);
const [showDetails, setShowDetails] = useState(true);
const properties = object?.properties || [];
- const excludedPropertyIds = new Set(["added_date", "last_opened_date", "last_modified_date", "last_modified_by"]);
- const additionalProperties = properties.filter((property) => !excludedPropertyIds.has(property.id));
+ const excludedPropertyKeys = new Set([
+ bundledPropKeys.addedDate,
+ bundledPropKeys.lastModifiedDate,
+ bundledPropKeys.lastOpenedDate,
+ bundledPropKeys.lastModifiedBy,
+ bundledPropKeys.links,
+ ]);
+ const additionalProperties = properties.filter((property) => !excludedPropertyKeys.has(property.key));
useEffect(() => {
if (objectError) {
- showToast(Toast.Style.Failure, "Failed to fetch object", objectError.message);
+ showFailureToast(objectError, { title: "Failed to fetch object" });
}
}, [objectError]);
- useEffect(() => {
- if (objectExportError) {
- showToast(Toast.Style.Failure, "Failed to fetch object as markdown", objectExportError.message);
- }
- }, [objectExportError]);
-
const formatOrder: { [key: string]: number } = {
text: 0,
number: 1,
@@ -76,38 +84,45 @@ export function ObjectDetail({
}
// For properties in the 'text' group, ensure that 'description' comes first
- if (aGroup === "text" && bGroup === "text") {
- if (a.id === "description" && b.id !== "description") return -1;
- if (b.id === "description" && a.id !== "description") return 1;
+ if (aGroup === PropertyFormat.Text && bGroup === PropertyFormat.Text) {
+ if (a.key === bundledPropKeys.description && b.key !== bundledPropKeys.description) return -1;
+ if (b.key === bundledPropKeys.description && a.key !== bundledPropKeys.description) return 1;
}
return a.name.localeCompare(b.name);
});
- function renderDetailMetadata(property: Property) {
- const titleText = property.name || property.id.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
+ function renderDetailMetadata(property: PropertyWithValue) {
+ const titleText = property.name || property.key.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
- if (property.format === "text") {
+ if (property.format === PropertyFormat.Text) {
return (
);
}
- if (property.format === "number") {
+ if (property.format === PropertyFormat.Number) {
return (
+
);
} else {
return (
0) {
return (
-
+
{tags.map((tag) => (
))}
@@ -151,19 +166,19 @@ export function ObjectDetail({
} else {
return (
);
}
}
- if (property.format === "date") {
+ if (property.format === PropertyFormat.Date) {
return (
0) {
return (
-
+
{files.map((file) => (
))}
@@ -187,34 +202,34 @@ export function ObjectDetail({
} else {
return (
);
}
}
- if (property.format === "checkbox") {
+ if (property.format === PropertyFormat.Checkbox) {
return (
);
}
- if (property.format === "url") {
+ if (property.format === PropertyFormat.Url) {
if (property.url) {
if (linkDisplay === "text") {
return (
0) {
+ if (property.format === PropertyFormat.Objects) {
+ if (Array.isArray(property.objects) && property.objects.length > 0) {
return (
-
- {property.object.map((objectItem, index) => {
+
+ {property.objects.map((objectItem, index) => {
const handleAction = () => {
push(
);
})}
);
+ } else {
+ return (
+
+ );
}
}
return null;
@@ -331,7 +355,7 @@ export function ObjectDetail({
const rendered = renderDetailMetadata(property);
if (rendered) {
if (previousGroup !== null && currentGroup !== previousGroup) {
- renderedDetailComponents.push();
+ renderedDetailComponents.push();
}
renderedDetailComponents.push(rendered);
previousGroup = currentGroup;
@@ -359,7 +383,7 @@ export function ObjectDetail({
);
- const descIndex = renderedDetailComponents.findIndex((el) => el.key === "description");
+ const descIndex = renderedDetailComponents.findIndex((el) => el.key === bundledPropKeys.description);
if (descIndex >= 0) {
renderedDetailComponents.splice(descIndex + 1, 0, typeTag);
} else {
@@ -367,13 +391,13 @@ export function ObjectDetail({
}
}
- const markdown = objectExport?.markdown ?? "";
+ const markdown = object?.markdown ?? "";
const updatedMarkdown = injectEmojiIntoHeading(markdown, object?.icon);
return (
0 ? (
@@ -385,14 +409,15 @@ export function ObjectDetail({
space={space}
objectId={objectId}
title={title}
+ mutate={mutate}
mutateObject={mutateObject}
- mutateExport={mutateObjectExport}
- objectExport={objectExport}
layout={layout}
+ object={object}
viewType={viewType}
isGlobalSearch={isGlobalSearch}
isNoPinView={false}
isPinned={isPinned}
+ isDetailView={true}
showDetails={showDetails}
onToggleDetails={() => setShowDetails((prev) => !prev)}
/>
diff --git a/extensions/anytype/src/components/ObjectList.tsx b/extensions/anytype/src/components/ObjectList.tsx
index c8c8318a039..bd230ad4c29 100644
--- a/extensions/anytype/src/components/ObjectList.tsx
+++ b/extensions/anytype/src/components/ObjectList.tsx
@@ -1,10 +1,27 @@
-import { Icon, List, showToast, Toast } from "@raycast/api";
-import { MutatePromise } from "@raycast/utils";
+import { Icon, List } from "@raycast/api";
+import { MutatePromise, showFailureToast } from "@raycast/utils";
import { useEffect, useState } from "react";
-import { EmptyViewObject, ObjectListItem } from ".";
-import { useMembers, usePinnedMembers, usePinnedObjects, usePinnedTypes, useSearch, useTypes } from "../hooks";
-import { Member, MemberStatus, Space, SpaceObject, Type } from "../models";
-import { defaultTintColor, formatMemberRole, localStorageKeys, pluralize, processObject } from "../utils";
+import { EmptyViewObject, EmptyViewProperty, EmptyViewType, ObjectListItem } from ".";
+import {
+ useMembers,
+ usePinnedMembers,
+ usePinnedObjects,
+ usePinnedProperties,
+ usePinnedTypes,
+ useProperties,
+ useSearch,
+ useTypes,
+} from "../hooks";
+import { Member, MemberStatus, Property, Space, SpaceObject, Type } from "../models";
+import {
+ defaultTintColor,
+ formatMemberRole,
+ isUserProperty,
+ isUserType,
+ localStorageKeys,
+ pluralize,
+ processObject,
+} from "../utils";
type ObjectListProps = {
space: Space;
@@ -14,6 +31,8 @@ export enum ViewType {
// browse
objects = "Object", // is "all" view in global search
types = "Type",
+ properties = "Property",
+ tags = "Tag",
members = "Member",
templates = "Template",
@@ -34,6 +53,9 @@ export function ObjectList({ space }: ObjectListProps) {
[],
);
const { types, typesError, isLoadingTypes, mutateTypes, typesPagination } = useTypes(space.id);
+ const { properties, propertiesError, isLoadingProperties, mutateProperties, propertiesPagination } = useProperties(
+ space.id,
+ );
const { members, membersError, isLoadingMembers, mutateMembers, membersPagination } = useMembers(space.id);
const { pinnedObjects, pinnedObjectsError, isLoadingPinnedObjects, mutatePinnedObjects } = usePinnedObjects(
localStorageKeys.suffixForViewsPerSpace(space.id, ViewType.objects),
@@ -41,6 +63,8 @@ export function ObjectList({ space }: ObjectListProps) {
const { pinnedTypes, pinnedTypesError, isLoadingPinnedTypes, mutatePinnedTypes } = usePinnedTypes(
localStorageKeys.suffixForViewsPerSpace(space.id, ViewType.types),
);
+ const { pinnedProperties, pinnedPropertiesError, isLoadingPinnedProperties, mutatePinnedProperties } =
+ usePinnedProperties(localStorageKeys.suffixForViewsPerSpace(space.id, ViewType.properties));
const { pinnedMembers, pinnedMembersError, isLoadingPinnedMembers, mutatePinnedMembers } = usePinnedMembers(
localStorageKeys.suffixForViewsPerSpace(space.id, ViewType.members),
);
@@ -50,28 +74,25 @@ export function ObjectList({ space }: ObjectListProps) {
const paginationMap: Partial> = {
[ViewType.objects]: objectsPagination,
[ViewType.types]: typesPagination,
+ [ViewType.properties]: propertiesPagination,
[ViewType.members]: membersPagination,
};
setPagination(paginationMap[currentView]);
- }, [currentView, objects, types, members]);
+ }, [currentView, objects, types, properties, members]);
useEffect(() => {
- if (objectsError || typesError || membersError) {
- showToast(
- Toast.Style.Failure,
- "Failed to fetch latest data",
- objectsError?.message || typesError?.message || membersError?.message,
- );
+ if (objectsError || typesError || propertiesError || membersError) {
+ showFailureToast(objectsError || typesError || propertiesError || membersError, {
+ title: "Failed to fetch latest data",
+ });
}
}, [objectsError, typesError, membersError]);
useEffect(() => {
- if (pinnedObjectsError || pinnedTypesError || pinnedMembersError) {
- showToast(
- Toast.Style.Failure,
- "Failed to fetch pinned data",
- pinnedObjectsError?.message || pinnedTypesError?.message || pinnedMembersError?.message,
- );
+ if (pinnedObjectsError || pinnedTypesError || pinnedPropertiesError || pinnedMembersError) {
+ showFailureToast(pinnedObjectsError || pinnedTypesError || pinnedPropertiesError || pinnedMembersError, {
+ title: "Failed to fetch pinned data",
+ });
}
}, [pinnedObjectsError, pinnedTypesError, pinnedMembersError]);
@@ -86,10 +107,34 @@ export function ObjectList({ space }: ObjectListProps) {
icon: type.icon,
title: type.name,
subtitle: { value: "", tooltip: "" },
- accessories: [isPinned ? { icon: Icon.Star, tooltip: "Pinned" } : {}],
- mutate: [mutateTypes, mutatePinnedTypes as MutatePromise],
- member: undefined,
- layout: "",
+ accessories: [
+ ...(isPinned ? [{ icon: Icon.Star, tooltip: "Pinned" }] : []),
+ ...(!isUserType(type.key) ? [{ icon: Icon.Lock, tooltip: "System" }] : []),
+ ],
+ mutate: [mutateTypes, mutatePinnedTypes as MutatePromise],
+ object: type,
+ layout: type.layout,
+ isPinned,
+ };
+ };
+
+ const processProperty = (property: Property, isPinned: boolean) => {
+ return {
+ spaceId: space.id,
+ id: property.id,
+ icon: property.icon,
+ title: property.name,
+ subtitle: { value: "", tooltip: "" },
+ accessories: [
+ ...(isPinned ? [{ icon: Icon.Star, tooltip: "Pinned" }] : []),
+ ...(!isUserProperty(property.key) ? [{ icon: Icon.Lock, tooltip: "System" }] : []),
+ ],
+ mutate: [
+ mutateProperties,
+ mutatePinnedProperties as MutatePromise,
+ ],
+ object: property,
+ layout: undefined,
isPinned,
};
};
@@ -100,7 +145,7 @@ export function ObjectList({ space }: ObjectListProps) {
id: member.id,
icon: member.icon,
title: member.name,
- subtitle: { value: member.global_name, tooltip: `Global Name: ${member.global_name}` },
+ subtitle: { value: member.global_name, tooltip: `ANY Name: ${member.global_name}` },
accessories: [
...(isPinned ? [{ icon: Icon.Star, tooltip: "Pinned" }] : []),
member.status === MemberStatus.Joining
@@ -112,9 +157,9 @@ export function ObjectList({ space }: ObjectListProps) {
tooltip: `Role: ${formatMemberRole(member.role)}`,
},
],
- mutate: [mutateMembers, mutatePinnedMembers as MutatePromise],
- member: member,
- layout: "",
+ mutate: [mutateMembers, mutatePinnedMembers as MutatePromise],
+ object: member,
+ layout: undefined,
isPinned,
};
};
@@ -153,6 +198,20 @@ export function ObjectList({ space }: ObjectListProps) {
return { processedPinned, processedRegular };
}
+ case ViewType.properties: {
+ const processedPinned = pinnedProperties?.length
+ ? pinnedProperties
+ .filter((property) => filterItems([property], searchText).length > 0)
+ .map((property) => processProperty(property, true))
+ : [];
+ const processedRegular = properties
+ .filter((property) => !pinnedProperties?.some((pinned) => pinned.id === property.id))
+ .filter((property) => filterItems([property], searchText).length > 0)
+ .map((property) => processProperty(property, false));
+
+ return { processedPinned, processedRegular };
+ }
+
case ViewType.members: {
const processedPinned = pinnedMembers?.length
? pinnedMembers
@@ -180,16 +239,18 @@ export function ObjectList({ space }: ObjectListProps) {
const isLoading =
isLoadingObjects ||
isLoadingTypes ||
+ isLoadingProperties ||
isLoadingMembers ||
isLoadingPinnedObjects ||
isLoadingPinnedTypes ||
+ isLoadingPinnedProperties ||
isLoadingPinnedMembers;
return (
+
}
@@ -232,12 +298,13 @@ export function ObjectList({ space }: ObjectListProps) {
subtitle={item.subtitle}
accessories={item.accessories}
mutate={item.mutate}
- member={item.member}
+ object={item.object}
layout={item.layout}
viewType={currentView}
isGlobalSearch={false}
isNoPinView={false}
isPinned={item.isPinned}
+ searchText={searchText}
/>
))}
@@ -257,23 +324,51 @@ export function ObjectList({ space }: ObjectListProps) {
subtitle={item.subtitle}
accessories={item.accessories}
mutate={item.mutate}
- member={item.member}
+ object={item.object}
layout={item.layout}
viewType={currentView}
isGlobalSearch={false}
isNoPinView={false}
isPinned={item.isPinned}
+ searchText={searchText}
/>
))}
) : (
-
+ (() => {
+ switch (currentView) {
+ case ViewType.types:
+ return (
+
+ );
+ case ViewType.properties:
+ return (
+
+ );
+ default:
+ return (
+
+ );
+ }
+ })()
)}
);
diff --git a/extensions/anytype/src/components/ObjectListItem.tsx b/extensions/anytype/src/components/ObjectListItem.tsx
index c502021ec7e..7e07e1e1568 100644
--- a/extensions/anytype/src/components/ObjectListItem.tsx
+++ b/extensions/anytype/src/components/ObjectListItem.tsx
@@ -1,7 +1,7 @@
import { Image, List } from "@raycast/api";
import { MutatePromise } from "@raycast/utils";
import { ObjectActions, ViewType } from ".";
-import { Member, Space, SpaceObject, Type, View } from "../models";
+import { Member, ObjectLayout, Property, Space, SpaceObject, Type, View } from "../models";
type ObjectListItemProps = {
space: Space;
@@ -16,14 +16,15 @@ type ObjectListItemProps = {
tooltip?: string;
tag?: { value: string; color: string; tooltip: string };
}[];
- mutate: MutatePromise[];
+ mutate: MutatePromise[];
mutateViews?: MutatePromise;
- member?: Member | undefined;
- layout: string;
+ object: SpaceObject | Type | Property | Member;
+ layout: ObjectLayout | undefined;
viewType: ViewType;
isGlobalSearch: boolean;
isNoPinView: boolean;
isPinned: boolean;
+ searchText: string;
};
export function ObjectListItem({
@@ -35,12 +36,13 @@ export function ObjectListItem({
accessories,
mutate,
mutateViews,
- member,
+ object,
layout,
viewType,
isGlobalSearch,
isNoPinView,
isPinned,
+ searchText,
}: ObjectListItemProps) {
return (
}
/>
diff --git a/extensions/anytype/src/components/SpaceListItem.tsx b/extensions/anytype/src/components/SpaceListItem.tsx
index 4c8be657c32..cb6069a74f7 100644
--- a/extensions/anytype/src/components/SpaceListItem.tsx
+++ b/extensions/anytype/src/components/SpaceListItem.tsx
@@ -14,9 +14,10 @@ type SpaceListItemProps = {
}[];
mutate: MutatePromise[];
isPinned: boolean;
+ searchText: string;
};
-export function SpaceListItem({ space, icon, accessories, mutate, isPinned }: SpaceListItemProps) {
+export function SpaceListItem({ space, icon, accessories, mutate, isPinned, searchText }: SpaceListItemProps) {
return (
}
+ actions={}
/>
);
}
diff --git a/extensions/anytype/src/components/UpdateForm/UpdateObjectForm.tsx b/extensions/anytype/src/components/UpdateForm/UpdateObjectForm.tsx
new file mode 100644
index 00000000000..ae793104b9d
--- /dev/null
+++ b/extensions/anytype/src/components/UpdateForm/UpdateObjectForm.tsx
@@ -0,0 +1,374 @@
+import { Action, ActionPanel, Form, Icon, showToast, Toast, useNavigation } from "@raycast/api";
+import { MutatePromise, showFailureToast, useForm } from "@raycast/utils";
+import { formatRFC3339 } from "date-fns";
+import { useEffect, useMemo, useState } from "react";
+import { updateObject } from "../../api";
+import { useSearch, useTagsMap } from "../../hooks";
+import {
+ IconFormat,
+ ObjectIcon,
+ PropertyFieldValue,
+ PropertyFormat,
+ PropertyLinkWithValue,
+ RawSpaceObjectWithBody,
+ SpaceObject,
+ SpaceObjectWithBody,
+ UpdateObjectRequest,
+} from "../../models";
+import { bundledPropKeys, bundledTypeKeys, defaultTintColor, getNumberFieldValidations, isEmoji } from "../../utils";
+
+interface UpdateObjectFormValues {
+ name?: string;
+ icon?: string;
+ description?: string;
+ [key: string]: PropertyFieldValue;
+}
+
+interface UpdateObjectFormProps {
+ spaceId: string;
+ object: RawSpaceObjectWithBody;
+ mutateObjects: MutatePromise[];
+ mutateObject?: MutatePromise;
+}
+
+export function UpdateObjectForm({ spaceId, object, mutateObjects, mutateObject }: UpdateObjectFormProps) {
+ const { pop } = useNavigation();
+ const [objectSearchText, setObjectSearchText] = useState("");
+
+ const properties = object.type.properties.filter((p) => !Object.values(bundledPropKeys).includes(p.key));
+ const numberFieldValidations = useMemo(() => getNumberFieldValidations(properties), [properties]);
+
+ const { objects, objectsError, isLoadingObjects } = useSearch(spaceId, objectSearchText, []);
+ const { tagsMap, tagsError, isLoadingTags } = useTagsMap(
+ spaceId,
+ properties
+ .filter((prop) => prop.format === PropertyFormat.Select || prop.format === PropertyFormat.MultiSelect)
+ .map((prop) => prop.id),
+ );
+
+ useEffect(() => {
+ if (objectsError || tagsError) {
+ showFailureToast(objectsError || tagsError, { title: "Failed to load data" });
+ }
+ }, [objectsError, tagsError]);
+
+ // Map existing property entries to form field values
+ const initialPropertyValues: Record = properties.reduce(
+ (acc, prop) => {
+ const entry = object.properties.find((p) => p.key === prop.key);
+ if (entry) {
+ let v: PropertyFieldValue;
+ switch (prop.format) {
+ case PropertyFormat.Text:
+ v = entry.text ?? "";
+ break;
+ case PropertyFormat.Select:
+ v = entry.select?.id ?? "";
+ break;
+ case PropertyFormat.Url:
+ v = entry.url ?? "";
+ break;
+ case PropertyFormat.Email:
+ v = entry.email ?? "";
+ break;
+ case PropertyFormat.Phone:
+ v = entry.phone ?? "";
+ break;
+ case PropertyFormat.Number:
+ v = entry.number ?? "";
+ break;
+ case PropertyFormat.MultiSelect:
+ v = entry.multi_select?.map((tag) => tag.id) ?? [];
+ break;
+ case PropertyFormat.Date:
+ v = entry.date ? new Date(entry.date) : undefined;
+ break;
+ case PropertyFormat.Checkbox:
+ v = entry.checkbox ?? false;
+ break;
+ case PropertyFormat.Files:
+ v = entry.files ?? [];
+ break;
+ case PropertyFormat.Objects:
+ v = entry.objects ?? [];
+ break;
+ default:
+ v = undefined;
+ }
+ acc[prop.key] = v;
+ }
+ return acc;
+ },
+ {} as Record,
+ );
+
+ const descriptionEntry = object.properties.find((p) => p.key === bundledPropKeys.description);
+ const initialIconValue = object.icon.format === IconFormat.Emoji ? (object.icon.emoji ?? "") : "";
+
+ const initialValues: UpdateObjectFormValues = {
+ name: object.name,
+ icon: initialIconValue,
+ description: descriptionEntry?.text ?? "",
+ ...initialPropertyValues,
+ };
+
+ const { handleSubmit, itemProps } = useForm({
+ initialValues: initialValues,
+ onSubmit: async (values) => {
+ try {
+ await showToast({ style: Toast.Style.Animated, title: "Updating object…" });
+
+ const propertiesEntries: PropertyLinkWithValue[] = [];
+ properties.forEach((prop) => {
+ const raw = itemProps[prop.key]?.value;
+ const entry: PropertyLinkWithValue = { key: prop.key, format: prop.format };
+ switch (prop.format) {
+ case PropertyFormat.Text:
+ entry.text = String(raw);
+ break;
+ case PropertyFormat.Select:
+ entry.select = raw != null && raw !== "" ? String(raw) : null;
+ break;
+ case PropertyFormat.Url:
+ entry.url = String(raw);
+ break;
+ case PropertyFormat.Email:
+ entry.email = String(raw);
+ break;
+ case PropertyFormat.Phone:
+ entry.phone = String(raw);
+ break;
+ case PropertyFormat.Number:
+ entry.number = raw != null && raw !== "" ? Number(raw) : null;
+ break;
+ case PropertyFormat.MultiSelect:
+ entry.multi_select = Array.isArray(raw) ? (raw as string[]) : [];
+ break;
+ case PropertyFormat.Date:
+ if (raw instanceof Date) {
+ entry.date = formatRFC3339(raw);
+ } else {
+ entry.date = null;
+ }
+ break;
+ case PropertyFormat.Checkbox:
+ entry.checkbox = Boolean(raw);
+ break;
+ case PropertyFormat.Files:
+ entry.files = Array.isArray(raw) ? raw : typeof raw === "string" && raw ? [raw] : [];
+ break;
+ case PropertyFormat.Objects:
+ entry.objects = Array.isArray(raw) ? raw : typeof raw === "string" && raw ? [raw] : [];
+ break;
+ default:
+ console.warn(`Unsupported property format: ${prop.format}`);
+ break;
+ }
+ propertiesEntries.push(entry);
+ });
+
+ const descriptionRaw = itemProps[bundledPropKeys.description]?.value;
+ if (descriptionRaw !== undefined && descriptionRaw !== null) {
+ propertiesEntries.push({
+ key: bundledPropKeys.description,
+ format: PropertyFormat.Text,
+ text: String(descriptionRaw),
+ });
+ }
+
+ const iconField = values.icon as string;
+ let iconPayload: ObjectIcon | undefined;
+ if (iconField !== initialIconValue) {
+ iconPayload = { format: IconFormat.Emoji, emoji: iconField };
+ }
+
+ const payload: UpdateObjectRequest = {
+ name: values.name || "",
+ ...(iconPayload !== undefined && { icon: iconPayload }),
+ properties: propertiesEntries,
+ };
+
+ await updateObject(spaceId, object.id, payload);
+
+ await showToast(Toast.Style.Success, "Object updated");
+ mutateObjects.forEach((mutate) => mutate());
+ if (mutateObject) {
+ mutateObject();
+ }
+ pop();
+ } catch (error) {
+ await showFailureToast(error, { title: "Failed to update object" });
+ }
+ },
+ validation: {
+ name: (v: PropertyFieldValue) => {
+ const s = typeof v === "string" ? v.trim() : "";
+ if (![bundledTypeKeys.bookmark, bundledTypeKeys.note].includes(object.type.key) && !s) {
+ return "Name is required";
+ }
+ },
+ icon: (v: PropertyFieldValue) => {
+ if (typeof v === "string" && v && !isEmoji(v)) {
+ return "Icon must be a single emoji";
+ }
+ },
+ ...numberFieldValidations,
+ },
+ });
+
+ return (
+
+ )}
+ {![bundledTypeKeys.task, bundledTypeKeys.note, bundledTypeKeys.profile].includes(object.type.key) && (
+
+ )}
+
+
+
+
+ {properties.map((prop) => {
+ const tags = (tagsMap && tagsMap[prop.id]) ?? [];
+ const id = prop.key;
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { value, defaultValue, ...restItemProps } = itemProps[id];
+
+ switch (prop.format) {
+ case PropertyFormat.Text:
+ case PropertyFormat.Url:
+ case PropertyFormat.Email:
+ case PropertyFormat.Phone:
+ return (
+
+ );
+ case PropertyFormat.Number:
+ return (
+
+ );
+ case PropertyFormat.Select:
+ return (
+
+
+ {tags.map((tag) => (
+
+ ))}
+
+ );
+ case PropertyFormat.MultiSelect:
+ return (
+
+ {tags.map((tag) => (
+
+ ))}
+
+ );
+ case PropertyFormat.Date:
+ return (
+
+ );
+ case PropertyFormat.Files:
+ // TODO: implement file picker
+ return null;
+ case PropertyFormat.Checkbox:
+ return (
+
+ );
+ case PropertyFormat.Objects:
+ return (
+
+ {!objectSearchText && (
+
+ )}
+ {objects
+ .filter((candidate) => candidate.id !== object.id)
+ .map((object) => (
+
+ ))}
+
+ );
+
+ default:
+ return null;
+ }
+ })}
+
+ );
+}
diff --git a/extensions/anytype/src/components/UpdateForm/UpdatePropertyForm.tsx b/extensions/anytype/src/components/UpdateForm/UpdatePropertyForm.tsx
new file mode 100644
index 00000000000..931b5a7bf00
--- /dev/null
+++ b/extensions/anytype/src/components/UpdateForm/UpdatePropertyForm.tsx
@@ -0,0 +1,75 @@
+import { Action, ActionPanel, Form, Icon, showToast, Toast, useNavigation } from "@raycast/api";
+import { MutatePromise, showFailureToast, useForm } from "@raycast/utils";
+import { updateProperty } from "../../api"; // ← import your new helper
+import { Property, PropertyFormat } from "../../models";
+
+export interface UpdatePropertyFormValues {
+ name: string;
+ format?: string;
+}
+
+interface UpdatePropertyFormProps {
+ spaceId: string;
+ property: Property;
+ mutateProperties: MutatePromise[];
+}
+
+export function UpdatePropertyForm({ spaceId, property, mutateProperties }: UpdatePropertyFormProps) {
+ const { pop } = useNavigation();
+ const { handleSubmit, itemProps } = useForm({
+ initialValues: {
+ name: property.name,
+ },
+ onSubmit: async (values) => {
+ try {
+ await showToast({ style: Toast.Style.Animated, title: "Updating property..." });
+
+ await updateProperty(spaceId, property.id, { name: values.name || "" });
+
+ showToast(Toast.Style.Success, "Property updated successfully");
+ mutateProperties.forEach((mutate) => mutate());
+ pop();
+ } catch (error) {
+ await showFailureToast(error, { title: "Failed to update property" });
+ }
+ },
+ validation: {
+ name: (v) => (!v ? "Name is required" : undefined),
+ },
+ });
+
+ const propertyFormatKeys = Object.keys(PropertyFormat) as Array;
+
+ return (
+ {}}
+ onFocus={() => {}}
+ info="Format is read-only"
+ >
+ PropertyFormat[key] === property.format) || property.format}
+ icon={{ source: `icons/property/${property.format}.svg` }}
+ />
+
+
+
+ );
+}
diff --git a/extensions/anytype/src/components/UpdateForm/UpdateSpaceForm.tsx b/extensions/anytype/src/components/UpdateForm/UpdateSpaceForm.tsx
new file mode 100644
index 00000000000..b117f5ee610
--- /dev/null
+++ b/extensions/anytype/src/components/UpdateForm/UpdateSpaceForm.tsx
@@ -0,0 +1,66 @@
+import { Action, ActionPanel, Form, Icon, showToast, Toast, useNavigation } from "@raycast/api";
+import { MutatePromise, showFailureToast, useForm } from "@raycast/utils";
+import { updateSpace } from "../../api";
+import { Space } from "../../models";
+
+export interface UpdateSpaceFormValues {
+ name: string;
+ description: string;
+}
+
+interface UpdateSpaceFormProps {
+ space: Space;
+ mutateSpaces: MutatePromise[];
+}
+
+export function UpdateSpaceForm({ space, mutateSpaces }: UpdateSpaceFormProps) {
+ const { pop } = useNavigation();
+
+ const { handleSubmit, itemProps } = useForm({
+ initialValues: {
+ name: space.name,
+ description: space.description,
+ },
+ onSubmit: async (values) => {
+ try {
+ await showToast({
+ style: Toast.Style.Animated,
+ title: "Updating space…",
+ });
+
+ await updateSpace(space.id, {
+ name: values.name || "",
+ description: values.description || "",
+ });
+
+ showToast(Toast.Style.Success, "Space updated successfully");
+ mutateSpaces.forEach((mutate) => mutate());
+ pop();
+ } catch (error) {
+ await showFailureToast(error, { title: "Failed to update space" });
+ }
+ },
+ validation: {
+ name: (v) => (!v ? "Name is required" : undefined),
+ },
+ });
+
+ return (
+
+
+
+ );
+}
diff --git a/extensions/anytype/src/components/UpdateForm/UpdateTagForm.tsx b/extensions/anytype/src/components/UpdateForm/UpdateTagForm.tsx
new file mode 100644
index 00000000000..784a87fa6a3
--- /dev/null
+++ b/extensions/anytype/src/components/UpdateForm/UpdateTagForm.tsx
@@ -0,0 +1,79 @@
+import { Action, ActionPanel, Form, Icon, showToast, Toast, useNavigation } from "@raycast/api";
+import { showFailureToast, useForm } from "@raycast/utils";
+import { updateTag } from "../../api";
+import { Color, Tag } from "../../models";
+import { colorToHex, hexToColor } from "../../utils";
+
+export interface UpdateTagFormValues {
+ name: string;
+ color: string;
+}
+
+interface UpdateTagFormProps {
+ spaceId: string;
+ propertyId: string;
+ tag: Tag;
+ mutateTags: () => void;
+}
+
+export function UpdateTagForm({ spaceId, propertyId, tag, mutateTags }: UpdateTagFormProps) {
+ const { pop } = useNavigation();
+
+ const { handleSubmit, itemProps } = useForm({
+ initialValues: {
+ name: tag.name,
+ color: hexToColor[tag.color] as Color,
+ },
+ onSubmit: async (values) => {
+ try {
+ await showToast({
+ style: Toast.Style.Animated,
+ title: "Updating tag…",
+ });
+
+ await updateTag(spaceId, propertyId, tag.id, {
+ name: values.name || "",
+ color: values.color as Color,
+ });
+
+ showToast(Toast.Style.Success, "Tag updated successfully");
+ mutateTags();
+ pop();
+ } catch (error) {
+ await showFailureToast(error, { title: "Failed to update tag" });
+ }
+ },
+ validation: {
+ name: (v) => (!v ? "Name is required" : undefined),
+ color: (v) => (!v ? "Color is required" : undefined),
+ },
+ });
+
+ const tagColorKeys = Object.keys(Color) as Array;
+
+ return (
+
+
+ {tagColorKeys.map((key) => {
+ const value = Color[key];
+ return (
+
+ );
+ })}
+
+
+ );
+}
diff --git a/extensions/anytype/src/components/UpdateForm/UpdateTypeForm.tsx b/extensions/anytype/src/components/UpdateForm/UpdateTypeForm.tsx
new file mode 100644
index 00000000000..f3723571cc2
--- /dev/null
+++ b/extensions/anytype/src/components/UpdateForm/UpdateTypeForm.tsx
@@ -0,0 +1,119 @@
+import { Action, ActionPanel, Form, Icon, showToast, Toast, useNavigation } from "@raycast/api";
+import { MutatePromise, showFailureToast, useForm } from "@raycast/utils";
+import { useState } from "react";
+import { updateType } from "../../api";
+import { useProperties } from "../../hooks";
+import { IconFormat, ObjectLayout, PropertyLink, RawType, Type, TypeLayout, UpdateTypeRequest } from "../../models";
+import { isEmoji } from "../../utils";
+
+export interface UpdateTypeFormValues {
+ name: string;
+ plural_name: string;
+ icon?: string;
+ layout: string;
+ properties?: string[];
+}
+
+interface UpdateTypeFormProps {
+ spaceId: string;
+ type: RawType;
+ mutateTypes: MutatePromise[];
+}
+
+export function UpdateTypeForm({ spaceId, type, mutateTypes }: UpdateTypeFormProps) {
+ const { pop } = useNavigation();
+ const [loading, setLoading] = useState(false);
+ const { properties } = useProperties(spaceId);
+
+ const initialValues: UpdateTypeFormValues = {
+ name: type.name,
+ plural_name: type.plural_name,
+ icon: type.icon.format === IconFormat.Emoji ? type.icon.emoji : undefined,
+ layout: type.layout,
+ properties: type.properties?.map((p) => p.key) || [],
+ };
+
+ const { handleSubmit, itemProps } = useForm({
+ initialValues,
+ onSubmit: async (values) => {
+ setLoading(true);
+ try {
+ await showToast({ style: Toast.Style.Animated, title: "Updating type..." });
+
+ const propertyLinks: PropertyLink[] =
+ values.properties?.map((key) => {
+ const prop = properties.find((p) => p.key === key)!;
+ return { key: prop.key, format: prop.format, name: prop.name };
+ }) || [];
+
+ const request: UpdateTypeRequest = {
+ name: values.name,
+ plural_name: values.plural_name,
+ icon: { format: IconFormat.Emoji, emoji: values.icon || "" },
+ ...(availableLayouts.includes(values.layout as TypeLayout) ? { layout: values.layout as TypeLayout } : {}),
+ properties: propertyLinks,
+ };
+
+ await updateType(spaceId, type.id, request);
+
+ await showToast(Toast.Style.Success, "Type updated successfully");
+ mutateTypes.forEach((mutate) => mutate());
+ pop();
+ } catch (error) {
+ await showFailureToast(error, { title: "Failed to update type" });
+ } finally {
+ setLoading(false);
+ }
+ },
+ validation: {
+ name: (v) => (!v ? "Name is required" : undefined),
+ plural_name: (v) => (!v ? "Plural name is required" : undefined),
+ icon: (v) => (v && !isEmoji(v) ? "Icon must be a single emoji" : undefined),
+ },
+ });
+
+ const layoutKeys = Object.keys(TypeLayout) as Array;
+ const availableLayouts = Object.values(TypeLayout);
+ const isFixedLayout = !availableLayouts.includes(type.layout as unknown as TypeLayout);
+
+ return (
+
+
+
+ {isFixedLayout ? (
+
+ ).find(
+ (k) => ObjectLayout[k] === initialValues.layout,
+ )!
+ }
+ />
+
+ ) : (
+
+ {layoutKeys.map((layout) => {
+ const value = TypeLayout[layout];
+ return ;
+ })}
+
+ )}
+
+ {properties.map((prop) => (
+
+ ))}
+
+
+ );
+}
diff --git a/extensions/anytype/src/components/index.ts b/extensions/anytype/src/components/index.ts
index a9b3b0c202e..b82d9f6c344 100644
--- a/extensions/anytype/src/components/index.ts
+++ b/extensions/anytype/src/components/index.ts
@@ -1,13 +1,27 @@
-export * from "./CollectionList";
-export * from "./CreateObjectForm";
-export * from "./CreateSpaceForm";
-export * from "./EmptyViewObject";
-export * from "./EmptyViewSpace";
+export * from "./Actions/ListSubmenu";
+export * from "./Actions/ObjectActions";
+export * from "./Actions/SpaceActions";
+export * from "./CreateForm/CreateObjectForm";
+export * from "./CreateForm/CreatePropertyForm";
+export * from "./CreateForm/CreateSpaceForm";
+export * from "./CreateForm/CreateTagForm";
+export * from "./CreateForm/CreateTypeForm";
+export * from "./EmptyView/EmptyViewObject";
+export * from "./EmptyView/EmptyViewProperty";
+export * from "./EmptyView/EmptyViewSpace";
+export * from "./EmptyView/EmptyViewTag";
+export * from "./EmptyView/EmptyViewType";
export * from "./EnsureAuthenticated";
-export * from "./ObjectActions";
+export * from "./Lists/CollectionList";
+export * from "./Lists/SpaceList";
+export * from "./Lists/TagList";
+export * from "./Lists/TemplateList";
export * from "./ObjectDetail";
export * from "./ObjectList";
export * from "./ObjectListItem";
-export * from "./SpaceActions";
export * from "./SpaceListItem";
-export * from "./TemplateList";
+export * from "./UpdateForm/UpdateObjectForm";
+export * from "./UpdateForm/UpdatePropertyForm";
+export * from "./UpdateForm/UpdateSpaceForm";
+export * from "./UpdateForm/UpdateTagForm";
+export * from "./UpdateForm/UpdateTypeForm";
diff --git a/extensions/anytype/src/create-object.tsx b/extensions/anytype/src/create-object.tsx
index 7e50d03f195..16c85487ef8 100644
--- a/extensions/anytype/src/create-object.tsx
+++ b/extensions/anytype/src/create-object.tsx
@@ -1,17 +1,27 @@
import { LaunchProps } from "@raycast/api";
import { CreateObjectForm, EnsureAuthenticated } from "./components";
-import { useCreateObjectData } from "./hooks";
-
+import { PropertyFieldValue } from "./models";
export interface CreateObjectFormValues {
- space?: string;
- type?: string;
- template?: string;
- list?: string;
+ spaceId?: string;
+ typeId?: string;
+ templateId?: string;
+ listId?: string;
name?: string;
icon?: string;
description?: string;
body?: string;
source?: string;
+
+ /**
+ * Dynamic property values coming from the selected Type definition.
+ * Keys are the property `key` strings and values depend on the property format:
+ * - "text" & "select" -> string
+ * - "number" -> string (raw text input before cast)
+ * - "date" -> Date | null (Raycast DatePicker returns a Date object)
+ * - "multi_select" -> string[]
+ * - "checkbox" -> boolean
+ */
+ [key: string]: PropertyFieldValue;
}
interface LaunchContext {
@@ -45,43 +55,5 @@ function CreateObject({ draftValues, launchContext }: CreateObjectProps) {
...draftValues, // `draftValues` takes precedence
};
- const {
- spaces,
- types,
- templates,
- lists,
- selectedSpace,
- setSelectedSpace,
- selectedType,
- setSelectedType,
- selectedTemplate,
- setSelectedTemplate,
- selectedList,
- setSelectedList,
- listSearchText,
- setListSearchText,
- isLoading,
- } = useCreateObjectData(mergedValues);
-
- return (
-
- );
+ return ;
}
diff --git a/extensions/anytype/src/hooks/index.ts b/extensions/anytype/src/hooks/index.ts
index 0d502dc4b4c..1f12ae91978 100644
--- a/extensions/anytype/src/hooks/index.ts
+++ b/extensions/anytype/src/hooks/index.ts
@@ -1,5 +1,4 @@
export * from "./useCreateObjectData";
-export * from "./useExport";
export * from "./useGlobalSearch";
export * from "./useMembers";
export * from "./useObject";
@@ -7,9 +6,12 @@ export * from "./useObjects";
export * from "./useObjectsInList";
export * from "./usePinnedMembers";
export * from "./usePinnedObjects";
+export * from "./usePinnedProperties";
export * from "./usePinnedSpaces";
export * from "./usePinnedTypes";
+export * from "./useProperties";
export * from "./useSearch";
export * from "./useSpaces";
+export * from "./useTags";
export * from "./useTemplates";
export * from "./useTypes";
diff --git a/extensions/anytype/src/hooks/useCreateObjectData.ts b/extensions/anytype/src/hooks/useCreateObjectData.ts
index 3574a742853..3b53de6bc23 100644
--- a/extensions/anytype/src/hooks/useCreateObjectData.ts
+++ b/extensions/anytype/src/hooks/useCreateObjectData.ts
@@ -1,42 +1,42 @@
-import { showToast, Toast } from "@raycast/api";
-import { useCachedPromise } from "@raycast/utils";
+import { showFailureToast, useCachedPromise } from "@raycast/utils";
import { useEffect, useMemo, useState } from "react";
import { CreateObjectFormValues } from "../create-object";
-import { fetchAllTemplatesForSpace, fetchAllTypesForSpace } from "../utils";
+import { bundledTypeKeys, fetchAllTemplatesForSpace, fetchAllTypesForSpace } from "../utils";
import { useSearch } from "./useSearch";
import { useSpaces } from "./useSpaces";
export function useCreateObjectData(initialValues?: CreateObjectFormValues) {
- const [selectedSpace, setSelectedSpace] = useState(initialValues?.space || "");
- const [selectedType, setSelectedType] = useState(initialValues?.type || "");
- const [selectedTemplate, setSelectedTemplate] = useState(initialValues?.template || "");
- const [selectedList, setSelectedList] = useState(initialValues?.list || "");
+ const [selectedSpaceId, setSelectedSpaceId] = useState(initialValues?.spaceId || "");
+ const [selectedTypeId, setSelectedTypeId] = useState(initialValues?.typeId || "");
+ const [selectedTemplateId, setSelectedTemplateId] = useState(initialValues?.templateId || "");
+ const [selectedListId, setSelectedListId] = useState(initialValues?.listId || "");
const [listSearchText, setListSearchText] = useState("");
+ const [objectSearchText, setObjectSearchText] = useState("");
const { spaces, spacesError, isLoadingSpaces } = useSpaces();
const {
objects: lists,
objectsError: listsError,
isLoadingObjects: isLoadingLists,
- } = useSearch(selectedSpace, listSearchText, ["ot-collection"]);
+ } = useSearch(selectedSpaceId, listSearchText, [bundledTypeKeys.collection]);
const restrictedTypes = [
- "ot-audio",
- "ot-chat",
- "ot-file",
- "ot-image",
- "ot-objectType",
- "ot-tag",
- "ot-template",
- "ot-video",
- "ot-participant",
+ bundledTypeKeys.audio,
+ bundledTypeKeys.chat,
+ bundledTypeKeys.file,
+ bundledTypeKeys.image,
+ bundledTypeKeys.object_type,
+ bundledTypeKeys.tag,
+ bundledTypeKeys.template,
+ bundledTypeKeys.video,
+ bundledTypeKeys.participant,
];
const {
data: allTypes,
error: typesError,
isLoading: isLoadingTypes,
- } = useCachedPromise(fetchAllTypesForSpace, [selectedSpace], { execute: !!selectedSpace });
+ } = useCachedPromise(fetchAllTypesForSpace, [selectedSpaceId], { execute: !!selectedSpaceId });
const types = useMemo(() => {
if (!allTypes) return [];
@@ -47,38 +47,41 @@ export function useCreateObjectData(initialValues?: CreateObjectFormValues) {
data: templates,
error: templatesError,
isLoading: isLoadingTemplates,
- } = useCachedPromise(fetchAllTemplatesForSpace, [selectedSpace, selectedType], {
- execute: !!selectedSpace && !!selectedType,
+ } = useCachedPromise(fetchAllTemplatesForSpace, [selectedSpaceId, selectedTypeId], {
+ execute: !!selectedSpaceId && !!selectedTypeId,
initialData: [],
});
+ const { objects, objectsError, isLoadingObjects } = useSearch(selectedSpaceId, objectSearchText, []);
+
useEffect(() => {
- if (spacesError || typesError || templatesError || listsError) {
- showToast(
- Toast.Style.Failure,
- "Failed to fetch latest data",
- spacesError?.message || typesError?.message || templatesError?.message || listsError?.message,
- );
+ if (spacesError || typesError || templatesError || listsError || objectsError) {
+ showFailureToast(spacesError || typesError || templatesError || listsError || objectsError, {
+ title: "Failed to fetch latest data",
+ });
}
}, [spacesError, typesError, templatesError, listsError]);
- const isLoading = isLoadingSpaces || isLoadingTypes || isLoadingTemplates || isLoadingLists;
+ const isLoading = isLoadingSpaces || isLoadingTypes || isLoadingTemplates || isLoadingLists || isLoadingObjects;
return {
spaces,
types,
templates,
lists,
- selectedSpace,
- setSelectedSpace,
- selectedType,
- setSelectedType,
- selectedTemplate,
- setSelectedTemplate,
- selectedList,
- setSelectedList,
+ objects,
+ selectedSpaceId,
+ setSelectedSpaceId,
+ selectedTypeId,
+ setSelectedTypeId,
+ selectedTemplateId,
+ setSelectedTemplateId,
+ selectedListId,
+ setSelectedListId,
listSearchText,
setListSearchText,
+ objectSearchText,
+ setObjectSearchText,
isLoading,
};
}
diff --git a/extensions/anytype/src/hooks/useExport.ts b/extensions/anytype/src/hooks/useExport.ts
deleted file mode 100644
index 3714ed2576e..00000000000
--- a/extensions/anytype/src/hooks/useExport.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { useCachedPromise } from "@raycast/utils";
-import { getExport } from "../api";
-import { ExportFormat } from "../models";
-
-export function useExport(spaceId: string, objectId: string, format: ExportFormat) {
- const { data, error, mutate, isLoading } = useCachedPromise(
- async (spaceId, objectId, format) => {
- const response = await getExport(spaceId, objectId, format);
- return response;
- },
- [spaceId, objectId, format],
- {
- execute: !!spaceId && !!objectId && !!format,
- },
- );
-
- return {
- objectExport: data,
- objectExportError: error,
- isLoadingObjectExport: isLoading,
- mutateObjectExport: mutate,
- };
-}
diff --git a/extensions/anytype/src/hooks/useGlobalSearch.ts b/extensions/anytype/src/hooks/useGlobalSearch.ts
index c7122771f1a..180e750b50b 100644
--- a/extensions/anytype/src/hooks/useGlobalSearch.ts
+++ b/extensions/anytype/src/hooks/useGlobalSearch.ts
@@ -13,7 +13,7 @@ export function useGlobalSearch(query: string, types: string[]) {
const sortDirection = sortPreference === "name" ? SortDirection.Ascending : SortDirection.Descending;
const response = await globalSearch(
- { query, types, sort: { property: sortPreference, direction: sortDirection } },
+ { query, types, sort: { property_key: sortPreference, direction: sortDirection } },
{ offset, limit: apiLimit },
);
diff --git a/extensions/anytype/src/hooks/useObject.ts b/extensions/anytype/src/hooks/useObject.ts
index 300ba24febf..f68257cc0de 100644
--- a/extensions/anytype/src/hooks/useObject.ts
+++ b/extensions/anytype/src/hooks/useObject.ts
@@ -1,10 +1,11 @@
import { useCachedPromise } from "@raycast/utils";
import { getObject } from "../api";
+import { BodyFormat } from "../models";
-export function useObject(spaceId: string, objectId: string) {
+export function useObject(spaceId: string, objectId: string, format: BodyFormat) {
const { data, error, isLoading, mutate } = useCachedPromise(
async (spaceId: string, objectId: string) => {
- const response = await getObject(spaceId, objectId);
+ const response = await getObject(spaceId, objectId, format);
return response.object;
},
[spaceId, objectId],
diff --git a/extensions/anytype/src/hooks/useObjects.ts b/extensions/anytype/src/hooks/useObjects.ts
index 969f3202d1a..728d4464431 100644
--- a/extensions/anytype/src/hooks/useObjects.ts
+++ b/extensions/anytype/src/hooks/useObjects.ts
@@ -17,6 +17,7 @@ export function useObjects(spaceId: string) {
[spaceId],
{
keepPreviousData: true,
+ execute: !!spaceId,
},
);
diff --git a/extensions/anytype/src/hooks/usePinnedMembers.ts b/extensions/anytype/src/hooks/usePinnedMembers.ts
index 722cc4fced5..011b556392f 100644
--- a/extensions/anytype/src/hooks/usePinnedMembers.ts
+++ b/extensions/anytype/src/hooks/usePinnedMembers.ts
@@ -1,6 +1,6 @@
import { useCachedPromise } from "@raycast/utils";
import { getMember } from "../api";
-import { ErrorWithStatus, getPinned, removePinned } from "../utils";
+import { errorConnectionMessage, ErrorWithStatus, getPinned, removePinned } from "../utils";
export function usePinnedMembers(key: string) {
const { data, error, isLoading, mutate } = useCachedPromise(
@@ -13,7 +13,9 @@ export function usePinnedMembers(key: string) {
return response.member;
} catch (error) {
const typedError = error as ErrorWithStatus;
- if (typedError.status === 404 || typedError.status === 410) {
+ if (typedError.message === errorConnectionMessage) {
+ throw error;
+ } else if (typedError.status === 404 || typedError.status === 410) {
await removePinned(pinned.spaceId, pinned.objectId, key);
}
return null;
diff --git a/extensions/anytype/src/hooks/usePinnedObjects.ts b/extensions/anytype/src/hooks/usePinnedObjects.ts
index addcdab3e84..a97a5168b79 100644
--- a/extensions/anytype/src/hooks/usePinnedObjects.ts
+++ b/extensions/anytype/src/hooks/usePinnedObjects.ts
@@ -1,7 +1,7 @@
import { MutatePromise, useCachedPromise } from "@raycast/utils";
import { getObject } from "../api";
-import { Member, SpaceObject, Type } from "../models";
-import { ErrorWithStatus, getPinned, removePinned } from "../utils";
+import { BodyFormat, Member, Property, SpaceObject, Type } from "../models";
+import { errorConnectionMessage, ErrorWithStatus, getPinned, removePinned } from "../utils";
export function usePinnedObjects(key: string) {
const { data, error, isLoading, mutate } = useCachedPromise(
@@ -10,15 +10,17 @@ export function usePinnedObjects(key: string) {
const objects = await Promise.all(
pinnedObjects.map(async (pinned) => {
try {
- const response = await getObject(pinned.spaceId, pinned.objectId);
- if (response.object?.archived) {
+ const response = await getObject(pinned.spaceId, pinned.objectId, BodyFormat.Markdown);
+ if (response.object.archived) {
await removePinned(pinned.spaceId, pinned.objectId, key);
return null;
}
- return response.object;
+ return response.object as SpaceObject;
} catch (error) {
const typedError = error as ErrorWithStatus;
- if (typedError.status === 404 || typedError.status === 410) {
+ if (typedError.message === errorConnectionMessage) {
+ throw error;
+ } else if (typedError.status === 404 || typedError.status === 410) {
await removePinned(pinned.spaceId, pinned.objectId, key);
}
return null;
@@ -37,6 +39,6 @@ export function usePinnedObjects(key: string) {
pinnedObjects: data as SpaceObject[],
pinnedObjectsError: error,
isLoadingPinnedObjects: isLoading,
- mutatePinnedObjects: mutate as MutatePromise,
+ mutatePinnedObjects: mutate as MutatePromise,
};
}
diff --git a/extensions/anytype/src/hooks/usePinnedProperties.ts b/extensions/anytype/src/hooks/usePinnedProperties.ts
new file mode 100644
index 00000000000..18a5c7297ca
--- /dev/null
+++ b/extensions/anytype/src/hooks/usePinnedProperties.ts
@@ -0,0 +1,44 @@
+import { useCachedPromise } from "@raycast/utils";
+import { getProperty } from "../api";
+import { errorConnectionMessage, ErrorWithStatus, getPinned, removePinned } from "../utils";
+
+export function usePinnedProperties(key: string) {
+ const { data, error, isLoading, mutate } = useCachedPromise(
+ async (key: string) => {
+ const pinnedProperties = await getPinned(key);
+ const properties = await Promise.all(
+ pinnedProperties.map(async (pinned) => {
+ try {
+ const response = await getProperty(pinned.spaceId, pinned.objectId);
+ // TODO: enable this when the API supports it
+ // if (response.property?.archived) {
+ // await removePinned(pinned.spaceId, pinned.objectId, key);
+ // return null;
+ // }
+ return response.property;
+ } catch (error) {
+ const typedError = error as ErrorWithStatus;
+ if (typedError.message === errorConnectionMessage) {
+ throw error;
+ } else if (typedError.status === 404 || typedError.status === 410) {
+ await removePinned(pinned.spaceId, pinned.objectId, key);
+ }
+ return null;
+ }
+ }),
+ );
+ return properties.filter((property) => property !== null);
+ },
+ [key],
+ {
+ keepPreviousData: true,
+ },
+ );
+
+ return {
+ pinnedProperties: data,
+ pinnedPropertiesError: error,
+ isLoadingPinnedProperties: isLoading,
+ mutatePinnedProperties: mutate,
+ };
+}
diff --git a/extensions/anytype/src/hooks/usePinnedSpaces.ts b/extensions/anytype/src/hooks/usePinnedSpaces.ts
index ef0008ec1ec..a3d64329060 100644
--- a/extensions/anytype/src/hooks/usePinnedSpaces.ts
+++ b/extensions/anytype/src/hooks/usePinnedSpaces.ts
@@ -1,6 +1,6 @@
import { useCachedPromise } from "@raycast/utils";
import { getSpace } from "../api";
-import { ErrorWithStatus, getPinned, localStorageKeys, removePinned } from "../utils";
+import { errorConnectionMessage, ErrorWithStatus, getPinned, localStorageKeys, removePinned } from "../utils";
export function usePinnedSpaces() {
const { data, error, isLoading, mutate } = useCachedPromise(
@@ -14,7 +14,9 @@ export function usePinnedSpaces() {
return response.space;
} catch (error) {
const typedError = error as ErrorWithStatus;
- if (typedError.status === 404 || typedError.status === 410) {
+ if (typedError.message === errorConnectionMessage) {
+ throw error;
+ } else if (typedError.status === 404 || typedError.status === 410) {
await removePinned(pinned.spaceId, pinned.objectId, key);
}
return null;
diff --git a/extensions/anytype/src/hooks/usePinnedTypes.ts b/extensions/anytype/src/hooks/usePinnedTypes.ts
index 60e6b91fec5..a79df6d8bcb 100644
--- a/extensions/anytype/src/hooks/usePinnedTypes.ts
+++ b/extensions/anytype/src/hooks/usePinnedTypes.ts
@@ -1,6 +1,6 @@
import { useCachedPromise } from "@raycast/utils";
import { getType } from "../api";
-import { ErrorWithStatus, getPinned, removePinned } from "../utils";
+import { errorConnectionMessage, ErrorWithStatus, getPinned, removePinned } from "../utils";
export function usePinnedTypes(key: string) {
const { data, error, isLoading, mutate } = useCachedPromise(
@@ -10,14 +10,16 @@ export function usePinnedTypes(key: string) {
pinnedTypes.map(async (pinned) => {
try {
const response = await getType(pinned.spaceId, pinned.objectId);
- if (response.type?.archived) {
+ if (response.type.archived) {
await removePinned(pinned.spaceId, pinned.objectId, key);
return null;
}
return response.type;
} catch (error) {
const typedError = error as ErrorWithStatus;
- if (typedError.status === 404 || typedError.status === 410) {
+ if (typedError.message === errorConnectionMessage) {
+ throw error;
+ } else if (typedError.status === 404 || typedError.status === 410) {
await removePinned(pinned.spaceId, pinned.objectId, key);
}
return null;
diff --git a/extensions/anytype/src/hooks/useProperties.ts b/extensions/anytype/src/hooks/useProperties.ts
new file mode 100644
index 00000000000..8d8306f208a
--- /dev/null
+++ b/extensions/anytype/src/hooks/useProperties.ts
@@ -0,0 +1,34 @@
+import { useCachedPromise } from "@raycast/utils";
+import { useMemo } from "react";
+import { getProperties } from "../api";
+import { apiLimit } from "../utils/constant";
+
+export function useProperties(spaceId: string) {
+ const { data, error, isLoading, mutate, pagination } = useCachedPromise(
+ (spaceId: string) => async (options: { page: number }) => {
+ const offset = options.page * apiLimit;
+ const response = await getProperties(spaceId, { offset, limit: apiLimit });
+
+ return {
+ data: response.properties,
+ hasMore: response.pagination.has_more,
+ };
+ },
+ [spaceId],
+ {
+ keepPreviousData: true,
+ execute: !!spaceId,
+ },
+ );
+
+ // filter empty data to prevent flickering at the bottom
+ const filteredData = useMemo(() => data?.filter((property) => property) || [], [data]);
+
+ return {
+ properties: filteredData,
+ propertiesError: error,
+ isLoadingProperties: isLoading,
+ mutateProperties: mutate,
+ propertiesPagination: pagination,
+ };
+}
diff --git a/extensions/anytype/src/hooks/useSearch.ts b/extensions/anytype/src/hooks/useSearch.ts
index 1e3e0b78210..0504773f9e6 100644
--- a/extensions/anytype/src/hooks/useSearch.ts
+++ b/extensions/anytype/src/hooks/useSearch.ts
@@ -5,7 +5,7 @@ import { search } from "../api";
import { SortDirection } from "../models";
import { apiLimit } from "../utils";
-export function useSearch(spaceId: string, query: string, types: string[]) {
+export function useSearch(spaceId: string, query: string, types: string[], config?: { execute?: boolean }) {
const { data, error, isLoading, mutate, pagination } = useCachedPromise(
(spaceId: string, query: string, types: string[]) => async (options: { page: number }) => {
const offset = options.page * apiLimit;
@@ -14,7 +14,7 @@ export function useSearch(spaceId: string, query: string, types: string[]) {
const response = await search(
spaceId,
- { query, types, sort: { property: sortPreference, direction: sortDirection } },
+ { query, types, sort: { property_key: sortPreference, direction: sortDirection } },
{ offset, limit: apiLimit },
);
@@ -26,7 +26,7 @@ export function useSearch(spaceId: string, query: string, types: string[]) {
[spaceId, query, types],
{
keepPreviousData: true,
- execute: !!spaceId,
+ execute: !!spaceId && config?.execute !== false,
},
);
diff --git a/extensions/anytype/src/hooks/useTags.ts b/extensions/anytype/src/hooks/useTags.ts
new file mode 100644
index 00000000000..ad15d517ace
--- /dev/null
+++ b/extensions/anytype/src/hooks/useTags.ts
@@ -0,0 +1,64 @@
+import { useCachedPromise } from "@raycast/utils";
+import { useMemo } from "react";
+import { getTags } from "../api";
+import { apiLimit } from "../utils";
+
+export function useTags(spaceId: string, propertyId: string) {
+ const { data, error, isLoading, mutate, pagination } = useCachedPromise(
+ (spaceId: string, propertyId: string) => async (options: { page: number }) => {
+ const offset = options.page * apiLimit;
+ const response = await getTags(spaceId, propertyId, { offset, limit: apiLimit });
+
+ return {
+ data: response.tags,
+ hasMore: response.pagination.has_more,
+ };
+ },
+ [spaceId, propertyId],
+ {
+ keepPreviousData: true,
+ execute: !!spaceId && !!propertyId,
+ },
+ );
+
+ // filter empty data to prevent flickering at the bottom
+ const filteredData = useMemo(() => data?.filter((tag) => tag) || [], [data]);
+
+ return {
+ tags: filteredData,
+ tagsError: error,
+ isLoadingTags: isLoading,
+ mutateTags: mutate,
+ tagsPagination: pagination,
+ };
+}
+
+export function useTagsMap(spaceId: string, propertyIds: string[]) {
+ const { data, error, isLoading, mutate } = useCachedPromise(
+ async (spaceId: string, propertyIds: string[]) => {
+ const results = await Promise.all(
+ propertyIds.map(async (propertyId) => {
+ const response = await getTags(spaceId, propertyId, { offset: 0, limit: apiLimit });
+ return { propertyId, tags: response.tags };
+ }),
+ );
+ const tagsMap: Record = {};
+ results.forEach(({ propertyId, tags }) => {
+ tagsMap[propertyId] = tags;
+ });
+ return tagsMap;
+ },
+ [spaceId, propertyIds],
+ {
+ keepPreviousData: true,
+ execute: !!spaceId && propertyIds.length > 0,
+ },
+ );
+
+ return {
+ tagsMap: data,
+ tagsError: error,
+ isLoadingTags: isLoading,
+ mutateTags: mutate,
+ };
+}
diff --git a/extensions/anytype/src/mappers/members.ts b/extensions/anytype/src/mappers/members.ts
index e377842c6c4..d3b9482d774 100644
--- a/extensions/anytype/src/mappers/members.ts
+++ b/extensions/anytype/src/mappers/members.ts
@@ -1,4 +1,4 @@
-import { Member, RawMember } from "../models";
+import { Member, ObjectLayout, RawMember } from "../models";
import { getIconWithFallback } from "../utils";
/**
@@ -20,11 +20,11 @@ export async function mapMembers(members: RawMember[]): Promise {
* @returns The display-ready `Member` object.
*/
export async function mapMember(member: RawMember): Promise {
- const icon = await getIconWithFallback(member.icon, "participant");
+ const icon = await getIconWithFallback(member.icon, ObjectLayout.Participant);
return {
...member,
- name: member.name || "Untitled",
+ name: member.name?.trim() || "Untitled", // empty string comes as \n
icon,
};
}
diff --git a/extensions/anytype/src/mappers/objects.ts b/extensions/anytype/src/mappers/objects.ts
index 8c2716fe215..edfdc70e3fa 100644
--- a/extensions/anytype/src/mappers/objects.ts
+++ b/extensions/anytype/src/mappers/objects.ts
@@ -1,7 +1,17 @@
import { getPreferenceValues } from "@raycast/api";
-import { getObjectWithoutMappedDetails } from "../api";
-import { Property, RawSpaceObject, SortProperty, SpaceObject } from "../models";
-import { colorMap, getIconWithFallback } from "../utils";
+import { getObjectWithoutMappedProperties } from "../api";
+import {
+ BodyFormat,
+ PropertyFormat,
+ PropertyWithValue,
+ RawSpaceObject,
+ RawSpaceObjectWithBody,
+ SortProperty,
+ SpaceObject,
+ SpaceObjectWithBody,
+} from "../models";
+import { bundledPropKeys, getIconWithFallback } from "../utils";
+import { mapTag } from "./properties";
import { mapType } from "./types";
/**
@@ -18,13 +28,15 @@ export async function mapObjects(objects: RawSpaceObject[]): Promise {
- if (sort === SortProperty.Name) {
- // When sorting by name, keep the 'LastModifiedDate' property for tooltip purposes
- return property.id === SortProperty.LastModifiedDate;
- }
- return property.id === sort;
- }),
+ properties: await Promise.all(
+ (object.properties?.filter((property) => {
+ if (sort === SortProperty.Name) {
+ // When sorting by name, keep the 'LastModifiedDate' property for tooltip purposes
+ return property.key === SortProperty.LastModifiedDate;
+ }
+ return property.key === sort || property.key === bundledPropKeys.source; // keep source to open bookmarks in browser
+ }) || []) as PropertyWithValue[],
+ ),
};
}),
);
@@ -33,102 +45,97 @@ export async function mapObjects(objects: RawSpaceObject[]): Promise {
+export async function mapObject(
+ object: RawSpaceObject | RawSpaceObjectWithBody,
+): Promise {
const icon = await getIconWithFallback(object.icon, object.layout, object.type);
const mappedProperties = await Promise.all(
(object.properties || []).map(async (property) => {
- let mappedProperty: Property = {
+ let mappedProperty: PropertyWithValue = {
id: property.id,
- name: property.name,
+ key: property.key,
+ name: property.name || "Untitled",
format: property.format,
};
switch (property.format) {
- case "text":
+ case PropertyFormat.Text:
mappedProperty = {
...mappedProperty,
text: typeof property.text === "string" ? property.text.trim() : "",
};
break;
- case "number":
+ case PropertyFormat.Number:
mappedProperty = {
...mappedProperty,
number: property.number !== undefined && property.number !== null ? property.number : 0,
};
break;
- case "select":
+ case PropertyFormat.Select:
if (property.select) {
mappedProperty = {
...mappedProperty,
- select: {
- id: property.select.id || "",
- name: property.select.name || "",
- color: colorMap[property.select.color] || property.select.color,
- },
+ select: mapTag(property.select),
};
}
break;
- case "multi_select":
+ case PropertyFormat.MultiSelect:
if (property.multi_select) {
mappedProperty = {
...mappedProperty,
- multi_select: property.multi_select.map((tag) => ({
- id: tag.id || "",
- name: tag.name || "",
- color: colorMap[tag.color] || tag.color,
- })),
+ multi_select: property.multi_select.map((tag) => mapTag(tag)),
};
}
break;
- case "date":
+ case PropertyFormat.Date:
mappedProperty = {
...mappedProperty,
date: property.date ? new Date(property.date).toISOString() : "",
};
break;
- case "file":
- if (property.file) {
+ case PropertyFormat.Files:
+ if (property.files) {
mappedProperty = {
...mappedProperty,
- file: await mapObjectWithoutDetails(object.space_id, property.file),
+ files: await mapObjectWithoutProperties(object.space_id, property.files),
};
}
break;
- case "checkbox":
+ case PropertyFormat.Checkbox:
mappedProperty = {
...mappedProperty,
checkbox: property.checkbox || false,
};
break;
- case "url":
+ case PropertyFormat.Url:
mappedProperty = {
...mappedProperty,
url: typeof property.url === "string" ? property.url.trim() : "",
};
break;
- case "email":
+ case PropertyFormat.Email:
mappedProperty = {
...mappedProperty,
email: typeof property.email === "string" ? property.email.trim() : "",
};
break;
- case "phone":
+ case PropertyFormat.Phone:
mappedProperty = {
...mappedProperty,
phone: typeof property.phone === "string" ? property.phone.trim() : "",
};
break;
- case "object":
- if (property.object) {
+ case PropertyFormat.Objects:
+ if (property.objects) {
mappedProperty = {
...mappedProperty,
- object: await mapObjectWithoutDetails(object.space_id, property.object),
+ objects: await mapObjectWithoutProperties(object.space_id, property.objects),
};
}
break;
default:
- console.warn(`Unknown property format: ${property.format}`);
+ console.warn(`Unknown property format: '${property.format}' for property '${property.key}'`);
}
return mappedProperty;
@@ -138,22 +145,18 @@ export async function mapObject(object: RawSpaceObject): Promise {
return {
...object,
icon,
- name: object.name || `${object.snippet.split("\n")[0]}...` || "Untitled",
+ name: object.name?.trim() || `${object.snippet.split("\n")[0]}...` || "Untitled",
type: await mapType(object.type),
properties: mappedProperties,
};
}
-export async function mapObjectWithoutDetails(spaceId: string, object: SpaceObject[]): Promise {
+export async function mapObjectWithoutProperties(spaceId: string, object: string[]): Promise {
const rawItems = Array.isArray(object) ? object : [object];
return await Promise.all(
rawItems.map(async (item) => {
if (typeof item === "string") {
- const fetched = await getObjectWithoutMappedDetails(spaceId, item);
- if (!fetched) {
- throw new Error(`getRawObject returned null for item ${item}`);
- }
- return fetched;
+ return await getObjectWithoutMappedProperties(spaceId, item, BodyFormat.Markdown);
} else {
return item;
}
diff --git a/extensions/anytype/src/mappers/properties.ts b/extensions/anytype/src/mappers/properties.ts
new file mode 100644
index 00000000000..0c8114e2a96
--- /dev/null
+++ b/extensions/anytype/src/mappers/properties.ts
@@ -0,0 +1,59 @@
+import { Image } from "@raycast/api";
+import { Property, PropertyFormat, RawProperty, RawTag, Tag } from "../models";
+import { colorToHex } from "../utils";
+
+export function mapProperties(properties: RawProperty[]): Property[] {
+ return properties.map((property) => {
+ return mapProperty(property);
+ });
+}
+
+export function mapProperty(property: RawProperty): Property {
+ return {
+ ...property,
+ name: property.name?.trim() || "Untitled",
+ icon: getIconForProperty(property.format),
+ };
+}
+
+export function getIconForProperty(format: PropertyFormat): Image.ImageLike {
+ const tintColor = { light: "grey", dark: "grey" };
+ switch (format) {
+ case PropertyFormat.Text:
+ return { source: "icons/property/text.svg", tintColor: tintColor };
+ case PropertyFormat.Number:
+ return { source: "icons/property/number.svg", tintColor: tintColor };
+ case PropertyFormat.Select:
+ return { source: "icons/property/select.svg", tintColor: tintColor };
+ case PropertyFormat.MultiSelect:
+ return { source: "icons/property/multi_select.svg", tintColor: tintColor };
+ case PropertyFormat.Date:
+ return { source: "icons/property/date.svg", tintColor: tintColor };
+ case PropertyFormat.Files:
+ return { source: "icons/property/files.svg", tintColor: tintColor };
+ case PropertyFormat.Checkbox:
+ return { source: "icons/property/checkbox.svg", tintColor: tintColor };
+ case PropertyFormat.Url:
+ return { source: "icons/property/url.svg", tintColor: tintColor };
+ case PropertyFormat.Email:
+ return { source: "icons/property/email.svg", tintColor: tintColor };
+ case PropertyFormat.Phone:
+ return { source: "icons/property/phone.svg", tintColor: tintColor };
+ case PropertyFormat.Objects:
+ return { source: "icons/property/objects.svg", tintColor: tintColor };
+ }
+}
+
+export function mapTags(tags: RawTag[]): Tag[] {
+ return tags.map((tag) => {
+ return mapTag(tag);
+ });
+}
+
+export function mapTag(tag: RawTag): Tag {
+ return {
+ ...tag,
+ name: tag.name?.trim() || "Untitled",
+ color: colorToHex[tag.color] || tag.color,
+ };
+}
diff --git a/extensions/anytype/src/mappers/spaces.ts b/extensions/anytype/src/mappers/spaces.ts
index dd6cd2e8ab0..8713a4f61fd 100644
--- a/extensions/anytype/src/mappers/spaces.ts
+++ b/extensions/anytype/src/mappers/spaces.ts
@@ -24,7 +24,7 @@ export async function mapSpace(space: RawSpace): Promise {
return {
...space,
- name: space.name || "Untitled",
+ name: space.name?.trim() || "Untitled",
icon,
};
}
diff --git a/extensions/anytype/src/mappers/templates.ts b/extensions/anytype/src/mappers/templates.ts
deleted file mode 100644
index 0325770ae9f..00000000000
--- a/extensions/anytype/src/mappers/templates.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { RawTemplate, Template } from "../models";
-import { getIconWithFallback } from "../utils";
-
-/**
- * Map raw `Template` objects from the API into display-ready data (e.g., icon).
- */
-export async function mapTemplates(templates: RawTemplate[]): Promise {
- return Promise.all(
- templates.map(async (template) => {
- return mapTemplate(template);
- }),
- );
-}
-
-/**
- * Map raw `Template` object from the API into display-ready data (e.g., icon).
- */
-export async function mapTemplate(template: RawTemplate): Promise {
- const icon = await getIconWithFallback(template.icon, "template");
-
- return {
- ...template,
- name: template.name || "Untitled",
- icon: icon,
- };
-}
diff --git a/extensions/anytype/src/mappers/types.ts b/extensions/anytype/src/mappers/types.ts
index efcae6978ce..c02c3241d42 100644
--- a/extensions/anytype/src/mappers/types.ts
+++ b/extensions/anytype/src/mappers/types.ts
@@ -21,6 +21,7 @@ export async function mapType(type: RawType): Promise {
return {
...type,
name: type.name?.trim() || "Untitled", // empty string comes as \n
+ plural_name: type.plural_name?.trim() || "Untitled",
icon: icon,
};
}
diff --git a/extensions/anytype/src/models/export.ts b/extensions/anytype/src/models/export.ts
deleted file mode 100644
index 8d3e65ab0e2..00000000000
--- a/extensions/anytype/src/models/export.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export interface Export {
- markdown: string;
-}
-
-export enum ExportFormat {
- Markdown = "markdown",
- Protobuf = "protobuf",
-}
diff --git a/extensions/anytype/src/models/icon.ts b/extensions/anytype/src/models/icon.ts
index a6e6cf69138..23b10b07158 100644
--- a/extensions/anytype/src/models/icon.ts
+++ b/extensions/anytype/src/models/icon.ts
@@ -4,10 +4,33 @@ export enum IconFormat {
Icon = "icon",
}
-export interface ObjectIcon {
- format: IconFormat;
- emoji?: string;
- file?: string;
- name?: string;
- color?: string;
+export enum Color {
+ Grey = "grey",
+ Yellow = "yellow",
+ Orange = "orange",
+ Red = "red",
+ Pink = "pink",
+ Purple = "purple",
+ Blue = "blue",
+ Ice = "ice",
+ Teal = "teal",
+ Lime = "lime",
}
+
+export interface EmojiIcon {
+ format: IconFormat.Emoji;
+ emoji: string;
+}
+
+export interface FileIcon {
+ format: IconFormat.File;
+ file: string;
+}
+
+export interface NamedIcon {
+ format: IconFormat.Icon;
+ name: string;
+ color?: Color | string;
+}
+
+export type ObjectIcon = EmojiIcon | FileIcon | NamedIcon;
diff --git a/extensions/anytype/src/models/index.ts b/extensions/anytype/src/models/index.ts
index a331bca54f2..52622a872a3 100644
--- a/extensions/anytype/src/models/index.ts
+++ b/extensions/anytype/src/models/index.ts
@@ -1,11 +1,11 @@
export * from "./auth";
-export * from "./export";
export * from "./icon";
export * from "./list";
export * from "./member";
export * from "./object";
export * from "./pagination";
+export * from "./property";
export * from "./search";
export * from "./space";
-export * from "./template";
+export * from "./tag";
export * from "./type";
diff --git a/extensions/anytype/src/models/object.ts b/extensions/anytype/src/models/object.ts
index ae70e585f24..baff83b48c0 100644
--- a/extensions/anytype/src/models/object.ts
+++ b/extensions/anytype/src/models/object.ts
@@ -1,14 +1,33 @@
import { Image } from "@raycast/api";
-import { ObjectIcon, RawType, Type } from ".";
+import {
+ ObjectIcon,
+ ObjectLayout,
+ PropertyLinkWithValue,
+ PropertyWithValue,
+ RawProperty,
+ RawPropertyWithValue,
+ RawType,
+ Type,
+} from ".";
+
+export enum BodyFormat {
+ Markdown = "md",
+ JSON = "json",
+}
export interface CreateObjectRequest {
name: string;
icon: ObjectIcon;
- description: string;
body: string;
- source: string;
template_id: string;
type_key: string;
+ properties: PropertyLinkWithValue[];
+}
+
+export interface UpdateObjectRequest {
+ name?: string;
+ icon?: ObjectIcon;
+ properties?: PropertyLinkWithValue[];
}
export interface RawSpaceObject {
@@ -18,24 +37,26 @@ export interface RawSpaceObject {
icon: ObjectIcon;
type: RawType;
snippet: string;
- layout: string;
+ layout: ObjectLayout;
space_id: string;
archived: boolean;
- properties: Property[];
+ properties: RawPropertyWithValue[];
}
-export interface RawSpaceObjectWithBlocks extends RawSpaceObject {
- blocks: Block[];
+export interface RawSpaceObjectWithBody extends RawSpaceObject {
+ markdown: string;
}
-export interface SpaceObject extends Omit {
- type: Type;
+export interface SpaceObject extends Omit {
icon: Image.ImageLike;
+ type: Type;
+ properties: PropertyWithValue[];
}
-export interface SpaceObjectWithBlocks extends Omit {
+export interface SpaceObjectWithBody extends Omit {
type: Type;
icon: Image.ImageLike;
+ properties: PropertyWithValue[];
}
export interface Block {
@@ -46,7 +67,7 @@ export interface Block {
vertical_align: string;
text: Text;
file: File;
- property: Property;
+ property: RawProperty;
}
export interface Text {
@@ -68,26 +89,3 @@ export interface File {
state: string;
style: string;
}
-
-export interface Property {
- id: string;
- name: string;
- format: string;
- text?: string;
- number?: number;
- select?: Tag;
- multi_select?: Tag[];
- date?: string;
- file?: SpaceObject[];
- checkbox?: boolean;
- url?: string;
- email?: string;
- phone?: string;
- object?: SpaceObject[];
-}
-
-export interface Tag {
- id: string;
- name: string;
- color: string;
-}
diff --git a/extensions/anytype/src/models/property.ts b/extensions/anytype/src/models/property.ts
new file mode 100644
index 00000000000..38e6f56732b
--- /dev/null
+++ b/extensions/anytype/src/models/property.ts
@@ -0,0 +1,86 @@
+import { Image } from "@raycast/api";
+import { RawTag, Tag } from ".";
+import { SpaceObject } from "./object";
+
+export type PropertyFieldValue = string | number | boolean | string[] | Date | null | undefined;
+
+export enum PropertyFormat {
+ Text = "text",
+ Number = "number",
+ Select = "select",
+ MultiSelect = "multi_select",
+ Date = "date",
+ Files = "files",
+ Checkbox = "checkbox",
+ Url = "url",
+ Email = "email",
+ Phone = "phone",
+ Objects = "objects",
+}
+
+export interface CreatePropertyRequest {
+ name: string;
+ format: PropertyFormat;
+}
+
+export interface UpdatePropertyRequest {
+ name: string;
+}
+
+export interface RawProperty {
+ object: string;
+ id: string;
+ key: string;
+ name: string;
+ format: PropertyFormat;
+}
+
+export interface Property extends RawProperty {
+ icon: Image.ImageLike;
+}
+export interface RawPropertyWithValue {
+ id: string;
+ key: string;
+ name: string;
+ format: PropertyFormat;
+ text?: string;
+ number?: number;
+ select?: RawTag;
+ multi_select?: RawTag[];
+ date?: string;
+ files?: string[];
+ checkbox?: boolean;
+ url?: string;
+ email?: string;
+ phone?: string;
+ objects?: string[];
+}
+
+export interface PropertyWithValue extends Omit {
+ select?: Tag;
+ multi_select?: Tag[];
+ files?: SpaceObject[];
+ objects?: SpaceObject[];
+}
+
+export interface PropertyLink {
+ key: string;
+ name: string;
+ format: PropertyFormat;
+}
+
+export interface PropertyLinkWithValue {
+ key: string;
+ format: PropertyFormat;
+ text?: string;
+ number?: number | null;
+ select?: string | null;
+ multi_select?: string[];
+ date?: string | null;
+ files?: string[];
+ checkbox?: boolean;
+ url?: string;
+ email?: string;
+ phone?: string;
+ objects?: string[];
+}
diff --git a/extensions/anytype/src/models/search.ts b/extensions/anytype/src/models/search.ts
index 72e9c6f5d6b..03436c7442e 100644
--- a/extensions/anytype/src/models/search.ts
+++ b/extensions/anytype/src/models/search.ts
@@ -17,6 +17,6 @@ export interface SearchRequest {
}
export interface SortOptions {
- property: SortProperty;
+ property_key: SortProperty;
direction: SortDirection;
}
diff --git a/extensions/anytype/src/models/space.ts b/extensions/anytype/src/models/space.ts
index 1f3c6cfa7a0..4dba30001ff 100644
--- a/extensions/anytype/src/models/space.ts
+++ b/extensions/anytype/src/models/space.ts
@@ -6,6 +6,11 @@ export interface CreateSpaceRequest {
description: string;
}
+export interface UpdateSpaceRequest {
+ name?: string;
+ description?: string;
+}
+
export interface RawSpace {
object: string;
id: string;
diff --git a/extensions/anytype/src/models/tag.ts b/extensions/anytype/src/models/tag.ts
new file mode 100644
index 00000000000..8ecdd760c11
--- /dev/null
+++ b/extensions/anytype/src/models/tag.ts
@@ -0,0 +1,22 @@
+import { Color } from ".";
+
+export interface CreateTagRequest {
+ name: string;
+ color: Color;
+}
+
+export interface UpdateTagRequest {
+ name: string;
+ color?: Color;
+}
+
+export interface RawTag {
+ id: string;
+ key: string;
+ name: string;
+ color: Color;
+}
+
+export interface Tag extends Omit {
+ color: string;
+}
diff --git a/extensions/anytype/src/models/template.ts b/extensions/anytype/src/models/template.ts
deleted file mode 100644
index 563c54a7baf..00000000000
--- a/extensions/anytype/src/models/template.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { Image } from "@raycast/api";
-import { ObjectIcon } from ".";
-
-export interface RawTemplate {
- object: string;
- id: string;
- name: string;
- icon: ObjectIcon;
-}
-
-export interface Template extends Omit {
- icon: Image.ImageLike;
-}
diff --git a/extensions/anytype/src/models/type.ts b/extensions/anytype/src/models/type.ts
index 1672c816010..2c8f4193654 100644
--- a/extensions/anytype/src/models/type.ts
+++ b/extensions/anytype/src/models/type.ts
@@ -1,14 +1,50 @@
import { Image } from "@raycast/api";
-import { ObjectIcon } from ".";
+import { ObjectIcon, PropertyLink, RawProperty } from ".";
+
+export enum TypeLayout {
+ Basic = "basic",
+ Profile = "profile",
+ Action = "action",
+ Note = "note",
+}
+
+export enum ObjectLayout {
+ Basic = "basic",
+ Profile = "profile",
+ Action = "action",
+ Note = "note",
+ Bookmark = "bookmark",
+ Set = "set",
+ Collection = "collection",
+ Participant = "participant",
+}
+
+export interface CreateTypeRequest {
+ name: string;
+ plural_name: string;
+ icon: ObjectIcon;
+ Layout: TypeLayout;
+ Properties: PropertyLink[];
+}
+
+export interface UpdateTypeRequest {
+ name?: string;
+ plural_name?: string;
+ icon?: ObjectIcon;
+ layout?: TypeLayout;
+ properties?: PropertyLink[];
+}
export interface RawType {
object: string;
id: string;
key: string;
name: string;
+ plural_name: string;
icon: ObjectIcon;
- recommended_layout: string;
+ layout: ObjectLayout;
archived: boolean;
+ properties: RawProperty[];
}
export interface Type extends Omit {
diff --git a/extensions/anytype/src/search-anytype.tsx b/extensions/anytype/src/search-anytype.tsx
index 36641a76d06..be8ec5ec7c5 100644
--- a/extensions/anytype/src/search-anytype.tsx
+++ b/extensions/anytype/src/search-anytype.tsx
@@ -1,9 +1,11 @@
-import { Icon, Image, List, showToast, Toast } from "@raycast/api";
+import { Icon, Image, List } from "@raycast/api";
+import { showFailureToast } from "@raycast/utils";
import { useEffect, useMemo, useState } from "react";
import { EmptyViewObject, EnsureAuthenticated, ObjectListItem, ViewType } from "./components";
import { useGlobalSearch, usePinnedObjects, useSpaces } from "./hooks";
import { SpaceObject } from "./models";
import {
+ bundledTypeKeys,
defaultTintColor,
fetchTypeKeysForLists,
fetchTypeKeysForPages,
@@ -89,18 +91,16 @@ function Search() {
[ViewType.pages]: typeKeysForPages,
[ViewType.tasks]: typeKeysForTasks,
[ViewType.lists]: typeKeysForLists,
- [ViewType.bookmarks]: ["ot-bookmark"],
+ [ViewType.bookmarks]: [bundledTypeKeys.bookmark],
};
setTypes(viewToType[currentView] ?? []);
}, [currentView, typeKeysForPages, typeKeysForTasks]);
useEffect(() => {
if (objectsError || spacesError || pinnedObjectsError) {
- showToast(
- Toast.Style.Failure,
- "Failed to fetch latest data",
- objectsError?.message || spacesError?.message || pinnedObjectsError?.message,
- );
+ showFailureToast(objectsError || spacesError || pinnedObjectsError, {
+ title: "Failed to fetch latest data",
+ });
}
}, [objectsError, spacesError, pinnedObjectsError]);
@@ -138,7 +138,6 @@ function Search() {
// Process pinned objects and filter by search term
const processedPinnedObjects = pinnedObjects?.length
? pinnedObjects
- // TODO: decide on wanted behavior for pinned objects
.filter((object) => types.length === 0 || types.includes(object.type.key))
.filter((object) => filterObjectsBySearchTerm([object], searchText).length > 0)
.map((object) => processObjectWithSpaceIcon(object, true))
@@ -209,11 +208,13 @@ function Search() {
subtitle={object.subtitle}
accessories={object.accessories}
mutate={[mutateObjects, mutatePinnedObjects]}
+ object={object.object}
layout={object.layout}
viewType={currentView}
isGlobalSearch={true}
isNoPinView={false}
isPinned={object.isPinned}
+ searchText={searchText}
/>
))}
@@ -233,11 +234,13 @@ function Search() {
subtitle={object.subtitle}
accessories={object.accessories}
mutate={[mutateObjects, mutatePinnedObjects]}
+ object={object.object}
layout={object.layout}
viewType={currentView}
isGlobalSearch={true}
isNoPinView={false}
isPinned={object.isPinned}
+ searchText={searchText}
/>
))}
diff --git a/extensions/anytype/src/tools/add-to-list.ts b/extensions/anytype/src/tools/add-to-list.ts
index f9f82414168..d1d27b2b1f4 100644
--- a/extensions/anytype/src/tools/add-to-list.ts
+++ b/extensions/anytype/src/tools/add-to-list.ts
@@ -9,7 +9,7 @@ type Input = {
/**
* The unique identifier of the list to add the object to.
- * This value can be obtained from the `search-anytype` or `search-space` tools by searching for type of `ot-collection`.
+ * This value can be obtained from the `search-anytype` or `search-space` tools by searching for type of 'collection'.
*/
listId: string;
diff --git a/extensions/anytype/src/tools/approve-member.ts b/extensions/anytype/src/tools/approve-member.ts
index 1757d713de6..6b063e01ffe 100644
--- a/extensions/anytype/src/tools/approve-member.ts
+++ b/extensions/anytype/src/tools/approve-member.ts
@@ -56,11 +56,11 @@ export const confirmation: Tool.Confirmation = async (input) => {
info: [
{
name: "Space",
- value: s.space?.name,
+ value: s.space.name,
},
{
name: "Name",
- value: m.member?.name,
+ value: m.member.name,
},
{
name: "Role",
diff --git a/extensions/anytype/src/tools/create-object.ts b/extensions/anytype/src/tools/create-object.ts
index c7443422d42..37c0b78f378 100644
--- a/extensions/anytype/src/tools/create-object.ts
+++ b/extensions/anytype/src/tools/create-object.ts
@@ -1,6 +1,7 @@
import { Tool } from "@raycast/api";
import { createObject, getSpace, getType } from "../api";
-import { IconFormat } from "../models";
+import { IconFormat, PropertyFormat, PropertyLinkWithValue } from "../models";
+import { bundledPropKeys } from "../utils";
type Input = {
/**
@@ -49,7 +50,7 @@ type Input = {
body?: string;
/**
- * The URL of the bookmark, applicable when creating an object with type_key='ot-bookmark'.
+ * The URL of the bookmark, applicable when creating an object with type_key='bookmark'.
* This value should be chosen based on the user's input.
* If not given, set as an empty string.
*/
@@ -60,17 +61,34 @@ type Input = {
* Create a new object in the specified space.
* This function creates an object with the specified details in the specified space.
* The object is created with the specified name, icon, description, body.
- * When creating objects of type 'ot-bookmark', ensure the source URL is provided. The icon, name, and description should not be manually set, as they will be automatically populated upon fetching the URL.
+ * When creating objects of type 'bookmark', ensure the source URL is provided. The icon, name, and description should not be manually set, as they will be automatically populated upon fetching the URL.
*/
export default async function tool({ spaceId, type_key, name, icon, description, body, source }: Input) {
+ // TODO: implement properties key-value parsing
+ const propertyEntries: PropertyLinkWithValue[] = [];
+ if (description) {
+ propertyEntries.push({
+ key: bundledPropKeys.description,
+ format: PropertyFormat.Text,
+ text: description,
+ });
+ }
+
+ if (source) {
+ propertyEntries.push({
+ key: bundledPropKeys.source,
+ format: PropertyFormat.Url,
+ url: source,
+ });
+ }
+
const { object } = await createObject(spaceId, {
name: name || "",
icon: { format: IconFormat.Emoji, emoji: icon || "" },
- description: description || "",
body: body || "",
- source: source || "",
template_id: "", // not supported here
type_key: type_key,
+ properties: propertyEntries,
});
if (!object) {
@@ -100,11 +118,11 @@ export const confirmation: Tool.Confirmation = async (input) => {
info: [
{
name: "Space",
- value: s.space?.name,
+ value: s.space.name,
},
{
name: "Type",
- value: t.type?.name || "",
+ value: t.type.name || "",
},
{
name: "Name",
diff --git a/extensions/anytype/src/tools/get-list-items.ts b/extensions/anytype/src/tools/get-list-items.ts
index fce957b67e9..d33e7a67410 100644
--- a/extensions/anytype/src/tools/get-list-items.ts
+++ b/extensions/anytype/src/tools/get-list-items.ts
@@ -10,7 +10,7 @@ type Input = {
/**
* The unique identifier of the list to get items from.
- * This value can be obtained from the `search-space`or `search-anytype` tool and specifying types as 'ot-collection'.
+ * This value can be obtained from the `search-space`or `search-anytype` tool and specifying types as 'collection'.
*/
listId: string;
};
diff --git a/extensions/anytype/src/tools/get-object.ts b/extensions/anytype/src/tools/get-object.ts
index 8849269b187..c94da750161 100644
--- a/extensions/anytype/src/tools/get-object.ts
+++ b/extensions/anytype/src/tools/get-object.ts
@@ -1,5 +1,5 @@
-import { getExport, getObject } from "../api";
-import { ExportFormat } from "../models";
+import { getObject } from "../api";
+import { BodyFormat } from "../models";
type Input = {
/**
@@ -21,16 +21,9 @@ type Input = {
* that matches the specified ID.
*/
export default async function tool({ spaceId, objectId }: Input) {
- const { object } = await getObject(spaceId, objectId);
- const { markdown } = await getExport(spaceId, objectId, ExportFormat.Markdown);
+ const { object } = await getObject(spaceId, objectId, BodyFormat.Markdown);
- if (!object) {
- return {
- markdown,
- };
- }
-
- const results = {
+ return {
object: object.object,
name: object.name,
id: object.id,
@@ -41,10 +34,6 @@ export default async function tool({ spaceId, objectId }: Input) {
type_key: object.type.key,
},
properties: object.properties,
- };
-
- return {
- results,
- markdown,
+ markdown: object.markdown,
};
}
diff --git a/extensions/anytype/src/tools/get-types.ts b/extensions/anytype/src/tools/get-types.ts
index 3d08b3dd094..fbfc1d7bdd7 100644
--- a/extensions/anytype/src/tools/get-types.ts
+++ b/extensions/anytype/src/tools/get-types.ts
@@ -26,12 +26,12 @@ export default async function tool({ spaceId }: Input) {
offset += apiLimitMax;
}
- const results = allTypes.map(({ object, name, id, key: type_key, recommended_layout }) => ({
+ const results = allTypes.map(({ object, name, id, key: type_key, layout }) => ({
object,
name,
id,
type_key,
- recommended_layout,
+ layout,
}));
return {
diff --git a/extensions/anytype/src/tools/reject-member.ts b/extensions/anytype/src/tools/reject-member.ts
index c4bff6fcb65..01ae28f2155 100644
--- a/extensions/anytype/src/tools/reject-member.ts
+++ b/extensions/anytype/src/tools/reject-member.ts
@@ -48,11 +48,11 @@ export const confirmation: Tool.Confirmation = async (input) => {
info: [
{
name: "Space",
- value: s.space?.name,
+ value: s.space.name,
},
{
name: "Name",
- value: m.member?.name,
+ value: m.member.name,
},
],
};
diff --git a/extensions/anytype/src/tools/remove-from-list.ts b/extensions/anytype/src/tools/remove-from-list.ts
index dcf79164c6f..68533d14663 100644
--- a/extensions/anytype/src/tools/remove-from-list.ts
+++ b/extensions/anytype/src/tools/remove-from-list.ts
@@ -9,7 +9,7 @@ type Input = {
/**
* The unique identifier of the list to remove the object from.
- * This value can be obtained from the `search-space` or `search-anytype` tools by searching for type of `ot-collection`.
+ * This value can be obtained from the `search-space` or `search-anytype` tools by searching for type of 'collection'.
*/
listId: string;
diff --git a/extensions/anytype/src/tools/remove-member.ts b/extensions/anytype/src/tools/remove-member.ts
index 9dd7665cd93..f588b85eabe 100644
--- a/extensions/anytype/src/tools/remove-member.ts
+++ b/extensions/anytype/src/tools/remove-member.ts
@@ -48,11 +48,11 @@ export const confirmation: Tool.Confirmation = async (input) => {
info: [
{
name: "Space",
- value: s.space?.name,
+ value: s.space.name,
},
{
name: "Name",
- value: m.member?.name,
+ value: m.member.name,
},
],
};
diff --git a/extensions/anytype/src/tools/search-anytype.ts b/extensions/anytype/src/tools/search-anytype.ts
index c83dae112b3..eac6897aa80 100644
--- a/extensions/anytype/src/tools/search-anytype.ts
+++ b/extensions/anytype/src/tools/search-anytype.ts
@@ -1,5 +1,5 @@
import { globalSearch } from "../api";
-import { SortDirection, SortProperty } from "../models";
+import { SortDirection, SortOptions, SortProperty } from "../models";
import { apiLimit } from "../utils";
type Input = {
@@ -12,7 +12,7 @@ type Input = {
/**
* The types of objects to search for, identified by their type_key or id.
* This value can be obtained from the `getTypes` tool.
- * When user asks for 'list' objects, search for 'ot-set' and 'ot-collection' types.
+ * When user asks for 'list' objects, search for 'set' and 'collection' types.
* If no types are specified, the search will include all types of objects.
*/
types?: string[];
@@ -34,7 +34,7 @@ type Input = {
* This value can be "last_modified_date", "last_opened_date", "created_date" or "name".
* Default value is "last_modified_date".
*/
- property?: SortProperty;
+ propertyKey?: SortProperty;
};
};
@@ -46,8 +46,8 @@ type Input = {
*/
export default async function tool({ query, types, sort }: Input) {
types = types ?? [];
- const sortOptions = {
- property: sort?.property ?? SortProperty.LastModifiedDate,
+ const sortOptions: SortOptions = {
+ property_key: sort?.propertyKey ?? SortProperty.LastModifiedDate,
direction: sort?.direction ?? SortDirection.Descending,
};
diff --git a/extensions/anytype/src/tools/search-space.ts b/extensions/anytype/src/tools/search-space.ts
index 41f5ad04710..b88aa0a6c38 100644
--- a/extensions/anytype/src/tools/search-space.ts
+++ b/extensions/anytype/src/tools/search-space.ts
@@ -1,5 +1,5 @@
import { search } from "../api";
-import { SortDirection, SortProperty } from "../models";
+import { SortDirection, SortOptions, SortProperty } from "../models";
import { apiLimit } from "../utils";
type Input = {
@@ -18,7 +18,7 @@ type Input = {
/**
* The types of objects to search for, identified by their type_key or id.
* This value should be obtained from the `getTypes` tool and must be called if users request to search for objects of a certain type.
- * When user asks for 'list' objects, search for 'ot-set' and 'ot-collection' types.
+ * When user asks for 'list' objects, search for 'set' and 'collection' types.
* If no types are specified, the search will include all types of objects.
*/
types?: string[];
@@ -40,7 +40,7 @@ type Input = {
* This value can be "last_modified_date", "last_opened_date", "created_date" or "name".
* Default value is "last_modified_date".
*/
- property?: SortProperty;
+ propertyKey?: SortProperty;
};
};
@@ -52,8 +52,8 @@ type Input = {
*/
export default async function tool({ spaceId, query, types, sort }: Input) {
types = types ?? [];
- const sortOptions = {
- property: sort?.property ?? SortProperty.LastModifiedDate,
+ const sortOptions: SortOptions = {
+ property_key: sort?.propertyKey ?? SortProperty.LastModifiedDate,
direction: sort?.direction ?? SortDirection.Descending,
};
diff --git a/extensions/anytype/src/tools/update-member.ts b/extensions/anytype/src/tools/update-member.ts
index c58ab031227..d5bde8453cc 100644
--- a/extensions/anytype/src/tools/update-member.ts
+++ b/extensions/anytype/src/tools/update-member.ts
@@ -55,11 +55,11 @@ export const confirmation: Tool.Confirmation = async (input) => {
info: [
{
name: "Space",
- value: s.space?.name,
+ value: s.space.name,
},
{
name: "Name",
- value: m.member?.name,
+ value: m.member.name,
},
{
name: "New Role",
diff --git a/extensions/anytype/src/utils/constant.ts b/extensions/anytype/src/utils/constant.ts
index a2c6733b7dc..39fadceffdc 100644
--- a/extensions/anytype/src/utils/constant.ts
+++ b/extensions/anytype/src/utils/constant.ts
@@ -1,9 +1,10 @@
import { getPreferenceValues } from "@raycast/api";
import { ViewType } from "../components";
+import { BodyFormat } from "../models";
import { encodeQueryParams } from "./query";
// Strings
-export const apiAppName = "raycast_v1_0125";
+export const apiAppName = "raycast_v3_0425";
export const anytypeNetwork = "N83gJpVd9MuNRZAuJLZ7LiMntTThhPc6DtzWWVjb1M3PouVU";
export const errorConnectionMessage = "Can't connect to API. Please ensure Anytype is running and reachable.";
@@ -13,7 +14,7 @@ export const downloadUrl = "https://download.anytype.io/";
export const anytypeSpaceDeeplink = (spaceId: string) => `anytype://main/object/_blank_/space.id/${spaceId}`;
// Numbers
-export const currentApiVersion = "2025-03-17";
+export const currentApiVersion = "2025-04-22";
export const apiLimit = getPreferenceValues().limit;
export const apiLimitMax = 1000;
export const iconWidth = 64;
@@ -32,8 +33,48 @@ export const localStorageKeys = {
},
};
+export const apiKeyPrefixes = {
+ properties: "",
+ types: "",
+ tags: "",
+};
+
+// API Property/Type Keys
+export const bundledPropKeys = {
+ description: `${apiKeyPrefixes.properties}description`,
+ type: `${apiKeyPrefixes.properties}type`,
+ addedDate: `${apiKeyPrefixes.properties}added_date`,
+ createdDate: `${apiKeyPrefixes.properties}created_date`,
+ createdBy: `${apiKeyPrefixes.properties}creator`,
+ lastModifiedDate: `${apiKeyPrefixes.properties}last_modified_date`,
+ lastModifiedBy: `${apiKeyPrefixes.properties}last_modified_by`,
+ lastOpenedDate: `${apiKeyPrefixes.properties}last_opened_date`,
+ links: `${apiKeyPrefixes.properties}links`,
+ backlinks: `${apiKeyPrefixes.properties}backlinks`,
+ source: `${apiKeyPrefixes.properties}source`,
+};
+
+export const bundledTypeKeys = {
+ audio: `${apiKeyPrefixes.types}audio`,
+ bookmark: `${apiKeyPrefixes.types}bookmark`,
+ chat: `${apiKeyPrefixes.types}chat`,
+ collection: `${apiKeyPrefixes.types}collection`,
+ file: `${apiKeyPrefixes.types}file`,
+ note: `${apiKeyPrefixes.types}note`,
+ image: `${apiKeyPrefixes.types}image`,
+ object_type: `${apiKeyPrefixes.types}object_type`,
+ page: `${apiKeyPrefixes.types}page`,
+ participant: `${apiKeyPrefixes.types}participant`,
+ profile: `${apiKeyPrefixes.types}profile`,
+ set: `${apiKeyPrefixes.types}set`,
+ tag: `${apiKeyPrefixes.types}tag`,
+ task: `${apiKeyPrefixes.types}task`,
+ template: `${apiKeyPrefixes.types}template`,
+ video: `${apiKeyPrefixes.types}video`,
+};
+
// Colors
-export const colorMap: { [key: string]: string } = {
+export const colorToHex: { [key: string]: string } = {
grey: "#b6b6b6",
yellow: "#ecd91b",
orange: "#ffb522",
@@ -45,6 +86,18 @@ export const colorMap: { [key: string]: string } = {
teal: "#0fc8ba",
lime: "#5dd400",
};
+export const hexToColor: { [key: string]: string } = {
+ "#b6b6b6": "grey",
+ "#ecd91b": "yellow",
+ "#ffb522": "orange",
+ "#f55522": "red",
+ "#e51ca0": "pink",
+ "#ab50cc": "purple",
+ "#3e58eb": "blue",
+ "#2aa7ee": "ice",
+ "#0fc8ba": "teal",
+ "#5dd400": "lime",
+};
export const defaultTintColor = { light: "black", dark: "white" };
// API Endpoints
@@ -59,12 +112,6 @@ export const apiEndpoints = {
method: "POST",
}),
- // export
- getExport: (spaceId: string, objectId: string, format: string) => ({
- url: `${apiUrl}/spaces/${spaceId}/objects/${objectId}/${format}`,
- method: "GET",
- }),
-
// lists
getListViews: (spaceId: string, listId: string, options: { offset: number; limit: number }) => ({
url: `${apiUrl}/spaces/${spaceId}/lists/${listId}/views${encodeQueryParams(options)}`,
@@ -84,22 +131,74 @@ export const apiEndpoints = {
}),
// objects
+ getObject: (spaceId: string, objectId: string, format: BodyFormat) => ({
+ url: `${apiUrl}/spaces/${spaceId}/objects/${objectId}${encodeQueryParams({ format })}`,
+ method: "GET",
+ }),
+ getObjects: (spaceId: string, options: { offset: number; limit: number }) => ({
+ url: `${apiUrl}/spaces/${spaceId}/objects${encodeQueryParams(options)}`,
+ method: "GET",
+ }),
createObject: (spaceId: string) => ({
url: `${apiUrl}/spaces/${spaceId}/objects`,
method: "POST",
}),
+ updateObject: (spaceId: string, objectId: string) => ({
+ url: `${apiUrl}/spaces/${spaceId}/objects/${objectId}`,
+ method: "PATCH",
+ }),
deleteObject: (spaceId: string, objectId: string) => ({
url: `${apiUrl}/spaces/${spaceId}/objects/${objectId}`,
method: "DELETE",
}),
- getObject: (spaceId: string, objectId: string) => ({
- url: `${apiUrl}/spaces/${spaceId}/objects/${objectId}`,
+ getExport: (spaceId: string, objectId: string, format: string) => ({
+ url: `${apiUrl}/spaces/${spaceId}/objects/${objectId}/${format}`,
method: "GET",
}),
- getObjects: (spaceId: string, options: { offset: number; limit: number }) => ({
- url: `${apiUrl}/spaces/${spaceId}/objects${encodeQueryParams(options)}`,
+
+ // properties
+ getProperties: (spaceId: string, options: { offset: number; limit: number }) => ({
+ url: `${apiUrl}/spaces/${spaceId}/properties${encodeQueryParams(options)}`,
+ method: "GET",
+ }),
+ getProperty: (spaceId: string, propertyId: string) => ({
+ url: `${apiUrl}/spaces/${spaceId}/properties/${propertyId}`,
method: "GET",
}),
+ createProperty: (spaceId: string) => ({
+ url: `${apiUrl}/spaces/${spaceId}/properties`,
+ method: "POST",
+ }),
+ updateProperty: (spaceId: string, propertyId: string) => ({
+ url: `${apiUrl}/spaces/${spaceId}/properties/${propertyId}`,
+ method: "PATCH",
+ }),
+ deleteProperty: (spaceId: string, propertyId: string) => ({
+ url: `${apiUrl}/spaces/${spaceId}/properties/${propertyId}`,
+ method: "DELETE",
+ }),
+
+ // tags
+ getTags: (spaceId: string, propertyId: string, options: { offset: number; limit: number }) => ({
+ url: `${apiUrl}/spaces/${spaceId}/properties/${propertyId}/tags${encodeQueryParams(options)}`,
+ method: "GET",
+ }),
+ getTag: (spaceId: string, propertyId: string, tagId: string) => ({
+ url: `${apiUrl}/spaces/${spaceId}/properties/${propertyId}/tags/${tagId}`,
+ method: "GET",
+ }),
+ createTag: (spaceId: string, propertyId: string) => ({
+ url: `${apiUrl}/spaces/${spaceId}/properties/${propertyId}/tags`,
+ method: "POST",
+ }),
+ updateTag: (spaceId: string, propertyId: string, tagId: string) => ({
+ url: `${apiUrl}/spaces/${spaceId}/properties/${propertyId}/tags/${tagId}`,
+ method: "PATCH",
+ }),
+ deleteTag: (spaceId: string, propertyId: string, tagId: string) => ({
+ url: `${apiUrl}/spaces/${spaceId}/properties/${propertyId}/tags/${tagId}`,
+ method: "DELETE",
+ }),
// search
globalSearch: (options: { offset: number; limit: number }) => ({
@@ -112,10 +211,6 @@ export const apiEndpoints = {
}),
// spaces
- createSpace: {
- url: `${apiUrl}/spaces`,
- method: "POST",
- },
getSpace: (spaceId: string) => ({
url: `${apiUrl}/spaces/${spaceId}`,
method: "GET",
@@ -124,6 +219,14 @@ export const apiEndpoints = {
url: `${apiUrl}/spaces${encodeQueryParams(options)}`,
method: "GET",
}),
+ createSpace: {
+ url: `${apiUrl}/spaces`,
+ method: "POST",
+ },
+ updateSpace: (spaceId: string) => ({
+ url: `${apiUrl}/spaces/${spaceId}`,
+ method: "PATCH",
+ }),
// members
getMember: (spaceId: string, objectId: string) => ({
@@ -149,6 +252,18 @@ export const apiEndpoints = {
url: `${apiUrl}/spaces/${spaceId}/types${encodeQueryParams(options)}`,
method: "GET",
}),
+ createType: (spaceId: string) => ({
+ url: `${apiUrl}/spaces/${spaceId}/types`,
+ method: "POST",
+ }),
+ updateType: (spaceId: string, typeId: string) => ({
+ url: `${apiUrl}/spaces/${spaceId}/types/${typeId}`,
+ method: "PATCH",
+ }),
+ deleteType: (spaceId: string, typeId: string) => ({
+ url: `${apiUrl}/spaces/${spaceId}/types/${typeId}`,
+ method: "DELETE",
+ }),
// templates
getTemplate: (spaceId: string, typeId: string, templateId: string) => ({
diff --git a/extensions/anytype/src/utils/form.ts b/extensions/anytype/src/utils/form.ts
new file mode 100644
index 00000000000..4ae66438201
--- /dev/null
+++ b/extensions/anytype/src/utils/form.ts
@@ -0,0 +1,23 @@
+import { PropertyFormat, RawPropertyWithValue } from "../models";
+
+/**
+ * Form validation to ensure that that number fields contain only numbers.
+ */
+export function getNumberFieldValidations(
+ properties: RawPropertyWithValue[],
+): Record string | undefined> {
+ return properties
+ .filter((prop) => prop.format === PropertyFormat.Number)
+ .reduce(
+ (acc, prop) => {
+ acc[prop.key] = (value: unknown) => {
+ const str = typeof value === "string" ? value : undefined;
+ if (str && isNaN(Number(str))) {
+ return "Value must be a number";
+ }
+ };
+ return acc;
+ },
+ {} as Record string | undefined>,
+ );
+}
diff --git a/extensions/anytype/src/utils/icon.ts b/extensions/anytype/src/utils/icon.ts
index 5deafe0b1ac..d99b966159a 100644
--- a/extensions/anytype/src/utils/icon.ts
+++ b/extensions/anytype/src/utils/icon.ts
@@ -1,7 +1,7 @@
import { Icon, Image } from "@raycast/api";
import fetch from "node-fetch";
-import { ObjectIcon, RawType } from "../models";
-import { colorMap, iconWidth } from "./constant";
+import { IconFormat, ObjectIcon, ObjectLayout, RawType } from "../models";
+import { colorToHex, iconWidth } from "./constant";
/**
* Determine which icon to show for a given Object. Icon can be url or emoji.
@@ -13,31 +13,31 @@ import { colorMap, iconWidth } from "./constant";
export async function getIconWithFallback(icon: ObjectIcon, layout: string, type?: RawType): Promise {
if (icon && icon.format) {
// type built-in icons
- if (icon.format === "icon" && icon.name) {
- return await getCustomTypeIcon(icon.name, icon.color);
+ if (icon.format === IconFormat.Icon && icon.name) {
+ return getCustomTypeIcon(icon.name, icon.color);
}
// file reference
- if (icon.format === "file" && icon.file) {
+ if (icon.format === IconFormat.File && icon.file) {
const fileSource = await getFile(icon.file);
if (fileSource) {
return { source: fileSource, mask: getMaskForObject(icon.file, layout) };
}
- if (type?.icon.format === "icon" && type?.icon.name) {
- return await getCustomTypeIcon(type.icon.name, "grey");
+ if (type?.icon.format === IconFormat.Icon && type?.icon.name) {
+ return getCustomTypeIcon(type.icon.name, "grey");
}
return await fallbackToLayout(layout);
}
// regular emoji
- if (icon.format === "emoji" && icon.emoji) {
+ if (icon.format === IconFormat.Emoji && icon.emoji) {
return icon.emoji;
}
}
// fallback to grey version of type built-in icon
- if (type?.icon && type.icon.format === "icon" && type.icon.name) {
- return await getCustomTypeIcon(type?.icon.name, "grey");
+ if (type?.icon && type.icon.format === IconFormat.Icon && type.icon.name) {
+ return getCustomTypeIcon(type?.icon.name, "grey");
}
// fallback to layout
@@ -51,23 +51,23 @@ export async function getIconWithFallback(icon: ObjectIcon, layout: string, type
*/
async function fallbackToLayout(layout: string): Promise {
switch (layout) {
- case "todo":
- return await getCustomTypeIcon("checkbox", "grey");
- case "set":
- case "collection":
- return await getCustomTypeIcon("layers", "grey");
- case "participant":
- return await getCustomTypeIcon("person", "grey");
- case "bookmark":
- return await getCustomTypeIcon("bookmark", "grey");
+ case ObjectLayout.Action:
+ return getCustomTypeIcon("checkbox", "grey");
+ case ObjectLayout.Set:
+ case ObjectLayout.Collection:
+ return getCustomTypeIcon("layers", "grey");
+ case ObjectLayout.Participant:
+ return getCustomTypeIcon("person", "grey");
+ case ObjectLayout.Bookmark:
+ return getCustomTypeIcon("bookmark", "grey");
case "type":
- return await getCustomTypeIcon("extension-puzzle", "grey");
+ return getCustomTypeIcon("extension-puzzle", "grey");
case "template":
- return await getCustomTypeIcon("copy", "grey");
+ return getCustomTypeIcon("copy", "grey");
case "space":
return Icon.BullsEye;
default:
- return await getCustomTypeIcon("document", "grey");
+ return getCustomTypeIcon("document", "grey");
}
}
@@ -77,12 +77,12 @@ async function fallbackToLayout(layout: string): Promise {
* @param color The color of the icon.
* @returns The base64 data URI of the icon.
*/
-export async function getCustomTypeIcon(name: string, color?: string): Promise {
+export function getCustomTypeIcon(name: string, color?: string): Image.ImageLike {
return {
source: `icons/type/${name}.svg`,
tintColor: {
- light: colorMap[color || "grey"],
- dark: colorMap[color || "grey"],
+ light: colorToHex[color || "grey"],
+ dark: colorToHex[color || "grey"],
},
};
}
@@ -132,7 +132,7 @@ export async function fetchWithTimeout(url: string, timeout: number): Promise,
- mutatePinnedObjects?: MutatePromise,
+ mutateObjects: MutatePromise,
+ mutatePinnedObjects?: MutatePromise,
) {
const { sort } = getPreferenceValues();
// If sort is 'Name', fall back to using 'LastModifiedDate' for date details
const sortForDate = sort === SortProperty.Name ? SortProperty.LastModifiedDate : sort;
- const dateProperty = object.properties.find((property) => property.id === sortForDate);
+ const dateProperty = object.properties.find((property) => property.key === sortForDate);
const date = dateProperty && dateProperty.date ? dateProperty.date : undefined;
const hasValidDate = date && new Date(date).getTime() !== 0;
@@ -40,8 +40,10 @@ export function processObject(
text: hasValidDate ? undefined : "—",
},
],
- mutate: [mutateObjects, mutatePinnedObjects].filter(Boolean) as MutatePromise[],
- member: undefined,
+ mutate: [mutateObjects, mutatePinnedObjects].filter(Boolean) as MutatePromise<
+ SpaceObject[] | Type[] | Property[] | Member[]
+ >[],
+ object: object,
layout: object.layout,
isPinned,
};
diff --git a/extensions/anytype/src/utils/string.ts b/extensions/anytype/src/utils/string.ts
index a282b8e495b..0669d1f3103 100644
--- a/extensions/anytype/src/utils/string.ts
+++ b/extensions/anytype/src/utils/string.ts
@@ -1,4 +1,5 @@
import { getPreferenceValues, Image } from "@raycast/api";
+import emojiRegex from "emoji-regex";
import { MemberRole, SortProperty } from "../models";
/**
@@ -9,7 +10,12 @@ export function pluralize(
noun: string,
{ suffix = "s", withNumber = false }: { suffix?: string; withNumber?: boolean } = {},
): string {
- const pluralizedNoun = `${noun}${count !== 1 ? suffix : ""}`;
+ let pluralizedNoun;
+ if (noun.endsWith("y") && count !== 1) {
+ pluralizedNoun = `${noun.slice(0, -1)}ies`;
+ } else {
+ pluralizedNoun = `${noun}${count !== 1 ? suffix : ""}`;
+ }
return withNumber ? `${count} ${pluralizedNoun}` : pluralizedNoun;
}
@@ -20,7 +26,7 @@ export function getDateLabel(): string {
const { sort } = getPreferenceValues();
switch (sort) {
case SortProperty.CreatedDate:
- return "Created Date";
+ return "Creation Date";
case SortProperty.LastModifiedDate:
return "Last Modified Date";
case SortProperty.LastOpenedDate:
@@ -82,7 +88,18 @@ export function formatMemberRole(role: string): string {
export function injectEmojiIntoHeading(markdown: string, icon?: Image.ImageLike): string {
if (typeof icon !== "string") return markdown;
const trimmedIcon = icon.trim();
- const emojiRegex = /^(?:\p{Extended_Pictographic}(?:\p{Grapheme_Extend}|\u200D\p{Extended_Pictographic})*)+$/u;
- if (!emojiRegex.test(trimmedIcon)) return markdown;
+ if (!isEmoji(trimmedIcon)) return markdown;
return markdown.replace(/^(#+) (.*)/, (_, hashes, heading) => `${hashes} ${trimmedIcon} ${heading}`);
}
+
+/**
+ * Checks if a string is a valid emoji.
+ *
+ * @param s The string to check.
+ * @returns True if the string is a valid emoji, false otherwise.
+ */
+export function isEmoji(s: string) {
+ const re = emojiRegex();
+ const match = re.exec(s);
+ return match !== null && match[0] === s;
+}
diff --git a/extensions/anytype/src/utils/type.ts b/extensions/anytype/src/utils/type.ts
index 2fe2d7d91b2..88fb05b2c96 100644
--- a/extensions/anytype/src/utils/type.ts
+++ b/extensions/anytype/src/utils/type.ts
@@ -1,13 +1,6 @@
import { getTemplates, getTypes } from "../api";
-import { Space, Template, Type } from "../models";
-import { apiLimitMax } from "../utils";
-
-/**
- * Checks if a given `Type` is a list type.
- */
-export function typeIsList(layout: string): boolean {
- return layout === "set" || layout === "collection";
-}
+import { ObjectLayout, Space, SpaceObject, Type } from "../models";
+import { apiKeyPrefixes, apiLimitMax, bundledTypeKeys } from "../utils";
/**
* Fetches all `Type`s from a single space, doing pagination if necessary.
@@ -46,8 +39,8 @@ export async function getAllTypesFromSpaces(spaces: Space[]): Promise {
/**
* Fetches all `Template`s from a single space and type, doing pagination if necessary.
*/
-export async function fetchAllTemplatesForSpace(spaceId: string, typeId: string): Promise {
- const allTemplates: Template[] = [];
+export async function fetchAllTemplatesForSpace(spaceId: string, typeId: string): Promise {
+ const allTemplates: SpaceObject[] = [];
let hasMore = true;
let offset = 0;
@@ -66,52 +59,68 @@ export async function fetchAllTemplatesForSpace(spaceId: string, typeId: string)
*/
export async function fetchTypeKeysForPages(
spaces: Space[],
- uniqueKeysForTasks: string[],
- uniqueKeysForLists: string[],
+ typeKeysForTasks: string[],
+ typeKeysForLists: string[],
): Promise {
const excludedKeysForPages = new Set([
// not shown anywhere
- "ot-audio",
- "ot-chat",
- "ot-file",
- "ot-image",
- "ot-objectType",
- "ot-tag",
- "ot-template",
- "ot-video",
+ bundledTypeKeys.audio,
+ bundledTypeKeys.chat,
+ bundledTypeKeys.file,
+ bundledTypeKeys.image,
+ bundledTypeKeys.object_type,
+ bundledTypeKeys.tag,
+ bundledTypeKeys.template,
+ bundledTypeKeys.video,
// shown in other views
- "ot-set",
- "ot-collection",
- "ot-bookmark",
- "ot-participant",
- ...uniqueKeysForTasks,
- ...uniqueKeysForLists,
+ bundledTypeKeys.set,
+ bundledTypeKeys.collection,
+ bundledTypeKeys.bookmark,
+ bundledTypeKeys.participant,
+ ...typeKeysForTasks,
+ ...typeKeysForLists,
]);
const allTypes = await getAllTypesFromSpaces(spaces);
- const uniqueKeysSet = new Set(allTypes.map((type) => type.key).filter((key) => !excludedKeysForPages.has(key)));
- return Array.from(uniqueKeysSet);
+ const pageTypeKeys = new Set(allTypes.map((type) => type.key).filter((key) => !excludedKeysForPages.has(key)));
+ return Array.from(pageTypeKeys);
}
/**
- * Fetches all unique type keys for task types.
+ * Fetches all type keys for task types.
*/
export async function fetchTypesKeysForTasks(spaces: Space[]): Promise {
const tasksTypes = await getAllTypesFromSpaces(spaces);
- const uniqueKeys = new Set(tasksTypes.filter((type) => type.recommended_layout === "todo").map((type) => type.key));
- return Array.from(uniqueKeys);
+ const taskTypeKeys = new Set(
+ tasksTypes.filter((type) => type.layout === ObjectLayout.Action).map((type) => type.key),
+ );
+ return Array.from(taskTypeKeys);
}
/**
- * Fetches all unique type keys for list types.
+ * Fetches all type keys for list types.
*/
export async function fetchTypeKeysForLists(spaces: Space[]): Promise {
const listsTypes = await getAllTypesFromSpaces(spaces);
- const typeKeys = new Set(
+ const listTypeKeys = new Set(
listsTypes
- .filter((type) => type.recommended_layout === "set" || type.recommended_layout === "collection")
+ .filter((type) => type.layout === ObjectLayout.Set || type.layout === ObjectLayout.Collection)
.map((type) => type.key),
);
- return Array.from(typeKeys);
+ return Array.from(listTypeKeys);
+}
+
+/**
+ * Checks if a type is custom user type or not (built-in system type).
+ */
+export function isUserType(key: string): boolean {
+ return apiKeyPrefixes.types.length + 24 === key.length && /\d/.test(key);
+}
+
+/**
+ * Checks if a property is custom user property or not (built-in system property).
+ */
+export function isUserProperty(key: string): boolean {
+ return apiKeyPrefixes.properties.length + 24 === key.length && /\d/.test(key);
}