-
Notifications
You must be signed in to change notification settings - Fork 41
example share folder
Two recipes: server-side (invite from PHP) and client-side (listen for share events).
add_action( 'init', function () {
// Imagine we just created a folder via openstation_files_create_folder
// and want to grant the team Editor role read access programmatically.
if ( ! function_exists( 'openstation_folder_share_invite' ) ) {
return;
}
$folder_id = (int) get_option( 'my_plugin_shared_folder_id' );
$owner_id = (int) get_post_field( 'post_author', $folder_id ); // or wherever you stash it
$share_id = openstation_folder_share_invite(
$folder_id,
$owner_id,
'role', // principal_type: 'user' | 'role'
'editor', // principal_ref: user id (stringified) or role slug
'read' // capability: 'read' | 'write'
);
if ( is_wp_error( $share_id ) ) {
error_log( 'Could not invite editor role: ' . $share_id->get_error_message() );
}
} );A pending share row appears in wp_desktop_mode_folder_shares.
The next heartbeat tick delivers it to every editor's desktop
shell. They accept or deny via the modal.
add_action( 'openstation_files_share_accepted', function ( $share_id, $row, $user_id ) {
// Send a welcome notification when someone accepts an invite.
wp_mail(
get_userdata( $user_id )->user_email,
'Welcome to the team folder',
'You can now collaborate inside the folder.'
);
}, 10, 3 );
add_action( 'openstation_files_share_revoked', function ( $share_id, $row, $actor_id ) {
// Audit log every revoke.
error_log( "Share #{$share_id} revoked by user {$actor_id}" );
}, 10, 3 );The default is owner-only. Plugins that ship a "team admin" concept can extend it:
add_filter( 'openstation_files_share_can_manage', function ( $can, $folder_id, $user_id, $folder ) {
if ( $can ) {
return $can;
}
return user_can( $user_id, 'manage_team_folders' );
}, 10, 4 );Default eligibility is "any role that carries edit_posts".
Restrict (or broaden) to your site's reality:
add_filter( 'openstation_files_share_eligible_roles', function ( $roles ) {
// Only allow sharing with the Editor and a custom "team_lead" role.
return array_filter( $roles, function ( $r ) {
return in_array( $r['slug'], array( 'editor', 'team_lead' ), true );
} );
} );The user autocomplete (GET /files/users/search) is gated by
edit_posts at the request level; if you've added a custom role
that should appear in the picker, give it edit_posts or add a
custom permission callback via:
add_filter( 'openstation_files_share_user_query_args', function ( $args, $req_params ) {
// E.g. only return users in the same multisite blog as the actor.
$args[ 'blog_id' ] = get_current_blog_id();
return $args;
}, 10, 2 );Share state lives in the cross-bundle shares store at the slot
'desktop-files/shares' — subscribe to it to react to share
lifecycle changes (new pending invites, accepts, revokes) from
any bundle:
import { createSharedStore } from '...';
const store = createSharedStore( 'desktop-files/shares', () => ( {
byFolder: new Map(),
pending: [],
sharesVersion: 0,
deniedFolders: new Set(),
} ) );
store.subscribe( ( s ) => {
console.log( 'Pending invites:', s.pending.length );
} );The default UI hooks are the title-bar button on folder windows and the "Share folder…" item in the tile context menu. To open the modal from your own code path:
import { openShareSettingsModal } from '/path/to/share-settings-modal';
openShareSettingsModal( {
folderId: 42,
folderName: 'Marketing assets',
ownerName: 'Daniel',
} );Server-side conflict detection is opt-in per request:
import { updatePlacement } from '...';
try {
await updatePlacement(
placementId,
{ parentId: targetFolder },
currentUpdatedAtMs, // ← If-Match value
);
} catch ( err ) {
if ( err instanceof FilesConflictError ) {
console.log( 'Lost to', err.detail.actor.name );
}
}Pass 0 (or omit) the third arg to keep last-write-wins
semantics.
openstation_files_can_delete_folder runs after the ownership
check and before the cascade. Return a WP_Error or anything
other than true to block — the cascade proceeds only when the
filter resolves to exactly true (the default).
add_filter(
'openstation_files_can_delete_folder',
function ( $can, $folder_id, $user_id, $folder ) {
// Block delete when the folder has >5 active recipients
// until the owner explicitly revokes the shares first.
if ( ! function_exists( 'openstation_files_get_folder_shares' ) ) {
return $can;
}
$shares = (array) openstation_files_get_folder_shares( $folder_id );
$active = array_filter( $shares, fn( $s ) => 'denied' !== $s['state'] );
if ( count( $active ) > 5 ) {
return new WP_Error(
'my_plugin_too_many_recipients',
'Revoke shares before deleting a folder with >5 recipients.',
array( 'status' => 409 )
);
}
return $can;
},
10,
4
);openstation_files_after_delete_folder_cascade fires once per
folder delete with a structured summary of everything that was
torn down — useful for audit logs, cross-tenant cleanup, or
sending notifications to affected recipients.
add_action(
'openstation_files_after_delete_folder_cascade',
function ( $root_folder_id, $user_id, $summary ) {
error_log( sprintf(
'User %d deleted folder %d → cascade removed %d folder(s), %d share(s), %d pointing placement(s), %d inside placement(s).',
$user_id,
$root_folder_id,
count( $summary['folders_deleted'] ),
count( $summary['shares_revoked'] ),
count( $summary['placements_pointing'] ),
count( $summary['placements_inside'] )
) );
},
10,
3
);Per-share openstation_files_share_revoked actions fire during
the cascade for each share that gets torn down, so plugins
already listening to that signal don't need to subscribe to the
summary too — both fire.
add_action(
'openstation_folder_renamed',
function ( $folder_id, $new_name, $old_name, $user_id ) {
// Mirror the rename into a sidecar plugin table.
my_plugin_update_folder_label( $folder_id, $new_name );
},
10,
4
);The framework already bumps every placement pointing at the renamed folder so connected clients see the new title via the next heartbeat tick — no extra work needed for live UI sync.
- folder-sharing.md — full architecture.
- hooks-reference.md.
This wiki is generated from the docs/ directory — edits made here are overwritten by the next sync.
To change a page, open a pull request against docs/.
Guides
- Development guide
- Releasing openstation
- Agents security model
- API Index
- Architecture
- Bridge protocol — wiring overview
- <os-*> component reference
- Native Desktop Host — Experimental
- Desktop themes
- Dock customization — two registries, one mental model
- The event-driven framework
- Files on the Desktop
- Folder sharing
- Getting Started
- Hooks Reference
- Icons
- JavaScript Reference
- The Living Tree — algorithm definition
- Mio
- Native Windows & Framework Interop
- Plugin compatibility layer
- Progressive Web App (PWA)
- Station Home
- Using openstation from your own plugin
Migration notes
- Migration: built-in activity channels move to the os/ namespace
- Migration: window, wallpaper and widget bundles load on demand
- Migration — the navigation model
- Migration: a native window's tabs move to the window chrome
All examples
- AI Agents — extend and invoke from a plugin
- wp.os.ai.ask() — programmatic AI Copilot
- Tune the AI model config
- Custom arrange-menu action
- Open a child window its owner can't cover
- Style a specific admin page inside the iframe
- Code Blue — register your plugin's log file
- Open a file in the Code editor (deep-link from any window)
- Connect to a window — title-bar button + iframe pub/sub
- Content changes — live-refresh every window listing your type
- Custom window chrome (Experimental)
- Register a custom unfocused-window effect
- Example: render a data table
- Real file storage — react to uploads, gate policy, share from PHP
- React to a window being set free onto the real desktop
- Cross-window devtools — instrumentation primitives
- Add a dock item with a badge
- Decorate the dock without forking the renderer
- Replace the dock rail entirely
- Retune the Drafts widget's AI writing assistant
- Gate OpenStation by role
- Iframe-initiated window opens
- Build a feed reader without the bookkeeping
- Inject data into openStationConfig
- Render a list without losing clicks — renderKeyedList()
- Example: layout primitives (body → panel → row → col)
- Use <os-*> components from a plugin that ships as a zip
- Restyle and drive Mio
- Add an action that works on a whole selection
- WP Explorer — custom post types and their folder
- Add an action button to a WP Explorer preview pane
- Example: native Posts window
- Example: native window with tabs
- Native windows
- Customize note → post conversion
- Send a notification
- OAuth relay — connect to an external service
- OS-file drop
- <os-flyout> — window-scoped sliding card
- Plugins window — extras
- Track who's around — wp.os.presence
- Example: progress bar
- PWA install — surface your own button
- React to window events
- Example: extend the Trash
- Register a slash-command
- Register a desktop theme from a plugin
- Register a game
- Example: register a desktop icon (Jorvy)
- Register a wallpaper
- Register a widget
- Related entities — extend the title bar's "Related" menu
- The native-window render ctx
- Programmatic folder sharing
- Share state across multi-bundle plugins — wp.os.createSharedStore()
- Example: loading spinner
- Add an opt-in card to Station Home
- Accept drops on your desktop icon
- Give a tile two icons, one per state
- Add a row to a window's ⋯ menu
- Example: window activity & the status ring
- Window controls
- Subscribe to window lifecycle events
- Window links — relate windows and restyle the ties (Experimental)
- Window loading state — spinner overlay & ready signal
- Show a banner at the top of a window
- Pulse a window's icon — Window.requestAttention()
- Register a custom window reveal
- Window slots
- Window themes
- Native window with bundle-bound config