@rightkit/workspace-ui 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +24 -14
- package/dist/client.d.ts +190 -0
- package/dist/client.js +2 -0
- package/dist/hooks/useDrive.d.ts +26 -0
- package/dist/hooks/useDrive.js +158 -0
- package/dist/host.d.ts +109 -0
- package/dist/host.js +33 -0
- package/dist/icons.d.ts +34 -0
- package/dist/icons.js +33 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +11 -2
- package/dist/lib/avatar.d.ts +4 -0
- package/dist/lib/avatar.js +13 -0
- package/dist/lib/calendarUtils.d.ts +11 -0
- package/dist/lib/calendarUtils.js +42 -0
- package/dist/views/CalendarEventFormModal.d.ts +1 -1
- package/dist/views/CalendarEventFormModal.js +1 -1
- package/dist/views/CalendarEventPopover.d.ts +1 -1
- package/dist/views/CalendarEventPopover.js +3 -2
- package/dist/views/CalendarGrids.d.ts +1 -1
- package/dist/views/CalendarGrids.js +1 -1
- package/dist/views/CalendarView.js +34 -35
- package/dist/views/ContactsView.js +24 -27
- package/dist/views/DriveFileMenu.d.ts +2 -2
- package/dist/views/DriveFileMenu.js +3 -3
- package/dist/views/DrivePreviewModal.d.ts +2 -2
- package/dist/views/DrivePreviewModal.js +4 -4
- package/dist/views/DriveView.js +23 -24
- package/dist/views/MeetView.js +11 -11
- package/dist/views/TasksView.js +29 -29
- package/dist/views/driveFileGlyph.d.ts +3 -2
- package/dist/views/nav/CalendarNav.d.ts +1 -0
- package/dist/views/nav/CalendarNav.js +93 -0
- package/dist/views/nav/ContactsNav.d.ts +1 -0
- package/dist/views/nav/ContactsNav.js +22 -0
- package/dist/views/nav/DriveNav.d.ts +1 -0
- package/dist/views/nav/DriveNav.js +44 -0
- package/dist/views/nav/TasksNav.d.ts +1 -0
- package/dist/views/nav/TasksNav.js +22 -0
- package/package.json +23 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0 (unpublished)
|
|
4
|
+
|
|
5
|
+
Taken on the recommended option of the shape review, pending the owner's confirmation.
|
|
6
|
+
|
|
7
|
+
- BREAKING: the views render over a host the app supplies (`WorkspaceHostProvider`, `WorkspaceHost`) and import
|
|
8
|
+
nothing from `@rightkit/mail-ui`: no store, hooks, IPC, icons or account switcher of the mail app. Calls go
|
|
9
|
+
through the typed `WorkspaceClient`; shared state through the host's `useState` selector over the
|
|
10
|
+
`WorkspaceState` slices; errors, notices, dialogs, preferences, compose, downloads and view switches through
|
|
11
|
+
host actions; the account switcher is an optional host component.
|
|
12
|
+
- Moved here from `@rightkit/mail-ui` (deleted there): the Drive, calendar, contacts and tasks navs, `useDrive`,
|
|
13
|
+
the calendar helpers (`calendarUtils`, `WEEKDAYS`), the avatar helpers (`colorFor`, `initialOf`) and the
|
|
14
|
+
workspace data model (`CalEvent`, `DriveFile`, `ContactFull`, `EventForm`, ...). mail-ui imports them from here.
|
|
15
|
+
- Glyphs from `lucide-react` (`Icon`), not the mail app's icon set.
|
|
16
|
+
- Emailing a contact opens a new message through `composeTo` without switching the host to its inbox first.
|
|
17
|
+
- Dependencies: `@rightkit/mail-ui` removed; `lucide-react` added. New subpath exports `./client`, `./host`,
|
|
18
|
+
`./lib/avatar`, `./lib/calendarUtils`.
|
|
19
|
+
|
|
3
20
|
## 0.1.0
|
|
4
21
|
|
|
5
22
|
- First release. Calendar, contacts, drive, tasks and meeting views for workspace-style apps.
|
package/README.md
CHANGED
|
@@ -1,36 +1,46 @@
|
|
|
1
1
|
# @rightkit/workspace-ui
|
|
2
2
|
|
|
3
|
-
Calendar, contacts, drive, tasks and meeting views for workspace-style apps.
|
|
3
|
+
Calendar, contacts, drive, tasks and meeting views, and their navs, for workspace-style apps. The views hold no
|
|
4
|
+
IPC, no store and no product name: they render over a typed client and state the host supplies.
|
|
4
5
|
|
|
5
6
|
| Item | Value |
|
|
6
7
|
|---|---|
|
|
7
8
|
| Where it runs | App webview UI (React) |
|
|
8
|
-
| Version | 0.
|
|
9
|
+
| Version | 0.2.0 |
|
|
9
10
|
| License | MIT OR Apache-2.0 |
|
|
10
11
|
|
|
11
12
|
## Entry points
|
|
12
13
|
|
|
13
14
|
| Import | Contents |
|
|
14
15
|
|---|---|
|
|
15
|
-
| `@rightkit/workspace-ui` |
|
|
16
|
-
| `@rightkit/workspace-ui
|
|
17
|
-
| `@rightkit/workspace-ui
|
|
16
|
+
| `@rightkit/workspace-ui` | Views (`CalendarView`, `ContactsView`, `DriveView`, `TasksView`, `MeetView`, the calendar and Drive dialogs), navs (`CalendarNav`, `ContactsNav`, `DriveNav`, `TasksNav`), `useDrive`, the host contract, the data model, `Icon`, vCard and avatar helpers, calendar helpers. |
|
|
17
|
+
| `@rightkit/workspace-ui/client` | The data model (`CalEvent`, `DriveFile`, `ContactFull`, ...) and `WorkspaceClient`. |
|
|
18
|
+
| `@rightkit/workspace-ui/host` | `WorkspaceHost`, `WorkspaceHostProvider`, `useWorkspaceHost`, `useWorkspaceState`, `WorkspaceState`. |
|
|
19
|
+
| `@rightkit/workspace-ui/lib/avatar` | `colorFor`, `initialOf` (coloured-initial avatars). |
|
|
20
|
+
| `@rightkit/workspace-ui/lib/calendarUtils` | Day and week arithmetic, `rangeFor`, `eventDay`, `WEEKDAYS`. |
|
|
18
21
|
|
|
19
|
-
##
|
|
20
|
-
|
|
21
|
-
`@rightkit/mail-ui`
|
|
22
|
+
## Host contract
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
Render the views and navs inside one `<WorkspaceHostProvider host={host}>`. `host` is a `WorkspaceHost`:
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
| Field | What the host supplies |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `client` | `WorkspaceClient`: every calendar, tasks, Drive, contacts and Meet call, plus `openExternal` and `saveTextDownload`, over the host's own transport. |
|
|
29
|
+
| `useState` | A selector hook over the host's state holding the `WorkspaceState` slices (`calendars`, `events`, `drivePath`, `taskLists`, ... with `set*` setters, and `activeAccount`). A zustand store with those fields satisfies it directly, so the host's other UI can share them. |
|
|
30
|
+
| `reportError`, `notify`, `dialog` | The host's error banner, toast, and confirm/prompt dialog. |
|
|
31
|
+
| `savePreference` | Persists `shownCals`, `calView`, `driveViewMode`. |
|
|
32
|
+
| `composeTo`, `linkIntoCompose` | Open a new message; put a Drive link into the open message (or a new one). |
|
|
33
|
+
| `saveDownload` | Saves (and may scan) a Drive download. |
|
|
34
|
+
| `openView` | Switches to another workspace view (Meet's "schedule" opens the calendar). |
|
|
35
|
+
| `AccountSwitcher` | Optional; shown in each view's header. |
|
|
26
36
|
|
|
27
|
-
|
|
37
|
+
MailRight's host is `mailWorkspaceHost` in `@rightkit/mail-ui`.
|
|
28
38
|
|
|
29
|
-
|
|
30
|
-
|
|
39
|
+
Glyphs come from Lucide (`lucide-react`); the package imports no app's icon set. Hosts bundle the package with
|
|
40
|
+
Vite; Tailwind hosts add `node_modules/@rightkit/workspace-ui/dist/**/*.js` to their content globs.
|
|
31
41
|
|
|
32
42
|
## Testing
|
|
33
43
|
|
|
34
44
|
```sh
|
|
35
|
-
pnpm --dir packages/workspace-ui
|
|
45
|
+
pnpm --dir packages/workspace-ui typecheck
|
|
36
46
|
```
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
export interface CalendarInfo {
|
|
2
|
+
id: string;
|
|
3
|
+
summary: string;
|
|
4
|
+
background_color: string;
|
|
5
|
+
primary: boolean;
|
|
6
|
+
access_role: string;
|
|
7
|
+
}
|
|
8
|
+
export interface Attendee {
|
|
9
|
+
email: string;
|
|
10
|
+
response_status: string;
|
|
11
|
+
}
|
|
12
|
+
export interface CalEvent {
|
|
13
|
+
id: string;
|
|
14
|
+
calendar_id: string;
|
|
15
|
+
summary: string;
|
|
16
|
+
description: string;
|
|
17
|
+
location: string;
|
|
18
|
+
start: string;
|
|
19
|
+
end: string;
|
|
20
|
+
all_day: boolean;
|
|
21
|
+
html_link: string;
|
|
22
|
+
meet_link: string;
|
|
23
|
+
organizer: string;
|
|
24
|
+
attendees: Attendee[];
|
|
25
|
+
my_response: string;
|
|
26
|
+
is_recurring: boolean;
|
|
27
|
+
recurring_event_id: string;
|
|
28
|
+
}
|
|
29
|
+
export interface EventInput {
|
|
30
|
+
summary: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
location?: string;
|
|
33
|
+
startRfc3339: string;
|
|
34
|
+
endRfc3339: string;
|
|
35
|
+
allDay?: boolean;
|
|
36
|
+
timeZone: string;
|
|
37
|
+
attendees?: string[];
|
|
38
|
+
reminderMinutes?: number | null;
|
|
39
|
+
addMeet?: boolean;
|
|
40
|
+
recurrence?: string | null;
|
|
41
|
+
}
|
|
42
|
+
export interface TaskListInfo {
|
|
43
|
+
id: string;
|
|
44
|
+
title: string;
|
|
45
|
+
}
|
|
46
|
+
export interface TaskItem {
|
|
47
|
+
id: string;
|
|
48
|
+
title: string;
|
|
49
|
+
notes: string;
|
|
50
|
+
due: string;
|
|
51
|
+
status: string;
|
|
52
|
+
list_id: string;
|
|
53
|
+
list_title: string;
|
|
54
|
+
}
|
|
55
|
+
export interface DriveFile {
|
|
56
|
+
id: string;
|
|
57
|
+
name: string;
|
|
58
|
+
mime_type: string;
|
|
59
|
+
modified_time: string;
|
|
60
|
+
web_view_link: string;
|
|
61
|
+
size: string;
|
|
62
|
+
icon_link: string;
|
|
63
|
+
thumbnail_link: string;
|
|
64
|
+
is_folder: boolean;
|
|
65
|
+
parents: string[];
|
|
66
|
+
}
|
|
67
|
+
export interface DriveListing {
|
|
68
|
+
files: DriveFile[];
|
|
69
|
+
next_page_token: string;
|
|
70
|
+
}
|
|
71
|
+
export interface MeetSpace {
|
|
72
|
+
name: string;
|
|
73
|
+
meeting_uri: string;
|
|
74
|
+
meeting_code: string;
|
|
75
|
+
}
|
|
76
|
+
export interface BusySlot {
|
|
77
|
+
start: string;
|
|
78
|
+
end: string;
|
|
79
|
+
}
|
|
80
|
+
export interface FreeBusyResult {
|
|
81
|
+
calendar_id: string;
|
|
82
|
+
busy: BusySlot[];
|
|
83
|
+
}
|
|
84
|
+
export interface StorageQuota {
|
|
85
|
+
limit: string;
|
|
86
|
+
usage: string;
|
|
87
|
+
usage_in_drive: string;
|
|
88
|
+
}
|
|
89
|
+
export interface DrivePermission {
|
|
90
|
+
id: string;
|
|
91
|
+
role: string;
|
|
92
|
+
kind: string;
|
|
93
|
+
email: string;
|
|
94
|
+
display_name: string;
|
|
95
|
+
}
|
|
96
|
+
export interface Recipient {
|
|
97
|
+
name: string;
|
|
98
|
+
email: string;
|
|
99
|
+
photo_url: string;
|
|
100
|
+
}
|
|
101
|
+
export interface ContactFull {
|
|
102
|
+
resource_name: string;
|
|
103
|
+
etag: string;
|
|
104
|
+
display_name: string;
|
|
105
|
+
given_name: string;
|
|
106
|
+
family_name: string;
|
|
107
|
+
emails: string[];
|
|
108
|
+
phones: string[];
|
|
109
|
+
org: string;
|
|
110
|
+
photo_url: string;
|
|
111
|
+
}
|
|
112
|
+
export interface ContactGroup {
|
|
113
|
+
resource_name: string;
|
|
114
|
+
name: string;
|
|
115
|
+
member_count: number;
|
|
116
|
+
}
|
|
117
|
+
/** The event editor's form state (the calendar view's create/edit dialog). */
|
|
118
|
+
export type EventForm = {
|
|
119
|
+
id?: string;
|
|
120
|
+
summary: string;
|
|
121
|
+
start: string;
|
|
122
|
+
end: string;
|
|
123
|
+
allDay: boolean;
|
|
124
|
+
attendees: string;
|
|
125
|
+
reminderMinutes: string;
|
|
126
|
+
addMeet: boolean;
|
|
127
|
+
recurrence: string;
|
|
128
|
+
recurEnd: "never" | "until" | "count";
|
|
129
|
+
recurUntil: string;
|
|
130
|
+
recurCount: string;
|
|
131
|
+
recurByday: string[];
|
|
132
|
+
};
|
|
133
|
+
/** The new-task form. */
|
|
134
|
+
export type TaskCreate = {
|
|
135
|
+
title: string;
|
|
136
|
+
notes: string;
|
|
137
|
+
};
|
|
138
|
+
/** Duplicate contacts found by the dedupe tool: each primary with the entries that repeat it. */
|
|
139
|
+
export type ContactDupes = {
|
|
140
|
+
primary: ContactFull;
|
|
141
|
+
dups: ContactFull[];
|
|
142
|
+
}[];
|
|
143
|
+
/** The Drive shelves the nav offers. */
|
|
144
|
+
export type DriveShelf = "my" | "recent" | "shared" | "starred" | "trash";
|
|
145
|
+
/**
|
|
146
|
+
* Every call the workspace views make. The host implements it over its own transport (MailRight: Tauri commands
|
|
147
|
+
* in `@rightkit/mail-ui/lib/ipc`); the views never name a command. `accountId` is the host's account key.
|
|
148
|
+
*/
|
|
149
|
+
export interface WorkspaceClient {
|
|
150
|
+
listCalendars(accountId: string): Promise<CalendarInfo[]>;
|
|
151
|
+
listEvents(accountId: string, calendarId: string, timeMin: string, timeMax: string): Promise<CalEvent[]>;
|
|
152
|
+
createEvent(accountId: string, calendarId: string, ev: EventInput): Promise<CalEvent>;
|
|
153
|
+
updateEvent(accountId: string, calendarId: string, eventId: string, ev: EventInput): Promise<void>;
|
|
154
|
+
deleteEvent(accountId: string, calendarId: string, eventId: string): Promise<void>;
|
|
155
|
+
truncateEventSeries(accountId: string, calendarId: string, recurringEventId: string, occurrenceStart: string): Promise<void>;
|
|
156
|
+
rsvpEvent(accountId: string, calendarId: string, eventId: string, selfEmail: string, response: string): Promise<void>;
|
|
157
|
+
freeBusy(accountId: string, calendarIds: string[], timeMin: string, timeMax: string): Promise<FreeBusyResult[]>;
|
|
158
|
+
listTasks(accountId: string, showCompleted?: boolean): Promise<TaskItem[]>;
|
|
159
|
+
listTaskLists(accountId: string): Promise<TaskListInfo[]>;
|
|
160
|
+
createTaskList(accountId: string, title: string): Promise<TaskListInfo>;
|
|
161
|
+
renameTaskList(accountId: string, listId: string, title: string): Promise<void>;
|
|
162
|
+
deleteTaskList(accountId: string, listId: string): Promise<void>;
|
|
163
|
+
clearCompletedTasks(accountId: string, listId: string): Promise<void>;
|
|
164
|
+
createTaskIn(accountId: string, listId: string, title: string, notes: string, due: string): Promise<void>;
|
|
165
|
+
setTaskStatus(accountId: string, listId: string, taskId: string, completed: boolean): Promise<void>;
|
|
166
|
+
deleteTask(accountId: string, listId: string, taskId: string): Promise<void>;
|
|
167
|
+
listDriveFiles(accountId: string, folderId?: string, query?: string, pageToken?: string, shelf?: string, mimeFilter?: string): Promise<DriveListing>;
|
|
168
|
+
driveStorageQuota(accountId: string): Promise<StorageQuota>;
|
|
169
|
+
driveUpload(accountId: string, filename: string, mimeType: string, data: number[], parentId?: string): Promise<DriveFile>;
|
|
170
|
+
/** Downloads to the host's download folder and resolves to the saved path. */
|
|
171
|
+
driveDownload(accountId: string, fileId: string, mimeType: string, filename: string): Promise<string>;
|
|
172
|
+
driveCreateFolder(accountId: string, name: string, parentId?: string): Promise<DriveFile>;
|
|
173
|
+
driveRename(accountId: string, fileId: string, newName: string): Promise<DriveFile>;
|
|
174
|
+
driveMove(accountId: string, fileId: string, newParent: string, oldParent?: string): Promise<DriveFile>;
|
|
175
|
+
driveTrash(accountId: string, fileId: string): Promise<void>;
|
|
176
|
+
driveShare(accountId: string, fileId: string, email: string, role: string): Promise<string>;
|
|
177
|
+
driveListPermissions(accountId: string, fileId: string): Promise<DrivePermission[]>;
|
|
178
|
+
driveRevokePermission(accountId: string, fileId: string, permissionId: string): Promise<void>;
|
|
179
|
+
searchRecipients(accountId: string, query: string): Promise<Recipient[]>;
|
|
180
|
+
listAllContacts(accountId: string): Promise<ContactFull[]>;
|
|
181
|
+
createContact(accountId: string, name: string, emails: string[], phones: string[]): Promise<void>;
|
|
182
|
+
updateContact(accountId: string, resourceName: string, etag: string, name: string, emails: string[], phones: string[]): Promise<void>;
|
|
183
|
+
deleteContact(accountId: string, resourceName: string): Promise<void>;
|
|
184
|
+
listContactGroups(accountId: string): Promise<ContactGroup[]>;
|
|
185
|
+
createMeet(accountId: string): Promise<MeetSpace>;
|
|
186
|
+
/** Opens a URL in the user's browser. */
|
|
187
|
+
openExternal(url: string): Promise<void>;
|
|
188
|
+
/** Saves text as a download and resolves to the saved path. */
|
|
189
|
+
saveTextDownload(filename: string, text: string): Promise<string>;
|
|
190
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { DriveFile, DriveShelf } from "../client.js";
|
|
2
|
+
export declare const FOLDER_MIME = "application/vnd.google-apps.folder";
|
|
3
|
+
export declare function useDrive(): {
|
|
4
|
+
activeAccount: string | null;
|
|
5
|
+
driveShelf: DriveShelf;
|
|
6
|
+
drivePath: {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
}[];
|
|
10
|
+
driveQuery: string;
|
|
11
|
+
driveType: string;
|
|
12
|
+
setDriveQuery: import("../host.js").FieldSetter<string>;
|
|
13
|
+
currentFolderId: () => string | undefined;
|
|
14
|
+
reload: (folderId: string | undefined, query: string, shelf?: DriveShelf, typeFilter?: string) => Promise<void>;
|
|
15
|
+
selectShelf: (shelf: DriveShelf) => void;
|
|
16
|
+
setTypeFilter: (t: string) => void;
|
|
17
|
+
openFolder: (f: DriveFile) => void;
|
|
18
|
+
breadcrumbTo: (idx: number) => void;
|
|
19
|
+
refresh: () => void;
|
|
20
|
+
download: (f: DriveFile) => void;
|
|
21
|
+
upload: (files: FileList | File[]) => Promise<void>;
|
|
22
|
+
newFolder: () => Promise<void>;
|
|
23
|
+
rename: (f: DriveFile) => Promise<void>;
|
|
24
|
+
trash: (f: DriveFile) => Promise<void>;
|
|
25
|
+
insertDriveLink: (f: DriveFile) => void;
|
|
26
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { useWorkspaceClient, useWorkspaceHost, useWorkspaceState } from "../host.js";
|
|
2
|
+
// Shared Drive logic for DriveNav (the contextual sidebar) and DriveView (the
|
|
3
|
+
// file grid). Reads/writes Drive state through the host state so both surfaces stay
|
|
4
|
+
// in sync (shelf selection in the nav re-lists the grid, etc.). Transient modal
|
|
5
|
+
// state (move/share pickers) stays local to DriveView since only it renders them.
|
|
6
|
+
export const FOLDER_MIME = "application/vnd.google-apps.folder";
|
|
7
|
+
export function useDrive() {
|
|
8
|
+
const { listDriveFiles, driveDownload, driveUpload, driveCreateFolder, driveRename, driveTrash } = useWorkspaceClient();
|
|
9
|
+
const showDialog = useWorkspaceHost().dialog;
|
|
10
|
+
const saveAndScan = useWorkspaceHost().saveDownload;
|
|
11
|
+
const linkIntoCompose = useWorkspaceHost().linkIntoCompose;
|
|
12
|
+
const activeAccount = useWorkspaceState((s) => s.activeAccount);
|
|
13
|
+
const setErr = useWorkspaceHost().reportError;
|
|
14
|
+
const showUndo = useWorkspaceHost().notify;
|
|
15
|
+
const driveType = useWorkspaceState((s) => s.driveType);
|
|
16
|
+
const setDriveType = useWorkspaceState((s) => s.setDriveType);
|
|
17
|
+
const driveQuery = useWorkspaceState((s) => s.driveQuery);
|
|
18
|
+
const setDriveQuery = useWorkspaceState((s) => s.setDriveQuery);
|
|
19
|
+
const driveShelf = useWorkspaceState((s) => s.driveShelf);
|
|
20
|
+
const setDriveShelf = useWorkspaceState((s) => s.setDriveShelf);
|
|
21
|
+
const drivePath = useWorkspaceState((s) => s.drivePath);
|
|
22
|
+
const setDrivePath = useWorkspaceState((s) => s.setDrivePath);
|
|
23
|
+
const setDriveToken = useWorkspaceState((s) => s.setDriveToken);
|
|
24
|
+
const setDriveFiles = useWorkspaceState((s) => s.setDriveFiles);
|
|
25
|
+
const setDriveBusy = useWorkspaceState((s) => s.setDriveBusy);
|
|
26
|
+
/** Current Drive folder id for write ops + reload (undefined = My Drive root). */
|
|
27
|
+
function currentFolderId() {
|
|
28
|
+
return driveShelf === "my" && drivePath.length ? drivePath[drivePath.length - 1].id : undefined;
|
|
29
|
+
}
|
|
30
|
+
async function reload(folderId, query, shelf = driveShelf, typeFilter = driveType) {
|
|
31
|
+
if (!activeAccount)
|
|
32
|
+
return;
|
|
33
|
+
setDriveBusy(true);
|
|
34
|
+
try {
|
|
35
|
+
const listing = await listDriveFiles(activeAccount, folderId, query || undefined, undefined, shelf === "my" ? undefined : shelf, typeFilter || undefined);
|
|
36
|
+
setDriveFiles(listing.files);
|
|
37
|
+
setDriveToken(listing.next_page_token);
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
setErr(String(e));
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
setDriveBusy(false);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Switch Drive shelf (My Drive / Recent / Shared / Starred / Trash) — clears folder nav. */
|
|
47
|
+
function selectShelf(shelf) {
|
|
48
|
+
setDriveShelf(shelf);
|
|
49
|
+
setDrivePath([]);
|
|
50
|
+
setDriveQuery("");
|
|
51
|
+
reload(undefined, "", shelf, driveType);
|
|
52
|
+
}
|
|
53
|
+
function setTypeFilter(t) {
|
|
54
|
+
setDriveType(t);
|
|
55
|
+
reload(currentFolderId(), driveQuery, driveShelf, t);
|
|
56
|
+
}
|
|
57
|
+
function openFolder(f) {
|
|
58
|
+
setDriveShelf("my");
|
|
59
|
+
setDrivePath((p) => [...p, { id: f.id, name: f.name }]);
|
|
60
|
+
setDriveQuery("");
|
|
61
|
+
reload(f.id, "", "my", driveType);
|
|
62
|
+
}
|
|
63
|
+
function breadcrumbTo(idx) {
|
|
64
|
+
const next = idx < 0 ? [] : drivePath.slice(0, idx + 1);
|
|
65
|
+
setDrivePath(next);
|
|
66
|
+
setDriveQuery("");
|
|
67
|
+
reload(next.length ? next[next.length - 1].id : undefined, "", "my", driveType);
|
|
68
|
+
}
|
|
69
|
+
function refresh() {
|
|
70
|
+
reload(currentFolderId(), driveQuery, driveShelf, driveType);
|
|
71
|
+
}
|
|
72
|
+
function download(f) {
|
|
73
|
+
if (!activeAccount)
|
|
74
|
+
return;
|
|
75
|
+
saveAndScan(f.name, () => driveDownload(activeAccount, f.id, f.mime_type, f.name));
|
|
76
|
+
}
|
|
77
|
+
async function newFolder() {
|
|
78
|
+
if (!activeAccount)
|
|
79
|
+
return;
|
|
80
|
+
const name = await showDialog({ kind: "prompt", message: "New folder name" });
|
|
81
|
+
if (!name || !name.trim())
|
|
82
|
+
return;
|
|
83
|
+
driveCreateFolder(activeAccount, name.trim(), currentFolderId())
|
|
84
|
+
.then(() => {
|
|
85
|
+
showUndo("Folder created");
|
|
86
|
+
refresh();
|
|
87
|
+
})
|
|
88
|
+
.catch((e) => setErr(String(e)));
|
|
89
|
+
}
|
|
90
|
+
/** Upload one or more files into the current folder, then refresh. */
|
|
91
|
+
async function upload(files) {
|
|
92
|
+
if (!activeAccount)
|
|
93
|
+
return;
|
|
94
|
+
const list = Array.from(files);
|
|
95
|
+
if (list.length === 0)
|
|
96
|
+
return;
|
|
97
|
+
const parent = currentFolderId();
|
|
98
|
+
setDriveBusy(true);
|
|
99
|
+
let ok = 0;
|
|
100
|
+
for (const file of list) {
|
|
101
|
+
try {
|
|
102
|
+
const data = Array.from(new Uint8Array(await file.arrayBuffer()));
|
|
103
|
+
await driveUpload(activeAccount, file.name, file.type, data, parent);
|
|
104
|
+
ok++;
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
setErr(String(e));
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
setDriveBusy(false);
|
|
112
|
+
if (ok > 0) {
|
|
113
|
+
showUndo(`Uploaded ${ok} file${ok > 1 ? "s" : ""}`);
|
|
114
|
+
refresh();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function rename(f) {
|
|
118
|
+
if (!activeAccount)
|
|
119
|
+
return;
|
|
120
|
+
const name = await showDialog({ kind: "prompt", message: "Rename to", initial: f.name });
|
|
121
|
+
if (!name || !name.trim() || name.trim() === f.name)
|
|
122
|
+
return;
|
|
123
|
+
driveRename(activeAccount, f.id, name.trim())
|
|
124
|
+
.then(() => {
|
|
125
|
+
showUndo("Renamed");
|
|
126
|
+
refresh();
|
|
127
|
+
})
|
|
128
|
+
.catch((e) => setErr(String(e)));
|
|
129
|
+
}
|
|
130
|
+
async function trash(f) {
|
|
131
|
+
if (!activeAccount)
|
|
132
|
+
return;
|
|
133
|
+
if ((await showDialog({ kind: "confirm", message: `Move "${f.name}" to Drive trash? You can restore it from Google Drive.` })) !== "ok")
|
|
134
|
+
return;
|
|
135
|
+
driveTrash(activeAccount, f.id)
|
|
136
|
+
.then(() => {
|
|
137
|
+
showUndo("Moved to Drive trash");
|
|
138
|
+
refresh();
|
|
139
|
+
})
|
|
140
|
+
.catch((e) => setErr(String(e)));
|
|
141
|
+
}
|
|
142
|
+
/** Insert a Drive file's share link into the compose body (open compose if needed). */
|
|
143
|
+
function insertDriveLink(f) {
|
|
144
|
+
const link = f.web_view_link;
|
|
145
|
+
if (!link) {
|
|
146
|
+
setErr("That item has no shareable link.");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const html = `<a href="${link}">${f.name}</a>`;
|
|
150
|
+
showUndo(linkIntoCompose(html) === "inserted" ? `Linked ${f.name}` : `Linked ${f.name} into a new email`);
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
activeAccount, driveShelf, drivePath, driveQuery, driveType,
|
|
154
|
+
setDriveQuery,
|
|
155
|
+
currentFolderId, reload, selectShelf, setTypeFilter, openFolder,
|
|
156
|
+
breadcrumbTo, refresh, download, upload, newFolder, rename, trash, insertDriveLink,
|
|
157
|
+
};
|
|
158
|
+
}
|
package/dist/host.d.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { type ComponentType, type ReactNode } from "react";
|
|
2
|
+
import type { CalEvent, CalendarInfo, ContactDupes, ContactGroup, DriveFile, DriveShelf, EventForm, StorageQuota, TaskCreate, TaskItem, TaskListInfo, WorkspaceClient } from "./client.js";
|
|
3
|
+
/** The workspace slices of the host's state. */
|
|
4
|
+
export interface WorkspaceFields {
|
|
5
|
+
calendars: CalendarInfo[];
|
|
6
|
+
selectedCal: string;
|
|
7
|
+
shownCals: string[];
|
|
8
|
+
calView: "month" | "week" | "day";
|
|
9
|
+
calCursor: number;
|
|
10
|
+
events: CalEvent[];
|
|
11
|
+
openEvent: CalEvent | null;
|
|
12
|
+
eventForm: EventForm | null;
|
|
13
|
+
freeBusyMsg: string;
|
|
14
|
+
calendarBusy: boolean;
|
|
15
|
+
/** A pending "new event" seed (Meet's "schedule for later" fills it, the calendar opens the form with it). */
|
|
16
|
+
eventSeed: {
|
|
17
|
+
summary: string;
|
|
18
|
+
withMeet: boolean;
|
|
19
|
+
} | null;
|
|
20
|
+
gtasks: TaskItem[];
|
|
21
|
+
taskLists: TaskListInfo[];
|
|
22
|
+
taskListId: string;
|
|
23
|
+
taskDue: string;
|
|
24
|
+
showCompletedTasks: boolean;
|
|
25
|
+
listMgrOpen: boolean;
|
|
26
|
+
taskFormOpen: boolean;
|
|
27
|
+
taskCreate: TaskCreate;
|
|
28
|
+
tasksBusy: boolean;
|
|
29
|
+
driveFiles: DriveFile[];
|
|
30
|
+
drivePath: {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
}[];
|
|
34
|
+
driveQuery: string;
|
|
35
|
+
driveToken: string;
|
|
36
|
+
driveShelf: DriveShelf;
|
|
37
|
+
driveType: string;
|
|
38
|
+
driveViewMode: "grid" | "list";
|
|
39
|
+
driveQuota: StorageQuota | null;
|
|
40
|
+
drivePreview: DriveFile | null;
|
|
41
|
+
driveBusy: boolean;
|
|
42
|
+
contactGroups: ContactGroup[];
|
|
43
|
+
contactsQuery: string;
|
|
44
|
+
contactsBusy: boolean;
|
|
45
|
+
contactDupes: ContactDupes | null;
|
|
46
|
+
}
|
|
47
|
+
/** A setter that takes a value or an updater, like React's `setState`. */
|
|
48
|
+
export type FieldSetter<T> = (v: T | ((prev: T) => T)) => void;
|
|
49
|
+
/** `setX` for every field `x`. */
|
|
50
|
+
export type WorkspaceSetters = {
|
|
51
|
+
[K in keyof WorkspaceFields as `set${Capitalize<K & string>}`]: FieldSetter<WorkspaceFields[K]>;
|
|
52
|
+
};
|
|
53
|
+
/** What `useState` selects from. `activeAccount` is the host's current account key (read only). */
|
|
54
|
+
export type WorkspaceState = WorkspaceFields & WorkspaceSetters & {
|
|
55
|
+
activeAccount: string | null;
|
|
56
|
+
};
|
|
57
|
+
/** The preferences the views persist through the host. */
|
|
58
|
+
export interface WorkspacePreferences {
|
|
59
|
+
shownCals: string[];
|
|
60
|
+
calView: "month" | "week" | "day";
|
|
61
|
+
driveViewMode: "grid" | "list";
|
|
62
|
+
}
|
|
63
|
+
export type WorkspaceDialog = {
|
|
64
|
+
kind: "confirm";
|
|
65
|
+
message: string;
|
|
66
|
+
} | {
|
|
67
|
+
kind: "prompt";
|
|
68
|
+
message: string;
|
|
69
|
+
initial?: string;
|
|
70
|
+
};
|
|
71
|
+
/** The views a workspace view can ask the host to show. */
|
|
72
|
+
export type WorkspaceView = "calendar" | "contacts" | "drive" | "tasks" | "meet";
|
|
73
|
+
export interface WorkspaceHost {
|
|
74
|
+
client: WorkspaceClient;
|
|
75
|
+
/** Selector hook over the host's state. */
|
|
76
|
+
useState: <T>(selector: (s: WorkspaceState) => T) => T;
|
|
77
|
+
/** Shows an error to the user. */
|
|
78
|
+
reportError(message: string): void;
|
|
79
|
+
/** Shows a short notice (the host's toast). */
|
|
80
|
+
notify(message: string): void;
|
|
81
|
+
/** Confirm or prompt. Resolves to "ok" (confirm), the entered text (prompt), or null when cancelled. */
|
|
82
|
+
dialog(cfg: WorkspaceDialog): Promise<string | null>;
|
|
83
|
+
/** Persists a view preference. */
|
|
84
|
+
savePreference<K extends keyof WorkspacePreferences>(key: K, value: WorkspacePreferences[K]): void;
|
|
85
|
+
/** Opens a new message to `to` with `subject` and an HTML `body`. */
|
|
86
|
+
composeTo(to: string, subject: string, body: string): void;
|
|
87
|
+
/**
|
|
88
|
+
* Puts an HTML link into the message being written, or into a new message when none is open. Resolves to which
|
|
89
|
+
* one happened, so the view can say so.
|
|
90
|
+
*/
|
|
91
|
+
linkIntoCompose(html: string): "inserted" | "new-message";
|
|
92
|
+
/** Runs a download the host saves (and may scan) under `label`. */
|
|
93
|
+
saveDownload(label: string, download: () => Promise<string>): void;
|
|
94
|
+
/** Switches the host to another workspace view. */
|
|
95
|
+
openView(view: WorkspaceView): void;
|
|
96
|
+
/** The host's account switcher, shown in each view's header. */
|
|
97
|
+
AccountSwitcher?: ComponentType;
|
|
98
|
+
}
|
|
99
|
+
/** Supplies the host to every workspace view and nav below it. */
|
|
100
|
+
export declare function WorkspaceHostProvider({ host, children }: {
|
|
101
|
+
host: WorkspaceHost;
|
|
102
|
+
children?: ReactNode;
|
|
103
|
+
}): import("react").JSX.Element;
|
|
104
|
+
export declare function useWorkspaceHost(): WorkspaceHost;
|
|
105
|
+
/** Selects from the host's workspace state. */
|
|
106
|
+
export declare function useWorkspaceState<T>(selector: (s: WorkspaceState) => T): T;
|
|
107
|
+
export declare function useWorkspaceClient(): WorkspaceClient;
|
|
108
|
+
/** The host's account switcher, or nothing. */
|
|
109
|
+
export declare function HostAccountSwitcher(): import("react").JSX.Element | null;
|
package/dist/host.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// The host contract of the workspace views (calendar, contacts, drive, tasks, meet and their navs).
|
|
3
|
+
//
|
|
4
|
+
// The package holds no IPC, no store and no product name. The host supplies, once, above the views:
|
|
5
|
+
// - `client`: every data call (`WorkspaceClient`), over the host's own transport;
|
|
6
|
+
// - `useState`: a selector hook over the host's state that holds the `WorkspaceState` slices (a zustand store
|
|
7
|
+
// with these fields and `set*` setters satisfies it directly), so the views and the host's navs share state;
|
|
8
|
+
// - the host actions the views trigger: errors, notices, dialogs, saved preferences, compose, downloads, view
|
|
9
|
+
// switches, and an optional account switcher.
|
|
10
|
+
import { createContext, useContext } from "react";
|
|
11
|
+
const HostContext = createContext(null);
|
|
12
|
+
/** Supplies the host to every workspace view and nav below it. */
|
|
13
|
+
export function WorkspaceHostProvider({ host, children }) {
|
|
14
|
+
return _jsx(HostContext.Provider, { value: host, children: children });
|
|
15
|
+
}
|
|
16
|
+
export function useWorkspaceHost() {
|
|
17
|
+
const host = useContext(HostContext);
|
|
18
|
+
if (!host)
|
|
19
|
+
throw new Error("@rightkit/workspace-ui: render the views inside <WorkspaceHostProvider host={...}>");
|
|
20
|
+
return host;
|
|
21
|
+
}
|
|
22
|
+
/** Selects from the host's workspace state. */
|
|
23
|
+
export function useWorkspaceState(selector) {
|
|
24
|
+
return useWorkspaceHost().useState(selector);
|
|
25
|
+
}
|
|
26
|
+
export function useWorkspaceClient() {
|
|
27
|
+
return useWorkspaceHost().client;
|
|
28
|
+
}
|
|
29
|
+
/** The host's account switcher, or nothing. */
|
|
30
|
+
export function HostAccountSwitcher() {
|
|
31
|
+
const Switcher = useWorkspaceHost().AccountSwitcher;
|
|
32
|
+
return Switcher ? _jsx(Switcher, {}) : null;
|
|
33
|
+
}
|
package/dist/icons.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type CSSProperties } from "react";
|
|
2
|
+
import { type LucideIcon } from "lucide-react";
|
|
3
|
+
declare const GLYPHS: {
|
|
4
|
+
calendar: LucideIcon;
|
|
5
|
+
check: LucideIcon;
|
|
6
|
+
"chevron-left": LucideIcon;
|
|
7
|
+
"chevron-right": LucideIcon;
|
|
8
|
+
clock: LucideIcon;
|
|
9
|
+
doc: LucideIcon;
|
|
10
|
+
draft: LucideIcon;
|
|
11
|
+
drive: LucideIcon;
|
|
12
|
+
folder: LucideIcon;
|
|
13
|
+
grid: LucideIcon;
|
|
14
|
+
image: LucideIcon;
|
|
15
|
+
list: LucideIcon;
|
|
16
|
+
more: LucideIcon;
|
|
17
|
+
people: LucideIcon;
|
|
18
|
+
"person-add": LucideIcon;
|
|
19
|
+
plus: LucideIcon;
|
|
20
|
+
star: LucideIcon;
|
|
21
|
+
tag: LucideIcon;
|
|
22
|
+
trash: LucideIcon;
|
|
23
|
+
upload: LucideIcon;
|
|
24
|
+
video: LucideIcon;
|
|
25
|
+
};
|
|
26
|
+
export type WorkspaceIconName = keyof typeof GLYPHS;
|
|
27
|
+
/** A workspace glyph. Decorative (hidden from assistive technology); stroke follows the text colour. */
|
|
28
|
+
export declare const Icon: import("react").MemoExoticComponent<({ name, size, className, style, }: {
|
|
29
|
+
name: WorkspaceIconName;
|
|
30
|
+
size?: number;
|
|
31
|
+
className?: string;
|
|
32
|
+
style?: CSSProperties;
|
|
33
|
+
}) => import("react").JSX.Element>;
|
|
34
|
+
export {};
|
package/dist/icons.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// The workspace views' glyphs, from Lucide (lucide-react). One name per glyph the views use, so a view names a
|
|
3
|
+
// glyph without importing an app's icon set.
|
|
4
|
+
import { memo } from "react";
|
|
5
|
+
import { Calendar, Check, ChevronLeft, ChevronRight, Clock, Ellipsis, File, FileText, Folder, HardDrive, Image, LayoutGrid, List, Plus, Star, Tag, Trash2, Upload, UserPlus, Users, Video, } from "lucide-react";
|
|
6
|
+
const GLYPHS = {
|
|
7
|
+
calendar: Calendar,
|
|
8
|
+
check: Check,
|
|
9
|
+
"chevron-left": ChevronLeft,
|
|
10
|
+
"chevron-right": ChevronRight,
|
|
11
|
+
clock: Clock,
|
|
12
|
+
doc: File,
|
|
13
|
+
draft: FileText,
|
|
14
|
+
drive: HardDrive,
|
|
15
|
+
folder: Folder,
|
|
16
|
+
grid: LayoutGrid,
|
|
17
|
+
image: Image,
|
|
18
|
+
list: List,
|
|
19
|
+
more: Ellipsis,
|
|
20
|
+
people: Users,
|
|
21
|
+
"person-add": UserPlus,
|
|
22
|
+
plus: Plus,
|
|
23
|
+
star: Star,
|
|
24
|
+
tag: Tag,
|
|
25
|
+
trash: Trash2,
|
|
26
|
+
upload: Upload,
|
|
27
|
+
video: Video,
|
|
28
|
+
};
|
|
29
|
+
/** A workspace glyph. Decorative (hidden from assistive technology); stroke follows the text colour. */
|
|
30
|
+
export const Icon = memo(function Icon({ name, size = 16, className, style, }) {
|
|
31
|
+
const Glyph = GLYPHS[name];
|
|
32
|
+
return _jsx(Glyph, { size: size, className: className, style: style, strokeWidth: 1.75, "aria-hidden": "true", focusable: "false" });
|
|
33
|
+
});
|