Fix code quality violations for publish
Progressive breadcrumb resolution with caching, fix double headers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
402
app/RSpade/Breadcrumbs/Rsx_Breadcrumb_Resolver.js
Normal file → Executable file
402
app/RSpade/Breadcrumbs/Rsx_Breadcrumb_Resolver.js
Normal file → Executable file
@@ -1,58 +1,58 @@
|
||||
/**
|
||||
* Rsx_Breadcrumb_Resolver - Progressive breadcrumb resolution with caching
|
||||
*
|
||||
* Provides instant breadcrumb rendering using cached data while validating
|
||||
* against live action data in the background.
|
||||
* Provides instant breadcrumb rendering using cached data while resolving
|
||||
* live labels progressively in the background.
|
||||
*
|
||||
* Resolution Phases:
|
||||
* 1. Immediate Cached Render - If ALL chain links cached, render instantly
|
||||
* 2. Active Action Resolution - Await live action's breadcrumb methods
|
||||
* 3. Chain Walk - Walk parents using cache or detached action loads
|
||||
* 4. Render Chain - Emit chain structure (may have null labels)
|
||||
* 5. Progressive Updates - Update labels as they resolve
|
||||
* Resolution Flow:
|
||||
* 1. Keep previous breadcrumbs visible initially
|
||||
* 2. Try to load cached chain - if complete, display immediately
|
||||
* 3. Walk breadcrumb_parent() chain to get structure (URLs)
|
||||
* 4. Compare structure to cache:
|
||||
* - Different length or no cache shown: render skeleton with blank labels
|
||||
* - Same length: render skeleton pre-populated with cached labels
|
||||
* 5. Progressively resolve each label via breadcrumb_label()/breadcrumb_label_active()
|
||||
* 6. Cache final result when all labels resolved
|
||||
*
|
||||
* Important: Actions that need loaded data in breadcrumb methods should
|
||||
* await their own 'loaded' event internally, not rely on ready() being called.
|
||||
*
|
||||
* Usage:
|
||||
* const cancel = Rsx_Breadcrumb_Resolver.stream(action, url, (data) => {
|
||||
* // data.title - Page title (null if pending)
|
||||
* // data.chain - Array of {url, label, is_active, resolved}
|
||||
* // data.fully_resolved - True when all labels resolved
|
||||
* });
|
||||
*
|
||||
* // Call cancel() to stop streaming (e.g., on navigation)
|
||||
* const cancel = Rsx_Breadcrumb_Resolver.stream(action, url, callbacks);
|
||||
*/
|
||||
class Rsx_Breadcrumb_Resolver {
|
||||
/**
|
||||
* In-memory cache: url -> { label, label_active, parent_url }
|
||||
* Cleared on page reload. Invalidated by visiting pages.
|
||||
*/
|
||||
static _cache = {};
|
||||
|
||||
/**
|
||||
* Generation counter for cancellation detection.
|
||||
* Incremented on each stream() call and when cancelled.
|
||||
*/
|
||||
static _generation = 0;
|
||||
|
||||
/**
|
||||
* Cache key prefix for breadcrumb chains
|
||||
*/
|
||||
static CACHE_PREFIX = 'breadcrumb_chain:';
|
||||
|
||||
/**
|
||||
* Stream breadcrumb data as it becomes available
|
||||
*
|
||||
* @param {Spa_Action} action - The active action instance
|
||||
* @param {string} url - Current URL (pathname + search, no hash)
|
||||
* @param {Function} callback - Called with breadcrumb data on each update
|
||||
* @param {object} callbacks - Callback functions:
|
||||
* - on_chain(chain): Called when chain structure is ready (may have null labels)
|
||||
* - on_label_update(index, label): Called when individual label resolves
|
||||
* - on_complete(chain): Called when all labels resolved
|
||||
* @returns {Function} Cancel function - call to stop streaming
|
||||
*/
|
||||
static stream(action, url, callback) {
|
||||
static stream(action, url, callbacks) {
|
||||
const generation = ++Rsx_Breadcrumb_Resolver._generation;
|
||||
|
||||
// Normalize URL (strip hash if present)
|
||||
const normalized_url = Rsx_Breadcrumb_Resolver._normalize_url(url);
|
||||
|
||||
// Start async resolution (fires callbacks as data becomes available)
|
||||
Rsx_Breadcrumb_Resolver._resolve(action, normalized_url, generation, callback);
|
||||
// Start async resolution
|
||||
Rsx_Breadcrumb_Resolver._resolve(action, normalized_url, generation, callbacks);
|
||||
|
||||
// Return cancel function
|
||||
return () => {
|
||||
// Only increment if this is still the active generation
|
||||
if (generation === Rsx_Breadcrumb_Resolver._generation) {
|
||||
Rsx_Breadcrumb_Resolver._generation++;
|
||||
}
|
||||
@@ -75,217 +75,183 @@ class Rsx_Breadcrumb_Resolver {
|
||||
}
|
||||
|
||||
/**
|
||||
* Main resolution orchestrator
|
||||
* Get cached chain for a URL
|
||||
*/
|
||||
static async _resolve(action, url, generation, callback) {
|
||||
// Phase 1: Try immediate cached render
|
||||
const cached_chain = Rsx_Breadcrumb_Resolver._try_build_cached_chain(url);
|
||||
if (cached_chain) {
|
||||
// Full cache hit - render immediately
|
||||
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) return;
|
||||
|
||||
callback({
|
||||
title: null, // Title will be resolved in Phase 2
|
||||
chain: cached_chain,
|
||||
fully_resolved: false
|
||||
});
|
||||
}
|
||||
|
||||
// Phase 2: Await active action resolution
|
||||
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) return;
|
||||
|
||||
const active_data = await Rsx_Breadcrumb_Resolver._resolve_active_action(action, url);
|
||||
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) return;
|
||||
|
||||
// Check if parent changed from cached value (stale cache)
|
||||
const cached_entry = Rsx_Breadcrumb_Resolver._cache[url];
|
||||
const parent_changed = cached_chain && cached_entry &&
|
||||
cached_entry.parent_url !== active_data.parent_url;
|
||||
|
||||
// Phase 3-5: Walk chain and render progressively
|
||||
await Rsx_Breadcrumb_Resolver._walk_and_render_chain(
|
||||
active_data,
|
||||
url,
|
||||
generation,
|
||||
callback,
|
||||
parent_changed || !cached_chain // Force re-render if parent changed or no cached render
|
||||
);
|
||||
static _get_cached_chain(url) {
|
||||
const cache_key = Rsx_Breadcrumb_Resolver.CACHE_PREFIX + url;
|
||||
return Rsx_Storage.session_get(cache_key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 1: Try to build a complete chain from cache
|
||||
* Returns null if any link is missing from cache
|
||||
* Cache a resolved chain
|
||||
*/
|
||||
static _try_build_cached_chain(url) {
|
||||
const chain = [];
|
||||
let current_url = url;
|
||||
static _cache_chain(url, chain) {
|
||||
const cache_key = Rsx_Breadcrumb_Resolver.CACHE_PREFIX + url;
|
||||
// Store simplified chain data (url, label, is_active)
|
||||
const cache_data = chain.map(item => ({
|
||||
url: item.url,
|
||||
label: item.label,
|
||||
is_active: item.is_active
|
||||
}));
|
||||
Rsx_Storage.session_set(cache_key, cache_data);
|
||||
}
|
||||
|
||||
while (current_url) {
|
||||
const entry = Rsx_Breadcrumb_Resolver._cache[current_url];
|
||||
if (!entry) {
|
||||
// Cache miss - can't build complete chain
|
||||
return null;
|
||||
/**
|
||||
* Main resolution orchestrator
|
||||
*/
|
||||
static async _resolve(action, url, generation, callbacks) {
|
||||
// Step 1: Try to get cached chain
|
||||
const cached_chain = Rsx_Breadcrumb_Resolver._get_cached_chain(url);
|
||||
let showed_cached = false;
|
||||
|
||||
if (cached_chain && is_array(cached_chain) && cached_chain.length > 0) {
|
||||
// Show cached chain immediately
|
||||
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) return;
|
||||
callbacks.on_chain(cached_chain.map(item => ({
|
||||
url: item.url,
|
||||
label: item.label,
|
||||
is_active: item.is_active,
|
||||
resolved: true
|
||||
})));
|
||||
showed_cached = true;
|
||||
}
|
||||
|
||||
// Step 2: Walk the chain structure to get URLs
|
||||
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) return;
|
||||
|
||||
const chain_structure = await Rsx_Breadcrumb_Resolver._walk_chain_structure(action, url, generation);
|
||||
if (!chain_structure || !Rsx_Breadcrumb_Resolver._is_active(generation)) return;
|
||||
|
||||
// Step 3: Compare to cache and build display chain
|
||||
const chain_length_matches = showed_cached && cached_chain.length === chain_structure.length;
|
||||
const should_use_cached_labels = showed_cached && chain_length_matches;
|
||||
|
||||
// Build the chain with either cached labels or nulls
|
||||
const chain = chain_structure.map((item, index) => {
|
||||
let label = null;
|
||||
if (should_use_cached_labels && cached_chain[index]) {
|
||||
label = cached_chain[index].label;
|
||||
}
|
||||
return {
|
||||
url: item.url,
|
||||
label: label,
|
||||
is_active: item.is_active,
|
||||
resolved: false,
|
||||
action: item.action // Keep action reference for label resolution
|
||||
};
|
||||
});
|
||||
|
||||
// Step 4: Emit chain structure (with cached labels or nulls)
|
||||
// Only emit if we didn't show cached OR if structure differs
|
||||
if (!showed_cached || !chain_length_matches) {
|
||||
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) return;
|
||||
callbacks.on_chain(chain.map(item => ({
|
||||
url: item.url,
|
||||
label: item.label,
|
||||
is_active: item.is_active,
|
||||
resolved: item.label !== null
|
||||
})));
|
||||
}
|
||||
|
||||
// Step 5: Resolve labels progressively
|
||||
const label_promises = chain.map(async (item, index) => {
|
||||
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) return;
|
||||
|
||||
try {
|
||||
let label;
|
||||
if (item.is_active) {
|
||||
// Active item uses breadcrumb_label_active()
|
||||
label = await item.action.breadcrumb_label_active();
|
||||
} else {
|
||||
// Parent items use breadcrumb_label()
|
||||
label = await item.action.breadcrumb_label();
|
||||
}
|
||||
|
||||
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) return;
|
||||
|
||||
// Update chain and notify
|
||||
chain[index].label = label;
|
||||
chain[index].resolved = true;
|
||||
callbacks.on_label_update(index, label);
|
||||
} catch (e) {
|
||||
console.warn('Breadcrumb label resolution failed:', e);
|
||||
// Use generic label when resolution fails
|
||||
chain[index].label = item.is_active ? 'Page' : 'Link';
|
||||
chain[index].resolved = true;
|
||||
callbacks.on_label_update(index, chain[index].label);
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for all labels to resolve
|
||||
await Promise.all(label_promises);
|
||||
|
||||
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) return;
|
||||
|
||||
// Step 6: Cache the final result and notify completion
|
||||
Rsx_Breadcrumb_Resolver._cache_chain(url, chain);
|
||||
|
||||
callbacks.on_complete(chain.map(item => ({
|
||||
url: item.url,
|
||||
label: item.label,
|
||||
is_active: item.is_active,
|
||||
resolved: true
|
||||
})));
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk the chain structure by following breadcrumb_parent()
|
||||
* Returns array of {url, is_active, action} from root to current
|
||||
*/
|
||||
static async _walk_chain_structure(action, url, generation) {
|
||||
const chain = [];
|
||||
|
||||
// Start with current action
|
||||
let current_url = url;
|
||||
let current_action = action;
|
||||
|
||||
while (current_action && current_url) {
|
||||
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) return null;
|
||||
|
||||
chain.unshift({
|
||||
url: current_url,
|
||||
label: current_url === url ? entry.label_active : entry.label,
|
||||
is_active: current_url === url,
|
||||
resolved: true
|
||||
action: current_action
|
||||
});
|
||||
|
||||
current_url = entry.parent_url;
|
||||
// Get parent URL
|
||||
let parent_url = null;
|
||||
try {
|
||||
parent_url = await current_action.breadcrumb_parent();
|
||||
} catch (e) {
|
||||
console.warn('breadcrumb_parent() failed:', e);
|
||||
}
|
||||
|
||||
if (!parent_url) break;
|
||||
|
||||
// Load parent action (detached)
|
||||
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) return null;
|
||||
|
||||
const parent_action = await Spa.load_detached_action(parent_url, {
|
||||
use_cached_data: true,
|
||||
skip_render_and_ready: true
|
||||
});
|
||||
|
||||
if (!parent_action) break;
|
||||
|
||||
current_url = parent_url;
|
||||
current_action = parent_action;
|
||||
}
|
||||
|
||||
return chain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 2: Resolve all breadcrumb data from the live active action
|
||||
* Clear cached chain for a URL (if needed for invalidation)
|
||||
*/
|
||||
static async _resolve_active_action(action, url) {
|
||||
// Await all breadcrumb methods in parallel
|
||||
const [title, label, label_active, parent_url] = await Promise.all([
|
||||
action.page_title(),
|
||||
action.breadcrumb_label(),
|
||||
action.breadcrumb_label_active(),
|
||||
action.breadcrumb_parent()
|
||||
]);
|
||||
|
||||
// Update cache with fresh values
|
||||
Rsx_Breadcrumb_Resolver._cache[url] = {
|
||||
label: label,
|
||||
label_active: label_active || label,
|
||||
parent_url: parent_url || null
|
||||
};
|
||||
|
||||
return {
|
||||
title: title,
|
||||
label: label,
|
||||
label_active: label_active || label,
|
||||
parent_url: parent_url || null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 3-5: Walk parent chain and render progressively
|
||||
*/
|
||||
static async _walk_and_render_chain(active_data, url, generation, callback, force_render) {
|
||||
// Start chain with active action
|
||||
const chain = [{
|
||||
url: url,
|
||||
label: active_data.label_active,
|
||||
is_active: true,
|
||||
resolved: true
|
||||
}];
|
||||
|
||||
// Track pending async operations for progressive updates
|
||||
let pending_count = 0;
|
||||
let chain_complete = false;
|
||||
|
||||
// Helper to emit current state
|
||||
const emit = () => {
|
||||
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) return;
|
||||
|
||||
callback({
|
||||
title: active_data.title,
|
||||
chain: [...chain], // Clone to prevent mutation issues
|
||||
fully_resolved: chain_complete && pending_count === 0
|
||||
});
|
||||
};
|
||||
|
||||
// Walk up parent chain
|
||||
let parent_url = active_data.parent_url;
|
||||
|
||||
while (parent_url) {
|
||||
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) return;
|
||||
|
||||
const cached = Rsx_Breadcrumb_Resolver._cache[parent_url];
|
||||
|
||||
if (cached) {
|
||||
// Cache hit - use cached values immediately
|
||||
chain.unshift({
|
||||
url: parent_url,
|
||||
label: cached.label,
|
||||
is_active: false,
|
||||
resolved: true
|
||||
});
|
||||
parent_url = cached.parent_url;
|
||||
} else {
|
||||
// Cache miss - need to load detached action
|
||||
// Add placeholder entry
|
||||
const entry_url = parent_url;
|
||||
|
||||
chain.unshift({
|
||||
url: entry_url,
|
||||
label: null, // Placeholder - will be resolved
|
||||
is_active: false,
|
||||
resolved: false
|
||||
});
|
||||
|
||||
pending_count++;
|
||||
|
||||
// Load the detached action synchronously for chain structure
|
||||
const detached = await Spa.load_detached_action(entry_url, { use_cached_data: true });
|
||||
|
||||
if (!Rsx_Breadcrumb_Resolver._is_active(generation)) {
|
||||
if (detached) detached.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (detached) {
|
||||
// Get parent_url to continue chain walk
|
||||
const next_parent = await detached.breadcrumb_parent();
|
||||
|
||||
// Get label while we have the action
|
||||
const label = await detached.breadcrumb_label();
|
||||
const label_active = await detached.breadcrumb_label_active();
|
||||
|
||||
// Update cache
|
||||
Rsx_Breadcrumb_Resolver._cache[entry_url] = {
|
||||
label: label,
|
||||
label_active: label_active || label,
|
||||
parent_url: next_parent || null
|
||||
};
|
||||
|
||||
// Update chain entry with resolved label
|
||||
const found_entry = chain.find(e => e.url === entry_url);
|
||||
if (found_entry) {
|
||||
found_entry.label = label;
|
||||
found_entry.resolved = true;
|
||||
pending_count--;
|
||||
}
|
||||
|
||||
detached.stop();
|
||||
parent_url = next_parent;
|
||||
} else {
|
||||
// Failed to load - stop chain here
|
||||
parent_url = null;
|
||||
}
|
||||
}
|
||||
static clear_cache(url) {
|
||||
if (url) {
|
||||
const cache_key = Rsx_Breadcrumb_Resolver.CACHE_PREFIX + url;
|
||||
Rsx_Storage.session_remove(cache_key);
|
||||
}
|
||||
|
||||
// Chain structure complete
|
||||
chain_complete = true;
|
||||
|
||||
// Emit final state (or intermediate if still pending)
|
||||
if (force_render || pending_count === 0) {
|
||||
emit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cache (called on window reload automatically)
|
||||
* Can be called manually if needed
|
||||
*/
|
||||
static clear_cache() {
|
||||
Rsx_Breadcrumb_Resolver._cache = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache entry for debugging/inspection
|
||||
*/
|
||||
static get_cached(url) {
|
||||
return Rsx_Breadcrumb_Resolver._cache[url] || null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -835,9 +835,22 @@ LIFECYCLE EVENT CALLBACKS
|
||||
Supported Events:
|
||||
'render' - Fires after render phase completes
|
||||
'create' - Fires after create phase completes
|
||||
'load' - Fires after load phase completes
|
||||
'load' - Fires after load phase completes (data available)
|
||||
'ready' - Fires after ready phase completes (fully initialized)
|
||||
|
||||
Awaiting Data in Async Methods:
|
||||
When a method needs data that may not be loaded yet, await the 'load'
|
||||
event. If the event already fired, the callback executes immediately:
|
||||
|
||||
async get_display_name() {
|
||||
// Wait for on_load() to complete if not already
|
||||
await new Promise(resolve => this.on('load', resolve));
|
||||
return `${this.data.first_name} ${this.data.last_name}`;
|
||||
}
|
||||
|
||||
This pattern is useful for breadcrumb methods or other async accessors
|
||||
that need loaded data but may be called before on_load() completes.
|
||||
|
||||
Basic usage:
|
||||
// Get component instance and register callback
|
||||
const component = $('#my-component').component();
|
||||
|
||||
466
app/RSpade/man/primary_secondary_nav_breadcrumbs.txt
Executable file
466
app/RSpade/man/primary_secondary_nav_breadcrumbs.txt
Executable file
@@ -0,0 +1,466 @@
|
||||
PRIMARY_SECONDARY_NAV_BREADCRUMBS(3) RSX Framework Manual PRIMARY_SECONDARY_NAV_BREADCRUMBS(3)
|
||||
|
||||
NAME
|
||||
Primary, Secondary Navigation and Breadcrumbs - SPA navigation systems
|
||||
|
||||
SYNOPSIS
|
||||
// Primary navigation (sidebar) in layout
|
||||
class Frontend_Spa_Layout extends Spa_Layout {
|
||||
on_create() {
|
||||
this.state = {
|
||||
nav_sections: [
|
||||
{ title: 'Overview', items: [
|
||||
{ label: 'Dashboard', icon: 'bi-house', href: Rsx.Route('...') }
|
||||
]}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Secondary navigation (sublayout sidebar)
|
||||
class Settings_Layout extends Spa_Layout {
|
||||
static ACTION_CONFIG = {
|
||||
'Settings_General_Action': { icon: 'bi-gear', label: 'General' },
|
||||
'Settings_Profile_Action': { icon: 'bi-person', label: 'Profile' }
|
||||
};
|
||||
}
|
||||
|
||||
// Breadcrumb methods in actions
|
||||
class Contacts_View_Action extends Spa_Action {
|
||||
async page_title() { return this.data.contact.name; }
|
||||
async breadcrumb_label() { return this.data.contact.name; }
|
||||
async breadcrumb_label_active() { return 'View Contact'; }
|
||||
async breadcrumb_parent() { return Rsx.Route('Contacts_Index_Action'); }
|
||||
}
|
||||
|
||||
DESCRIPTION
|
||||
RSX provides a comprehensive navigation system for SPAs consisting of:
|
||||
|
||||
1. Primary Navigation - Main sidebar with sections and links
|
||||
2. Secondary Navigation - Sublayout sidebars for feature areas
|
||||
3. Breadcrumbs - Progressive URL-based navigation trail
|
||||
|
||||
The system uses session storage caching for instant navigation feedback
|
||||
while resolving live data in the background.
|
||||
|
||||
PRIMARY NAVIGATION
|
||||
The primary navigation is typically a sidebar defined in the main SPA
|
||||
layout (e.g., Frontend_Spa_Layout). It uses the Sidebar_Nav component.
|
||||
|
||||
Layout Structure:
|
||||
// Frontend_Spa_Layout.js
|
||||
class Frontend_Spa_Layout extends Spa_Layout {
|
||||
on_create() {
|
||||
this.state = {
|
||||
nav_sections: [
|
||||
{
|
||||
title: 'Overview',
|
||||
items: [
|
||||
{
|
||||
label: 'Dashboard',
|
||||
icon: 'bi-house-door',
|
||||
href: Rsx.Route('Dashboard_Index_Action'),
|
||||
},
|
||||
{
|
||||
label: 'Calendar',
|
||||
icon: 'bi-calendar',
|
||||
href: Rsx.Route('Calendar_Index_Action'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Business',
|
||||
items: [
|
||||
{ label: 'Clients', icon: 'bi-people',
|
||||
href: Rsx.Route('Clients_Index_Action') },
|
||||
{ label: 'Contacts', icon: 'bi-person-rolodex',
|
||||
href: Rsx.Route('Contacts_Index_Action') },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
on_action(url, action_name, args) {
|
||||
// Update active state on navigation
|
||||
this.sid('sidebar_nav').on_action(url, action_name, args);
|
||||
}
|
||||
}
|
||||
|
||||
Template Structure:
|
||||
<Define:Frontend_Spa_Layout>
|
||||
<nav class="app-sidebar">
|
||||
<Sidebar_Nav $sid="sidebar_nav"
|
||||
$sections=this.state.nav_sections />
|
||||
</nav>
|
||||
<main $sid="content"></main>
|
||||
</Define:Frontend_Spa_Layout>
|
||||
|
||||
Sidebar_Nav Component:
|
||||
Renders navigation sections with automatic active state based on
|
||||
current URL. Supports icons (Bootstrap Icons), labels, and href.
|
||||
|
||||
on_action(url, action_name, args):
|
||||
Called by layout to update active link highlighting.
|
||||
Matches current URL to nav item hrefs.
|
||||
|
||||
SECONDARY NAVIGATION (SUBLAYOUTS)
|
||||
Secondary navigation is used for feature areas with multiple related
|
||||
pages (e.g., Settings). Implemented as sublayouts with their own sidebar.
|
||||
|
||||
Sublayout Pattern:
|
||||
// Settings_Layout.js
|
||||
class Settings_Layout extends Spa_Layout {
|
||||
// Configure nav items per action
|
||||
static ACTION_CONFIG = {
|
||||
'Settings_General_Action': {
|
||||
icon: 'bi-gear',
|
||||
label: 'General Settings'
|
||||
},
|
||||
'Settings_Profile_Action': {
|
||||
icon: 'bi-person',
|
||||
label: 'Profile'
|
||||
},
|
||||
'Settings_User_Management_Index_Action': {
|
||||
icon: 'bi-people',
|
||||
label: 'User Management'
|
||||
},
|
||||
};
|
||||
|
||||
on_create() {
|
||||
this._build_nav_items();
|
||||
}
|
||||
|
||||
_build_nav_items() {
|
||||
this.state.nav_items = [];
|
||||
for (const [action_name, config] of
|
||||
Object.entries(Settings_Layout.ACTION_CONFIG)) {
|
||||
this.state.nav_items.push({
|
||||
action: action_name,
|
||||
icon: config.icon,
|
||||
label: config.label,
|
||||
href: Rsx.Route(action_name),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
on_action(url, action_name, args) {
|
||||
this._update_active_nav(action_name);
|
||||
}
|
||||
|
||||
_update_active_nav(action_name) {
|
||||
this.$sid('nav').find('.active').removeClass('active');
|
||||
this.$sid('nav')
|
||||
.find(`[data-action="${action_name}"]`)
|
||||
.addClass('active');
|
||||
}
|
||||
}
|
||||
|
||||
Template Structure:
|
||||
<Define:Settings_Layout>
|
||||
<div class="settings-layout">
|
||||
<aside class="settings-sidebar">
|
||||
<nav $sid="nav">
|
||||
<% for (let item of this.state.nav_items) { %>
|
||||
<a href="<%= item.href %>"
|
||||
data-action="<%= item.action %>"
|
||||
class="settings-sidebar__item">
|
||||
<i class="bi <%= item.icon %>"></i>
|
||||
<span><%= item.label %></span>
|
||||
</a>
|
||||
<% } %>
|
||||
</nav>
|
||||
</aside>
|
||||
<div class="settings-content" $sid="content"></div>
|
||||
</div>
|
||||
</Define:Settings_Layout>
|
||||
|
||||
Action Declaration:
|
||||
Actions declare both outer and inner layouts:
|
||||
|
||||
@route('/frontend/settings/general')
|
||||
@layout('Frontend_Spa_Layout') // Outer layout
|
||||
@layout('Settings_Layout') // Sublayout (nested)
|
||||
@spa('Frontend_Spa_Controller::index')
|
||||
class Settings_General_Action extends Spa_Action { }
|
||||
|
||||
BREADCRUMB SYSTEM
|
||||
Breadcrumbs provide hierarchical navigation context. The system uses
|
||||
progressive resolution with caching for instant display.
|
||||
|
||||
Action Breadcrumb Methods:
|
||||
All methods are async and can await the 'load' event if needed.
|
||||
|
||||
page_title()
|
||||
Returns the page title for the header and document.title.
|
||||
Example: "Edit: John Smith"
|
||||
|
||||
breadcrumb_label()
|
||||
Returns the label when this page appears as a PARENT in
|
||||
another page's breadcrumb trail.
|
||||
Example: "John Smith" (when viewing edit page)
|
||||
|
||||
breadcrumb_label_active()
|
||||
Returns the label when this page is the CURRENT page
|
||||
(rightmost breadcrumb, no link).
|
||||
Example: "Edit Contact"
|
||||
|
||||
breadcrumb_parent()
|
||||
Returns the URL of the parent page, or null for root.
|
||||
Example: Rsx.Route('Contacts_View_Action', { id: this.args.id })
|
||||
|
||||
Example Implementation:
|
||||
class Contacts_View_Action extends Spa_Action {
|
||||
on_create() {
|
||||
this.data.contact = { first_name: '', last_name: '' };
|
||||
}
|
||||
|
||||
async on_load() {
|
||||
this.data.contact = await Contact_Model.fetch(this.args.id);
|
||||
}
|
||||
|
||||
// Helper to await loaded data
|
||||
async _await_loaded() {
|
||||
if (this.data.contact && this.data.contact.id) return;
|
||||
await new Promise(resolve => this.on('load', resolve));
|
||||
}
|
||||
|
||||
async page_title() {
|
||||
await this._await_loaded();
|
||||
const c = this.data.contact;
|
||||
return `${c.first_name} ${c.last_name}`.trim() || 'Contact';
|
||||
}
|
||||
|
||||
async breadcrumb_label() {
|
||||
await this._await_loaded();
|
||||
const c = this.data.contact;
|
||||
return `${c.first_name} ${c.last_name}`.trim() || 'Contact';
|
||||
}
|
||||
|
||||
async breadcrumb_label_active() {
|
||||
return 'View Contact'; // Static, no await needed
|
||||
}
|
||||
|
||||
async breadcrumb_parent() {
|
||||
return Rsx.Route('Contacts_Index_Action'); // Static route
|
||||
}
|
||||
}
|
||||
|
||||
Awaiting Loaded Data:
|
||||
Breadcrumb methods are called BEFORE on_load() completes. If a method
|
||||
needs loaded data (e.g., contact name), it must await the 'load' event:
|
||||
|
||||
async _await_loaded() {
|
||||
// Check if data is already loaded
|
||||
if (this.data.contact && this.data.contact.id) return;
|
||||
// Otherwise wait for 'load' event
|
||||
await new Promise(resolve => this.on('load', resolve));
|
||||
}
|
||||
|
||||
The 'load' event fires immediately if already past that lifecycle
|
||||
phase, so this pattern is safe to call multiple times.
|
||||
|
||||
RSX_BREADCRUMB_RESOLVER
|
||||
Framework class that handles breadcrumb resolution with caching.
|
||||
|
||||
Location: /system/app/RSpade/Breadcrumbs/Rsx_Breadcrumb_Resolver.js
|
||||
|
||||
API:
|
||||
Rsx_Breadcrumb_Resolver.stream(action, url, callbacks)
|
||||
Streams breadcrumb data progressively.
|
||||
|
||||
Parameters:
|
||||
action - Current Spa_Action instance
|
||||
url - Current URL (pathname + search)
|
||||
callbacks - Object with callback functions:
|
||||
on_chain(chain) - Chain structure ready
|
||||
on_label_update(idx, lbl) - Individual label resolved
|
||||
on_complete(chain) - All labels resolved
|
||||
|
||||
Returns: Cancel function
|
||||
|
||||
Rsx_Breadcrumb_Resolver.clear_cache(url)
|
||||
Clears cached breadcrumb chain for a URL.
|
||||
|
||||
Resolution Flow:
|
||||
1. Check session cache for URL's breadcrumb chain
|
||||
2. If cached, display immediately
|
||||
3. Walk breadcrumb_parent() chain to get structure (URLs)
|
||||
4. Compare to cache:
|
||||
- Different length: render skeleton with blank labels
|
||||
- Same length: render with cached labels
|
||||
5. Resolve each label via breadcrumb_label()/breadcrumb_label_active()
|
||||
6. Update display as each label resolves
|
||||
7. Cache final result
|
||||
|
||||
Caching:
|
||||
Uses Rsx_Storage.session_set/get() with key prefix 'breadcrumb_chain:'
|
||||
Cached data: Array of { url, label, is_active }
|
||||
|
||||
LAYOUT INTEGRATION
|
||||
The layout handles header rendering and breadcrumb streaming.
|
||||
|
||||
Frontend_Spa_Layout Integration:
|
||||
class Frontend_Spa_Layout extends Spa_Layout {
|
||||
static TITLE_CACHE_PREFIX = 'header_text:';
|
||||
|
||||
on_action(url, action_name, args) {
|
||||
// Update page title with caching
|
||||
this._update_page_title(url);
|
||||
|
||||
// Cancel previous breadcrumb stream
|
||||
if (this._breadcrumb_cancel) {
|
||||
this._breadcrumb_cancel();
|
||||
}
|
||||
|
||||
// Start new breadcrumb stream
|
||||
this._breadcrumb_cancel = Rsx_Breadcrumb_Resolver.stream(
|
||||
this.action,
|
||||
url,
|
||||
{
|
||||
on_chain: (chain) => this._on_breadcrumb_chain(chain),
|
||||
on_label_update: (i, l) => this._on_breadcrumb_label_update(i, l),
|
||||
on_complete: (chain) => this._on_breadcrumb_complete(chain)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async _update_page_title(url) {
|
||||
const $title = this.$sid('page_title');
|
||||
const cache_key = Frontend_Spa_Layout.TITLE_CACHE_PREFIX + url;
|
||||
|
||||
// Clear, show cached, then resolve live
|
||||
$title.html(' ');
|
||||
const cached = Rsx_Storage.session_get(cache_key);
|
||||
if (cached) {
|
||||
$title.html(cached);
|
||||
document.title = 'RSX - ' + cached;
|
||||
}
|
||||
|
||||
const live = await this.action.page_title();
|
||||
if (live) {
|
||||
$title.html(live);
|
||||
document.title = 'RSX - ' + live;
|
||||
Rsx_Storage.session_set(cache_key, live);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BREADCRUMB_NAV COMPONENT
|
||||
Renders the breadcrumb trail from a chain array.
|
||||
|
||||
Location: /rsx/theme/components/page/breadcrumb_nav.jqhtml
|
||||
|
||||
Props:
|
||||
$crumbs - Array of { label, url, is_active, resolved }
|
||||
label: string or null (shows placeholder if null)
|
||||
url: string or null (no link if null or is_active)
|
||||
is_active: boolean (current page, no link)
|
||||
resolved: boolean (false shows loading state)
|
||||
|
||||
Usage:
|
||||
$breadcrumbs.component('Breadcrumb_Nav', {
|
||||
crumbs: [
|
||||
{ label: 'Contacts', url: '/contacts', is_active: false },
|
||||
{ label: 'John Smith', url: '/contacts/view/1', is_active: false },
|
||||
{ label: 'Edit Contact', url: null, is_active: true }
|
||||
]
|
||||
});
|
||||
|
||||
PAGE HEADER STRUCTURE
|
||||
Standard page header with title, breadcrumbs, and action buttons.
|
||||
|
||||
Template Pattern:
|
||||
<header class="page-title-header">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<h1 $sid="page_title"></h1>
|
||||
<nav $sid="page_breadcrumbs"></nav>
|
||||
</div>
|
||||
<div $sid="page_actions"></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
Action Buttons:
|
||||
Actions define page_actions() to provide header buttons:
|
||||
|
||||
class Contacts_View_Action extends Spa_Action {
|
||||
page_actions() {
|
||||
return `
|
||||
<div class="d-flex gap-2">
|
||||
<a href="${Rsx.Route('Contacts_Index_Action')}"
|
||||
class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Back
|
||||
</a>
|
||||
<a href="${Rsx.Route('Contacts_Edit_Action', this.args.id)}"
|
||||
class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
The layout renders these via _render_page_actions().
|
||||
|
||||
BREADCRUMB CHAIN EXAMPLE
|
||||
For URL /contacts/edit/1, the breadcrumb chain resolves as:
|
||||
|
||||
Chain Structure (URLs):
|
||||
1. /contacts (Contacts_Index_Action)
|
||||
2. /contacts/view/1 (Contacts_View_Action)
|
||||
3. /contacts/edit/1 (Contacts_Edit_Action) - ACTIVE
|
||||
|
||||
Resolution Process:
|
||||
1. Contacts_Edit_Action.breadcrumb_parent() -> /contacts/view/1
|
||||
2. Load detached Contacts_View_Action
|
||||
3. Contacts_View_Action.breadcrumb_parent() -> /contacts
|
||||
4. Load detached Contacts_Index_Action
|
||||
5. Contacts_Index_Action.breadcrumb_parent() -> null (root)
|
||||
|
||||
Label Resolution (parallel):
|
||||
- Index: breadcrumb_label() -> "Contacts"
|
||||
- View: breadcrumb_label() -> "John Smith" (awaits load)
|
||||
- Edit: breadcrumb_label_active() -> "Edit Contact"
|
||||
|
||||
Final Display:
|
||||
Contacts > John Smith > Edit Contact
|
||||
|
||||
CACHING STRATEGY
|
||||
Two caches are used for instant navigation feedback:
|
||||
|
||||
Page Title Cache:
|
||||
Key: 'header_text:/contacts/edit/1'
|
||||
Value: "Edit: John Smith"
|
||||
|
||||
Breadcrumb Chain Cache:
|
||||
Key: 'breadcrumb_chain:/contacts/edit/1'
|
||||
Value: [
|
||||
{ url: '/contacts', label: 'Contacts', is_active: false },
|
||||
{ url: '/contacts/view/1', label: 'John Smith', is_active: false },
|
||||
{ url: '/contacts/edit/1', label: 'Edit Contact', is_active: true }
|
||||
]
|
||||
|
||||
Cache Behavior:
|
||||
- Cached values display immediately on navigation
|
||||
- Live values resolve in background
|
||||
- Cache updates when live values complete
|
||||
- Previous breadcrumbs stay visible until new chain ready
|
||||
|
||||
FILES
|
||||
/system/app/RSpade/Breadcrumbs/Rsx_Breadcrumb_Resolver.js
|
||||
Breadcrumb resolution with progressive streaming and caching
|
||||
|
||||
/rsx/app/frontend/Frontend_Spa_Layout.js
|
||||
Main layout with navigation and header integration
|
||||
|
||||
/rsx/theme/components/page/breadcrumb_nav.jqhtml
|
||||
Breadcrumb rendering component
|
||||
|
||||
/rsx/theme/components/nav/sidebar_nav.jqhtml
|
||||
Primary sidebar navigation component
|
||||
|
||||
SEE ALSO
|
||||
spa(3), jqhtml(3), storage(3)
|
||||
|
||||
RSX Framework 2025-12-16 PRIMARY_SECONDARY_NAV_BREADCRUMBS(3)
|
||||
0
app/RSpade/resource/vscode_extension/out/auto_rename_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/auto_rename_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/auto_rename_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/auto_rename_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/class_refactor_code_actions.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/class_refactor_code_actions.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/class_refactor_code_actions.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/class_refactor_code_actions.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/class_refactor_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/class_refactor_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/class_refactor_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/class_refactor_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/combined_semantic_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/combined_semantic_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/combined_semantic_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/combined_semantic_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/comment_file_reference_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/comment_file_reference_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/comment_file_reference_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/comment_file_reference_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/config.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/config.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/config.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/config.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/convention_method_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/convention_method_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/convention_method_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/convention_method_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/debug_client.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/debug_client.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/debug_client.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/debug_client.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/decoration_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/decoration_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/decoration_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/decoration_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/definition_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/definition_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/definition_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/definition_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/extension.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/extension.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/extension.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/extension.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/file_watcher.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/file_watcher.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/file_watcher.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/file_watcher.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/folder_color_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/folder_color_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/folder_color_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/folder_color_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/folding_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/folding_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/folding_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/folding_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/formatting_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/formatting_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/formatting_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/formatting_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/git_diff_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/git_diff_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/git_diff_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/git_diff_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/git_status_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/git_status_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/git_status_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/git_status_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/ide_bridge_client.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/ide_bridge_client.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/ide_bridge_client.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/ide_bridge_client.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/jqhtml_lifecycle_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/jqhtml_lifecycle_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/jqhtml_lifecycle_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/jqhtml_lifecycle_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/laravel_completion_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/laravel_completion_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/laravel_completion_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/laravel_completion_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/php_attribute_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/php_attribute_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/php_attribute_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/php_attribute_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/refactor_code_actions.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/refactor_code_actions.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/refactor_code_actions.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/refactor_code_actions.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/refactor_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/refactor_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/refactor_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/refactor_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/sort_class_methods_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/sort_class_methods_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/sort_class_methods_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/sort_class_methods_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/symlink_redirect_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/symlink_redirect_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/symlink_redirect_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/symlink_redirect_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/that_variable_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/that_variable_provider.js
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/that_variable_provider.js.map
Normal file → Executable file
0
app/RSpade/resource/vscode_extension/out/that_variable_provider.js.map
Normal file → Executable file
Reference in New Issue
Block a user