feat: bilingual support (FA/EN), IRANSans typography, RTL layout, and interactive product detail pages

This commit is contained in:
s.esmaeili
2026-10-04 07:11:52 +03:30
commit a001fa5d5f
302 changed files with 35491 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
import type { LiveClient } from "./core";
/**
* Reads and writes outside React: the same reads as the hooks, as promises,
* plus all writes. Use it in event handlers and plain modules.
*/
export declare const clickup: LiveClient;
+78
View File
@@ -0,0 +1,78 @@
import type { Delta, IsoDate, Ref, UseQueryOptions, UseQueryResult } from "./core";
export declare const COMMENT_TYPES: readonly ["message", "ai", "activity"];
/**
* `name` is a one-line preview of the body. The last ancestor is the task,
* or for a reply the comment it replies to.
*/
export interface Comment extends Ref<"comment"> {
ancestors: Array<Ref<"space" | "folder" | "list" | "task" | "comment">>;
createdAt: IsoDate;
updatedAt: IsoDate;
type: (typeof COMMENT_TYPES)[number];
/** The body; render it with `RichText`. */
body: Delta;
creator: Ref<"user">;
assignee?: Ref<"user">;
/** Set once the comment is resolved; there is no separate resolved flag. */
resolvedBy?: Ref<"user">;
/** One entry per emoji, with the workspace members who reacted. */
reactions: Array<{
emoji: string;
users: Ref<"user">[];
}>;
}
/**
* `resolved` resolves or reopens the comment (read it back through
* `resolvedBy`), `assignee: null` unassigns, and `myReactions` replaces the
* viewer's reactions, leaving everyone else's.
*/
export interface CommentPatch {
/** Replaces the body; read it back as `body`. */
bodyMarkdown?: string;
assignee?: Ref<"user"> | null;
resolved?: boolean;
myReactions?: string[];
}
/** `parent` is the task to comment on, or the comment to reply to. */
export interface CreateCommentInput {
parent: Ref<"task" | "comment">;
/** The body, as markdown. */
bodyMarkdown: string;
assignee?: Ref<"user">;
}
export declare const COMMENT_ORDERS: readonly ["createdAt"];
/**
* Needs `parent` or `ancestor`. The other fields filter and sort on the
* server before `limit` applies.
* @example
* useComments({ ancestor: task, resolved: false, assignee: "me" });
*/
export interface CommentSelector {
/** A task, for its top-level comments; or a comment from `useComments`, for its replies. */
parent?: Ref<"task" | "comment">;
/** A task, for all its comments including replies. */
ancestor?: Ref<"task"> | string;
/** Defaults to 100, at most 500. */
limit?: number;
resolved?: boolean;
/** `"me"` is the viewer. */
assignee?: Ref<"user"> | "me";
creator?: Ref<"user"> | "me";
body?: {
contains: string;
};
/** Oldest first when omitted; `direction` defaults to `"asc"`. */
orderBy?: {
field: (typeof COMMENT_ORDERS)[number];
direction?: "asc" | "desc";
};
}
export interface CommentsApi {
list(selector: CommentSelector): Promise<Comment[]>;
/** Comments on tasks; ids from chats or docs are left out. */
get(targets: Array<Ref<"comment"> | string>): Promise<Comment[]>;
create(input: CreateCommentInput): Promise<Ref<"comment">>;
update(comment: Ref<"comment"> | string, patch: CommentPatch): Promise<void>;
}
export declare function useComments(selector: CommentSelector | Array<Ref<"comment"> | string>, options?: UseQueryOptions<Comment[]>): UseQueryResult<Comment[]>;
+72
View File
@@ -0,0 +1,72 @@
import type { Delta, Ref } from "./core";
import type { CustomField } from "./customFields";
import type { Status, Task } from "./tasks";
import type { ReactElement } from "react";
export interface AvatarProps {
/** A user or team, e.g. `task.creator` or one of `task.assignees`. */
of: Ref<"user" | "team">;
/** Diameter in px; 28 by default. */
size?: number;
className?: string;
}
/** A round avatar: the image, else initials for a user or a people glyph for a team. */
export declare function Avatar({ of, size, className, }: AvatarProps): ReactElement;
export interface AvatarGroupProps {
/** Users and teams, e.g. `task.assignees`. */
of: Array<Ref<"user" | "team">>;
/** How many to show before the rest collapse into a `+N` bubble; 3 by default. */
max?: number;
/** Diameter of each avatar in px; 28 by default. */
size?: number;
className?: string;
}
/** Overlapping avatars with a `+N` bubble for the overflow; renders nothing for an empty list. */
export declare function AvatarGroup({ of, max, size, className, }: AvatarGroupProps): ReactElement | null;
export interface StatusBadgeProps {
/** `task.status`, or one from `useStatuses`. */
status: Status;
className?: string;
}
/** The status name as ClickUp's filled, uppercase badge. Sized by the surrounding font; `className="text-xs"` makes it compact. */
export declare function StatusBadge({ status, className, }: StatusBadgeProps): ReactElement;
export interface PriorityFlagProps {
/** `task.priority`; `undefined` renders nothing unless `showEmpty`. */
priority: Task["priority"];
/** Renders a slashed circle for `undefined`; off by default. */
showEmpty?: boolean;
className?: string;
}
/** ClickUp's priority flag, without a label. Sized by the surrounding font. */
export declare function PriorityFlag({ priority, showEmpty, className, }: PriorityFlagProps): ReactElement | null;
export interface TagProps {
/** One of `task.tags`. */
tag: Task["tags"][number];
className?: string;
}
/** A tag as ClickUp's filled pill. Sized by the surrounding font. */
export declare function Tag({ tag, className }: TagProps): ReactElement;
export interface CustomFieldValueProps {
/** One of `task.customFields`. */
value: Task["customFields"][number];
/** The field's definition from `useCustomFields`, matched by `id`; adds the option colors. */
field?: CustomField;
className?: string;
}
/**
* A custom field value in the shape its type calls for: a check, a number, a
* progress bar, a date, colored option pills, avatars for people, names for
* tasks, links for attachments. Sized by the surrounding font.
*/
export declare function CustomFieldValue({ value, field, className, }: CustomFieldValueProps): ReactElement;
export interface RichTextProps {
/** A task or comment `body`. */
value: Delta;
className?: string;
}
/**
* A body as ClickUp shows it, rendered by the host: mentions, tables, embeds
* and the rest. Empty while the HTML loads; plain text if the host cannot
* render it.
*/
export declare function RichText({ value, className }: RichTextProps): ReactElement;
+155
View File
@@ -0,0 +1,155 @@
import type { CommentsApi } from "./comments";
import type { CustomFieldsApi } from "./customFields";
import type { FoldersApi, ListsApi, SpacesApi } from "./hierarchy";
import type { InboxApi } from "./inbox";
import type { TeamsApi, User, UsersApi } from "./people";
import type { RemindersApi } from "./reminders";
import type { SearchHit, SearchQuery } from "./search";
import type { TasksApi } from "./tasks";
/**
* An ISO 8601 string as `Date#toISOString()` writes it; a plain `YYYY-MM-DD`
* is accepted as input and means that day in the viewer's timezone. A
* required date the host has no value for reads as the Unix epoch,
* `1970-01-01T00:00:00.000Z`.
*/
export type IsoDate = string;
/** Rich text as a Quill delta, plain JSON ops; render it with `RichText`. */
export interface Delta {
ops: ReadonlyArray<Record<string, unknown>>;
}
export declare const ASSET_KINDS: readonly ["space", "folder", "list", "status", "customField", "task", "taskType", "comment", "user", "team", "inboxBundle", "reminder"];
export type AssetKind = (typeof ASSET_KINDS)[number];
/**
* A pointer to an asset; every full asset (a `Task`, a `List`, …) is also a
* `Ref`, so anything a hook returns can be passed where a ref is expected.
* Where a parameter is `Ref | string`, the string is the asset's id.
*/
export interface Ref<K extends AssetKind = AssetKind> {
kind: K;
id: string;
/** Empty when the viewer cannot see the asset, e.g. a related task in a private list. */
name: string;
/**
* Link to the asset in ClickUp; empty for statuses, custom fields and
* reminders. Users and teams link to the workspace's People page.
*/
url: string;
/** Avatar or thumbnail, where the asset has one. */
image?: string;
}
/** `pending` until the first result or failure. */
export type QueryStatus = "pending" | "error" | "success";
/** TanStack Query v5's `useQuery` result. */
export interface UseQueryResult<T> {
/** Undefined until the first result; kept while refetching. */
data: T | undefined;
error: LiveError | null;
status: QueryStatus;
isPending: boolean;
/** Pending and fetching: the first result is on its way. */
isLoading: boolean;
/** True while a request is in flight, including the refresh behind a cached result. */
isFetching: boolean;
/** Fetching with data already showing. */
isRefetching: boolean;
/** True while `data` is the option's `placeholderData`, not a result. */
isPlaceholderData: boolean;
isError: boolean;
isSuccess: boolean;
/** When `data` was read from ClickUp, as a timestamp; 0 without data. */
dataUpdatedAt: number;
/** Reloads from ClickUp. Rarely needed, since writes refetch what they change. */
refetch: () => Promise<void>;
}
export interface UseQueryOptions<T = unknown> {
/** Pass false to skip fetching, e.g. while an input is still unknown. */
enabled?: boolean;
/** Shown as `data` until the first result, e.g. `keepPreviousData` to keep the last rows while a changed selector loads. */
placeholderData?: T | ((previous: T | undefined) => T | undefined);
/** Milliseconds between refetches while the tab is visible; never under 10 000. Rarely needed: writes refetch, and so does returning to the tab. */
refetchInterval?: number;
/** Pass false to wait for fresh data instead of painting the viewer's last result first. Rarely needed: the data refreshes anyway. */
cache?: boolean;
}
/** A `placeholderData` that keeps the previous selector's rows on screen while the new ones load. */
export declare function keepPreviousData<T>(previous: T | undefined): T | undefined;
/**
* - `denied`: the viewer declined the approval in ClickUp.
* - `forbidden`: the host or viewer is not allowed to; hide or disable the feature.
* - `invalid`: the call itself is wrong (selector, id or input); fix the code.
* - `failed`: something went wrong; retry when `retryable`.
*/
export type LiveErrorCode = "denied" | "forbidden" | "invalid" | "failed";
export declare class LiveError extends Error {
readonly code: LiveErrorCode;
/** True for rate limits, timeouts and other transient errors. */
readonly retryable: boolean;
/** The host's raw reason, for debugging. */
readonly reason?: string;
constructor(code: LiveErrorCode, message: string, options?: {
retryable?: boolean;
reason?: string;
});
}
/**
* The same reads as the hooks, as promises, plus all writes. Each changed
* field needs the viewer's approval in ClickUp; a write stops at the first
* denial.
*/
export interface LiveClient {
spaces: SpacesApi;
folders: FoldersApi;
lists: ListsApi;
customFields: CustomFieldsApi;
tasks: TasksApi;
comments: CommentsApi;
users: UsersApi;
teams: TeamsApi;
inbox: InboxApi;
reminders: RemindersApi;
/** Opens a ClickUp url in the app. */
navigate(url: string): Promise<void>;
/** ClickUp's own search, run as the viewer. */
search(query: SearchQuery): Promise<SearchHit[]>;
/** Sends a prompt to Brain, ClickUp's AI, on the user's behalf. */
askBrain(prompt: string): Promise<void>;
/** A task or comment `body` as the HTML ClickUp shows for it; `RichText` renders this. */
toHtml(body: Delta): Promise<string>;
/** The user viewing the artifact, known to the host up front: no read, no loading state. */
readonly viewer: User;
}
export type MutationStatus = "idle" | "pending" | "success" | "error";
export type MutationFunction<TData, TVariables> = (variables: TVariables) => Promise<TData>;
export interface MutationOptions<TData, TVariables> {
onSuccess?: (data: TData, variables: TVariables) => void;
onError?: (error: Error, variables: TVariables) => void;
onSettled?: (data: TData | undefined, error: Error | null, variables: TVariables) => void;
}
export interface UseMutationOptions<TData, TVariables> extends MutationOptions<TData, TVariables> {
mutationFn: MutationFunction<TData, TVariables>;
}
export interface UseMutationResult<TData, TVariables> {
data: TData | undefined;
error: Error | null;
/** What the last `mutate` was called with; undefined while idle. */
variables: TVariables | undefined;
isError: boolean;
isPending: boolean;
isSuccess: boolean;
status: MutationStatus;
/** Never throws; failures show up in `error`. */
mutate: (variables: TVariables, options?: MutationOptions<TData, TVariables>) => void;
mutateAsync: (variables: TVariables, options?: MutationOptions<TData, TVariables>) => Promise<TData>;
reset: () => void;
}
/**
* TanStack Query v5's `useMutation`; a bare function is taken as
* `mutationFn`. Call `mutate` from an event handler and leave the rest to
* the kit: on-screen queries update optimistically, roll back if the viewer
* declines, and refetch on success.
* @example
* const complete = useMutation({ mutationFn: (status: Status) => clickup.tasks.update(task, { status }) });
* <button disabled={complete.isPending} onClick={() => complete.mutate(done)}>Mark done</button>
*/
export declare function useMutation<TData, TVariables = void>(options: UseMutationOptions<TData, TVariables> | MutationFunction<TData, TVariables>): UseMutationResult<TData, TVariables>;
+22
View File
@@ -0,0 +1,22 @@
import type { Ref, UseQueryOptions, UseQueryResult } from "./core";
export declare const CUSTOM_FIELD_TYPES: readonly ["short_text", "text", "email", "url", "phone", "number", "currency", "checkbox", "date", "drop_down", "labels", "users", "tasks", "list_relationship", "emoji", "votes", "manual_progress", "automatic_progress", "attachment", "location", "formula", "signature", "button"];
/** A custom field's definition. Values live on each task in `customFields`, matched by `id`. */
export interface CustomField extends Ref<"customField"> {
/** Ends at the space, folder or list the field is configured on. */
ancestors: Array<Ref<"space" | "folder" | "list">>;
type: (typeof CUSTOM_FIELD_TYPES)[number];
/** For drop-down and labels fields: the allowed values, in display order. */
options?: Array<{
name: string;
color: string;
}>;
}
/** The fields available on a task, or on tasks in a space, folder or list. Inherited fields are included; `ancestors` shows where each is configured. */
export interface CustomFieldSelector {
parent: Ref<"task" | "space" | "folder" | "list">;
}
export interface CustomFieldsApi {
list(selector: CustomFieldSelector): Promise<CustomField[]>;
}
export declare function useCustomFields(selector: CustomFieldSelector, options?: UseQueryOptions<CustomField[]>): UseQueryResult<CustomField[]>;
+113
View File
@@ -0,0 +1,113 @@
import type { Ref, UseMutationResult, UseQueryOptions, UseQueryResult } from "./core";
import type { CustomField } from "./customFields";
import type { Status, Task } from "./tasks";
/**
* Typed records over a schema the `solution` CLI provisioned: a record is one
* task in the entity's List, its custom fields named by the schema's logical
* keys. Reads are shaped like TanStack Query's `useQuery`, writes like
* `useMutation`, and every write waits for the viewer's approval.
*/
export declare function defineEntity<R extends RecordBase, I extends object, P extends object, S extends string>(schema: EntitySchema): Entity<R, I, P, S>;
/**
* Tasks by id, in the order given. An empty list is a settled `[]` with no
* loading state, so a relationship hook fed an unlinked record renders its
* empty state at once.
*/
export declare function useTasksByIds(ids: readonly string[], options?: UseQueryOptions<Task[]>): UseQueryResult<Task[]>;
/** A property every task has, mapped to a record property by the schema. */
type NativeProperty = "name" | "status" | "assignees" | "dueDate" | "startDate" | "priority" | "description";
interface OptionSpec {
id: string;
/** The option's name in ClickUp; what the kit reads and writes. */
label: string;
}
interface FieldSpec {
/** The record property; a relationship becomes <key>Ids and <key>Refs. */
key: string;
/** The ClickUp custom field id. */
id: string;
name: string;
type: CustomField["type"];
required: boolean;
/** False keeps a type the kit cannot read off the record and out of the drift check. */
readable: boolean;
/** The kit cannot write this type yet; writes are rejected. */
readOnly: boolean;
/** Drop-down and labels only: logical option key to ClickUp option. */
options?: Readonly<Record<string, OptionSpec>>;
targetEntity?: string;
targetListId?: string;
}
/** One entity's frozen ids, as _schema.ts exports them. */
export interface EntitySchema {
key: string;
list: {
id: string;
name: string;
url: string;
};
taskType?: {
id: number;
name: string;
};
natives: ReadonlyArray<{
key: string;
native: NativeProperty;
}>;
fields: readonly FieldSpec[];
}
export interface RecordBase {
id: string;
/** Link to the task in ClickUp; safe to render for the viewer. */
url: string;
/** The underlying clickup-kit Task, for anything the schema does not model. */
task: Task;
}
/**
* status, statusType, limit and includeClosed are applied by ClickUp; the
* entity's task type, where and orderBy run in the browser on the decoded
* records. With ids, ClickUp returns exactly those tasks and every filter
* runs in the browser.
*/
export interface ListFilter<R, S extends string> {
/** Status names as shown in ClickUp, case-insensitive. */
status?: readonly S[];
statusType?: ReadonlyArray<Status["type"]>;
ids?: readonly string[];
limit?: number;
/** ClickUp hides tasks in the closing status unless this is true. */
includeClosed?: boolean;
where?: (record: R) => boolean;
/** Unset values sort last. */
orderBy?: {
field: keyof R;
direction?: "asc" | "desc";
};
}
interface EntityClient<R, I, P, S extends string> {
list(filter?: ListFilter<R, S>): Promise<R[]>;
get(id: string): Promise<R>;
/** Resolves with the new task's ref when the host reports it. */
create(input: I): Promise<Ref<"task"> | undefined>;
/** undefined leaves a property alone; null clears one that is optional. */
update(id: string, patch: P): Promise<void>;
/** Moves the task to the List's closing status. There is no delete. */
close(id: string): Promise<void>;
}
export interface Entity<R extends RecordBase, I, P, S extends string> {
schema: EntitySchema;
decode(task: Task): R;
client: EntityClient<R, I, P, S>;
useList(filter?: ListFilter<R, S>, options?: UseQueryOptions<Task[]>): UseQueryResult<R[]>;
/** data is undefined while loading and when no task has that id; isSuccess tells them apart. */
useOne(id: string | undefined, options?: UseQueryOptions<Task[]>): UseQueryResult<R | undefined>;
useMany(ids: readonly string[], options?: UseQueryOptions<Task[]>): UseQueryResult<R[]>;
useCreate(): UseMutationResult<Ref<"task"> | undefined, I>;
useUpdate(): UseMutationResult<void, {
id: string;
patch: P;
}>;
useClose(): UseMutationResult<void, string>;
}
+43
View File
@@ -0,0 +1,43 @@
import type { Ref, UseQueryOptions, UseQueryResult } from "./core";
export interface Space extends Ref<"space"> {
private: boolean;
}
export interface Folder extends Ref<"folder"> {
ancestors: Array<Ref<"space" | "folder">>;
private: boolean;
}
export interface List extends Ref<"list"> {
ancestors: Array<Ref<"space" | "folder">>;
private: boolean;
}
/** Needs `parent` or `ancestor`. */
export interface FolderSelector {
parent?: Ref<"space" | "folder">;
ancestor?: Ref<"space"> | string;
/** Defaults to 100, at most 500. */
limit?: number;
}
/** Needs `parent` or `ancestor`. */
export interface ListSelector {
parent?: Ref<"space" | "folder">;
ancestor?: Ref<"space"> | string;
/** Defaults to 100, at most 500. */
limit?: number;
}
export interface SpacesApi {
list(): Promise<Space[]>;
get(targets: Array<Ref<"space"> | string>): Promise<Space[]>;
}
export interface FoldersApi {
list(selector: FolderSelector): Promise<Folder[]>;
get(targets: Array<Ref<"folder"> | string>): Promise<Folder[]>;
}
export interface ListsApi {
list(selector: ListSelector): Promise<List[]>;
get(targets: Array<Ref<"list"> | string>): Promise<List[]>;
}
/** Every space, or exactly the given ones. */
export declare function useSpaces(targets?: Array<Ref<"space"> | string>, options?: UseQueryOptions<Space[]>): UseQueryResult<Space[]>;
export declare function useFolders(selector: FolderSelector | Array<Ref<"folder"> | string>, options?: UseQueryOptions<Folder[]>): UseQueryResult<Folder[]>;
export declare function useLists(selector: ListSelector | Array<Ref<"list"> | string>, options?: UseQueryOptions<List[]>): UseQueryResult<List[]>;
+55
View File
@@ -0,0 +1,55 @@
import type { IsoDate, Ref, UseQueryOptions, UseQueryResult } from "./core";
/** `primary` holds messages and mentions, `other` everything else, `reminder` reminders. */
export declare const INBOX_CATEGORIES: readonly ["primary", "other", "reminder"];
export declare const INBOX_STATES: readonly ["active", "snoozed", "cleared"];
export declare const NOTIFICATION_TYPES: readonly ["comment", "mention", "assignment", "status", "reminder", "other"];
/**
* One inbox row: the notifications about one asset, whose name and url the
* bundle mirrors. For a reminder, `related` is the reminder itself.
*/
export interface InboxBundle extends Ref<"inboxBundle"> {
category: (typeof INBOX_CATEGORIES)[number];
state: (typeof INBOX_STATES)[number];
unreadCount: number;
mentionsMe: boolean;
assignedToMe: boolean;
/** When a snoozed bundle comes back; for a reminder, when it is due. */
snoozedUntil?: IsoDate;
clearedAt?: IsoDate;
/** The asset the bundle is about. */
related?: Ref;
/** Newest first. */
notifications: Array<{
type: (typeof NOTIFICATION_TYPES)[number];
at: IsoDate;
/** Who acted; absent for automations. */
actor?: Ref<"user">;
/** What the notification points at, e.g. the comment itself. */
related?: Ref;
}>;
}
/**
* `state: "cleared"` clears, `"snoozed"` snoozes until `snoozedUntil`,
* `"active"` brings a cleared or snoozed bundle back. `read` marks it read or
* unread; read the state back through `unreadCount`.
*/
export interface InboxBundlePatch {
state?: InboxBundle["state"];
snoozedUntil?: IsoDate;
read?: boolean;
}
export interface InboxSelector {
/** Defaults to `"active"`. */
state?: InboxBundle["state"];
category?: InboxBundle["category"];
/** Defaults to 100, at most 500. */
limit?: number;
}
export interface InboxApi {
list(selector?: InboxSelector): Promise<InboxBundle[]>;
/** Any state, cleared included. */
get(targets: Array<Ref<"inboxBundle"> | string>): Promise<InboxBundle[]>;
update(bundle: Ref<"inboxBundle"> | string, patch: InboxBundlePatch): Promise<void>;
}
export declare function useInbox(selector?: InboxSelector | Array<Ref<"inboxBundle"> | string>, options?: UseQueryOptions<InboxBundle[]>): UseQueryResult<InboxBundle[]>;
+19
View File
@@ -0,0 +1,19 @@
import type { Ref, UseQueryOptions, UseQueryResult } from "./core";
/** A workspace member; `image` is the avatar. */
export type User = Ref<"user">;
export interface Team extends Ref<"team"> {
members: Array<Ref<"user">>;
}
export interface UsersApi {
list(): Promise<User[]>;
get(targets: Array<Ref<"user"> | string>): Promise<User[]>;
}
export interface TeamsApi {
list(): Promise<Team[]>;
get(targets: Array<Ref<"team"> | string>): Promise<Team[]>;
}
/** Every member, or exactly the given ones. */
export declare function useUsers(targets?: Array<Ref<"user"> | string>, options?: UseQueryOptions<User[]>): UseQueryResult<User[]>;
/** Every team, or exactly the given ones. */
export declare function useTeams(targets?: Array<Ref<"team"> | string>, options?: UseQueryOptions<Team[]>): UseQueryResult<Team[]>;
+31
View File
@@ -0,0 +1,31 @@
import type { IsoDate, Ref, UseQueryOptions, UseQueryResult } from "./core";
export interface Reminder extends Ref<"reminder"> {
dueAt?: IsoDate;
/** Set once completed; there is no separate flag. */
completedAt?: IsoDate;
related?: Ref<"task">;
}
/** `completed: true` cannot be undone. */
export interface ReminderPatch {
dueAt?: IsoDate;
completed?: true;
}
export interface CreateReminderInput {
name: string;
dueAt?: IsoDate;
related?: Ref<"task"> | string;
}
export interface ReminderSelector {
includeCompleted?: boolean;
/** Defaults to 100, at most 500. */
limit?: number;
}
export interface RemindersApi {
list(selector?: ReminderSelector): Promise<Reminder[]>;
/** Completed ones included. */
get(targets: Array<Ref<"reminder"> | string>): Promise<Reminder[]>;
create(input: CreateReminderInput): Promise<Ref<"reminder">>;
update(reminder: Ref<"reminder"> | string, patch: ReminderPatch): Promise<void>;
}
export declare function useReminders(selector?: ReminderSelector | Array<Ref<"reminder"> | string>, options?: UseQueryOptions<Reminder[]>): UseQueryResult<Reminder[]>;
+29
View File
@@ -0,0 +1,29 @@
import type { IsoDate, Ref, UseQueryOptions, UseQueryResult } from "./core";
export declare const SEARCH_KINDS: readonly ["task", "list", "folder", "space", "comment"];
export declare const SEARCH_ORDERS: readonly ["createdAt", "updatedAt", "dueAt"];
/** A ref plus why and when it matched. For the full asset, load it through its kind, e.g. `useTasks([hit.id])`. */
export interface SearchHit extends Ref<(typeof SEARCH_KINDS)[number]> {
/** A plain-text preview; the passage that matched when `keywords` were given. */
snippet?: string;
createdAt: IsoDate;
updatedAt: IsoDate;
/** When the viewer last opened it. */
viewedAt?: IsoDate;
}
export interface SearchQuery {
/** Matched against names, descriptions and comments. Omit for a plain listing, ordered by `orderBy`. */
keywords?: string;
/** Every searchable kind by default. */
kinds?: Array<SearchHit["kind"]>;
ancestor?: Ref<"space" | "folder" | "list">;
/** Relevance when omitted; `direction` defaults to `"desc"`. */
orderBy?: {
field: (typeof SEARCH_ORDERS)[number];
direction?: "asc" | "desc";
};
/** Defaults to 25, at most 50. */
limit?: number;
}
/** ClickUp's own search, run as the viewer; hits come in ClickUp's order. */
export declare function useSearch(query: SearchQuery, options?: UseQueryOptions<SearchHit[]>): UseQueryResult<SearchHit[]>;
+169
View File
@@ -0,0 +1,169 @@
import type { Delta, IsoDate, Ref, UseQueryOptions, UseQueryResult } from "./core";
import type { CustomField } from "./customFields";
export declare const PRIORITIES: readonly ["urgent", "high", "normal", "low"];
/** `open` is not started, `active` in progress, `done` finished, and `closed` the list's final status. */
export declare const STATUS_TYPES: readonly ["open", "active", "done", "closed"];
export interface Status extends Ref<"status"> {
type: (typeof STATUS_TYPES)[number];
color: string;
}
/** The effective statuses of a list, folder or space: what a task created there uses, in board order. */
export interface StatusSelector {
/** A bare string is a list id. */
parent: Ref<"space" | "folder" | "list"> | string;
}
export interface Task extends Ref<"task"> {
/** The space, folder and home list, then the parent task if this is a subtask. */
ancestors: Array<Ref<"space" | "folder" | "list" | "task">>;
/** Other lists the task appears in, besides its home list. */
otherLists: Array<Ref<"list">>;
private: boolean;
createdAt: IsoDate;
updatedAt: IsoDate;
startAt?: IsoDate;
dueAt?: IsoDate;
status: Status;
type: TaskType;
priority?: (typeof PRIORITIES)[number];
/** Sprint points. */
points?: number;
tags: Array<{
name: string;
color: string;
}>;
/** Set fields only; definitions are in `useCustomFields`, matched by `id`. `value` follows `type`: dates are `IsoDate`, drop-downs and labels option names, attachments urls, users and tasks refs. */
customFields: Array<{
id: string;
name: string;
type: CustomField["type"];
value: string | number | boolean | string[] | Ref<"user" | "team" | "task">[];
}>;
/** The description; render it with `RichText`. */
body: Delta;
creator: Ref<"user">;
assignees: Array<Ref<"user" | "team">>;
watchers: Array<Ref<"user">>;
checklists: ChecklistItem[];
}
/** A task's checklists are headings; the items nested below them are checkable, at any depth. */
export interface ChecklistItem {
/** Empty for an entry that does not exist yet. */
id: string;
name: string;
/** Absent on headings. */
done?: boolean;
items: ChecklistItem[];
}
/**
* The fields to change: `undefined` leaves one alone, `null` clears it.
* Collections replace the whole list, so read-modify-write them; checklists
* are diffed by `id`, so keep existing ids and give new entries an empty one.
* @example
* await clickup.tasks.update(task, { dueAt: null, assignees: [...task.assignees, user] });
*/
export interface TaskPatch {
name?: string;
/** Replaces the description; read it back as `body`. */
bodyMarkdown?: string;
/** One of the home list's statuses, from `useStatuses`. */
status?: Ref<"status">;
type?: Ref<"taskType"> | string;
priority?: Task["priority"] | null;
points?: number | null;
startAt?: IsoDate | null;
dueAt?: IsoDate | null;
/** Makes the task a subtask of `parent`. */
parent?: Ref<"task"> | string;
assignees?: Task["assignees"];
/** Only the viewer can be added or removed. */
watchers?: Task["watchers"];
tags?: Task["tags"];
/** Users, tasks and attachments fields cannot be written yet and reject with `forbidden`. */
customFields?: Task["customFields"];
checklists?: Task["checklists"];
}
/** `list` is where the task goes; `parent` makes it a subtask of that task. */
export interface CreateTaskInput {
list: Ref<"list"> | string;
parent?: Ref<"task"> | string;
name: string;
/** The description, as markdown. */
bodyMarkdown?: string;
status?: Ref<"status">;
type?: Ref<"taskType"> | string;
priority?: Task["priority"];
dueAt?: IsoDate;
startAt?: IsoDate;
assignees?: Task["assignees"];
}
/** Relative days, in the viewer's timezone. */
export declare const DATE_PRESETS: readonly ["today", "tomorrow", "next7Days"];
/** Inclusive day bounds; give one or both. */
export interface DateRange {
from?: IsoDate;
to?: IsoDate;
}
export type DateFilter = (typeof DATE_PRESETS)[number] | DateRange;
export declare const TASK_ORDERS: readonly ["name", "priority", "status", "startAt", "dueAt", "createdAt", "updatedAt"];
/**
* Needs `parent` or `ancestor`. The other fields filter and sort on the
* server before `limit` applies.
* @example
* useTasks({ ancestor: space, assignees: ["me"], dueAt: "overdue" });
* useTasks({ parent: list, status: { type: ["active"] }, orderBy: { field: "priority" } });
*/
export interface TaskSelector {
parent?: Ref<"list"> | string;
ancestor?: Ref<"space" | "folder" | "list">;
/** Defaults to 100, at most 500. */
limit?: number;
/** Defaults to false. Hides tasks in the list's final status only; `done` tasks still come. */
includeClosed?: boolean;
/** Statuses from `useStatuses`, or every status of these types. */
status?: Ref<"status">[] | {
type: Status["type"][];
};
/** Users and teams, `"me"` for the viewer; or `"none"` for unassigned tasks, `"any"` for assigned ones. */
assignees?: Array<Ref<"user" | "team"> | "me"> | "none" | "any";
priority?: Array<NonNullable<Task["priority"]>>;
tags?: string[];
/** Tasks whose name contains this text. Descriptions and comments are `useSearch`'s job. */
name?: {
contains: string;
};
startAt?: DateFilter | "none" | "any";
dueAt?: DateFilter | "none" | "any" | "overdue";
createdAt?: DateFilter;
updatedAt?: DateFilter;
/** The host's own order when omitted; `direction` defaults to `"asc"`. */
orderBy?: {
field: (typeof TASK_ORDERS)[number];
direction?: "asc" | "desc";
};
}
/** One of the workspace's task types, e.g. Task, Milestone, Bug. */
export interface TaskType extends Ref<"taskType"> {
description?: string;
}
export interface TasksApi {
list(selector: TaskSelector): Promise<Task[]>;
get(targets: Array<Ref<"task"> | string>): Promise<Task[]>;
create(input: CreateTaskInput): Promise<Ref<"task">>;
update(task: Ref<"task"> | string, patch: TaskPatch): Promise<void>;
/**
* One read-only `SELECT … FROM tasks`, as the viewer; the rows hold the selected columns by name.
* @example
* clickup.tasks.query(`SELECT status, COUNT(*) AS n FROM tasks WHERE list = '${LIST_ID}' GROUP BY status`)
*/
query(sql: string): Promise<Array<Record<string, unknown>>>;
/** The workspace's task types. */
types(): Promise<TaskType[]>;
/** The effective statuses of a list, folder or space. */
statuses(selector: StatusSelector): Promise<Status[]>;
}
export declare function useTasks(selector: TaskSelector | Array<Ref<"task"> | string>, options?: UseQueryOptions<Task[]>): UseQueryResult<Task[]>;
/** The rows of `clickup.tasks.query(sql)`, refetched after every task write. */
export declare function useTaskQuery(sql: string, options?: UseQueryOptions<Array<Record<string, unknown>>>): UseQueryResult<Array<Record<string, unknown>>>;
export declare function useTaskTypes(options?: UseQueryOptions<TaskType[]>): UseQueryResult<TaskType[]>;
export declare function useStatuses(selector: StatusSelector, options?: UseQueryOptions<Status[]>): UseQueryResult<Status[]>;