power-compass 0.0.1-alpha.1 → 0.0.1-alpha.3

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/package.json CHANGED
@@ -1,13 +1,19 @@
1
1
  {
2
2
  "name": "power-compass",
3
- "version": "0.0.1-alpha.1",
3
+ "version": "0.0.1-alpha.3",
4
4
  "type": "module",
5
- "main": "./dist/compass.cjs.js",
6
- "module": "./dist/compass.es.js",
5
+ "main": "./dist/power-compass.cjs.js",
6
+ "module": "./dist/power-compass.es.js",
7
+ "types": "./dist/power-compass.es.d.ts",
7
8
  "exports": {
8
9
  ".": {
9
- "import": "./dist/compass.es.js",
10
- "require": "./dist/compass.cjs.js"
10
+ "import": "./dist/power-compass.es.js",
11
+ "require": "./dist/power-compass.cjs.js",
12
+ "types": "./dist/power-compass.es.d.ts"
13
+ },
14
+ "./cjs": {
15
+ "require": "./dist/power-compass.cjs.js",
16
+ "types": "./dist/power-compass.es.d.ts"
11
17
  }
12
18
  },
13
19
  "scripts": {
@@ -35,6 +41,7 @@
35
41
  "devDependencies": {
36
42
  "@powerhome/eslint-config": "0.3.0",
37
43
  "@testing-library/react-hooks": "^8.0.1",
44
+ "@types/lodash": "^4.17.23",
38
45
  "@types/react": "^19.2.9",
39
46
  "@typescript-eslint/eslint-plugin": "8.18.0",
40
47
  "@typescript-eslint/parser": "8.18.0",
package/.eslintrc.json DELETED
@@ -1,6 +0,0 @@
1
- {
2
- "extends": "@powerhome",
3
- "globals": {
4
- "RequestInit": true
5
- }
6
- }
package/index.html DELETED
@@ -1,13 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>power-compass</title>
8
- </head>
9
- <body>
10
- <div id="root"></div>
11
- <script type="module" src="/src/main.tsx"></script>
12
- </body>
13
- </html>
package/jest.config.js DELETED
@@ -1,11 +0,0 @@
1
- export default {
2
- automock: false,
3
- maxWorkers: 5,
4
- preset: "ts-jest",
5
- testEnvironment: "jest-environment-jsdom",
6
- transformIgnorePatterns: ["/node_modules/(?!playbook-ui)"],
7
- transform: {
8
- "^.+\\.tsx?$": ["ts-jest", { tsconfig: "./tsconfig.json" }],
9
- "^.+\\.scss$": "jest-scss-transform",
10
- },
11
- }
@@ -1,9 +0,0 @@
1
- import { createContext } from "react"
2
-
3
- export interface CompassContext {
4
- backends?: string[]
5
- params?: Record<string, string>
6
- contextId: string | number
7
- }
8
-
9
- export const Compass = createContext<CompassContext | undefined>(undefined)
@@ -1,38 +0,0 @@
1
- import React, { useState, useEffect } from "react"
2
- import findIndex from "lodash/findIndex"
3
- import nth from "lodash/nth"
4
- import flatMap from "lodash/flatMap"
5
- import get from "lodash/get"
6
-
7
- import type { SearchResult, ResultEntry } from "../types"
8
-
9
- type Cursor = {
10
- cursor?: ResultEntry<unknown>
11
- previous: () => void
12
- next: () => void
13
- resetCursor: () => void
14
- updateCursor: React.Dispatch<
15
- React.SetStateAction<ResultEntry<unknown> | undefined>
16
- >
17
- }
18
-
19
- function useCursor(searchResult: SearchResult<unknown>[]): Cursor {
20
- const [cursor, updateCursor] = useState<ResultEntry<unknown> | undefined>()
21
- useEffect(() => {
22
- updateCursor(get(searchResult, "0.entries.0"))
23
- }, [searchResult])
24
-
25
- const entries = flatMap(searchResult, "entries")
26
- const moveCursor = (positions: number) => {
27
- const currentIndex = cursor ? findIndex(entries, cursor) : -1
28
- const nextIndex = (currentIndex + positions) % entries.length
29
- updateCursor(nth(entries, nextIndex))
30
- }
31
- const previous = () => moveCursor(-1)
32
- const next = () => moveCursor(1)
33
- const resetCursor = () => updateCursor(undefined)
34
-
35
- return { cursor, previous, next, resetCursor, updateCursor }
36
- }
37
-
38
- export default useCursor
@@ -1,14 +0,0 @@
1
- import { useState, useEffect } from "react"
2
-
3
- const useDebounce = (value: string, delay = 300) => {
4
- const [debouncedValue, updateDebouncedValue] = useState(value)
5
-
6
- useEffect(() => {
7
- const handler = setTimeout(() => updateDebouncedValue(value), delay)
8
- return () => clearTimeout(handler)
9
- }, [value, delay])
10
-
11
- return debouncedValue
12
- }
13
-
14
- export default useDebounce
@@ -1,18 +0,0 @@
1
- import { useEffect } from "react"
2
-
3
- const useShortcut = (shortcut: string, ref?: HTMLInputElement) => {
4
- useEffect(() => {
5
- const handler = (event: KeyboardEvent) => {
6
- const modifier = event.ctrlKey || event.metaKey
7
- if (modifier && shortcut === event.key.toLowerCase()) {
8
- event.preventDefault()
9
- event.stopPropagation()
10
- ref?.focus()
11
- }
12
- }
13
- document.body.addEventListener("keydown", handler)
14
- return () => document.body.removeEventListener("keydown", handler)
15
- }, [shortcut, ref])
16
- }
17
-
18
- export default useShortcut
package/src/http/fetch.ts DELETED
@@ -1,30 +0,0 @@
1
- export async function fetchBackend<T>(
2
- backend: string,
3
- contextId: string,
4
- service: string,
5
- query: ConstructorParameters<typeof URLSearchParams>[0],
6
- ): Promise<T> {
7
- const url = new URL(
8
- `${backend}/${contextId}/${service}`,
9
- window.location.origin,
10
- )
11
- url.search = new URLSearchParams(query).toString()
12
-
13
- console.debug("Fetching data from backend:", url.toString()) // eslint-disable-line no-console
14
-
15
- const respond = (response: Response) => {
16
- if (response.ok) {
17
- return response.json()
18
- } else {
19
- throw new Error(`${backend} reponse error: ${response.statusText}`)
20
- }
21
- }
22
-
23
- return fetch(url.toString(), { cache: "default" })
24
- .then(respond)
25
- .catch(() => fetch(url.toString(), { cache: "force-cache" }).then(respond))
26
- .catch((error) => {
27
- console.debug(`Failed to fetch from ${backend}:`, error) // eslint-disable-line no-console
28
- throw error
29
- })
30
- }
package/src/http/index.ts DELETED
@@ -1 +0,0 @@
1
- export * from "./fetch"
package/src/index.ts DELETED
@@ -1,5 +0,0 @@
1
- export * from "./http"
2
- export * from "./menu"
3
- export * from "./tasks"
4
- export * from "./search"
5
- export * from "./components/context"
@@ -1,56 +0,0 @@
1
- import type { MenuItem, CompassMenuResult } from "./types"
2
-
3
- export type FutureMenuItem = () => Promise<MenuItem>
4
- export type FilterValue = true | false | undefined | string
5
- export type PluginFilterKey = keyof typeof PluginFilters
6
- export type PluginFilter = {
7
- [Property in PluginFilterKey]?: FilterValue
8
- }
9
-
10
- const PluginMenuItems: (MenuItem | Promise<MenuItem>)[] = []
11
- const PluginFilters = {
12
- tagged(items: MenuItem[], tag: FilterValue): MenuItem[] {
13
- return items.filter((item) => item.tags?.includes(tag as string))
14
- },
15
- }
16
-
17
- function applyFilters(allItems: MenuItem[], filters: PluginFilter): MenuItem[] {
18
- const filterKeys = Object.keys(filters) as PluginFilterKey[]
19
-
20
- return filterKeys.reduce(
21
- (items: MenuItem[], filter: PluginFilterKey): MenuItem[] =>
22
- PluginFilters[filter](items, filters[filter]),
23
- allItems,
24
- )
25
- }
26
-
27
- export function fetchPlugin(
28
- filters: PluginFilter = {},
29
- ): Promise<CompassMenuResult> {
30
- return new Promise((resolve) => {
31
- const items = PluginMenuItems.map((item) => Promise.resolve(item))
32
-
33
- Promise.all(items)
34
- .then((resolvedItems) => resolvedItems.filter(Boolean))
35
- .then((resolvedItems) => applyFilters(resolvedItems, filters))
36
- .then((filteredItems) => resolve({ items: filteredItems }))
37
- .catch((error) =>
38
- resolve({
39
- items: [],
40
- error: {
41
- message: error instanceof Error ? error.message : String(error),
42
- },
43
- }),
44
- )
45
- })
46
- }
47
-
48
- export async function createMenuItem(
49
- item: MenuItem | FutureMenuItem,
50
- ): Promise<void> {
51
- if (typeof item === "function") {
52
- PluginMenuItems.push(item())
53
- } else {
54
- PluginMenuItems.push(item)
55
- }
56
- }
@@ -1,183 +0,0 @@
1
- /* global fetchMock */
2
-
3
- import { fetchMenu, fetchMenus } from "./fetchMenu"
4
- import type { MenuItem } from "../menu/types"
5
- import { expect, describe, beforeEach, it } from "vitest"
6
-
7
- const currentUserId = "123"
8
-
9
- describe("Compass Menu fetch", () => {
10
- beforeEach(() => {
11
- fetchMock.resetMocks()
12
- })
13
-
14
- describe("fetchMenu", () => {
15
- it("fetches the menu items", async () => {
16
- const items: MenuItem[] = [
17
- {
18
- label: "Test",
19
- url: "http://example.com/test1",
20
- icon: "test1",
21
- },
22
- ]
23
-
24
- fetchMock.mockIf(
25
- `http://example.com/compass/${currentUserId}/menu`,
26
- JSON.stringify(items),
27
- )
28
-
29
- const fetchResult = await fetchMenu(
30
- "http://example.com/compass",
31
- currentUserId,
32
- )
33
-
34
- expect(fetchResult).toEqual({ items })
35
- })
36
-
37
- it("fetches menu items with the given filters", async () => {
38
- const items: MenuItem[] = [
39
- {
40
- label: "Test",
41
- url: "http://example.com/test1",
42
- icon: "test1",
43
- },
44
- ]
45
-
46
- fetchMock.mockIf(
47
- `http://example.com/compass/${currentUserId}/menu?tagged=actions`,
48
- JSON.stringify(items),
49
- )
50
-
51
- const fetchResult = await fetchMenu(
52
- "http://example.com/compass",
53
- currentUserId,
54
- { tagged: "actions" },
55
- )
56
-
57
- expect(fetchResult).toEqual({ items })
58
- })
59
-
60
- it("fetches the menu items from local cache when the network is down", async () => {
61
- const items: MenuItem[] = [
62
- {
63
- label: "Test",
64
- url: "http://example.com/test1",
65
- icon: "test1",
66
- },
67
- ]
68
-
69
- fetchMock
70
- .mockImplementationOnce((url, options) => {
71
- expect(url).toEqual(
72
- `http://example.com/compass/${currentUserId}/menu`,
73
- )
74
- expect(options.cache).toEqual("default")
75
-
76
- return Promise.resolve(new Response(null, { status: 404 }))
77
- })
78
- .mockImplementationOnce((url, options) => {
79
- expect(url).toEqual(
80
- `http://example.com/compass/${currentUserId}/menu`,
81
- )
82
- expect(options.cache).toEqual("force-cache")
83
-
84
- return Promise.resolve(new Response(JSON.stringify(items)))
85
- })
86
-
87
- const fetchResult = await fetchMenu(
88
- "http://example.com/compass",
89
- currentUserId,
90
- )
91
-
92
- expect(fetchResult).toEqual({ items })
93
- })
94
-
95
- it("logs and ignores a backend when it fails to load and has no cache", async () => {
96
- fetchMock
97
- .mockImplementationOnce((url, options) => {
98
- expect(url).toEqual(
99
- `http://example.com/compass/${currentUserId}/menu`,
100
- )
101
- expect(options.cache).toEqual("default")
102
-
103
- return Promise.resolve(new Response(null, { status: 404 }))
104
- })
105
- .mockImplementationOnce((url, options) => {
106
- expect(url).toEqual(
107
- `http://example.com/compass/${currentUserId}/menu`,
108
- )
109
- expect(options.cache).toEqual("force-cache")
110
-
111
- return Promise.resolve(new Response(null, { status: 404 }))
112
- })
113
-
114
- const fetchResult = await fetchMenu(
115
- "http://example.com/compass",
116
- currentUserId,
117
- )
118
-
119
- expect(fetchResult).toEqual({
120
- items: [],
121
- error: { message: expect.any(String) },
122
- })
123
- })
124
- })
125
-
126
- describe("fetchMenus", () => {
127
- it("merges multiple menu backends", async () => {
128
- const items1: MenuItem[] = [
129
- {
130
- label: "Test Example 1",
131
- url: "http://example1.com/test1",
132
- icon: "test-example-1",
133
- },
134
- ]
135
- const items2: MenuItem[] = [
136
- {
137
- label: "Test Example 2",
138
- url: "http://example2.com/test2",
139
- icon: "test-example-2",
140
- },
141
- ]
142
-
143
- fetchMock.mockReturnValueOnce(
144
- Promise.resolve(new Response(JSON.stringify(items1))),
145
- )
146
- fetchMock.mockReturnValueOnce(
147
- Promise.resolve(new Response(JSON.stringify(items2))),
148
- )
149
-
150
- const fetchItems = await fetchMenus(
151
- ["http://example1.com/compass/", "http://example2.com/compass/"],
152
- currentUserId,
153
- )
154
-
155
- expect(fetchItems).toEqual([...items1, ...items2])
156
- })
157
-
158
- it("filters out errored menu backends", async () => {
159
- const items1: MenuItem[] = [
160
- {
161
- label: "Test Example 1",
162
- url: "http://example1.com/test1",
163
- icon: "test-example-1",
164
- },
165
- ]
166
-
167
- fetchMock.mockReturnValueOnce(
168
- Promise.resolve(new Response(JSON.stringify(items1))),
169
- )
170
- // Simulate an error result for the second backend
171
- fetchMock.mockReturnValueOnce(
172
- Promise.resolve(new Response(null, { status: 404 })),
173
- )
174
-
175
- const fetchItems = await fetchMenus(
176
- ["http://example1.com/compass/", "http://example2.com/compass/"],
177
- currentUserId,
178
- )
179
-
180
- expect(fetchItems).toEqual([...items1])
181
- })
182
- })
183
- })
@@ -1,42 +0,0 @@
1
- import { fetchBackend } from "../http"
2
- import { type MenuItem, type CompassMenuResult } from "./types"
3
-
4
- export type CompassMenuFilters = {
5
- tagged?: string
6
- }
7
-
8
- export async function fetchMenu(
9
- backend: string,
10
- contextId: string,
11
- filters: CompassMenuFilters = {},
12
- ): Promise<CompassMenuResult> {
13
- try {
14
- const items = await fetchBackend<MenuItem[]>(
15
- backend,
16
- contextId,
17
- "menu",
18
- filters,
19
- )
20
- return { items }
21
- } catch (e) {
22
- return {
23
- items: [],
24
- error: { message: e instanceof Error ? e.message : String(e) },
25
- }
26
- }
27
- }
28
-
29
- export async function fetchMenus(
30
- backends: string[],
31
- contextId: string,
32
- filters: CompassMenuFilters = {},
33
- ): Promise<MenuItem[]> {
34
- const menuPromises = backends.map((backend) =>
35
- fetchMenu(backend, contextId, filters),
36
- )
37
- const results = await Promise.all(menuPromises)
38
-
39
- return results
40
- .filter((result) => !result.error)
41
- .flatMap((result) => result.items)
42
- }
package/src/menu/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export * from "./useCompassMenu"
2
- export { createMenuItem } from "./clientMenu"
@@ -1,48 +0,0 @@
1
- import { type MenuItem } from "./types"
2
-
3
- export type FlatMenuItem = Omit<MenuItem, "items"> & {
4
- parent?: FlatMenuItem
5
- }
6
-
7
- export type TransformedMenuItem<
8
- Option extends TransformOptions = TransformOptions,
9
- > = Option["flatten"] extends true ? FlatMenuItem : MenuItem
10
-
11
- export const Transformations = {
12
- flatten(menuItems: MenuItem[], parent?: FlatMenuItem): FlatMenuItem[] {
13
- return menuItems
14
- .map(({ items, ...item }) => {
15
- const parentFlatItem = { parent, ...item }
16
-
17
- if (items) {
18
- return this.flatten(items, parentFlatItem)
19
- } else {
20
- return parentFlatItem
21
- }
22
- })
23
- .flat()
24
- },
25
-
26
- trim(menuItems: MenuItem[]): MenuItem[] {
27
- return menuItems.filter((item) => item.url || item.items || item.modal)
28
- },
29
- }
30
-
31
- export type TransformOption = true | false | undefined
32
- export type TransformOptionKey = keyof typeof Transformations
33
- export type TransformOptions = {
34
- [Property in TransformOptionKey]?: TransformOption
35
- }
36
-
37
- export function applyTransformations(
38
- items: MenuItem[],
39
- options: TransformOptions,
40
- ): TransformedMenuItem[] {
41
- const optionsUsed = Object.keys(options) as TransformOptionKey[]
42
-
43
- return optionsUsed.reduce(
44
- (items: MenuItem[], transformation: TransformOptionKey): MenuItem[] =>
45
- Transformations[transformation](items),
46
- items,
47
- )
48
- }
package/src/menu/types.ts DELETED
@@ -1,19 +0,0 @@
1
- import { type ComponentType } from "react"
2
-
3
- export type MenuItem = {
4
- badge?: number
5
- gravity?: number
6
- icon?: string
7
- items?: MenuItem[]
8
- label: string
9
- meta?: Record<string, string>
10
- tags?: string[]
11
- url?: string
12
- modal?: ComponentType
13
- onSelect?: () => void
14
- }
15
-
16
- export type CompassMenuResult = {
17
- items: MenuItem[]
18
- error?: { message: string }
19
- }