Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3ef3936
feat(files): list folders through graph behind a toggle
dschmidt Sep 1, 2026
2a6bf5b
feat: list folders through graph in a single request
dschmidt Sep 1, 2026
dc1b081
refactor: drop the graph listing toggle
dschmidt Sep 1, 2026
8cc1e9d
chore(web-client): regenerate libre-graph client
dschmidt Sep 6, 2026
c646eb3
refactor(web-client): use the generated ops for the graph listing
dschmidt Sep 6, 2026
b8cab74
fix(web-pkg): translate folder vaults on the graph listing too
dschmidt Sep 6, 2026
1804f71
feat: take has-preview from the expanded thumbnails
dschmidt Sep 6, 2026
3c9f931
fix(web-pkg): keep share paths relative to the share root
dschmidt Sep 7, 2026
2ca9c00
test(e2e): wait for the folder listing on either api
dschmidt Sep 7, 2026
30b4970
test(e2e): don't leave the upload response promise pending
dschmidt Sep 7, 2026
12ed3e2
test(e2e): authenticate basic auth requests with the username
dschmidt Sep 7, 2026
5f4377e
feat: list public links through graph as well
dschmidt Sep 7, 2026
ad892b8
feat(web-pkg): stat single items through graph
dschmidt Sep 7, 2026
5b162e8
refactor(web-pkg): stat the breadcrumb ancestors through graph
dschmidt Sep 7, 2026
be2aab6
fix(web-pkg): carry the graph error shape over to the callers
dschmidt Sep 7, 2026
ef4ce81
test(e2e): wait for the stat when a file opens in an app
dschmidt Sep 7, 2026
7be3bef
feat: resolve a public link through graph
dschmidt Sep 7, 2026
80a2c9d
fix: tell a single file link apart by the item behind it
dschmidt Sep 7, 2026
57c9c6c
feat(web-pkg): stat and list through graph for every caller
dschmidt Sep 7, 2026
94a65af
fix(design-system): close a drop on escape when the focus never enter…
dschmidt Sep 7, 2026
1b360c1
fix(web-client): treat a drive root as a folder
dschmidt Sep 7, 2026
a1c4b5d
fix(web-pkg): keep the vault working through the graph seam
dschmidt Sep 7, 2026
7820f7e
test: follow the public link item type and drop an unused import
dschmidt Sep 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/design-system/src/components/OcDrop/OcDrop.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,18 @@ describe('OcDrop', () => {
expect(wrapper.find('oc-mobile-drop-stub').exists()).toBeTruthy()
})
})

it('closes on escape when it was opened by pointer, so the focus never entered it', async () => {
const { wrapper } = dom()
document.querySelector<HTMLElement>('#trigger').click()
// no flushPromises: the drop is still positioning itself, escape has to
// close it even then
await nextTick()
expect(wrapper.find('.oc-drop').exists()).toBe(true)

document.body.dispatchEvent(new KeyboardEvent('keydown', { code: 'Escape', bubbles: true }))
await nextTick()

expect(wrapper.find('.oc-drop').exists()).toBe(false)
})
})
24 changes: 24 additions & 0 deletions packages/design-system/src/components/OcDrop/OcDrop.vue
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,9 @@ const showDrop = async ({
const anchorEl: HTMLElement | VirtualElement | null = anchorElement || unref(anchor)
activeAnchorElement = anchorEl
isOpen.value = true
// registered before the drop is positioned: everything below waits for a
// frame, and a key pressed in between has to close the drop as well
registerEventListener(document, 'keydown', handleDocumentKeydown, 'document')
await nextTick()
if (!anchorEl) {
console.warn('OcDrop cannot be opened: anchor element not found')
Expand All @@ -282,6 +285,11 @@ const showDrop = async ({
// fixes a timing issue with the rendering of the drop
await awaitAnimationFrame()

// escape can close the drop again while it is still positioning itself
if (!unref(isOpen) || !unref(drop)) {
return
}

if (isMenu) {
// if drop is a menu, set role="menu" on all ul elements in the drop for better screen reader support
const uls = unref(drop)?.getElementsByTagName('ul')
Expand Down Expand Up @@ -317,6 +325,10 @@ const showDrop = async ({
]
})

if (!unref(isOpen) || !unref(drop)) {
return
}

Object.assign(unref(drop).style, { left: `${x}px`, top: `${y}px` })
unref(anchor)?.setAttribute('aria-expanded', 'true')
emit('showDrop')
Expand Down Expand Up @@ -376,6 +388,18 @@ const handleDropClickOutside = async (event: Event) => {
}
}

// Escape has to close the drop even when the focus never entered it, which is
// the case whenever it was opened by pointer: a context menu on right click for
// instance. A key pressed inside the drop never reaches this handler, the drop's
// own one below stops it from travelling further.
const handleDocumentKeydown = (event: Event) => {
if (!isKeyboardEvent(event) || event.code !== 'Escape') {
return
}
hideDrop()
unref(anchor)?.focus()
}

const handleDropKeydown = (event: Event) => {
if (!isKeyboardEvent(event)) {
return
Expand Down
70 changes: 69 additions & 1 deletion packages/web-client/src/graph/driveItems/driveItems.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
import { DriveItemApiFactory, DrivesRootApiFactory, MeDriveApiFactory } from './../generated'
import {
DriveItem,
DriveItemApiAxiosParamCreator,
DriveItemApiFactory,
DrivesRootApiFactory,
MeDriveApiFactory
} from './../generated'
import type { GraphFactoryOptions } from './../types'
import type { GraphDriveItems } from './types'

// placeholder for the item id in a colon-syntax url. it consists of unreserved
// characters only, so it survives encodeURIComponent and can be swapped for the
// path after the generated param creator has built the url.
const COLON_PATH_PLACEHOLDER = '__colon_path__'

// the server recognizes a path lookup by a literal ':/' in the encoded url and
// expects every path segment to be percent-encoded, a ':' inside a name as
// '%3A'. encodeURIComponent does exactly that. See ResolveGraphPath in
// services/graph/pkg/middleware/path_lookup.go on the server side.
const colonPathRef = (path: string) =>
`root:/${path.split('/').filter(Boolean).map(encodeURIComponent).join('/')}`

export const DriveItemsFactory = ({
axiosClient,
config
Expand All @@ -16,6 +34,7 @@ export const DriveItemsFactory = ({
driveId,
itemId,
undefined,
undefined,
requestOptions
)
return data
Expand Down Expand Up @@ -63,6 +82,55 @@ export const DriveItemsFactory = ({
async listSharedWithMe(options, requestOptions) {
const { data } = await meDriveApiFactory.listSharedWithMe(options?.expand, requestOptions)
return data?.value || []
},

// statDriveItem stats an item by id or by path. The path form cannot go
// through the generated operation: it percent-encodes the item id, which
// turns the ':/' the server matches on into '%3A%2F'. So the request is
// built by the generated param creator and only the item segment is
// rewritten afterwards, keeping the query and headers generated.
async statDriveItem(driveId, ref, options, requestOptions) {
if (ref.itemId) {
const { data } = await driveItemApiFactory.getDriveItemV1(
driveId,
ref.itemId,
options?.select,
options?.expand,
requestOptions
)
return data
}

const { url, options: axiosOptions } = await DriveItemApiAxiosParamCreator(
config
).getDriveItemV1(
driveId,
COLON_PATH_PLACEHOLDER,
options?.select,
options?.expand,
requestOptions
)

const { data } = await axiosClient.request<DriveItem>({
...axiosOptions,
// the path form is anchored at the drive root, so it replaces the
// whole '/items/{item-id}' segment rather than just the id
url: `${config.basePath}${url.replace(
`items/${COLON_PATH_PLACEHOLDER}`,
colonPathRef(ref.path)
)}`
})
return data
},

async listDriveItemChildren(driveId, itemId, options, requestOptions) {
const { data } = await driveItemApiFactory.getDriveItemChildren(
driveId,
itemId,
options?.select,
requestOptions
)
return data?.value || []
}
}
}
31 changes: 30 additions & 1 deletion packages/web-client/src/graph/driveItems/types.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,41 @@
import { DriveItem } from '../generated'
import {
DriveItem,
GetDriveItemChildrenSelectEnum,
GetDriveItemV1ExpandEnum,
GetDriveItemV1SelectEnum
} from '../generated'
import type { GraphRequestOptions } from '../types'

export interface DriveItemStatOptions {
select?: Set<GetDriveItemV1SelectEnum>
expand?: Set<GetDriveItemV1ExpandEnum>
}

export interface DriveItemChildrenOptions {
select?: Set<GetDriveItemChildrenSelectEnum>
}

// a driveItem is addressed either by its id or by its path, never by both
export type DriveItemRef = { itemId: string; path?: never } | { itemId?: never; path: string }

export interface GraphDriveItems {
listDriveItemChildren: (
driveId: string,
itemId: string,
options?: DriveItemChildrenOptions,
requestOptions?: GraphRequestOptions
) => Promise<DriveItem[]>
getDriveItem: (
driveId: string,
itemId: string,
requestOptions?: GraphRequestOptions
) => Promise<DriveItem>
statDriveItem: (
driveId: string,
ref: DriveItemRef,
options?: DriveItemStatOptions,
requestOptions?: GraphRequestOptions
) => Promise<DriveItem>
createDriveItem: (
driveId: string,
data: DriveItem,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ docs/InvitationsApi.md
docs/InvitedUserMessageInfo.md
docs/ItemReference.md
docs/LivePhoto.md
docs/LockInfo.md
docs/MeChangepasswordApi.md
docs/MeDriveApi.md
docs/MeDriveRootApi.md
Expand Down
Loading