use super::drag_drop_state::TreeShellDragEffect; use super::filetree_runtime::{ FileTreeIntentEvent, FileTreeKeyboardCommand, FileTreeRuntimeAction, FileTreeRuntimeEnvironment, FileTreeRuntimeOutput, FileTreeRuntimeState, }; use super::page_runtime::{ PageTreeCommandDispatchEvent, PageTreeDropFeedback, PageTreeDropPosition, PageTreeIntentEvent, PageTreeRuntimeAction, PageTreeRuntimeEnvironment, PageTreeRuntimeOutput, PageTreeRuntimeState, }; use super::picker_runtime::{ PickerRuntimeAction, PickerRuntimeEnvironment, PickerRuntimeOutput, PickerRuntimePickTarget, PickerRuntimeState, }; use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum TreeShellRuntimeMode { Page, FileTree, Picker, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde( tag = "mode", rename_all = "camelCase", rename_all_fields = "camelCase" )] pub enum TreeShellRuntimeRequest { Page { request_id: String, environment: PageTreeRuntimeEnvironment, state: PageTreeRuntimeState, action: PageTreeRuntimeAction, }, FileTree { request_id: String, environment: FileTreeRuntimeEnvironment, state: FileTreeRuntimeState, action: FileTreeRuntimeAction, }, Picker { request_id: String, environment: PickerRuntimeEnvironment, state: PickerRuntimeState, action: PickerRuntimeAction, }, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TreeShellRuntimeResult { pub request_id: String, pub mode: TreeShellRuntimeMode, pub state: TreeShellRuntimeStateSnapshot, pub dom_patches: Vec, pub host_events: Vec, pub command_events: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "mode", content = "state", rename_all = "camelCase")] pub enum TreeShellRuntimeStateSnapshot { Page(PageTreeRuntimeState), FileTree(FileTreeRuntimeState), Picker(PickerRuntimeState), } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde( tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase" )] pub enum TreeShellDomPatch { PageState { focused_id: Option, expanded_ids: BTreeSet, drop_feedback: Option, }, FileTreeState { active_row_id: Option, selected_row_ids: BTreeSet, anchor_row_id: Option, focused_row_id: Option, drag_row_ids: Vec, drag_effect: Option, drop_target_row_id: Option, }, PickerState { active_item_key: Option, focus_dom: bool, }, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde( tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase" )] pub enum TreeShellHostEvent { PageOpen { node_id: String, }, PageContextMenu { node_id: String, }, FileTreeOpen { target: super::filetree_runtime::FileTreeOpenTarget, }, FileTreeContextMenu { row_id: String, target: super::filetree_runtime::FileTreeOpenTarget, }, FileTreeInternalDrop { target_row_id: Option, target: Option, row_ids: Vec, copy: bool, }, FileTreeExternalDrop { target_row_id: Option, target: Option, file_count: u32, }, FileTreeKeyboardCommand { command: FileTreeKeyboardCommand, row_ids: Vec, target_row_id: Option, }, PickerPickRoot, PickerPickDocument { document_id: String, }, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde( tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase" )] pub enum TreeShellCommandEvent { CreateNode { command_name: String, parent_node_id: Option, }, RenameNode { command_name: String, node_id: String, title: String, }, MoveSubtree { command_name: String, source_node_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] target_node_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] target_parent_id: Option, position: PageTreeDropPosition, #[serde(default, skip_serializing_if = "Option::is_none")] sort_order: Option, }, CopyResource { command_name: String, source_asset_ids: Vec, target_document_id: String, }, MoveResource { command_name: String, source_asset_ids: Vec, target_document_id: String, }, UploadResource { command_name: String, target_document_id: String, file_count: u32, }, ResourceLifecycle { command_name: String, resource_kind: String, asset_id: String, }, } pub fn reduce_tree_shell_runtime(request: TreeShellRuntimeRequest) -> TreeShellRuntimeResult { match request { TreeShellRuntimeRequest::Page { request_id, environment, state, action, } => { let transition = state.reduce(&environment, action); let dom_patches = transition .outputs .iter() .filter_map(|output| match output { PageTreeRuntimeOutput::DomPatch => Some(TreeShellDomPatch::PageState { focused_id: transition.state.focused_id.clone(), expanded_ids: transition.state.expanded_ids.clone(), drop_feedback: transition.state.drop_feedback.clone(), }), _ => None, }) .collect(); let host_events = transition .outputs .iter() .filter_map(|output| match output { PageTreeRuntimeOutput::Intent(PageTreeIntentEvent::Open { node_id }) => { Some(TreeShellHostEvent::PageOpen { node_id: node_id.clone(), }) } PageTreeRuntimeOutput::Intent(PageTreeIntentEvent::ContextMenu { node_id }) => { Some(TreeShellHostEvent::PageContextMenu { node_id: node_id.clone(), }) } _ => None, }) .collect(); let command_events = transition .outputs .iter() .filter_map(|output| match output { PageTreeRuntimeOutput::CommandDispatch( PageTreeCommandDispatchEvent::CreateNode { command_name, parent_node_id, }, ) => Some(TreeShellCommandEvent::CreateNode { command_name: command_name.to_string(), parent_node_id: parent_node_id.clone(), }), PageTreeRuntimeOutput::CommandDispatch( PageTreeCommandDispatchEvent::RenameNode { command_name, node_id, title, }, ) => Some(TreeShellCommandEvent::RenameNode { command_name: command_name.to_string(), node_id: node_id.clone(), title: title.clone(), }), PageTreeRuntimeOutput::CommandDispatch( PageTreeCommandDispatchEvent::MoveSubtree { command_name, source_node_id, target_node_id, target_parent_id, position, sort_order, }, ) => Some(TreeShellCommandEvent::MoveSubtree { command_name: command_name.to_string(), source_node_id: source_node_id.clone(), target_node_id: target_node_id.clone(), target_parent_id: target_parent_id.clone(), position: *position, sort_order: *sort_order, }), _ => None, }) .collect(); TreeShellRuntimeResult { request_id, mode: TreeShellRuntimeMode::Page, state: TreeShellRuntimeStateSnapshot::Page(transition.state), dom_patches, host_events, command_events, } } TreeShellRuntimeRequest::FileTree { request_id, environment, state, action, } => { let transition = state.reduce(&environment, action); let dom_patches = transition .outputs .iter() .filter_map(|output| match output { FileTreeRuntimeOutput::DomPatch => Some(TreeShellDomPatch::FileTreeState { active_row_id: transition.state.active_row_id.clone(), selected_row_ids: transition.state.selection.selected_row_ids.clone(), anchor_row_id: transition.state.selection.anchor_row_id.clone(), focused_row_id: transition.state.selection.focused_row_id.clone(), drag_row_ids: transition.state.drag_row_ids.clone(), drag_effect: transition.state.drag_effect, drop_target_row_id: transition.state.drop_target_row_id.clone(), }), _ => None, }) .collect(); let host_events = transition .outputs .iter() .filter_map(|output| match output { FileTreeRuntimeOutput::Intent(FileTreeIntentEvent::Open { target }) => { Some(TreeShellHostEvent::FileTreeOpen { target: target.clone(), }) } FileTreeRuntimeOutput::Intent(FileTreeIntentEvent::ContextMenu { row_id, target, }) => Some(TreeShellHostEvent::FileTreeContextMenu { row_id: row_id.clone(), target: target.clone(), }), FileTreeRuntimeOutput::Intent(FileTreeIntentEvent::InternalDrop { target_row_id, target, row_ids, copy, }) => Some(TreeShellHostEvent::FileTreeInternalDrop { target_row_id: target_row_id.clone(), target: target.clone(), row_ids: row_ids.clone(), copy: *copy, }), FileTreeRuntimeOutput::Intent(FileTreeIntentEvent::ExternalDrop { target_row_id, target, file_count, }) => Some(TreeShellHostEvent::FileTreeExternalDrop { target_row_id: target_row_id.clone(), target: target.clone(), file_count: *file_count, }), FileTreeRuntimeOutput::Intent(FileTreeIntentEvent::KeyboardCommand { command, row_ids, target_row_id, }) => Some(TreeShellHostEvent::FileTreeKeyboardCommand { command: *command, row_ids: row_ids.clone(), target_row_id: target_row_id.clone(), }), _ => None, }) .collect(); TreeShellRuntimeResult { request_id, mode: TreeShellRuntimeMode::FileTree, state: TreeShellRuntimeStateSnapshot::FileTree(transition.state), dom_patches, host_events, command_events: Vec::new(), } } TreeShellRuntimeRequest::Picker { request_id, environment, state, action, } => { let transition = state.reduce(&environment, action); let dom_patches = transition .outputs .iter() .filter_map(|output| match output { PickerRuntimeOutput::DomPatch { focus_dom } => { Some(TreeShellDomPatch::PickerState { active_item_key: transition.state.active_item_key.clone(), focus_dom: *focus_dom, }) } _ => None, }) .collect(); let host_events = transition .outputs .iter() .filter_map(|output| match output { PickerRuntimeOutput::Pick(PickerRuntimePickTarget::Root) => { Some(TreeShellHostEvent::PickerPickRoot) } PickerRuntimeOutput::Pick(PickerRuntimePickTarget::Document { document_id, }) => Some(TreeShellHostEvent::PickerPickDocument { document_id: document_id.clone(), }), _ => None, }) .collect(); TreeShellRuntimeResult { request_id, mode: TreeShellRuntimeMode::Picker, state: TreeShellRuntimeStateSnapshot::Picker(transition.state), dom_patches, host_events, command_events: Vec::new(), } } } } #[cfg(test)] mod tests { use super::{ reduce_tree_shell_runtime, TreeShellCommandEvent, TreeShellDomPatch, TreeShellHostEvent, TreeShellRuntimeMode, TreeShellRuntimeRequest, TreeShellRuntimeStateSnapshot, }; use crate::tree_shell::drag_drop_state::TreeShellDragEffect; use crate::tree_shell::filetree_runtime::{ FileTreeOpenTarget, FileTreeRuntimeAction, FileTreeRuntimeEnvironment, FileTreeRuntimeRow, FileTreeRuntimeState, }; use crate::tree_shell::filetree_selection::{ FileTreeSelectionModifiers, FileTreeSelectionState, }; use crate::tree_shell::page_runtime::{ PageTreeDropPosition, PageTreeRuntimeAction, PageTreeRuntimeEnvironment, PageTreeRuntimeState, }; use crate::tree_shell::picker_runtime::{ PickerRuntimeAction, PickerRuntimeEnvironment, PickerRuntimeItem, PickerRuntimeState, }; use serde_json::json; use std::collections::BTreeSet; fn ids(values: &[&str]) -> Vec { values.iter().map(|value| (*value).to_string()).collect() } fn set(values: &[&str]) -> BTreeSet { values.iter().map(|value| (*value).to_string()).collect() } #[test] fn runtime_request_result_are_wire_safe_and_camel_case() { let request = TreeShellRuntimeRequest::Page { request_id: "req-page".into(), environment: PageTreeRuntimeEnvironment { visible_node_ids: ids(&["doc:a", "doc:b"]), expandable_node_ids: set(&["doc:a"]), rows: Vec::new(), }, state: PageTreeRuntimeState { focused_id: Some("doc:a".into()), expanded_ids: BTreeSet::new(), drop_feedback: None, }, action: PageTreeRuntimeAction::MoveNext, }; let encoded = serde_json::to_value(&request).expect("runtime request should serialize"); assert_eq!( encoded, json!({ "mode": "page", "requestId": "req-page", "environment": { "visibleNodeIds": ["doc:a", "doc:b"], "expandableNodeIds": ["doc:a"] }, "state": { "focusedId": "doc:a", "expandedIds": [], "dropFeedback": null }, "action": { "kind": "moveNext" } }) ); let decoded: TreeShellRuntimeRequest = serde_json::from_value(encoded).expect("runtime request should deserialize"); let result = reduce_tree_shell_runtime(decoded); assert_eq!(result.request_id, "req-page"); assert_eq!(result.mode, TreeShellRuntimeMode::Page); assert_eq!( result.state, TreeShellRuntimeStateSnapshot::Page(PageTreeRuntimeState { focused_id: Some("doc:b".into()), expanded_ids: BTreeSet::new(), drop_feedback: None, }) ); assert_eq!( result.dom_patches, vec![TreeShellDomPatch::PageState { focused_id: Some("doc:b".into()), expanded_ids: BTreeSet::new(), drop_feedback: None, }] ); assert!(result.host_events.is_empty()); assert!(result.command_events.is_empty()); } #[test] fn runtime_api_maps_page_intents_and_commands_to_structured_events() { let environment = PageTreeRuntimeEnvironment { visible_node_ids: ids(&["doc:a", "doc:b"]), expandable_node_ids: set(&["doc:a"]), rows: Vec::new(), }; let state = PageTreeRuntimeState { focused_id: Some("doc:b".into()), ..PageTreeRuntimeState::default() }; let open = reduce_tree_shell_runtime(TreeShellRuntimeRequest::Page { request_id: "req-open".into(), environment: environment.clone(), state: state.clone(), action: PageTreeRuntimeAction::OpenFocused, }); assert_eq!( open.host_events, vec![TreeShellHostEvent::PageOpen { node_id: "doc:b".into(), }] ); let create_result = reduce_tree_shell_runtime(TreeShellRuntimeRequest::Page { request_id: "req-create".into(), environment: environment.clone(), state: state.clone(), action: PageTreeRuntimeAction::DispatchCreate { parent_node_id: Some("doc:a".into()), }, }); assert_eq!( create_result.command_events, vec![TreeShellCommandEvent::CreateNode { command_name: "tree.node.create".into(), parent_node_id: Some("doc:a".into()), }] ); let rename_result = reduce_tree_shell_runtime(TreeShellRuntimeRequest::Page { request_id: "req-rename".into(), environment: environment.clone(), state: state.clone(), action: PageTreeRuntimeAction::DispatchRename { node_id: "doc:b".into(), title: "新标题".into(), }, }); assert_eq!( rename_result.command_events, vec![TreeShellCommandEvent::RenameNode { command_name: "tree.node.rename".into(), node_id: "doc:b".into(), title: "新标题".into(), }] ); let move_result = reduce_tree_shell_runtime(TreeShellRuntimeRequest::Page { request_id: "req-move".into(), environment, state, action: PageTreeRuntimeAction::DispatchMove { source_node_id: "doc:b".into(), target_parent_id: Some("doc:a".into()), position: PageTreeDropPosition::Inside, }, }); assert_eq!( move_result.command_events, vec![TreeShellCommandEvent::MoveSubtree { command_name: "tree.subtree.move".into(), source_node_id: "doc:b".into(), target_node_id: None, target_parent_id: Some("doc:a".into()), position: PageTreeDropPosition::Inside, sort_order: None, }] ); } #[test] fn runtime_api_maps_filetree_and_picker_results_to_common_output_channels() { let filetree = reduce_tree_shell_runtime(TreeShellRuntimeRequest::FileTree { request_id: "req-filetree".into(), environment: FileTreeRuntimeEnvironment { visible_row_ids: ids(&["doc:root", "asset:image"]), rows: vec![ FileTreeRuntimeRow { row_id: "doc:root".into(), row_kind: "doc".into(), document_id: Some("root".into()), asset_id: None, }, FileTreeRuntimeRow { row_id: "asset:image".into(), row_kind: "asset".into(), document_id: Some("root".into()), asset_id: Some("image".into()), }, ], root_uri: None, }, state: FileTreeRuntimeState::default(), action: FileTreeRuntimeAction::SelectRow { row_id: "asset:image".into(), modifiers: FileTreeSelectionModifiers::default(), }, }); assert_eq!(filetree.mode, TreeShellRuntimeMode::FileTree); assert_eq!( filetree.dom_patches, vec![TreeShellDomPatch::FileTreeState { active_row_id: None, selected_row_ids: set(&["asset:image"]), anchor_row_id: Some("asset:image".into()), focused_row_id: Some("asset:image".into()), drag_row_ids: Vec::new(), drag_effect: None, drop_target_row_id: None, }] ); let drag_result = reduce_tree_shell_runtime(TreeShellRuntimeRequest::FileTree { request_id: "req-filetree-drag".into(), environment: FileTreeRuntimeEnvironment { visible_row_ids: ids(&["doc:root", "asset:image"]), rows: vec![ FileTreeRuntimeRow { row_id: "doc:root".into(), row_kind: "doc".into(), document_id: Some("root".into()), asset_id: None, }, FileTreeRuntimeRow { row_id: "asset:image".into(), row_kind: "asset".into(), document_id: Some("root".into()), asset_id: Some("image".into()), }, ], root_uri: None, }, state: FileTreeRuntimeState { selection: FileTreeSelectionState::from_selected(&["asset:image".to_string()]), ..FileTreeRuntimeState::default() }, action: FileTreeRuntimeAction::ResolveDragRows { row_id: "asset:image".into(), has_external_files: false, alt_key: false, }, }); assert_eq!( drag_result.dom_patches, vec![TreeShellDomPatch::FileTreeState { active_row_id: None, selected_row_ids: set(&["asset:image"]), anchor_row_id: Some("asset:image".into()), focused_row_id: Some("asset:image".into()), drag_row_ids: ids(&["asset:image"]), drag_effect: Some(TreeShellDragEffect::Move), drop_target_row_id: None, }] ); let drop_target_result = reduce_tree_shell_runtime(TreeShellRuntimeRequest::FileTree { request_id: "req-filetree-drop-target".into(), environment: FileTreeRuntimeEnvironment { visible_row_ids: ids(&["doc:root", "asset:image"]), rows: vec![ FileTreeRuntimeRow { row_id: "doc:root".into(), row_kind: "doc".into(), document_id: Some("root".into()), asset_id: None, }, FileTreeRuntimeRow { row_id: "asset:image".into(), row_kind: "asset".into(), document_id: Some("root".into()), asset_id: Some("image".into()), }, ], root_uri: None, }, state: FileTreeRuntimeState::default(), action: FileTreeRuntimeAction::UpdateDropTarget { row_id: Some("asset:image".into()), }, }); assert_eq!( drop_target_result.dom_patches, vec![TreeShellDomPatch::FileTreeState { active_row_id: None, selected_row_ids: BTreeSet::new(), anchor_row_id: None, focused_row_id: None, drag_row_ids: Vec::new(), drag_effect: None, drop_target_row_id: Some("asset:image".into()), }] ); let internal_drop = reduce_tree_shell_runtime(TreeShellRuntimeRequest::FileTree { request_id: "req-filetree-internal-drop".into(), environment: FileTreeRuntimeEnvironment { visible_row_ids: ids(&["doc:root", "asset:image"]), rows: vec![ FileTreeRuntimeRow { row_id: "doc:root".into(), row_kind: "doc".into(), document_id: Some("root".into()), asset_id: None, }, FileTreeRuntimeRow { row_id: "asset:image".into(), row_kind: "asset".into(), document_id: Some("root".into()), asset_id: Some("image".into()), }, ], root_uri: None, }, state: FileTreeRuntimeState { drag_row_ids: ids(&["asset:image"]), drag_effect: Some(TreeShellDragEffect::Move), drop_target_row_id: Some("doc:root".into()), ..FileTreeRuntimeState::default() }, action: FileTreeRuntimeAction::DispatchInternalDrop { target_row_id: Some("doc:root".into()), row_ids: ids(&["asset:image"]), copy: false, }, }); assert_eq!( internal_drop.host_events, vec![TreeShellHostEvent::FileTreeInternalDrop { target_row_id: Some("doc:root".into()), target: Some(FileTreeOpenTarget::Document { document_id: "root".into(), }), row_ids: ids(&["asset:image"]), copy: false, }] ); assert_eq!( internal_drop.dom_patches, vec![TreeShellDomPatch::FileTreeState { active_row_id: None, selected_row_ids: BTreeSet::new(), anchor_row_id: None, focused_row_id: None, drag_row_ids: Vec::new(), drag_effect: None, drop_target_row_id: None, }] ); let external_drop = reduce_tree_shell_runtime(TreeShellRuntimeRequest::FileTree { request_id: "req-filetree-external-drop".into(), environment: FileTreeRuntimeEnvironment { visible_row_ids: ids(&["doc:root"]), rows: vec![FileTreeRuntimeRow { row_id: "doc:root".into(), row_kind: "doc".into(), document_id: Some("root".into()), asset_id: None, }], root_uri: None, }, state: FileTreeRuntimeState::default(), action: FileTreeRuntimeAction::DispatchExternalDrop { target_row_id: Some("doc:root".into()), file_count: 2, }, }); assert_eq!( external_drop.host_events, vec![TreeShellHostEvent::FileTreeExternalDrop { target_row_id: Some("doc:root".into()), target: Some(FileTreeOpenTarget::Document { document_id: "root".into(), }), file_count: 2, }] ); let picker = reduce_tree_shell_runtime(TreeShellRuntimeRequest::Picker { request_id: "req-picker".into(), environment: PickerRuntimeEnvironment { items: vec![PickerRuntimeItem { item_key: "doc:target".into(), document_id: Some("target".into()), pickable: true, }], excluded_ids: BTreeSet::new(), allow_root_pick: false, }, state: PickerRuntimeState { active_item_key: Some("doc:target".into()), }, action: PickerRuntimeAction::Pick, }); assert_eq!(picker.mode, TreeShellRuntimeMode::Picker); assert_eq!( picker.host_events, vec![TreeShellHostEvent::PickerPickDocument { document_id: "target".into(), }] ); } #[test] fn runtime_command_event_schema_covers_tree_and_resource_command_channels() { let events = vec![ TreeShellCommandEvent::CreateNode { command_name: "tree.node.create".into(), parent_node_id: Some("doc:parent".into()), }, TreeShellCommandEvent::RenameNode { command_name: "tree.node.rename".into(), node_id: "doc:target".into(), title: "新标题".into(), }, TreeShellCommandEvent::MoveSubtree { command_name: "tree.subtree.move".into(), source_node_id: "doc:target".into(), target_node_id: Some("doc:sibling".into()), target_parent_id: Some("doc:parent".into()), position: PageTreeDropPosition::After, sort_order: Some(3), }, TreeShellCommandEvent::CopyResource { command_name: "tree.resource.copy".into(), source_asset_ids: vec!["asset:a".into()], target_document_id: "doc:target".into(), }, TreeShellCommandEvent::MoveResource { command_name: "tree.resource.move".into(), source_asset_ids: vec!["asset:a".into()], target_document_id: "doc:target".into(), }, TreeShellCommandEvent::UploadResource { command_name: "tree.resource.upload".into(), target_document_id: "doc:target".into(), file_count: 2, }, TreeShellCommandEvent::ResourceLifecycle { command_name: "tree.resource.archive".into(), resource_kind: "file".into(), asset_id: "asset:a".into(), }, TreeShellCommandEvent::ResourceLifecycle { command_name: "tree.resource.restore".into(), resource_kind: "file".into(), asset_id: "asset:a".into(), }, TreeShellCommandEvent::ResourceLifecycle { command_name: "tree.resource.purge".into(), resource_kind: "file".into(), asset_id: "asset:a".into(), }, TreeShellCommandEvent::ResourceLifecycle { command_name: "tree.resource.rename".into(), resource_kind: "file".into(), asset_id: "asset:a".into(), }, ]; let encoded = serde_json::to_value(&events).expect("command events should serialize"); assert_eq!( encoded, json!([ { "kind": "createNode", "commandName": "tree.node.create", "parentNodeId": "doc:parent" }, { "kind": "renameNode", "commandName": "tree.node.rename", "nodeId": "doc:target", "title": "新标题" }, { "kind": "moveSubtree", "commandName": "tree.subtree.move", "sourceNodeId": "doc:target", "targetNodeId": "doc:sibling", "targetParentId": "doc:parent", "position": "after", "sortOrder": 3 }, { "kind": "copyResource", "commandName": "tree.resource.copy", "sourceAssetIds": ["asset:a"], "targetDocumentId": "doc:target" }, { "kind": "moveResource", "commandName": "tree.resource.move", "sourceAssetIds": ["asset:a"], "targetDocumentId": "doc:target" }, { "kind": "uploadResource", "commandName": "tree.resource.upload", "targetDocumentId": "doc:target", "fileCount": 2 }, { "kind": "resourceLifecycle", "commandName": "tree.resource.archive", "resourceKind": "file", "assetId": "asset:a" }, { "kind": "resourceLifecycle", "commandName": "tree.resource.restore", "resourceKind": "file", "assetId": "asset:a" }, { "kind": "resourceLifecycle", "commandName": "tree.resource.purge", "resourceKind": "file", "assetId": "asset:a" }, { "kind": "resourceLifecycle", "commandName": "tree.resource.rename", "resourceKind": "file", "assetId": "asset:a" } ]) ); } }