hap-cli 0.8.25__tar.gz → 0.8.27__tar.gz
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.
- {hap_cli-0.8.25 → hap_cli-0.8.27}/PKG-INFO +1 -1
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/__init__.py +1 -1
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/chart_cmd.py +5 -2
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/instance_cmd.py +102 -2
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/record_cmd.py +25 -3
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/chart.py +30 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/record.py +29 -9
- hap_cli-0.8.27/hap_cli/guide/en/approval.md +118 -0
- hap_cli-0.8.27/hap_cli/guide/en/chart.md +150 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/guide/index.json +16 -0
- hap_cli-0.8.27/hap_cli/guide/zh-Hans/approval.md +105 -0
- hap_cli-0.8.27/hap_cli/guide/zh-Hans/chart.md +133 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/locale/messages.json +31 -7
- hap_cli-0.8.27/hap_cli/tests/test_approval_submit_fill_cli.py +143 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_chart.py +33 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_core.py +49 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli.egg-info/PKG-INFO +1 -1
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli.egg-info/SOURCES.txt +5 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/MANIFEST.in +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/README.md +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/app_live_tests/__init__.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/app_live_tests/__main__.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/app_live_tests/config.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/app_live_tests/harness.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/app_live_tests/smoke.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/app_live_tests/state.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/README.md +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/README_CN.md +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/__main__.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/__init__.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/app_cmd.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/app_creator_cmd.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/app_editor_cmd.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/attachment_cmd.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/auth_cmd.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/calendar_cmd.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/chat_cmd.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/chatbot_cmd.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/config_cmd.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/contact_cmd.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/department_cmd.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/group_cmd.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/guide_cmd.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/icon_cmd.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/node_cmd.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/optionset_cmd.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/page_cmd.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/post_cmd.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/region_cmd.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/role_cmd.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/update_cmd.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/upload_cmd.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/v3_registry.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/workflow_cmd.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/commands/worksheet_cmd.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/context.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/__init__.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/action_spec_adapter.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/__init__.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/_hapmeta/__init__.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/_hapmeta/control_type_codes.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/_hapmeta/worksheet_templates.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/charts.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/cleanup.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/compiler.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/config.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/design/design.schema.json +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/errors.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/examples/minimal.design.json +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/executor.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/fields.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/hap.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/merge.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/recording/__init__.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/recording/console.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/recording/jsonl.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/recording/mirror.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/recording/report.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/schema.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/seed/INSTRUCTIONS.md +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/seed/__init__.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/seed/cli.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/seed/executor.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/seed/resources/attachments.json +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/seed/resources/sample.docx +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/seed/resources/sample.pdf +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/seed/resources/sample_images.json +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/seed/template.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/selftest.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/smoke.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/steps.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/store.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/validate.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/app_creator/workflow_dsl.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/attachment.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/auth.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/calendar_mod.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/chat.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/chatbot.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/cli_access.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/contact.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/control_type_codes.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/department.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/editor/__init__.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/editor/_hapmeta/__init__.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/editor/_hapmeta/control_type_codes.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/editor/apply.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/editor/componentlower.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/editor/editspec/component.schema.json +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/editor/editspec/custom-action.schema.json +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/editor/editspec/envelope.schema.json +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/editor/editspec/field.schema.json +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/editor/editspec.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/editor/errors.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/editor/fieldlower.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/editor/jsonschema_mini.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/editor/models.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/editor/ops.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/editor/planner.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/editor/reader.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/field_normalizer.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/field_spec_adapter.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/filter_translator.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/flow_node.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/global_meta.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/group.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/guide.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/icon_index.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/instance.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/logger.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/optionset.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/page.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/post.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/response_crypto.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/role.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/role_perm_builder.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/session.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/token_crypto.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/update.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/upload.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/v3/__init__.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/v3/dispatcher.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/v3/render.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/v3/schema.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/view_spec_adapter.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/workflow.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/workflow_node_dsl.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/worksheet.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/core/worksheet_templates.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/guide/en/messages.md +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/guide/en/overview.md +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/guide/en/record.md +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/guide/en/setup.md +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/guide/en/skills.md +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/guide/zh-Hans/messages.md +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/guide/zh-Hans/overview.md +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/guide/zh-Hans/record.md +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/guide/zh-Hans/setup.md +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/guide/zh-Hans/skills.md +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/hap_cli.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/i18n.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/locale/__init__.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/locale/messages.schema.json +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/__init__.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/conftest.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_app_creator.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_app_editor.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_app_export_usage.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_attachment_share.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_auth_prerelease.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_bug_036430.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_bug_036750.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_chat_attachment_send.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_cli_access.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_config_cmd.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_content_language.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_control_lookup_guard.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_destructive_confirm_cli.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_full_e2e.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_global_meta.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_guide.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_i18n.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_instance_history_cli.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_integration.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_integration_approval.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_integration_approval_actions.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_integration_calendar.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_integration_destructive.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_integration_misc.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_integration_post.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_integration_social.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_integration_v3.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_integration_workflow.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_integration_worksheet_extra.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_member_relation_array_wrap.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_multi_profile.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_no_workflow_unsupported.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_org_approval_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_org_id_cli.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_org_id_docs.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_parameter_conventions.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_parameter_mapping_registry.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_record_batch_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_record_field_alias.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_record_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_record_write_fixes.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_select_add_option.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_select_rejects_relation_value.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_semantic_types_registry.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_update.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_update_mode.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_upload_host_resolution.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_v3_api_schema_loader.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_v3_dispatcher.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_v3_registry_coverage.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_v3_session.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_v3_yaml_translation.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_workflow_envelope_errors.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_workflow_list_cli.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_workflow_read_write_keys.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_workflow_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_worksheet_create_section.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_worksheet_crud_cli.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/tests/test_worksheet_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/utils/__init__.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/utils/formatting.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/utils/options.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli/utils/parameter_mapping.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli.egg-info/dependency_links.txt +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli.egg-info/entry_points.txt +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli.egg-info/requires.txt +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/hap_cli.egg-info/top_level.txt +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/__init__.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/__main__.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/_wftest.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/charts.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/cleanup.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/compiler.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/config.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/errors.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/executor.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/fields.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/hap.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/recording/__init__.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/recording/console.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/recording/jsonl.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/recording/mirror.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/recording/report.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/schema.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/seed/__init__.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/seed/cli.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/seed/executor.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/seed/template.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/selftest.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/smoke.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/step.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/steps.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/store.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/live_tests/workflow_dsl.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/setup.cfg +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/setup.py +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/INDEX.json +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/add_member_to_role.yaml +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/batch_delete_records.yaml +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/create_optionset.yaml +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/create_role.yaml +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/delete_optionset.yaml +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/delete_record.yaml +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/delete_role.yaml +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/delete_worksheet.yaml +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/get_app_info.yaml +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/get_app_knowledge_list.yaml +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/get_app_worksheets_list.yaml +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/get_optionset_list.yaml +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/get_record_details.yaml +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/get_record_discussions.yaml +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/get_record_list.yaml +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/get_record_logs.yaml +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/get_record_pivot_data.yaml +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/get_record_relations.yaml +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/get_record_share_link.yaml +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/get_regions.yaml +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/get_role_details.yaml +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/get_role_list.yaml +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/knowledge_search.yaml +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/leave_all_roles.yaml +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/remove_member_from_role.yaml +0 -0
- {hap_cli-0.8.25 → hap_cli-0.8.27}/sources/v3-api-schema/update_optionset.yaml +0 -0
|
@@ -123,7 +123,8 @@ def _resolve_app_id_for_filter(session, worksheet_id: str, extra: dict) -> str:
|
|
|
123
123
|
return (data or {}).get("appId", "") if isinstance(data, dict) else ""
|
|
124
124
|
|
|
125
125
|
|
|
126
|
-
@chart.command("create"
|
|
126
|
+
@chart.command("create", help="Create a new chart on a worksheet. "
|
|
127
|
+
"Chart spec shapes are in `hap guide chart`.")
|
|
127
128
|
@click.argument("worksheet_id")
|
|
128
129
|
@click.option("--name", required=True, help="Chart display name.")
|
|
129
130
|
@click.option("--report-type", "report_type", type=int, required=True,
|
|
@@ -163,7 +164,9 @@ def chart_create(ctx, worksheet_id, name, report_type, app_type, app_id,
|
|
|
163
164
|
ctx.handle_error(e)
|
|
164
165
|
|
|
165
166
|
|
|
166
|
-
@chart.command("update"
|
|
167
|
+
@chart.command("update", help="Update an existing chart's configuration. "
|
|
168
|
+
"Read the config with `chart get`, edit it, submit "
|
|
169
|
+
"it back — see `hap guide chart`.")
|
|
167
170
|
@click.argument("report_id")
|
|
168
171
|
@click.option("--app-id", "-a", required=True, help="Worksheet ID the chart attaches to.")
|
|
169
172
|
@click.option("--name", required=True, help="Chart display name.")
|
|
@@ -6,6 +6,11 @@ import click
|
|
|
6
6
|
|
|
7
7
|
from hap_cli.context import pass_context
|
|
8
8
|
from hap_cli.core import instance as inst_mod
|
|
9
|
+
from hap_cli.core import record as rec_mod
|
|
10
|
+
from hap_cli.commands.record_cmd import (
|
|
11
|
+
_fields_json_to_raw as _rec_fields_json_to_raw,
|
|
12
|
+
_parse_fields as _rec_parse_fields,
|
|
13
|
+
)
|
|
9
14
|
from hap_cli.utils.formatting import output_json, output_table, output_kv
|
|
10
15
|
from hap_cli.i18n import t
|
|
11
16
|
|
|
@@ -165,15 +170,110 @@ def instance_sign(ctx, instance_id, sign_to, before, work_id, opinion):
|
|
|
165
170
|
ctx.handle_error(e)
|
|
166
171
|
|
|
167
172
|
|
|
173
|
+
def _unwritten_fields(session, worksheet_id, row_id, controls):
|
|
174
|
+
"""Names of controls whose non-empty write did not land on the record.
|
|
175
|
+
|
|
176
|
+
A fill-in node only accepts the fields on its own form; anything else is
|
|
177
|
+
dropped silently. Only non-empty writes are checked, so deliberately
|
|
178
|
+
clearing a field is never reported.
|
|
179
|
+
"""
|
|
180
|
+
intended = [
|
|
181
|
+
c["controlId"] for c in controls
|
|
182
|
+
if str(c.get("value") or "").strip() not in ("", "[]")
|
|
183
|
+
]
|
|
184
|
+
if not intended:
|
|
185
|
+
return []
|
|
186
|
+
row = rec_mod.get_record(session, worksheet_id, row_id) or {}
|
|
187
|
+
# get_record returns the cell values as a JSON *string* under rowData.
|
|
188
|
+
values = row.get("rowData") or {}
|
|
189
|
+
if isinstance(values, str):
|
|
190
|
+
try:
|
|
191
|
+
values = _json.loads(values)
|
|
192
|
+
except ValueError:
|
|
193
|
+
return [] # unreadable read-back: don't block on a guess
|
|
194
|
+
if not isinstance(values, dict):
|
|
195
|
+
return []
|
|
196
|
+
return [cid for cid in intended
|
|
197
|
+
if str(values.get(cid) or "").strip() in ("", "[]")]
|
|
198
|
+
|
|
199
|
+
|
|
168
200
|
@instance.command("submit")
|
|
169
201
|
@click.argument("instance_id")
|
|
170
202
|
@click.option("--work-id", "-w", default="", help="Work item ID")
|
|
171
203
|
@click.option("--opinion", "-o", default="", help="Submit comment")
|
|
204
|
+
@click.option("--field", "-f", multiple=True,
|
|
205
|
+
help="Field value to fill in before submitting: FIELD=VALUE "
|
|
206
|
+
"(repeatable). FIELD is a field id, alias or name.")
|
|
207
|
+
@click.option("--fields-json", default="",
|
|
208
|
+
help="Field values to fill in before submitting, as one JSON "
|
|
209
|
+
"array: [{\"id\": \"<field id or alias>\", \"value\": ...}, ...]. "
|
|
210
|
+
"Use either this or -f, not both. Value formats per field "
|
|
211
|
+
"type are in `hap guide record`.")
|
|
212
|
+
@click.option("--dry-run", is_flag=True,
|
|
213
|
+
help="Resolve the task's record and show what would be written, "
|
|
214
|
+
"without filling anything in or submitting.")
|
|
172
215
|
@pass_context
|
|
173
|
-
def instance_submit(ctx, instance_id, work_id, opinion):
|
|
174
|
-
"""Submit a fill-in task.
|
|
216
|
+
def instance_submit(ctx, instance_id, work_id, opinion, field, fields_json, dry_run):
|
|
217
|
+
"""Submit a fill-in task, optionally filling in its form first.
|
|
218
|
+
|
|
219
|
+
The fields of a fill-in task are writable only while you hold the task,
|
|
220
|
+
so pass them here rather than editing the record separately. Working
|
|
221
|
+
through todos end to end is covered in `hap guide approval`.
|
|
222
|
+
"""
|
|
223
|
+
if field and fields_json:
|
|
224
|
+
raise click.UsageError("Pass exactly one of -f or --fields-json.")
|
|
175
225
|
try:
|
|
176
226
|
session = ctx.get_session()
|
|
227
|
+
raw = None
|
|
228
|
+
if field or fields_json:
|
|
229
|
+
raw = (_rec_fields_json_to_raw(fields_json) if fields_json
|
|
230
|
+
else _rec_parse_fields(field))
|
|
231
|
+
# Read-only resolution first: a fill-in task carries the record it
|
|
232
|
+
# writes to, which the caller should not have to look up.
|
|
233
|
+
target = None
|
|
234
|
+
if raw is not None:
|
|
235
|
+
target = inst_mod.get_work_item(session, instance_id, work_id=work_id)
|
|
236
|
+
if not (target or {}).get("worksheetId"):
|
|
237
|
+
raise click.ClickException(
|
|
238
|
+
"Could not resolve the record behind this task, so there "
|
|
239
|
+
"is nothing to fill in. Check the instance and work id.")
|
|
240
|
+
if dry_run:
|
|
241
|
+
ctx.output(
|
|
242
|
+
{"instanceId": instance_id, "workId": work_id,
|
|
243
|
+
"target": target, "fields": raw, "submitted": False},
|
|
244
|
+
lambda d: click.echo(
|
|
245
|
+
f"Would fill {len(raw or [])} field(s) on "
|
|
246
|
+
f"{(target or {}).get('worksheetId', '-')}/"
|
|
247
|
+
f"{(target or {}).get('rowId', '-')} and submit."),
|
|
248
|
+
)
|
|
249
|
+
return
|
|
250
|
+
if raw is not None:
|
|
251
|
+
ws_id, row_id = target["worksheetId"], target.get("rowId", "")
|
|
252
|
+
controls = rec_mod.encode_controls_by_id(
|
|
253
|
+
session, ws_id, raw, for_update=True)
|
|
254
|
+
# The record itself may be read-only outside the task; the
|
|
255
|
+
# instance/work pair is what grants the write.
|
|
256
|
+
rec_mod.update_record(
|
|
257
|
+
session, ws_id, row_id, controls,
|
|
258
|
+
instance_id=instance_id, work_id=work_id,
|
|
259
|
+
)
|
|
260
|
+
# A field the task's form does not contain is dropped by the
|
|
261
|
+
# server without any error, so the write must be read back
|
|
262
|
+
# before the task is submitted — otherwise the value is lost
|
|
263
|
+
# and the task is gone with it.
|
|
264
|
+
dropped = _unwritten_fields(session, ws_id, row_id, controls)
|
|
265
|
+
if dropped:
|
|
266
|
+
# Report the names the caller typed, not internal ids.
|
|
267
|
+
labels = dict(zip(
|
|
268
|
+
[c.get("controlId") for c in controls],
|
|
269
|
+
[str(r.get("controlId")) for r in raw],
|
|
270
|
+
)) if len(controls) == len(raw) else {}
|
|
271
|
+
shown = [labels.get(cid, cid) for cid in dropped]
|
|
272
|
+
raise click.ClickException(
|
|
273
|
+
"These values were not saved, so the task was NOT "
|
|
274
|
+
"submitted: " + ", ".join(shown) + ". A fill-in task "
|
|
275
|
+
"only accepts the fields on its own form; fill the rest "
|
|
276
|
+
"separately, or drop them and submit again.")
|
|
177
277
|
data = inst_mod.submit(session, instance_id, work_id, opinion)
|
|
178
278
|
ctx.output(data, lambda d: click.echo("Submitted."))
|
|
179
279
|
except Exception as e:
|
|
@@ -162,8 +162,17 @@ def _fields_json_to_raw(fields_json):
|
|
|
162
162
|
"--app-id", "-a", default="",
|
|
163
163
|
help="Application ID. Optional — the worksheet id is enough on its own; this only scopes the field lookup.",
|
|
164
164
|
)
|
|
165
|
+
@click.option("--instance-id", default="",
|
|
166
|
+
help="Workflow instance the edit belongs to. Pass together with "
|
|
167
|
+
"--work-id to fill in the form of a fill-in task, whose "
|
|
168
|
+
"fields are only writable while you hold that task. To fill "
|
|
169
|
+
"a task and submit it in one step use `approval submit -f`; "
|
|
170
|
+
"see `hap guide approval`.")
|
|
171
|
+
@click.option("--work-id", default="",
|
|
172
|
+
help="Workflow work-item the edit belongs to. Use with --instance-id.")
|
|
165
173
|
@pass_context
|
|
166
|
-
def record_update(ctx, worksheet_id, row_id, field, fields_json, no_workflow, app_id
|
|
174
|
+
def record_update(ctx, worksheet_id, row_id, field, fields_json, no_workflow, app_id,
|
|
175
|
+
instance_id, work_id):
|
|
167
176
|
"""Update a record.
|
|
168
177
|
|
|
169
178
|
Pass values either as repeated -f CONTROL_ID=VALUE pairs or as one
|
|
@@ -193,6 +202,7 @@ def record_update(ctx, worksheet_id, row_id, field, fields_json, no_workflow, ap
|
|
|
193
202
|
data = rec_mod.update_record(
|
|
194
203
|
session, worksheet_id, row_id, controls,
|
|
195
204
|
trigger_workflow=not no_workflow, app_id=app_id,
|
|
205
|
+
instance_id=instance_id, work_id=work_id,
|
|
196
206
|
)
|
|
197
207
|
ctx.output(data, lambda d: click.echo(f"Updated: {d}"))
|
|
198
208
|
except Exception as e:
|
|
@@ -374,16 +384,28 @@ def record_discussions(ctx, worksheet_id, row_id, page_size, page):
|
|
|
374
384
|
@click.option("--app-id", default="", help="Application ID (auto-detected from the worksheet if omitted)")
|
|
375
385
|
@click.option("--view-id", default="", help="View ID the discussion originates from")
|
|
376
386
|
@click.option("--reply-id", default="", help="ID of the discussion being replied to")
|
|
387
|
+
@click.option("--file", "-f", "files", multiple=True,
|
|
388
|
+
help="Attach a file to the comment (repeatable). Accepts a local "
|
|
389
|
+
"path or an http(s) URL.")
|
|
377
390
|
@pass_context
|
|
378
|
-
def record_add_discussion(ctx, worksheet_id, row_id, message, app_id, view_id, reply_id):
|
|
391
|
+
def record_add_discussion(ctx, worksheet_id, row_id, message, app_id, view_id, reply_id, files):
|
|
379
392
|
"""Add a discussion/comment to a record."""
|
|
380
393
|
try:
|
|
381
394
|
session = ctx.get_session()
|
|
395
|
+
attachments = None
|
|
396
|
+
if files:
|
|
397
|
+
from hap_cli.core import upload as up_mod
|
|
398
|
+
descriptors = up_mod.source_descriptors(list(files))
|
|
399
|
+
attachments = up_mod.upload_files(
|
|
400
|
+
session, descriptors,
|
|
401
|
+
worksheet_id=worksheet_id, app_id=app_id,
|
|
402
|
+
)
|
|
382
403
|
data = rec_mod.add_discussion(
|
|
383
404
|
session, worksheet_id, row_id, message,
|
|
384
405
|
app_id=app_id, view_id=view_id, reply_id=reply_id,
|
|
406
|
+
attachments=attachments,
|
|
385
407
|
)
|
|
386
|
-
ctx.output(data, lambda d: click.echo(
|
|
408
|
+
ctx.output(data, lambda d: click.echo("Discussion added."))
|
|
387
409
|
except Exception as e:
|
|
388
410
|
ctx.handle_error(e)
|
|
389
411
|
|
|
@@ -44,6 +44,30 @@ from hap_cli.core.session import Session
|
|
|
44
44
|
|
|
45
45
|
logger = logging.getLogger("hap_cli.chart")
|
|
46
46
|
|
|
47
|
+
# A chart that is saved without a ``filter`` block saves successfully but
|
|
48
|
+
# never renders: the statistics backend cannot scope the query and
|
|
49
|
+
# ``report/getData`` answers status -1/0, which the UI paints as
|
|
50
|
+
# "无法形成图表 / 构成要素不存在或已删除". The chart looks fine in the editor
|
|
51
|
+
# (which supplies its own default scope) and only breaks after save — the
|
|
52
|
+
# single most common way a hand-written ``chart create`` spec silently
|
|
53
|
+
# produces a blank chart. The frontend's ``getNewReport`` always injects a
|
|
54
|
+
# default time filter for a new worksheet chart; we mirror that here, but
|
|
55
|
+
# default to ``rangeType: 0`` (全部时间 / all-time) instead of the UI's
|
|
56
|
+
# "last 365 days" so the chart is never blank merely because the default
|
|
57
|
+
# window excluded older data. Callers that supply their own ``filter``
|
|
58
|
+
# (any non-empty dict) keep it untouched.
|
|
59
|
+
_DEFAULT_CHART_FILTER: dict[str, Any] = {
|
|
60
|
+
"filterRangeId": "ctime",
|
|
61
|
+
"filterRangeName": "创建时间",
|
|
62
|
+
"rangeType": 0,
|
|
63
|
+
"rangeValue": None,
|
|
64
|
+
"today": False,
|
|
65
|
+
"dynamicFilter": {
|
|
66
|
+
"startType": 1, "startCount": 1, "startUnit": 1,
|
|
67
|
+
"endType": 1, "endCount": 1, "endUnit": 1,
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
47
71
|
|
|
48
72
|
def list_charts(
|
|
49
73
|
session: Session,
|
|
@@ -218,6 +242,12 @@ def save_chart(
|
|
|
218
242
|
split_control_id = body["split"].get("controlId")
|
|
219
243
|
if split_control_id:
|
|
220
244
|
body["splitId"] = split_control_id
|
|
245
|
+
# A chart with no filter block saves but never renders (blank after
|
|
246
|
+
# save — see _DEFAULT_CHART_FILTER). Inject an all-time default when the
|
|
247
|
+
# caller omitted it; a caller-supplied filter (any non-empty value) is
|
|
248
|
+
# left untouched so an explicit scope always wins.
|
|
249
|
+
if not body.get("filter"):
|
|
250
|
+
body["filter"] = dict(_DEFAULT_CHART_FILTER)
|
|
221
251
|
# Resolve the chart's filter block before sending saveReportConfig.
|
|
222
252
|
if isinstance(body.get("filter"), dict):
|
|
223
253
|
body["filter"] = _ensure_filter_id(
|
|
@@ -965,6 +965,8 @@ def update_record(
|
|
|
965
965
|
controls: list[dict[str, Any]],
|
|
966
966
|
trigger_workflow: bool = True,
|
|
967
967
|
app_id: str = "",
|
|
968
|
+
instance_id: str = "",
|
|
969
|
+
work_id: str = "",
|
|
968
970
|
) -> dict[str, Any]:
|
|
969
971
|
"""Update an existing record.
|
|
970
972
|
|
|
@@ -973,6 +975,14 @@ def update_record(
|
|
|
973
975
|
AddWorksheetRow, UpdateWorksheetRow's schema has no ``appId``, and
|
|
974
976
|
inventing one would diverge from the frontend.
|
|
975
977
|
|
|
978
|
+
``instance_id`` / ``work_id`` identify a workflow step the caller is
|
|
979
|
+
acting inside. A fill-in node (填写节点) grants write permission on the
|
|
980
|
+
fields of its own form for the duration of that step; the record itself
|
|
981
|
+
may well be read-only otherwise, in which case a plain update answers
|
|
982
|
+
"权限不足". Passing the pair makes the write happen under the node's
|
|
983
|
+
permission, exactly as the web form does (UpdateWorksheetRow's
|
|
984
|
+
``instanceId`` / ``workId`` params).
|
|
985
|
+
|
|
976
986
|
Args:
|
|
977
987
|
session: Active session
|
|
978
988
|
worksheet_id: Worksheet ID
|
|
@@ -980,21 +990,24 @@ def update_record(
|
|
|
980
990
|
controls: List of {controlId, value} dicts
|
|
981
991
|
trigger_workflow: Whether to trigger workflows
|
|
982
992
|
app_id: Optional application ID (metadata scope only)
|
|
993
|
+
instance_id: Workflow instance ID the write belongs to
|
|
994
|
+
work_id: Workflow work-item ID the write belongs to
|
|
983
995
|
|
|
984
996
|
Returns:
|
|
985
997
|
Update result
|
|
986
998
|
"""
|
|
987
999
|
_reject_workflow_suppression(trigger_workflow)
|
|
1000
|
+
payload: dict[str, Any] = {
|
|
1001
|
+
"worksheetId": worksheet_id,
|
|
1002
|
+
"rowId": row_id,
|
|
1003
|
+
"newOldControl": controls,
|
|
1004
|
+
}
|
|
1005
|
+
if instance_id:
|
|
1006
|
+
payload["instanceId"] = instance_id
|
|
1007
|
+
if work_id:
|
|
1008
|
+
payload["workId"] = work_id
|
|
988
1009
|
return _raise_on_failed_write(
|
|
989
|
-
session.api_call(
|
|
990
|
-
"Worksheet",
|
|
991
|
-
"UpdateWorksheetRow",
|
|
992
|
-
{
|
|
993
|
-
"worksheetId": worksheet_id,
|
|
994
|
-
"rowId": row_id,
|
|
995
|
-
"newOldControl": controls,
|
|
996
|
-
},
|
|
997
|
-
),
|
|
1010
|
+
session.api_call("Worksheet", "UpdateWorksheetRow", payload),
|
|
998
1011
|
"update",
|
|
999
1012
|
)
|
|
1000
1013
|
|
|
@@ -1089,6 +1102,7 @@ def add_discussion(
|
|
|
1089
1102
|
app_id: str = "",
|
|
1090
1103
|
view_id: str = "",
|
|
1091
1104
|
reply_id: str = "",
|
|
1105
|
+
attachments: Optional[list[dict[str, Any]]] = None,
|
|
1092
1106
|
) -> dict[str, Any]:
|
|
1093
1107
|
"""Add a discussion/comment to a record (sourceType 8 / worksheet-row).
|
|
1094
1108
|
|
|
@@ -1117,6 +1131,12 @@ def add_discussion(
|
|
|
1117
1131
|
}
|
|
1118
1132
|
if reply_id:
|
|
1119
1133
|
data["replyId"] = reply_id
|
|
1134
|
+
# ``attachments`` carries uploaded-file descriptors (from
|
|
1135
|
+
# hap_cli.core.upload). The frontend WorkSheetCommenter sends them as a
|
|
1136
|
+
# JSON-encoded string under ``attachments`` (see pd-openweb
|
|
1137
|
+
# components/comment/commenter.jsx), so match that shape.
|
|
1138
|
+
if attachments:
|
|
1139
|
+
data["attachments"] = json.dumps(attachments, ensure_ascii=False)
|
|
1120
1140
|
result = session.api_call("Discussion", "AddDiscussion", data)
|
|
1121
1141
|
_ensure_discussion_ok(result, "add discussion")
|
|
1122
1142
|
return result
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Todos and approvals: find, fill, act
|
|
2
|
+
|
|
3
|
+
`hap approval` handles workflow todos: listing them, filling in the form of a
|
|
4
|
+
fill-in task, approving or rejecting, withdrawing and re-initiating. This topic
|
|
5
|
+
covers which command each kind of todo needs, plus the traps you will hit
|
|
6
|
+
without reading it.
|
|
7
|
+
|
|
8
|
+
> 🚨 Core risk of this topic: **the fields of a fill-in task cannot be written
|
|
9
|
+
> with a plain record update**. `hap worksheet record update` answers
|
|
10
|
+
> 权限不足 (insufficient permission), and that error points you in entirely the
|
|
11
|
+
> wrong direction (it reads as "your account lacks permission"). The right way
|
|
12
|
+
> is section 3.
|
|
13
|
+
|
|
14
|
+
## 1. See what is waiting
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
hap approval todo-count # counts per kind
|
|
18
|
+
hap approval todo --type 3 # only "waiting for me to fill in"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`--type` values: `-1` all, `0` initiated by me, `3` fill in, `4` approve,
|
|
22
|
+
`5` carbon copy.
|
|
23
|
+
|
|
24
|
+
Each todo carries two IDs that every later command needs:
|
|
25
|
+
|
|
26
|
+
- `id` — the **workflow instance id** (instanceId). It is named `id`, which is
|
|
27
|
+
easy to mistake for an id of the todo itself.
|
|
28
|
+
- `workId` — the **work-item id**, identifying this step for this person.
|
|
29
|
+
|
|
30
|
+
To find which business record a todo belongs to:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
hap approval work-item <instanceId> --work-id <workId> # -> {worksheetId, rowId}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 2. Approval tasks (type 4)
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
hap approval approve <instanceId> --work-id <workId> -o "Agreed"
|
|
40
|
+
hap approval reject <instanceId> --work-id <workId> -o "Does not qualify"
|
|
41
|
+
hap approval operations <instanceId> --work-id <workId> # what this step actually allows
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
When unsure which buttons a task supports, read `operations` first rather than
|
|
45
|
+
guessing.
|
|
46
|
+
|
|
47
|
+
## 3. Fill-in tasks (type 3): fill the form and submit
|
|
48
|
+
|
|
49
|
+
**Do it in one command, not two:**
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
hap approval submit <instanceId> --work-id <workId> \
|
|
53
|
+
-f 'Frontend owner=谭宏' -f 'Backend owner=陈旭' -o "Assigned"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For richer values use `--fields-json` (same shape as writing a record; per-type
|
|
57
|
+
value formats are in `hap guide record`):
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
hap approval submit <instanceId> --work-id <workId> \
|
|
61
|
+
--fields-json '[{"id":"Frontend owner","value":["<member accountId>"]}]'
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
To see which record would be written without touching anything:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
hap approval submit <instanceId> --work-id <workId> -f 'Amount=100' --dry-run
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Why it has to go this way
|
|
71
|
+
|
|
72
|
+
A fill-in node's fields are writable **only while you hold the task, and only
|
|
73
|
+
when the write carries the workflow identity**. So:
|
|
74
|
+
|
|
75
|
+
- A plain `hap worksheet record update <ws> <row> -f 'field=value'` returns
|
|
76
|
+
**权限不足**. This is not an account-permission problem; do not go asking an
|
|
77
|
+
admin for rights.
|
|
78
|
+
- If you must edit the record separately, carry the workflow identity:
|
|
79
|
+
`hap worksheet record update <ws> <row> --instance-id <instanceId> --work-id <workId> ...`
|
|
80
|
+
That is exactly what `approval submit -f` does internally.
|
|
81
|
+
|
|
82
|
+
### Only fields on the form can be filled
|
|
83
|
+
|
|
84
|
+
A fill-in node accepts **only the fields configured on its own form**. Anything
|
|
85
|
+
else is **discarded by the server with no error at all**. `approval submit`
|
|
86
|
+
therefore reads the write back before submitting; if a value did not land it
|
|
87
|
+
stops, names the field, and **does not submit** (the task survives, so you can
|
|
88
|
+
correct and retry). When you see that message, drop the field from the command
|
|
89
|
+
or have the flow designer add it to the node's form.
|
|
90
|
+
|
|
91
|
+
## 4. Withdraw and re-initiate
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
hap approval revoke <instanceId> # withdraw a flow you initiated
|
|
95
|
+
hap approval restart <instanceId> # re-initiate one you withdrew
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Notes:
|
|
99
|
+
|
|
100
|
+
- **Withdrawing uses the work item of the initiating step, not the approver's.**
|
|
101
|
+
Omit `--work-id` and let the CLI find that initiating work; it is more
|
|
102
|
+
reliable than filling one in by hand.
|
|
103
|
+
- `restart` requires the flow to have "allow re-initiation" enabled and
|
|
104
|
+
published, and the instance to actually be in the withdrawn state.
|
|
105
|
+
|
|
106
|
+
## 5. Other common commands
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
hap approval get <instanceId> # instance detail
|
|
110
|
+
hap approval op-history <instanceId> # processing history
|
|
111
|
+
hap approval urge <instanceId> --work-id <workId> # send a reminder
|
|
112
|
+
hap approval batch ... # bulk approve / reject
|
|
113
|
+
hap approval todo-filters ... # richer todo filtering
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The selection for a bulk operation is an **array of objects** (each with the
|
|
117
|
+
instance id and workId), not a list of id strings; see
|
|
118
|
+
`hap approval batch --help` for the exact parameters.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Charts: create, update, place on a custom page
|
|
2
|
+
|
|
3
|
+
`hap worksheet chart` builds a chart (report) on a worksheet; `hap custom-page`
|
|
4
|
+
places charts into a custom page to form a dashboard. This topic covers how to
|
|
5
|
+
shape a chart spec, the right way to edit a chart, and the most common trap —
|
|
6
|
+
"saved fine, blank on the page".
|
|
7
|
+
|
|
8
|
+
> 🚨 Core risk of this topic: **an under-specified chart still saves
|
|
9
|
+
> successfully but renders blank**, showing "无法形成图表 / 构成要素不存在或已删除"
|
|
10
|
+
> (cannot draw chart / component missing or deleted). It looks right in the
|
|
11
|
+
> editor and only breaks after save. Root cause is in section 3 — the CLI now
|
|
12
|
+
> backfills the missing filter scope, but you still need the model.
|
|
13
|
+
|
|
14
|
+
## 1. What a chart is made of
|
|
15
|
+
|
|
16
|
+
A chart hangs off **one worksheet** and needs at least three things:
|
|
17
|
+
|
|
18
|
+
- **Data source**: the worksheet ID (`WORKSHEET_ID` in the command, `appId` in
|
|
19
|
+
the spec). Note HAP's chart spec calls the worksheet `appId` — it is NOT the
|
|
20
|
+
application ID. When editing a chart, do not fill this with the app ID.
|
|
21
|
+
- **Dimension `xaxes`**: the grouping, e.g. by date (year) or by status. A
|
|
22
|
+
number chart (`report_type=10`) may have no dimension — just a value.
|
|
23
|
+
- **Value `yaxisList`**: what to aggregate, e.g. sum of amount, record count.
|
|
24
|
+
|
|
25
|
+
Plus a **required filter scope `filter`** (section 3).
|
|
26
|
+
|
|
27
|
+
## 2. Chart types (reportType integer)
|
|
28
|
+
|
|
29
|
+
Pass an integer to `--report-type`. **There is no 4** — an old API doc labels 4
|
|
30
|
+
as the number chart, which is wrong; the number chart is **10**.
|
|
31
|
+
|
|
32
|
+
| # | type | # | type |
|
|
33
|
+
|---|------|---|------|
|
|
34
|
+
| 1 | column | 10 | number |
|
|
35
|
+
| 2 | line | 11 | bidirectional bar |
|
|
36
|
+
| 3 | pie | 12 | scatter |
|
|
37
|
+
| 5 | radar | 13 | word cloud |
|
|
38
|
+
| 6 | funnel | 14 | gauge |
|
|
39
|
+
| 7 | dual-axis | 15 | progress |
|
|
40
|
+
| 8 | pivot | 16 | ranking |
|
|
41
|
+
| 9 | region map | 17 | world map |
|
|
42
|
+
|
|
43
|
+
## 3. Why "saved but blank": a missing filter
|
|
44
|
+
|
|
45
|
+
A chart **saves successfully even with no `filter` block** (`create` still
|
|
46
|
+
returns a `reportId`), but at render time the backend cannot scope the query and
|
|
47
|
+
`report/getData` returns status -1/0, so the page shows "cannot draw chart". This
|
|
48
|
+
is exactly the "fine in editor, blank after save" symptom: the editor supplies
|
|
49
|
+
its own default scope, the stored spec does not.
|
|
50
|
+
|
|
51
|
+
**The CLI now injects a default all-time filter when you omit `filter`**, so a
|
|
52
|
+
plain `chart create` no longer goes blank. You only need to write `filter`
|
|
53
|
+
yourself to **restrict the time range**:
|
|
54
|
+
|
|
55
|
+
```jsonc
|
|
56
|
+
"filter": {
|
|
57
|
+
"filterRangeId": "ctime", // which date field to scope by; "ctime"=create time
|
|
58
|
+
"filterRangeName": "创建时间",
|
|
59
|
+
"rangeType": 0, // 0=all time; 8=this month; 18=last N days (with rangeValue)
|
|
60
|
+
"rangeValue": null,
|
|
61
|
+
"today": false
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Common `rangeType`: `0` all, `4` this week, `8` this month, `11` this quarter,
|
|
66
|
+
`15` this year, `16` last year, `18` last N days (`rangeValue` = day count, e.g. 365).
|
|
67
|
+
|
|
68
|
+
## 4. Create: `hap worksheet chart create`
|
|
69
|
+
|
|
70
|
+
Pass the spec via `-j/--spec-json` (inline) or `-f/--spec-file` (file). Reference
|
|
71
|
+
fields by **controlId**; read fields with `hap worksheet fields WS_ID`.
|
|
72
|
+
|
|
73
|
+
**Column chart (amount by status):**
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
hap worksheet chart create WS_ID --name "Amount by status" --report-type 1 -j '{
|
|
77
|
+
"xaxes": {"controlId": "<status field>", "controlType": 11},
|
|
78
|
+
"yaxisList": [{"controlId": "<amount field>", "controlType": 6, "normType": 1}]
|
|
79
|
+
}'
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Number chart (no dimension, one value):** `--report-type 10`, spec with only `yaxisList`.
|
|
83
|
+
|
|
84
|
+
`yaxisList[].normType` aggregation: `1` sum, `2` max, `3` min, `4` avg, `5` count,
|
|
85
|
+
`6` distinct count. Numeric fields default to `1`, text/option fields use `5`.
|
|
86
|
+
Record count uses the special column
|
|
87
|
+
`{"controlId": "record_count", "controlName": "记录数量", "controlType": 10000000, "normType": 5}`.
|
|
88
|
+
|
|
89
|
+
**Date dimension**: add `particleSizeType` to `xaxes`: `5` year, `4` quarter,
|
|
90
|
+
`3` month, `2` week, `1` day.
|
|
91
|
+
|
|
92
|
+
**Condition filter** (not a time range but "status = done"): put a high-level
|
|
93
|
+
`items` array in `filter`; the CLI stores it as a saved filter and backfills the id:
|
|
94
|
+
|
|
95
|
+
```jsonc
|
|
96
|
+
"filter": {"items": [{"controlId": "<status field>", "dataType": 11, "spliceType": 1,
|
|
97
|
+
"filterType": 2, "values": ["<option key>"]}]}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
When using `items`, pass `--app-id <application id>` (the app, not the worksheet)
|
|
101
|
+
so the CLI can store the filter.
|
|
102
|
+
|
|
103
|
+
## 5. Update: read the config, change a few params, submit back
|
|
104
|
+
|
|
105
|
+
You don't rebuild a spec from scratch. **Read the current config, change what you
|
|
106
|
+
need, submit the whole thing back:**
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# 1. read the current config
|
|
110
|
+
hap --json worksheet chart get REPORT_ID --app-id WS_ID > chart.json
|
|
111
|
+
# 2. edit chart.json (name / normType / xaxes / add a filter, etc.)
|
|
112
|
+
# 3. submit it back (update needs --app-id=worksheetId, --name, --report-type)
|
|
113
|
+
hap worksheet chart update REPORT_ID --app-id WS_ID --name "New name" --report-type 1 -f chart.json
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Extra fields `get` returns (`controls`, `account`, `createdDate`) can be passed
|
|
117
|
+
back as-is; the server ignores them. `update` replaces the whole config, so edit
|
|
118
|
+
on top of the read config — don't send only the fields you changed.
|
|
119
|
+
|
|
120
|
+
## 6. Place on a custom page (dashboard)
|
|
121
|
+
|
|
122
|
+
Once the chart exists and you have its `reportId`, add it as a component with
|
|
123
|
+
`custom-page save`.
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# page version: read it first with hap --json custom-page info PAGE_ID, pass the same value
|
|
127
|
+
hap custom-page save PAGE_ID --version <current version> --components '[
|
|
128
|
+
{"type": 1, "value": "<reportId>", "worksheetId": "<WS_ID>",
|
|
129
|
+
"name": "Amount by status", "reportType": 1,
|
|
130
|
+
"config": {"objectId": "<32 hex chars>"},
|
|
131
|
+
"web": {"title": "", "titleVisible": false, "visible": true,
|
|
132
|
+
"layout": {"x": 0, "y": 0, "w": 24, "h": 10, "minW": 2, "minH": 4}},
|
|
133
|
+
"mobile": {"title": "", "titleVisible": false, "visible": true, "layout": null}}
|
|
134
|
+
]'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Key points:
|
|
138
|
+
|
|
139
|
+
- A chart component has `type` **1**, `value` = `reportId`, plus `worksheetId`
|
|
140
|
+
and `reportType`.
|
|
141
|
+
- `config.objectId` is a unique 32-hex string per component (used by linked filters).
|
|
142
|
+
- Layout is a **48-column grid**: `w` max 48; two charts per row at `w:24` each.
|
|
143
|
+
- `--version` must equal the page's current version, else "自定义页面保存失败";
|
|
144
|
+
a successful save bumps version by 1.
|
|
145
|
+
- `save` replaces the whole page: to keep existing components, read them via
|
|
146
|
+
`custom-page info` first and append the new one before submitting.
|
|
147
|
+
|
|
148
|
+
For building a whole app with dashboards, prefer the `hap-cli-app-creator` skill:
|
|
149
|
+
it generates full chart specs and page layouts from a friendly config. Use the
|
|
150
|
+
commands above to hand-edit a single chart or add one chart to an existing page.
|
|
@@ -24,6 +24,22 @@
|
|
|
24
24
|
"zh-Hans": "顺着通知或讨论找到它所属的那条应用记录。"
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
|
+
{
|
|
28
|
+
"name": "approval",
|
|
29
|
+
"title": {"en": "Todos and approvals", "zh-Hans": "待办与审批"},
|
|
30
|
+
"summary": {
|
|
31
|
+
"en": "Find, fill in and act on workflow todos. A fill-in task's fields need the workflow identity, or the write is refused.",
|
|
32
|
+
"zh-Hans": "查看、填写、办理工作流待办。填写类待办的字段必须带工作流身份写,否则报权限不足。"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"name": "chart",
|
|
37
|
+
"title": {"en": "Charts and dashboards", "zh-Hans": "统计图与看板"},
|
|
38
|
+
"summary": {
|
|
39
|
+
"en": "Chart spec shape, editing a chart, placing it on a custom page. Miss a field and it saves fine but renders blank.",
|
|
40
|
+
"zh-Hans": "图表规格怎么写、怎么改图、怎么放进自定义页。写少了照样保存成功,页面却空白。"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
27
43
|
{
|
|
28
44
|
"name": "skills",
|
|
29
45
|
"title": {"en": "When to hand off", "zh-Hans": "什么时候转交专项 skill"},
|