From f869ae03919b5bdb9665ca3e6ce20648927b4917 Mon Sep 17 00:00:00 2001 From: Airtable Date: Tue, 29 Apr 2025 00:03:10 +0000 Subject: [PATCH 01/14] @airtable/blocks@0.0.0-experimental-20e3506bd-20250428 --- package.json | 1 + packages/sdk/.storybook/config.js | 8 - packages/sdk/.storybook/main.js | 29 + packages/sdk/.storybook/manager.js | 9 + packages/sdk/.storybook/webpack.config.js | 8 - packages/sdk/README.md | 7 - packages/sdk/index.d.ts | 2 +- packages/sdk/models.d.ts | 2 +- packages/sdk/models.js | 2 +- packages/sdk/package.json | 63 +- packages/sdk/src/{ => base}/index.ts | 17 +- .../models/abstract_model_with_async_data.ts | 14 +- packages/sdk/src/base/models/base.ts | 242 ++++++ .../{ => base}/models/create_aggregators.ts | 2 +- packages/sdk/src/{ => base}/models/cursor.ts | 11 +- packages/sdk/src/{ => base}/models/field.ts | 485 +++-------- .../models/grouped_record_query_result.ts | 7 +- .../models/linked_records_query_result.ts | 8 +- packages/sdk/src/{ => base}/models/models.ts | 2 +- .../sdk/src/{ => base}/models/mutations.ts | 191 +---- .../sdk/src/{ => base}/models/object_pool.ts | 4 +- packages/sdk/src/{ => base}/models/record.ts | 212 +---- .../src/{ => base}/models/record_coloring.ts | 2 +- .../{ => base}/models/record_query_result.ts | 12 +- .../sdk/src/{ => base}/models/record_store.ts | 340 ++++---- packages/sdk/src/{ => base}/models/session.ts | 160 +--- packages/sdk/src/{ => base}/models/table.ts | 772 ++++++------------ .../models/table_or_view_query_result.ts | 7 +- packages/sdk/src/{ => base}/models/view.ts | 10 +- .../src/{ => base}/models/view_data_store.ts | 13 +- .../models/view_metadata_query_result.ts | 6 +- .../src/{ => base}/perform_record_action.ts | 4 +- packages/sdk/src/{ => base}/sdk.ts | 136 +-- .../sdk/src/{ => base}/settings_button.ts | 4 +- .../sdk/src/{ => base}/types/aggregators.ts | 0 .../{ => base}/types/airtable_interface.ts | 186 +---- .../{ => base}/types/backend_fetch_types.ts | 0 packages/sdk/src/base/types/base.ts | 11 + packages/sdk/src/{ => base}/types/cursor.ts | 5 +- .../sdk/src/{ => base}/types/mutations.ts | 69 +- packages/sdk/src/base/types/record.ts | 7 + .../{ => base}/types/record_action_data.ts | 4 +- packages/sdk/src/base/types/table.ts | 13 + .../sdk/src/{ => base}/types/undo_redo.ts | 2 +- packages/sdk/src/{ => base}/types/view.ts | 10 +- packages/sdk/src/{ => base}/types/viewport.ts | 0 .../sdk/src/{ => base}/ui/base_provider.tsx | 2 +- .../sdk/src/{ => base}/ui/baymax_utils.ts | 4 +- .../sdk/src/{ => base}/ui/block_wrapper.tsx | 10 +- packages/sdk/src/{ => base}/ui/box.tsx | 0 packages/sdk/src/{ => base}/ui/button.tsx | 8 +- .../sdk/src/{ => base}/ui/cell_renderer.tsx | 17 +- .../sdk/src/{ => base}/ui/choice_token.tsx | 4 +- .../src/{ => base}/ui/collaborator_token.tsx | 6 +- .../sdk/src/{ => base}/ui/color_palette.tsx | 6 +- .../{ => base}/ui/color_palette_synced.tsx | 8 +- .../src/{ => base}/ui/confirmation_dialog.tsx | 0 .../sdk/src/{ => base}/ui/control_sizes.ts | 5 +- .../ui/create_detect_element_resize.d.ts | 0 .../ui/create_detect_element_resize.js | 0 packages/sdk/src/{ => base}/ui/css_helpers.ts | 0 packages/sdk/src/{ => base}/ui/dialog.tsx | 0 .../src/{ => base}/ui/dialog_close_button.tsx | 0 .../sdk/src/{ => base}/ui/expand_record.ts | 0 .../src/{ => base}/ui/expand_record_list.ts | 2 +- .../ui/expand_record_picker_async.ts | 2 +- packages/sdk/src/{ => base}/ui/field_icon.tsx | 5 +- .../sdk/src/{ => base}/ui/field_picker.tsx | 13 +- .../src/{ => base}/ui/field_picker_synced.tsx | 13 +- packages/sdk/src/{ => base}/ui/form_field.tsx | 4 +- .../src/{ => base}/ui/geometry/geometry.ts | 0 .../sdk/src/{ => base}/ui/geometry/point.ts | 0 .../sdk/src/{ => base}/ui/geometry/rect.ts | 0 .../sdk/src/{ => base}/ui/geometry/size.ts | 0 .../sdk/src/{ => base}/ui/global_alert.tsx | 6 +- packages/sdk/src/{ => base}/ui/heading.tsx | 18 +- packages/sdk/src/{ => base}/ui/icon.tsx | 4 +- packages/sdk/src/{ => base}/ui/icon_config.ts | 3 +- .../src/{ => base}/ui/initialize_block.tsx | 4 +- packages/sdk/src/{ => base}/ui/input.tsx | 5 +- .../sdk/src/{ => base}/ui/input_synced.tsx | 8 +- packages/sdk/src/{ => base}/ui/key_codes.ts | 0 packages/sdk/src/{ => base}/ui/label.tsx | 2 +- packages/sdk/src/{ => base}/ui/link.tsx | 8 +- packages/sdk/src/{ => base}/ui/loader.tsx | 35 +- packages/sdk/src/{ => base}/ui/modal.tsx | 2 +- .../src/{ => base}/ui/model_picker_select.tsx | 4 +- packages/sdk/src/{ => base}/ui/popover.tsx | 4 +- .../sdk/src/{ => base}/ui/progress_bar.tsx | 6 +- .../sdk/src/{ => base}/ui/record_card.tsx | 36 +- .../src/{ => base}/ui/record_card_list.tsx | 10 +- packages/sdk/src/{ => base}/ui/select.tsx | 4 +- .../ui/select_and_select_buttons_helpers.ts | 0 .../sdk/src/{ => base}/ui/select_buttons.tsx | 6 +- .../{ => base}/ui/select_buttons_synced.tsx | 8 +- .../sdk/src/{ => base}/ui/select_synced.tsx | 8 +- packages/sdk/src/{ => base}/ui/switch.tsx | 5 +- .../sdk/src/{ => base}/ui/switch_synced.tsx | 6 +- packages/sdk/src/{ => base}/ui/synced.ts | 35 +- .../{ => base}/ui/system/all_styles_set.ts | 0 .../ui/system/appearance/appearance_set.ts | 0 .../ui/system/appearance/background_color.ts | 0 .../{ => base}/ui/system/appearance/border.ts | 0 .../ui/system/appearance/border_radius.ts | 0 .../ui/system/appearance/box_shadow.ts | 0 .../ui/system/appearance/opacity.ts | 0 .../ui/system/dimensions/dimensions_set.ts | 0 .../{ => base}/ui/system/dimensions/height.ts | 0 .../ui/system/dimensions/max_height.ts | 0 .../ui/system/dimensions/max_width.ts | 0 .../ui/system/dimensions/min_height.ts | 0 .../ui/system/dimensions/min_width.ts | 0 .../{ => base}/ui/system/dimensions/width.ts | 0 .../sdk/src/{ => base}/ui/system/display.ts | 0 .../ui/system/flex_container/align_content.ts | 0 .../ui/system/flex_container/align_items.ts | 0 .../flex_container/flex_container_set.ts | 0 .../system/flex_container/flex_direction.ts | 0 .../ui/system/flex_container/flex_wrap.ts | 0 .../system/flex_container/justify_content.ts | 0 .../ui/system/flex_container/justify_items.ts | 0 .../ui/system/flex_item/align_self.ts | 0 .../{ => base}/ui/system/flex_item/flex.ts | 0 .../ui/system/flex_item/flex_basis.ts | 0 .../ui/system/flex_item/flex_grow.ts | 0 .../ui/system/flex_item/flex_item_set.ts | 0 .../ui/system/flex_item/flex_shrink.ts | 0 .../ui/system/flex_item/justify_self.ts | 0 .../{ => base}/ui/system/flex_item/order.ts | 0 .../sdk/src/{ => base}/ui/system/index.ts | 0 .../sdk/src/{ => base}/ui/system/overflow.ts | 0 .../{ => base}/ui/system/position/bottom.ts | 0 .../src/{ => base}/ui/system/position/left.ts | 0 .../{ => base}/ui/system/position/position.ts | 0 .../ui/system/position/position_set.ts | 0 .../{ => base}/ui/system/position/right.ts | 0 .../src/{ => base}/ui/system/position/top.ts | 0 .../{ => base}/ui/system/position/z_index.ts | 0 .../{ => base}/ui/system/spacing/margin.ts | 0 .../{ => base}/ui/system/spacing/padding.ts | 0 .../ui/system/spacing/spacing_set.ts | 0 .../ui/system/typography/font_family.ts | 0 .../ui/system/typography/font_size.ts | 0 .../ui/system/typography/font_style.ts | 0 .../ui/system/typography/font_weight.ts | 0 .../ui/system/typography/letter_spacing.ts | 0 .../ui/system/typography/line_height.ts | 0 .../ui/system/typography/text_align.ts | 0 .../ui/system/typography/text_color.ts | 0 .../ui/system/typography/text_decoration.ts | 0 .../ui/system/typography/text_transform.ts | 0 .../ui/system/typography/typography_set.ts | 0 .../utils/create_responsive_prop_type.ts | 0 .../system/utils/create_style_prop_types.ts | 2 +- .../src/{ => base}/ui/system/utils/csstype.ts | 0 .../utils/ensure_numbers_are_within_scale.ts | 2 +- .../ui/system/utils/enum_prop_type_utils.ts | 29 + .../get_style_props_for_responsive_prop.ts | 6 +- .../src/{ => base}/ui/system/utils/types.ts | 0 .../sdk/src/{ => base}/ui/table_picker.tsx | 9 +- .../src/{ => base}/ui/table_picker_synced.tsx | 13 +- packages/sdk/src/{ => base}/ui/text.tsx | 13 +- .../sdk/src/{ => base}/ui/text_button.tsx | 5 +- .../ui/theme/default_theme/button_variants.ts | 0 .../ui/theme/default_theme/control_sizes.ts | 0 .../ui/theme/default_theme/heading_styles.ts | 2 +- .../ui/theme/default_theme/index.ts | 0 .../ui/theme/default_theme/input_variants.ts | 0 .../ui/theme/default_theme/link_variants.ts | 0 .../default_theme/select_buttons_variants.ts | 0 .../ui/theme/default_theme/select_variants.ts | 0 .../ui/theme/default_theme/switch_variants.ts | 0 .../default_theme/text_button_variants.ts | 0 .../ui/theme/default_theme/text_styles.ts | 0 .../ui/theme/default_theme/tokens.ts | 4 +- .../src/{ => base}/ui/theme/theme_context.ts | 2 +- .../sdk/src/{ => base}/ui/theme/use_theme.ts | 0 packages/sdk/src/{ => base}/ui/tooltip.tsx | 2 +- .../sdk/src/{ => base}/ui/types/aria_props.ts | 0 .../ui/types/data_attributes_prop.ts | 0 .../ui/types/tooltip_anchor_props.ts | 0 packages/sdk/src/{ => base}/ui/ui.ts | 16 +- .../{ => base}/ui/unstable_standalone_ui.ts | 6 +- packages/sdk/src/{ => base}/ui/use_base.ts | 14 +- packages/sdk/src/{ => base}/ui/use_cursor.ts | 7 +- .../sdk/src/{ => base}/ui/use_form_field.ts | 0 .../{ => base}/ui/use_record_action_data.ts | 9 +- packages/sdk/src/{ => base}/ui/use_records.ts | 18 +- packages/sdk/src/{ => base}/ui/use_session.ts | 14 +- .../src/{ => base}/ui/use_settings_button.ts | 9 +- .../src/{ => base}/ui/use_styled_system.ts | 0 .../ui/use_text_color_for_background_color.ts | 2 +- .../src/{ => base}/ui/use_view_metadata.ts | 4 +- .../sdk/src/{ => base}/ui/use_viewport.ts | 7 +- .../sdk/src/{ => base}/ui/view_picker.tsx | 11 +- .../src/{ => base}/ui/view_picker_synced.tsx | 13 +- .../src/{ => base}/ui/viewport_constraint.tsx | 7 +- .../src/{ => base}/ui/with_styled_system.tsx | 2 +- packages/sdk/src/{ => base}/undo_redo.ts | 4 +- .../src/{ => base}/unstable_testing_utils.ts | 13 +- packages/sdk/src/{ => base}/viewport.ts | 12 +- .../sdk/src/injected/airtable_interface.ts | 14 +- packages/sdk/src/interface/index.ts | 25 + packages/sdk/src/interface/models/base.ts | 32 + packages/sdk/src/interface/models/field.ts | 23 + .../sdk/src/interface/models/mutations.ts | 40 + packages/sdk/src/interface/models/record.ts | 25 + .../sdk/src/interface/models/record_store.ts | 57 ++ packages/sdk/src/interface/models/session.ts | 23 + packages/sdk/src/interface/models/table.ts | 28 + packages/sdk/src/interface/sdk.ts | 84 ++ .../src/interface/types/airtable_interface.ts | 77 ++ packages/sdk/src/interface/types/base.ts | 5 + packages/sdk/src/interface/types/mutations.ts | 20 + packages/sdk/src/interface/types/record.ts | 4 + packages/sdk/src/interface/types/table.ts | 10 + .../sdk/src/interface/ui/block_wrapper.tsx | 72 ++ .../sdk/src/interface/ui/expand_record.ts | 21 + .../sdk/src/interface/ui/initialize_block.tsx | 92 +++ packages/sdk/src/interface/ui/ui.ts | 16 + packages/sdk/src/interface/ui/use_base.ts | 38 + .../src/interface/ui/use_custom_properties.ts | 180 ++++ packages/sdk/src/interface/ui/use_records.ts | 50 ++ packages/sdk/src/interface/ui/use_run_info.ts | 37 + packages/sdk/src/interface/ui/use_session.ts | 35 + packages/sdk/src/sdk_mode.ts | 91 +++ packages/sdk/src/{ => shared}/color_utils.ts | 0 packages/sdk/src/{ => shared}/colors.ts | 0 packages/sdk/src/{ => shared}/error_utils.ts | 0 .../sdk/src/{ => shared}/event_tracker.ts | 2 +- .../sdk/src/{ => shared}/global_config.ts | 17 +- .../src/{ => shared}/models/abstract_model.ts | 17 +- .../base.ts => shared/models/base_core.ts} | 325 ++------ packages/sdk/src/shared/models/field_core.ts | 279 +++++++ .../sdk/src/shared/models/mutations_core.ts | 137 ++++ packages/sdk/src/shared/models/record_core.ts | 222 +++++ .../src/shared/models/record_store_core.ts | 190 +++++ .../sdk/src/shared/models/session_core.ts | 163 ++++ packages/sdk/src/shared/models/table_core.ts | 377 +++++++++ .../sdk/src/{ => shared}/private_utils.ts | 34 +- packages/sdk/src/shared/sdk_core.ts | 121 +++ .../shared/types/airtable_interface_core.ts | 112 +++ .../sdk/src/{ => shared}/types/attachment.ts | 3 +- .../base.ts => shared/types/base_core.ts} | 18 +- .../src/{ => shared}/types/collaborator.ts | 4 +- packages/sdk/src/{ => shared}/types/field.ts | 5 +- .../src/{ => shared}/types/global_config.ts | 0 packages/sdk/src/shared/types/hyper_ids.ts | 18 + .../types}/mutation_constants.ts | 0 .../sdk/src/shared/types/mutations_core.ts | 61 ++ .../{ => shared}/types/permission_levels.ts | 0 packages/sdk/src/{ => shared}/types/record.ts | 8 +- packages/sdk/src/{ => shared}/types/stat.ts | 0 .../table.ts => shared/types/table_core.ts} | 13 +- .../global_config_synced_component_helpers.ts | 7 +- packages/sdk/src/shared/ui/loader.tsx | 67 ++ .../sdk/src/{ => shared}/ui/remote_utils.ts | 0 .../sdk/src/{ => shared}/ui/sdk_context.ts | 6 +- .../src/{ => shared}/ui/use_array_identity.ts | 0 packages/sdk/src/shared/ui/use_base.ts | 15 + .../src/{ => shared}/ui/use_global_config.ts | 0 .../sdk/src/{ => shared}/ui/use_loadable.ts | 0 packages/sdk/src/shared/ui/use_session.ts | 13 + .../sdk/src/{ => shared}/ui/use_synced.ts | 0 .../sdk/src/{ => shared}/ui/use_watchable.ts | 0 .../sdk/src/{ => shared}/ui/with_hooks.tsx | 0 .../{ => shared}/unstable_private_utils.ts | 2 +- packages/sdk/src/{ => shared}/warning.ts | 7 +- packages/sdk/src/{ => shared}/watchable.ts | 0 .../abstract_mock_airtable_interface.ts | 38 +- packages/sdk/src/types/block.ts | 2 - packages/sdk/stories/all_controls.stories.tsx | 135 +-- .../sdk/stories/box.appearance.stories.tsx | 79 -- .../sdk/stories/box.dimensions.stories.tsx | 107 --- .../stories/box.flex_container.stories.tsx | 78 -- .../sdk/stories/box.flex_item.stories.tsx | 59 -- packages/sdk/stories/box.position.stories.tsx | 75 -- packages/sdk/stories/box.spacing.stories.tsx | 170 ---- packages/sdk/stories/box.stories.tsx | 255 ------ .../sdk/stories/box.typography.stories.tsx | 65 -- .../sdk/stories/box/appearance.stories.tsx | 86 ++ packages/sdk/stories/box/box.stories.tsx | 282 +++++++ .../sdk/stories/box/dimensions.stories.tsx | 120 +++ .../stories/box/flex_container.stories.tsx | 91 +++ .../sdk/stories/box/flex_item.stories.tsx | 72 ++ packages/sdk/stories/box/position.stories.tsx | 88 ++ packages/sdk/stories/box/spacing.stories.tsx | 197 +++++ .../sdk/stories/box/typography.stories.tsx | 84 ++ packages/sdk/stories/button.stories.tsx | 393 +++++---- .../sdk/stories/cell_renderer.stories.tsx | 20 +- packages/sdk/stories/choice_token.stories.tsx | 11 +- .../stories/collaborator_token.stories.tsx | 13 +- .../sdk/stories/color_palette.stories.tsx | 17 +- .../stories/confirmation_dialog.stories.tsx | 17 +- packages/sdk/stories/dialog.stories.tsx | 21 +- packages/sdk/stories/field_icon.stories.tsx | 20 +- packages/sdk/stories/field_picker.stories.tsx | 139 ++++ packages/sdk/stories/form_field.stories.tsx | 337 ++++---- packages/sdk/stories/heading.size.stories.tsx | 125 +-- packages/sdk/stories/heading.stories.tsx | 199 +++-- .../stories/helpers/categorize_style_props.ts | 4 +- packages/sdk/stories/helpers/code_utils.ts | 2 +- packages/sdk/stories/helpers/example.tsx | 18 +- .../stories/helpers/example_code_panel.tsx | 2 +- .../stories/helpers/fake_cell_renderer.tsx | 14 +- .../stories/helpers/fake_foreign_record.tsx | 2 +- .../sdk/stories/helpers/fake_record_card.tsx | 12 +- packages/sdk/stories/helpers/field_type.ts | 6 +- .../sdk/stories/helpers/style_prop_list.tsx | 8 +- packages/sdk/stories/icon.stories.tsx | 64 +- packages/sdk/stories/icon_example.tsx | 16 +- packages/sdk/stories/input.stories.tsx | 218 ++--- packages/sdk/stories/label.stories.tsx | 205 ++--- packages/sdk/stories/link.stories.tsx | 337 ++++---- packages/sdk/stories/loader.stories.tsx | 13 +- .../sdk/stories/model_pickers.stories.tsx | 328 -------- packages/sdk/stories/progress_bar.stories.tsx | 14 +- packages/sdk/stories/record_card.stories.tsx | 16 +- .../sdk/stories/record_card_list.stories.tsx | 18 +- packages/sdk/stories/select.stories.tsx | 172 ++-- .../sdk/stories/select_buttons.stories.tsx | 373 +++++---- packages/sdk/stories/switch.stories.tsx | 455 ++++++----- packages/sdk/stories/table_picker.stories.tsx | 136 +++ packages/sdk/stories/text.size.stories.tsx | 109 +-- packages/sdk/stories/text.stories.tsx | 243 +++--- packages/sdk/stories/text_button.stories.tsx | 313 +++---- packages/sdk/stories/tooltip.stories.tsx | 15 +- packages/sdk/stories/view_picker.stories.tsx | 139 ++++ .../airtable_interface_mocks/fixture_data.ts | 17 +- .../linked_records.ts | 4 +- .../mock_airtable_interface.ts | 17 +- .../project_tracker.tsx | 4 +- packages/sdk/test/error_utils.test.ts | 2 +- ...et_style_props_for_responsive_prop.test.ts | 2 +- packages/sdk/test/index.test.ts | 16 +- packages/sdk/test/models/base.test.ts | 10 +- packages/sdk/test/models/cursor.test.ts | 4 +- packages/sdk/test/models/field.test.ts | 8 +- .../linked_records_query_result.test.ts | 8 +- packages/sdk/test/models/mutations.test.ts | 19 +- packages/sdk/test/models/object_pool.test.ts | 2 +- packages/sdk/test/models/record.test.ts | 18 +- packages/sdk/test/models/session.test.ts | 8 +- packages/sdk/test/models/table.test.ts | 19 +- .../sdk/test/models/table_mutations.test.ts | 10 +- .../models/table_or_view_query_result.test.ts | 18 +- packages/sdk/test/models/view.test.ts | 18 +- .../models/view_metadata_query_result.test.ts | 10 +- packages/sdk/test/private_utils.test.ts | 2 +- packages/sdk/test/sdk.test.ts | 16 +- packages/sdk/test/test_helpers.ts | 2 +- packages/sdk/test/ui/base_provider.test.tsx | 4 +- packages/sdk/test/ui/block_wrapper.test.tsx | 6 +- packages/sdk/test/ui/box.test.tsx | 2 +- packages/sdk/test/ui/button.test.tsx | 2 +- packages/sdk/test/ui/cell_renderer.test.tsx | 6 +- packages/sdk/test/ui/choice_token.test.tsx | 2 +- .../sdk/test/ui/collaborator_token.test.tsx | 2 +- packages/sdk/test/ui/color_palette.test.tsx | 2 +- .../sdk/test/ui/color_palette_synced.test.tsx | 6 +- .../sdk/test/ui/confirmation_dialog.test.tsx | 2 +- packages/sdk/test/ui/dialog.test.tsx | 2 +- packages/sdk/test/ui/expand_record.test.tsx | 6 +- .../sdk/test/ui/expand_record_list.test.tsx | 6 +- .../ui/expand_record_picker_async.test.tsx | 8 +- packages/sdk/test/ui/field_icon.test.tsx | 6 +- packages/sdk/test/ui/field_picker.test.tsx | 6 +- .../sdk/test/ui/field_picker_synced.test.tsx | 6 +- packages/sdk/test/ui/form_field.test.tsx | 2 +- packages/sdk/test/ui/global_alert.test.tsx | 6 +- packages/sdk/test/ui/heading.test.tsx | 2 +- packages/sdk/test/ui/icon.test.tsx | 2 +- .../sdk/test/ui/initialize_block.test.tsx | 4 +- packages/sdk/test/ui/input.test.tsx | 2 +- packages/sdk/test/ui/input_synced.test.tsx | 6 +- packages/sdk/test/ui/label.test.tsx | 2 +- packages/sdk/test/ui/link.test.tsx | 2 +- packages/sdk/test/ui/loader.test.tsx | 2 +- packages/sdk/test/ui/modal.test.tsx | 2 +- packages/sdk/test/ui/popover.test.tsx | 2 +- packages/sdk/test/ui/progress_bar.test.tsx | 2 +- packages/sdk/test/ui/record_card.test.tsx | 6 +- .../sdk/test/ui/record_card_list.test.tsx | 6 +- packages/sdk/test/ui/remote_utils.test.ts | 2 +- packages/sdk/test/ui/select.test.tsx | 2 +- packages/sdk/test/ui/select_buttons.test.tsx | 2 +- .../test/ui/select_buttons_synced.test.tsx | 6 +- packages/sdk/test/ui/select_synced.test.tsx | 6 +- packages/sdk/test/ui/switch.test.tsx | 2 +- packages/sdk/test/ui/switch_synced.test.tsx | 6 +- packages/sdk/test/ui/synced.test.tsx | 8 +- packages/sdk/test/ui/table_picker.test.tsx | 6 +- .../sdk/test/ui/table_picker_synced.test.tsx | 6 +- packages/sdk/test/ui/text.test.tsx | 2 +- packages/sdk/test/ui/text_button.test.tsx | 2 +- packages/sdk/test/ui/tooltip.test.tsx | 2 +- packages/sdk/test/ui/ui.test.tsx | 4 +- .../test/ui/unstable_standalone_ui.test.tsx | 2 +- .../sdk/test/ui/use_array_identity.test.tsx | 2 +- packages/sdk/test/ui/use_loadable.test.tsx | 6 +- .../test/ui/use_record_action_data.test.tsx | 8 +- packages/sdk/test/ui/use_records.test.tsx | 10 +- .../sdk/test/ui/use_view_metadata.test.tsx | 8 +- packages/sdk/test/ui/use_watchable.test.tsx | 4 +- packages/sdk/test/ui/view_picker.test.tsx | 6 +- .../sdk/test/ui/view_picker_synced.test.tsx | 6 +- .../sdk/test/ui/viewport_constraint.test.tsx | 6 +- .../sdk/test/unstable_private_utils.test.ts | 2 +- packages/sdk/types.d.ts | 33 +- packages/sdk/ui.d.ts | 2 +- packages/sdk/ui.js | 2 +- packages/sdk/unstable_private_utils.d.ts | 2 +- packages/sdk/unstable_private_utils.js | 2 +- packages/sdk/unstable_standalone_ui.d.ts | 2 +- packages/sdk/unstable_standalone_ui.js | 2 +- packages/sdk/unstable_testing_utils.d.ts | 2 +- packages/sdk/unstable_testing_utils.js | 2 +- yarn.lock | 19 +- 418 files changed, 8266 insertions(+), 5955 deletions(-) delete mode 100644 packages/sdk/.storybook/config.js create mode 100644 packages/sdk/.storybook/main.js create mode 100644 packages/sdk/.storybook/manager.js rename packages/sdk/src/{ => base}/index.ts (81%) rename packages/sdk/src/{ => base}/models/abstract_model_with_async_data.ts (94%) create mode 100644 packages/sdk/src/base/models/base.ts rename packages/sdk/src/{ => base}/models/create_aggregators.ts (98%) rename packages/sdk/src/{ => base}/models/cursor.ts (97%) rename packages/sdk/src/{ => base}/models/field.ts (58%) rename packages/sdk/src/{ => base}/models/grouped_record_query_result.ts (97%) rename packages/sdk/src/{ => base}/models/linked_records_query_result.ts (98%) rename packages/sdk/src/{ => base}/models/models.ts (95%) rename packages/sdk/src/{ => base}/models/mutations.ts (84%) rename packages/sdk/src/{ => base}/models/object_pool.ts (97%) rename packages/sdk/src/{ => base}/models/record.ts (60%) rename packages/sdk/src/{ => base}/models/record_coloring.ts (98%) rename packages/sdk/src/{ => base}/models/record_query_result.ts (99%) rename packages/sdk/src/{ => base}/models/record_store.ts (74%) rename packages/sdk/src/{ => base}/models/session.ts (54%) rename packages/sdk/src/{ => base}/models/table.ts (84%) rename packages/sdk/src/{ => base}/models/table_or_view_query_result.ts (99%) rename packages/sdk/src/{ => base}/models/view.ts (96%) rename packages/sdk/src/{ => base}/models/view_data_store.ts (97%) rename packages/sdk/src/{ => base}/models/view_metadata_query_result.ts (97%) rename packages/sdk/src/{ => base}/perform_record_action.ts (98%) rename packages/sdk/src/{ => base}/sdk.ts (61%) rename packages/sdk/src/{ => base}/settings_button.ts (96%) rename packages/sdk/src/{ => base}/types/aggregators.ts (100%) rename packages/sdk/src/{ => base}/types/airtable_interface.ts (64%) rename packages/sdk/src/{ => base}/types/backend_fetch_types.ts (100%) create mode 100644 packages/sdk/src/base/types/base.ts rename packages/sdk/src/{ => base}/types/cursor.ts (56%) rename packages/sdk/src/{ => base}/types/mutations.ts (84%) create mode 100644 packages/sdk/src/base/types/record.ts rename packages/sdk/src/{ => base}/types/record_action_data.ts (90%) create mode 100644 packages/sdk/src/base/types/table.ts rename packages/sdk/src/{ => base}/types/undo_redo.ts (76%) rename packages/sdk/src/{ => base}/types/view.ts (89%) rename packages/sdk/src/{ => base}/types/viewport.ts (100%) rename packages/sdk/src/{ => base}/ui/base_provider.tsx (95%) rename packages/sdk/src/{ => base}/ui/baymax_utils.ts (99%) rename packages/sdk/src/{ => base}/ui/block_wrapper.tsx (95%) rename packages/sdk/src/{ => base}/ui/box.tsx (100%) rename packages/sdk/src/{ => base}/ui/button.tsx (96%) rename packages/sdk/src/{ => base}/ui/cell_renderer.tsx (95%) rename packages/sdk/src/{ => base}/ui/choice_token.tsx (96%) rename packages/sdk/src/{ => base}/ui/collaborator_token.tsx (95%) rename packages/sdk/src/{ => base}/ui/color_palette.tsx (98%) rename packages/sdk/src/{ => base}/ui/color_palette_synced.tsx (92%) rename packages/sdk/src/{ => base}/ui/confirmation_dialog.tsx (100%) rename packages/sdk/src/{ => base}/ui/control_sizes.ts (94%) rename packages/sdk/src/{ => base}/ui/create_detect_element_resize.d.ts (100%) rename packages/sdk/src/{ => base}/ui/create_detect_element_resize.js (100%) rename packages/sdk/src/{ => base}/ui/css_helpers.ts (100%) rename packages/sdk/src/{ => base}/ui/dialog.tsx (100%) rename packages/sdk/src/{ => base}/ui/dialog_close_button.tsx (100%) rename packages/sdk/src/{ => base}/ui/expand_record.ts (100%) rename packages/sdk/src/{ => base}/ui/expand_record_list.ts (97%) rename packages/sdk/src/{ => base}/ui/expand_record_picker_async.ts (98%) rename packages/sdk/src/{ => base}/ui/field_icon.tsx (90%) rename packages/sdk/src/{ => base}/ui/field_picker.tsx (91%) rename packages/sdk/src/{ => base}/ui/field_picker_synced.tsx (85%) rename packages/sdk/src/{ => base}/ui/form_field.tsx (97%) rename packages/sdk/src/{ => base}/ui/geometry/geometry.ts (100%) rename packages/sdk/src/{ => base}/ui/geometry/point.ts (100%) rename packages/sdk/src/{ => base}/ui/geometry/rect.ts (100%) rename packages/sdk/src/{ => base}/ui/geometry/size.ts (100%) rename packages/sdk/src/{ => base}/ui/global_alert.tsx (90%) rename packages/sdk/src/{ => base}/ui/heading.tsx (96%) rename packages/sdk/src/{ => base}/ui/icon.tsx (98%) rename packages/sdk/src/{ => base}/ui/icon_config.ts (99%) rename packages/sdk/src/{ => base}/ui/initialize_block.tsx (97%) rename packages/sdk/src/{ => base}/ui/input.tsx (97%) rename packages/sdk/src/{ => base}/ui/input_synced.tsx (89%) rename packages/sdk/src/{ => base}/ui/key_codes.ts (100%) rename packages/sdk/src/{ => base}/ui/label.tsx (98%) rename packages/sdk/src/{ => base}/ui/link.tsx (97%) rename packages/sdk/src/{ => base}/ui/loader.tsx (64%) rename packages/sdk/src/{ => base}/ui/modal.tsx (99%) rename packages/sdk/src/{ => base}/ui/model_picker_select.tsx (96%) rename packages/sdk/src/{ => base}/ui/popover.tsx (99%) rename packages/sdk/src/{ => base}/ui/progress_bar.tsx (96%) rename packages/sdk/src/{ => base}/ui/record_card.tsx (96%) rename packages/sdk/src/{ => base}/ui/record_card_list.tsx (98%) rename packages/sdk/src/{ => base}/ui/select.tsx (98%) rename packages/sdk/src/{ => base}/ui/select_and_select_buttons_helpers.ts (100%) rename packages/sdk/src/{ => base}/ui/select_buttons.tsx (97%) rename packages/sdk/src/{ => base}/ui/select_buttons_synced.tsx (90%) rename packages/sdk/src/{ => base}/ui/select_synced.tsx (89%) rename packages/sdk/src/{ => base}/ui/switch.tsx (97%) rename packages/sdk/src/{ => base}/ui/switch_synced.tsx (89%) rename packages/sdk/src/{ => base}/ui/synced.ts (58%) rename packages/sdk/src/{ => base}/ui/system/all_styles_set.ts (100%) rename packages/sdk/src/{ => base}/ui/system/appearance/appearance_set.ts (100%) rename packages/sdk/src/{ => base}/ui/system/appearance/background_color.ts (100%) rename packages/sdk/src/{ => base}/ui/system/appearance/border.ts (100%) rename packages/sdk/src/{ => base}/ui/system/appearance/border_radius.ts (100%) rename packages/sdk/src/{ => base}/ui/system/appearance/box_shadow.ts (100%) rename packages/sdk/src/{ => base}/ui/system/appearance/opacity.ts (100%) rename packages/sdk/src/{ => base}/ui/system/dimensions/dimensions_set.ts (100%) rename packages/sdk/src/{ => base}/ui/system/dimensions/height.ts (100%) rename packages/sdk/src/{ => base}/ui/system/dimensions/max_height.ts (100%) rename packages/sdk/src/{ => base}/ui/system/dimensions/max_width.ts (100%) rename packages/sdk/src/{ => base}/ui/system/dimensions/min_height.ts (100%) rename packages/sdk/src/{ => base}/ui/system/dimensions/min_width.ts (100%) rename packages/sdk/src/{ => base}/ui/system/dimensions/width.ts (100%) rename packages/sdk/src/{ => base}/ui/system/display.ts (100%) rename packages/sdk/src/{ => base}/ui/system/flex_container/align_content.ts (100%) rename packages/sdk/src/{ => base}/ui/system/flex_container/align_items.ts (100%) rename packages/sdk/src/{ => base}/ui/system/flex_container/flex_container_set.ts (100%) rename packages/sdk/src/{ => base}/ui/system/flex_container/flex_direction.ts (100%) rename packages/sdk/src/{ => base}/ui/system/flex_container/flex_wrap.ts (100%) rename packages/sdk/src/{ => base}/ui/system/flex_container/justify_content.ts (100%) rename packages/sdk/src/{ => base}/ui/system/flex_container/justify_items.ts (100%) rename packages/sdk/src/{ => base}/ui/system/flex_item/align_self.ts (100%) rename packages/sdk/src/{ => base}/ui/system/flex_item/flex.ts (100%) rename packages/sdk/src/{ => base}/ui/system/flex_item/flex_basis.ts (100%) rename packages/sdk/src/{ => base}/ui/system/flex_item/flex_grow.ts (100%) rename packages/sdk/src/{ => base}/ui/system/flex_item/flex_item_set.ts (100%) rename packages/sdk/src/{ => base}/ui/system/flex_item/flex_shrink.ts (100%) rename packages/sdk/src/{ => base}/ui/system/flex_item/justify_self.ts (100%) rename packages/sdk/src/{ => base}/ui/system/flex_item/order.ts (100%) rename packages/sdk/src/{ => base}/ui/system/index.ts (100%) rename packages/sdk/src/{ => base}/ui/system/overflow.ts (100%) rename packages/sdk/src/{ => base}/ui/system/position/bottom.ts (100%) rename packages/sdk/src/{ => base}/ui/system/position/left.ts (100%) rename packages/sdk/src/{ => base}/ui/system/position/position.ts (100%) rename packages/sdk/src/{ => base}/ui/system/position/position_set.ts (100%) rename packages/sdk/src/{ => base}/ui/system/position/right.ts (100%) rename packages/sdk/src/{ => base}/ui/system/position/top.ts (100%) rename packages/sdk/src/{ => base}/ui/system/position/z_index.ts (100%) rename packages/sdk/src/{ => base}/ui/system/spacing/margin.ts (100%) rename packages/sdk/src/{ => base}/ui/system/spacing/padding.ts (100%) rename packages/sdk/src/{ => base}/ui/system/spacing/spacing_set.ts (100%) rename packages/sdk/src/{ => base}/ui/system/typography/font_family.ts (100%) rename packages/sdk/src/{ => base}/ui/system/typography/font_size.ts (100%) rename packages/sdk/src/{ => base}/ui/system/typography/font_style.ts (100%) rename packages/sdk/src/{ => base}/ui/system/typography/font_weight.ts (100%) rename packages/sdk/src/{ => base}/ui/system/typography/letter_spacing.ts (100%) rename packages/sdk/src/{ => base}/ui/system/typography/line_height.ts (100%) rename packages/sdk/src/{ => base}/ui/system/typography/text_align.ts (100%) rename packages/sdk/src/{ => base}/ui/system/typography/text_color.ts (100%) rename packages/sdk/src/{ => base}/ui/system/typography/text_decoration.ts (100%) rename packages/sdk/src/{ => base}/ui/system/typography/text_transform.ts (100%) rename packages/sdk/src/{ => base}/ui/system/typography/typography_set.ts (100%) rename packages/sdk/src/{ => base}/ui/system/utils/create_responsive_prop_type.ts (100%) rename packages/sdk/src/{ => base}/ui/system/utils/create_style_prop_types.ts (90%) rename packages/sdk/src/{ => base}/ui/system/utils/csstype.ts (100%) rename packages/sdk/src/{ => base}/ui/system/utils/ensure_numbers_are_within_scale.ts (96%) create mode 100644 packages/sdk/src/base/ui/system/utils/enum_prop_type_utils.ts rename packages/sdk/src/{ => base}/ui/system/utils/get_style_props_for_responsive_prop.ts (93%) rename packages/sdk/src/{ => base}/ui/system/utils/types.ts (100%) rename packages/sdk/src/{ => base}/ui/table_picker.tsx (91%) rename packages/sdk/src/{ => base}/ui/table_picker_synced.tsx (85%) rename packages/sdk/src/{ => base}/ui/text.tsx (96%) rename packages/sdk/src/{ => base}/ui/text_button.tsx (98%) rename packages/sdk/src/{ => base}/ui/theme/default_theme/button_variants.ts (100%) rename packages/sdk/src/{ => base}/ui/theme/default_theme/control_sizes.ts (100%) rename packages/sdk/src/{ => base}/ui/theme/default_theme/heading_styles.ts (97%) rename packages/sdk/src/{ => base}/ui/theme/default_theme/index.ts (100%) rename packages/sdk/src/{ => base}/ui/theme/default_theme/input_variants.ts (100%) rename packages/sdk/src/{ => base}/ui/theme/default_theme/link_variants.ts (100%) rename packages/sdk/src/{ => base}/ui/theme/default_theme/select_buttons_variants.ts (100%) rename packages/sdk/src/{ => base}/ui/theme/default_theme/select_variants.ts (100%) rename packages/sdk/src/{ => base}/ui/theme/default_theme/switch_variants.ts (100%) rename packages/sdk/src/{ => base}/ui/theme/default_theme/text_button_variants.ts (100%) rename packages/sdk/src/{ => base}/ui/theme/default_theme/text_styles.ts (100%) rename packages/sdk/src/{ => base}/ui/theme/default_theme/tokens.ts (97%) rename packages/sdk/src/{ => base}/ui/theme/theme_context.ts (80%) rename packages/sdk/src/{ => base}/ui/theme/use_theme.ts (100%) rename packages/sdk/src/{ => base}/ui/tooltip.tsx (99%) rename packages/sdk/src/{ => base}/ui/types/aria_props.ts (100%) rename packages/sdk/src/{ => base}/ui/types/data_attributes_prop.ts (100%) rename packages/sdk/src/{ => base}/ui/types/tooltip_anchor_props.ts (100%) rename packages/sdk/src/{ => base}/ui/ui.ts (81%) rename packages/sdk/src/{ => base}/ui/unstable_standalone_ui.ts (89%) rename packages/sdk/src/{ => base}/ui/use_base.ts (79%) rename packages/sdk/src/{ => base}/ui/use_cursor.ts (86%) rename packages/sdk/src/{ => base}/ui/use_form_field.ts (100%) rename packages/sdk/src/{ => base}/ui/use_record_action_data.ts (90%) rename packages/sdk/src/{ => base}/ui/use_records.ts (95%) rename packages/sdk/src/{ => base}/ui/use_session.ts (78%) rename packages/sdk/src/{ => base}/ui/use_settings_button.ts (82%) rename packages/sdk/src/{ => base}/ui/use_styled_system.ts (100%) rename packages/sdk/src/{ => base}/ui/use_text_color_for_background_color.ts (85%) rename packages/sdk/src/{ => base}/ui/use_view_metadata.ts (93%) rename packages/sdk/src/{ => base}/ui/use_viewport.ts (83%) rename packages/sdk/src/{ => base}/ui/view_picker.tsx (92%) rename packages/sdk/src/{ => base}/ui/view_picker_synced.tsx (85%) rename packages/sdk/src/{ => base}/ui/viewport_constraint.tsx (96%) rename packages/sdk/src/{ => base}/ui/with_styled_system.tsx (98%) rename packages/sdk/src/{ => base}/undo_redo.ts (89%) rename packages/sdk/src/{ => base}/unstable_testing_utils.ts (57%) rename packages/sdk/src/{ => base}/viewport.ts (98%) create mode 100644 packages/sdk/src/interface/index.ts create mode 100644 packages/sdk/src/interface/models/base.ts create mode 100644 packages/sdk/src/interface/models/field.ts create mode 100644 packages/sdk/src/interface/models/mutations.ts create mode 100644 packages/sdk/src/interface/models/record.ts create mode 100644 packages/sdk/src/interface/models/record_store.ts create mode 100644 packages/sdk/src/interface/models/session.ts create mode 100644 packages/sdk/src/interface/models/table.ts create mode 100644 packages/sdk/src/interface/sdk.ts create mode 100644 packages/sdk/src/interface/types/airtable_interface.ts create mode 100644 packages/sdk/src/interface/types/base.ts create mode 100644 packages/sdk/src/interface/types/mutations.ts create mode 100644 packages/sdk/src/interface/types/record.ts create mode 100644 packages/sdk/src/interface/types/table.ts create mode 100644 packages/sdk/src/interface/ui/block_wrapper.tsx create mode 100644 packages/sdk/src/interface/ui/expand_record.ts create mode 100644 packages/sdk/src/interface/ui/initialize_block.tsx create mode 100644 packages/sdk/src/interface/ui/ui.ts create mode 100644 packages/sdk/src/interface/ui/use_base.ts create mode 100644 packages/sdk/src/interface/ui/use_custom_properties.ts create mode 100644 packages/sdk/src/interface/ui/use_records.ts create mode 100644 packages/sdk/src/interface/ui/use_run_info.ts create mode 100644 packages/sdk/src/interface/ui/use_session.ts create mode 100644 packages/sdk/src/sdk_mode.ts rename packages/sdk/src/{ => shared}/color_utils.ts (100%) rename packages/sdk/src/{ => shared}/colors.ts (100%) rename packages/sdk/src/{ => shared}/error_utils.ts (100%) rename packages/sdk/src/{ => shared}/event_tracker.ts (88%) rename packages/sdk/src/{ => shared}/global_config.ts (96%) rename packages/sdk/src/{ => shared}/models/abstract_model.ts (89%) rename packages/sdk/src/{models/base.ts => shared/models/base_core.ts} (58%) create mode 100644 packages/sdk/src/shared/models/field_core.ts create mode 100644 packages/sdk/src/shared/models/mutations_core.ts create mode 100644 packages/sdk/src/shared/models/record_core.ts create mode 100644 packages/sdk/src/shared/models/record_store_core.ts create mode 100644 packages/sdk/src/shared/models/session_core.ts create mode 100644 packages/sdk/src/shared/models/table_core.ts rename packages/sdk/src/{ => shared}/private_utils.ts (90%) create mode 100644 packages/sdk/src/shared/sdk_core.ts create mode 100644 packages/sdk/src/shared/types/airtable_interface_core.ts rename packages/sdk/src/{ => shared}/types/attachment.ts (90%) rename packages/sdk/src/{types/base.ts => shared/types/base_core.ts} (67%) rename packages/sdk/src/{ => shared}/types/collaborator.ts (93%) rename packages/sdk/src/{ => shared}/types/field.ts (99%) rename packages/sdk/src/{ => shared}/types/global_config.ts (100%) create mode 100644 packages/sdk/src/shared/types/hyper_ids.ts rename packages/sdk/src/{models => shared/types}/mutation_constants.ts (100%) create mode 100644 packages/sdk/src/shared/types/mutations_core.ts rename packages/sdk/src/{ => shared}/types/permission_levels.ts (100%) rename packages/sdk/src/{ => shared}/types/record.ts (68%) rename packages/sdk/src/{ => shared}/types/stat.ts (100%) rename packages/sdk/src/{types/table.ts => shared/types/table_core.ts} (63%) rename packages/sdk/src/{ => shared}/ui/global_config_synced_component_helpers.ts (78%) create mode 100644 packages/sdk/src/shared/ui/loader.tsx rename packages/sdk/src/{ => shared}/ui/remote_utils.ts (100%) rename packages/sdk/src/{ => shared}/ui/sdk_context.ts (61%) rename packages/sdk/src/{ => shared}/ui/use_array_identity.ts (100%) create mode 100644 packages/sdk/src/shared/ui/use_base.ts rename packages/sdk/src/{ => shared}/ui/use_global_config.ts (100%) rename packages/sdk/src/{ => shared}/ui/use_loadable.ts (100%) create mode 100644 packages/sdk/src/shared/ui/use_session.ts rename packages/sdk/src/{ => shared}/ui/use_synced.ts (100%) rename packages/sdk/src/{ => shared}/ui/use_watchable.ts (100%) rename packages/sdk/src/{ => shared}/ui/with_hooks.tsx (100%) rename packages/sdk/src/{ => shared}/unstable_private_utils.ts (71%) rename packages/sdk/src/{ => shared}/warning.ts (73%) rename packages/sdk/src/{ => shared}/watchable.ts (100%) delete mode 100644 packages/sdk/src/types/block.ts delete mode 100644 packages/sdk/stories/box.appearance.stories.tsx delete mode 100644 packages/sdk/stories/box.dimensions.stories.tsx delete mode 100644 packages/sdk/stories/box.flex_container.stories.tsx delete mode 100644 packages/sdk/stories/box.flex_item.stories.tsx delete mode 100644 packages/sdk/stories/box.position.stories.tsx delete mode 100644 packages/sdk/stories/box.spacing.stories.tsx delete mode 100644 packages/sdk/stories/box.stories.tsx delete mode 100644 packages/sdk/stories/box.typography.stories.tsx create mode 100644 packages/sdk/stories/box/appearance.stories.tsx create mode 100644 packages/sdk/stories/box/box.stories.tsx create mode 100644 packages/sdk/stories/box/dimensions.stories.tsx create mode 100644 packages/sdk/stories/box/flex_container.stories.tsx create mode 100644 packages/sdk/stories/box/flex_item.stories.tsx create mode 100644 packages/sdk/stories/box/position.stories.tsx create mode 100644 packages/sdk/stories/box/spacing.stories.tsx create mode 100644 packages/sdk/stories/box/typography.stories.tsx create mode 100644 packages/sdk/stories/field_picker.stories.tsx delete mode 100644 packages/sdk/stories/model_pickers.stories.tsx create mode 100644 packages/sdk/stories/table_picker.stories.tsx create mode 100644 packages/sdk/stories/view_picker.stories.tsx diff --git a/package.json b/package.json index e75c7556d..0b2aa42d1 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "http-cache-semantics": "^4.1.1", "parse-url": "^8.1.0", "psl": "^1.10.0", + "trim": "1.0.1", "typescript": "^5.4.5" }, "scripts": { diff --git a/packages/sdk/.storybook/config.js b/packages/sdk/.storybook/config.js deleted file mode 100644 index 556982b43..000000000 --- a/packages/sdk/.storybook/config.js +++ /dev/null @@ -1,8 +0,0 @@ -import {configure} from '@storybook/react'; - -const req = require.context('../stories', true, /\.stories\.[tj]sx?$/); -function loadStories() { - req.keys().forEach(filename => req(filename)); -} - -configure(loadStories, module); diff --git a/packages/sdk/.storybook/main.js b/packages/sdk/.storybook/main.js new file mode 100644 index 000000000..12adf5c05 --- /dev/null +++ b/packages/sdk/.storybook/main.js @@ -0,0 +1,29 @@ +import {join, dirname} from 'path'; + +/** + * This function is used to resolve the absolute path of a package. + * It is needed in projects that use Yarn PnP or are set up within a monorepo. + */ +function getAbsolutePath(value) { + return dirname(require.resolve(join(value, 'package.json'))); +} + +const config = { + stories: ['../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + + addons: [ + getAbsolutePath('@storybook/addon-actions'), + getAbsolutePath('@storybook/addon-links'), + getAbsolutePath('@storybook/addon-webpack5-compiler-babel'), + ], + + framework: { + name: getAbsolutePath('@storybook/react-webpack5'), + options: {}, + }, + + webpackFinal: async config => { + return config; + }, +}; +export default config; diff --git a/packages/sdk/.storybook/manager.js b/packages/sdk/.storybook/manager.js new file mode 100644 index 000000000..ed0afa3bb --- /dev/null +++ b/packages/sdk/.storybook/manager.js @@ -0,0 +1,9 @@ +import {addons} from '@storybook/manager-api'; +import {camelCase, upperFirst} from 'lodash'; + +addons.setConfig({ + sidebar: { + showRoots: false, + renderLabel: ({name, type}) => (type === 'story' ? name : upperFirst(camelCase(name))), + }, +}); diff --git a/packages/sdk/.storybook/webpack.config.js b/packages/sdk/.storybook/webpack.config.js index 6b31972ee..e69de29bb 100644 --- a/packages/sdk/.storybook/webpack.config.js +++ b/packages/sdk/.storybook/webpack.config.js @@ -1,8 +0,0 @@ -module.exports = ({config, mode}) => { - config.module.rules.push({ - test: /\.(ts|tsx)$/, - loader: require.resolve('babel-loader'), - }); - config.resolve.extensions.push('.ts', '.tsx'); - return config; -}; diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 71c4d84d2..12d820e35 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -8,13 +8,6 @@ To get started, check out the questions, feedback, or feature requests, we encourage you to post in the [community forum](https://community.airtable.com/c/developers/custom-blocks-beta/54). -This git repository contains a few related projects: - -- [The sdk itself](./packages/sdk) - this is what you use when building your extension! -- [A blocks testing framework](./packages/blocks-testing) - this is used to help test your - extension. -- [The new Blocks CLI](./packages/cli-next) - this is used to run your extension! - By using the software, you accept and agree to abide by terms of the developer agreement below: ## Developer agreement diff --git a/packages/sdk/index.d.ts b/packages/sdk/index.d.ts index 9fe8e1ff6..c3a112865 100644 --- a/packages/sdk/index.d.ts +++ b/packages/sdk/index.d.ts @@ -1,4 +1,4 @@ -import Sdk from './dist/types/src/sdk'; +import Sdk from './dist/types/src/base/sdk'; export const globalConfig: Sdk['globalConfig']; export const base: Sdk['base']; diff --git a/packages/sdk/models.d.ts b/packages/sdk/models.d.ts index af4f431c2..28c1a12fa 100644 --- a/packages/sdk/models.d.ts +++ b/packages/sdk/models.d.ts @@ -1 +1 @@ -export * from './dist/types/src/models/models'; +export * from './dist/types/src/base/models/models'; diff --git a/packages/sdk/models.js b/packages/sdk/models.js index 6916976e4..832abff19 100644 --- a/packages/sdk/models.js +++ b/packages/sdk/models.js @@ -1 +1 @@ -module.exports = require('./dist/cjs/models/models'); +module.exports = require('./dist/cjs/base/models/models'); diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 393f6cfdb..a3458b377 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -7,8 +7,40 @@ "url": "https://github.com/Airtable/blocks.git" }, "homepage": "https://airtable.com/developers/blocks", - "main": "dist/cjs/index.js", - "types": "index.d.ts", + "exports": { + "./unstable_private_utils": { + "types": "./dist/types/src/shared/unstable_private_utils.d.ts", + "default": "./dist/cjs/shared/unstable_private_utils.js" + }, + "./unstable_standalone_ui": { + "types": "./dist/types/src/base/ui/unstable_standalone_ui.d.ts", + "default": "./dist/cjs/base/ui/unstable_standalone_ui.js" + }, + "./unstable_testing_utils": { + "types": "./dist/types/src/base/unstable_testing_utils.d.ts", + "default": "./dist/cjs/base/unstable_testing_utils.js" + }, + "./types": { + "types": "./types.d.ts", + "default": "./types.js" + }, + "./base/models": { + "types": "./dist/types/src/base/models/models.d.ts", + "default": "./dist/cjs/base/models/models.js" + }, + "./base/ui": { + "types": "./dist/types/src/base/ui/ui.d.ts", + "default": "./dist/cjs/base/ui/ui.js" + }, + "./base": { + "types": "./dist/types/src/base/index.d.ts", + "default": "./dist/cjs/base/index.js" + }, + "./interface/ui": { + "types": "./dist/types/src/interface/ui/ui.d.ts", + "default": "./dist/cjs/interface/ui/ui.js" + } + }, "files": [ "dist", "types", @@ -46,14 +78,15 @@ "build:docs": "cd ../blocks-docs && yarn run build && cd ../sdk", "build": "yarn run build:clean && concurrently yarn:build:babel yarn:build:types", "watch": "yarn run build:clean && concurrently yarn:watch:babel yarn:watch:types", - "storybook": "start-storybook -p 6006", - "build-storybook": "build-storybook", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build", "deploy-storybook": "storybook-to-ghpages" }, "author": "", "license": "UNLICENSED", "devDependencies": { "@airtable-blocks-internal/changelog-publish": "^1.0.2", + "@airtable/eslint-plugin-blocks": "^1.0.4", "@babel/cli": "^7.7.5", "@babel/core": "^7.7.5", "@babel/plugin-proposal-class-properties": "^7.7.4", @@ -63,10 +96,12 @@ "@babel/preset-env": "^7.7.6", "@babel/preset-react": "^7.7.4", "@babel/preset-typescript": "^7.7.4", - "@storybook/addon-actions": "^5.2.8", - "@storybook/addon-links": "^5.2.8", - "@storybook/addons": "^5.2.8", - "@storybook/react": "^5.2.8", + "@storybook/addon-actions": "^8.4.7", + "@storybook/addon-links": "^8.4.7", + "@storybook/addon-webpack5-compiler-babel": "^3.0.5", + "@storybook/manager-api": "^8.4.7", + "@storybook/react": "^8.4.7", + "@storybook/react-webpack5": "^8.4.7", "@storybook/storybook-deployer": "^2.8.1", "@types/enzyme": "^3.10.4", "@types/enzyme-adapter-react-16": "^1.0.5", @@ -74,7 +109,8 @@ "@types/glob": "^7.1.1", "@types/hoist-non-react-statics": "^3.3.5", "@types/jest": "^24.0.23", - "@types/lodash.omit": "^4.5.6", + "@types/lodash.capitalize": "^4.2.9", + "@types/lodash.clamp": "^4.0.9", "@types/prettier": "^1.19.0", "@types/react-dom": "^16.9.24", "@types/react-window": "^1.8.8", @@ -92,15 +128,17 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsdoc": "^48.2.12", "eslint-plugin-react": "^7.34.2", - "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-hooks": "^5.2.0", "glob": "^7.1.6", "jest": "^24.9.0", + "lodash.capitalize": "^4.2.1", + "lodash.clamp": "^4.0.3", "prettier": "^1.19.1", "prism-react-renderer": "^1.0.2", + "storybook": "^8.4.7", "typescript": "^5.4.5" }, "dependencies": { - "@airtable/eslint-plugin-blocks": "^1.0.2", "@babel/runtime": "^7.7.6", "@styled-system/core": "^5.1.2", "@types/prop-types": "^15.7.12", @@ -110,7 +148,6 @@ "emotion": "^10.0.23", "fast-deep-equal": "^3.1.1", "hoist-non-react-statics": "^3.3.2", - "lodash.omit": "^4.5.0", "prop-types": "15.8.1", "react-window": "1.8.10", "use-subscription": "^1.3.0" @@ -143,7 +180,7 @@ }, "hooks": { "before:init": "../../bin/check-repo-for-release && yarn build && yarn test", - "after:bump": "yarn build", + "after:bump": "yarn build && rm -rf dist/types/{stories,test}", "after:release": "../../tools/git-mirror/bin/git-mirror sync @airtable/blocks@${version}" }, "npm": { diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/base/index.ts similarity index 81% rename from packages/sdk/src/index.ts rename to packages/sdk/src/base/index.ts index 38fdf53af..31e477da3 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/base/index.ts @@ -1,6 +1,7 @@ +import warn, {__injectSdkIntoWarning} from '../shared/warning'; +import getAirtableInterface from '../injected/airtable_interface'; +import {BaseSdkMode} from '../sdk_mode'; import {__injectSdkIntoPerformRecordAction} from './perform_record_action'; -import warn, {__injectSdkIntoWarning} from './warning'; -import getAirtableInterface from './injected/airtable_interface'; import Sdk from './sdk'; import {__injectSdkIntoCreateAggregators} from './models/create_aggregators'; import {__injectSdkIntoInitializeBlock} from './ui/initialize_block'; @@ -52,20 +53,10 @@ Object.defineProperty(module.exports, 'UI', { }, }); -Object.defineProperty(module.exports, 'models', { - enumerable: true, - get() { - warn( - '`import {models} from "@airtable/blocks"` is deprecated. Use `import * as models from "@airtable/blocks/models/models"` instead.', - ); - - return require('./models/models'); - }, -}); /** @internal */ export function __reset() { - __sdk = new Sdk(getAirtableInterface()); + __sdk = new Sdk(getAirtableInterface()); ({ base, diff --git a/packages/sdk/src/models/abstract_model_with_async_data.ts b/packages/sdk/src/base/models/abstract_model_with_async_data.ts similarity index 94% rename from packages/sdk/src/models/abstract_model_with_async_data.ts rename to packages/sdk/src/base/models/abstract_model_with_async_data.ts index a6087d4f5..c1bd6c07d 100644 --- a/packages/sdk/src/models/abstract_model_with_async_data.ts +++ b/packages/sdk/src/base/models/abstract_model_with_async_data.ts @@ -1,8 +1,14 @@ /** @module @airtable/blocks/models: Abstract models */ /** */ import Sdk from '../sdk'; -import {fireAndForgetPromise, FlowAnyFunction, FlowAnyObject, TimeoutId} from '../private_utils'; -import {invariant} from '../error_utils'; -import AbstractModel from './abstract_model'; +import { + fireAndForgetPromise, + FlowAnyFunction, + FlowAnyObject, + TimeoutId, +} from '../../shared/private_utils'; +import {invariant} from '../../shared/error_utils'; +import AbstractModel from '../../shared/models/abstract_model'; +import {BaseSdkMode} from '../../sdk_mode'; /** * Abstract superclass for all Blocks SDK models that need to fetch async data. @@ -12,7 +18,7 @@ import AbstractModel from './abstract_model'; abstract class AbstractModelWithAsyncData< DataType, WatchableKey extends string -> extends AbstractModel { +> extends AbstractModel { /** @internal */ static __DATA_UNLOAD_DELAY_MS = 1000; /** @internal */ diff --git a/packages/sdk/src/base/models/base.ts b/packages/sdk/src/base/models/base.ts new file mode 100644 index 000000000..32eedfe72 --- /dev/null +++ b/packages/sdk/src/base/models/base.ts @@ -0,0 +1,242 @@ +/** @module @airtable/blocks/models: Base */ /** */ +import {BaseCore, ChangedPathsForType, WatchableBaseKeys} from '../../shared/models/base_core'; +import {MutationTypes} from '../types/mutations'; +import {FieldType} from '../../shared/types/field'; +import {PermissionCheckResult} from '../../shared/types/mutations_core'; +import {BaseSdkMode} from '../../sdk_mode'; +import {TableId} from '../../shared/types/hyper_ids'; +import {entries} from '../../shared/private_utils'; +import {BaseData} from '../types/base'; +import BaseBlockSdk from '../sdk'; +import RecordStore from './record_store'; +import Table from './table'; + +/** + * Model class representing a base. + * + * If you want the base model to automatically recalculate whenever the base schema changes, try the + * {@link useBase} hook. Alternatively, you can manually subscribe to changes with + * {@link useWatchable} (recommended) or [Base#watch](/api/models/Base#watch). + * + * @example + * ```js + * import {base} from '@airtable/blocks'; + * + * console.log('The name of your base is', base.name); + * ``` + * @docsPath models/Base + */ +class Base extends BaseCore { + /** @internal */ + static _className = 'Base'; + + /** @internal */ + _constructTable(tableId: TableId): Table { + const recordStore = this.__getRecordStore(tableId); + return new Table(this, recordStore, tableId, this._sdk); + } + + /** @internal */ + _constructRecordStore(sdk: BaseBlockSdk, tableId: TableId): RecordStore { + return new RecordStore(sdk, tableId); + } + + /** @internal */ + _iterateTableIds(): Iterable { + return this._data.tableOrder; + } + + /** + * Checks whether the current user has permission to create a table. + * + * Accepts partial input, in the same format as {@link createTableAsync}. + * + * Returns `{hasPermission: true}` if the current user can update the specified record, + * `{hasPermission: false, reasonDisplayString: string}` otherwise. `reasonDisplayString` may be + * used to display an error message to the user. + * + * @param name name for the table. must be case-insensitive unique + * @param fields array of fields to create in the table + * + * @example + * ```js + * const createTableCheckResult = base.checkPermissionsForCreateTable(); + * + * if (!createTableCheckResult.hasPermission) { + * alert(createTableCheckResult.reasonDisplayString); + * } + * ``` + */ + checkPermissionsForCreateTable( + name?: string, + fields?: Array<{ + name?: string; + type?: FieldType; + options?: {[key: string]: unknown} | null; + description?: string | null; + }>, + ): PermissionCheckResult { + return this._sdk.__mutations.checkPermissionsForMutation({ + type: MutationTypes.CREATE_SINGLE_TABLE, + id: undefined, + name: name, + fields: fields?.map(field => { + return { + name: field.name, + config: field.type + ? { + type: field.type, + ...(field.options ? {options: field.options} : null), + } + : undefined, + description: field.description, + }; + }), + }); + } + + /** + * An alias for `checkPermissionsForCreateTable(name, fields).hasPermission`. + * + * Checks whether the current user has permission to create a table. + * + * Accepts partial input, in the same format as {@link createTableAsync}. + * + * @param name name for the table. must be case-insensitive unique + * @param fields array of fields to create in the table + * + * @example + * ```js + * const canCreateTable = table.hasPermissionToCreateTable(); + * + * if (!canCreateTable) { + * alert('not allowed!'); + * } + * ``` + */ + hasPermissionToCreateTable( + name?: string, + fields?: Array<{ + name?: string; + type?: FieldType; + options?: {[key: string]: unknown} | null; + description?: string | null; + }>, + ): boolean { + return this.checkPermissionsForCreateTable(name, fields).hasPermission; + } + + /** + * Creates a new table. + * + * Throws an error if the user does not have permission to create a table, if an invalid + * table name is provided, or if invalid fields are provided (invalid name, type, options or + * description). + * + * Refer to {@link FieldType} for supported field types, the write format for field options, and + * other specifics for certain field types. + * + * At least one field must be specified. The first field in the `fields` array will be used as + * the table's [primary field](https://support.airtable.com/hc/en-us/articles/202624179-The-Name-Field) + * and must be a supported primary field type. Fields must have case-insensitive unique names + * within the table. + * + * A default grid view will be created with all fields visible. + * + * This action is asynchronous. Unlike new records, new tables are **not** created + * optimistically locally. You must `await` the returned promise before using the new + * table in your extension. + * + * @param name name for the table. must be case-insensitive unique + * @param fields array of fields to create in the table: see below for an example. `name` and + * `type` must be specified for all fields, while `options` is only required for fields that + * have field options. `description` is optional and will be `''` if not specified or if + * specified as `null`. + * + * @example + * ```js + * async function createNewTable() { + * const name = 'My new table'; + * const fields = [ + * // Name will be the primary field of the table. + * {name: 'Name', type: FieldType.SINGLE_LINE_TEXT, description: 'This is the primary field'}, + * {name: 'Notes', type: FieldType.RICH_TEXT}, + * {name: 'Attachments', type: FieldType.MULTIPLE_ATTACHMENTS}, + * {name: 'Number', type: FieldType.NUMBER, options: { + * precision: 8, + * }}, + * {name: 'Select', type: FieldType.SINGLE_SELECT, options: { + * choices: [ + * {name: 'A'}, + * {name: 'B'}, + * ], + * }}, + * ]; + * + * if (base.hasPermissionToCreateTable(name, fields)) { + * await base.createTableAsync(name, fields); + * } + * } + * ``` + */ + async createTableAsync( + name: string, + fields: Array<{ + name: string; + type: FieldType; + options?: {[key: string]: unknown} | null; + description?: string | null; + }>, + ): Promise { + const tableId = this._sdk.__airtableInterface.idGenerator.generateTableId(); + + await this._sdk.__mutations.applyMutationAsync({ + id: tableId, + type: MutationTypes.CREATE_SINGLE_TABLE, + name, + fields: fields.map(field => { + return { + name: field.name, + config: { + type: field.type, + ...(field.options ? {options: field.options} : null), + }, + description: field.description ?? null, + }; + }), + }); + + return this.getTableById(tableId); + } + + /** + * @internal + */ + __triggerOnChangeForChangedPaths(changedPaths: ChangedPathsForType): void { + super.__triggerOnChangeForChangedPaths(changedPaths); + + let didSchemaChange = false; + if (changedPaths.tableOrder) { + this._onChange(WatchableBaseKeys.tables); + didSchemaChange = true; + + for (const [tableId, tableModel] of entries(this._tableModelsById)) { + if (tableModel.isDeleted) { + delete this._tableModelsById[tableId]; + } + } + + for (const [tableId, recordStore] of entries(this._tableRecordStoresByTableId)) { + if (recordStore && recordStore.isDeleted) { + recordStore.__onDataDeletion(); + delete this._tableRecordStoresByTableId[tableId]; + } + } + } + if (didSchemaChange) { + this._onChange(WatchableBaseKeys.schema); + } + } +} + +export default Base; diff --git a/packages/sdk/src/models/create_aggregators.ts b/packages/sdk/src/base/models/create_aggregators.ts similarity index 98% rename from packages/sdk/src/models/create_aggregators.ts rename to packages/sdk/src/base/models/create_aggregators.ts index 510b1cf93..fa8d170df 100644 --- a/packages/sdk/src/models/create_aggregators.ts +++ b/packages/sdk/src/base/models/create_aggregators.ts @@ -1,6 +1,6 @@ /** @module @airtable/blocks/models: Aggregators */ /** */ import {AggregatorKey} from '../types/aggregators'; -import {spawnError} from '../error_utils'; +import {spawnError} from '../../shared/error_utils'; import Sdk from '../sdk'; import Record from './record'; import Field from './field'; diff --git a/packages/sdk/src/models/cursor.ts b/packages/sdk/src/base/models/cursor.ts similarity index 97% rename from packages/sdk/src/models/cursor.ts rename to packages/sdk/src/base/models/cursor.ts index 6b96f7eb5..b223b9679 100644 --- a/packages/sdk/src/models/cursor.ts +++ b/packages/sdk/src/base/models/cursor.ts @@ -1,12 +1,9 @@ /** @module @airtable/blocks/models: Cursor */ /** */ import Sdk from '../sdk'; -import {ModelChange} from '../types/base'; -import {RecordId} from '../types/record'; -import {TableId} from '../types/table'; -import {ViewId} from '../types/view'; -import {FieldId} from '../types/field'; -import {isEnumValue, entries, ObjectValues, ObjectMap} from '../private_utils'; -import {invariant} from '../error_utils'; +import {ModelChange} from '../../shared/types/base_core'; +import {isEnumValue, entries, ObjectValues, ObjectMap} from '../../shared/private_utils'; +import {invariant} from '../../shared/error_utils'; +import {RecordId, FieldId, TableId, ViewId} from '../../shared/types/hyper_ids'; import AbstractModelWithAsyncData from './abstract_model_with_async_data'; import Table from './table'; import View from './view'; diff --git a/packages/sdk/src/models/field.ts b/packages/sdk/src/base/models/field.ts similarity index 58% rename from packages/sdk/src/models/field.ts rename to packages/sdk/src/base/models/field.ts index 0f95c4170..61f4e3b80 100644 --- a/packages/sdk/src/models/field.ts +++ b/packages/sdk/src/base/models/field.ts @@ -1,32 +1,12 @@ /** @module @airtable/blocks/models: Field */ /** */ +import {FieldCore} from '../../shared/models/field_core'; +import {FieldOptions} from '../../shared/types/field'; +import {UpdateFieldOptionsOpts, MutationTypes} from '../types/mutations'; +import {BaseSdkMode} from '../../sdk_mode'; +import {PermissionCheckResult} from '../../shared/types/mutations_core'; +import {values} from '../../shared/private_utils'; import {AggregatorKey} from '../types/aggregators'; -import Sdk from '../sdk'; -import {MutationTypes, PermissionCheckResult, UpdateFieldOptionsOpts} from '../types/mutations'; -import {FieldData, FieldType, FieldOptions, FieldConfig} from '../types/field'; -import {isEnumValue, cloneDeep, values, ObjectValues, FlowAnyObject} from '../private_utils'; -import {FieldTypeConfig} from '../types/airtable_interface'; -import AbstractModel from './abstract_model'; import {Aggregator} from './create_aggregators'; -import Table from './table'; - -const WatchableFieldKeys = Object.freeze({ - name: 'name' as const, - type: 'type' as const, - options: 'options' as const, - isComputed: 'isComputed' as const, - description: 'description' as const, - isFieldSynced: 'isFieldSynced' as const, -}); - -/** - * All the watchable keys in a field. - * - `name` - * - `type` - * - `options` - * - `isComputed` - * - `description` - */ -export type WatchableFieldKey = ObjectValues; /** * Model class representing a field in a table. @@ -41,244 +21,56 @@ export type WatchableFieldKey = ObjectValues; * ``` * @docsPath models/Field */ -class Field extends AbstractModel { +class Field extends FieldCore { /** @internal */ static _className = 'Field'; - /** @internal */ - static _isWatchableKey(key: string) { - return isEnumValue(WatchableFieldKeys, key); - } - /** @internal */ - _parentTable: Table; - /** @internal */ - _cachedFieldTypeConfigOrNull: FieldTypeConfig | null; - /** - * @internal - */ - constructor(sdk: Sdk, parentTable: Table, fieldId: string) { - super(sdk, fieldId); - this._parentTable = parentTable; - this._cachedFieldTypeConfigOrNull = null; - } - - /** - * @internal - */ - get _dataOrNullIfDeleted(): FieldData | null { - const tableData = this._baseData.tablesById[this.parentTable.id]; - return tableData?.fieldsById[this._id] ?? null; - } - /** - * The table that this field belongs to. Should never change because fields aren't moved between tables. - * - * @internal (since we may not be able to return parent model instances in the immutable models world) - * @example - * ```js - * const field = myTable.getFieldByName('Name'); - * console.log(field.parentTable.id === myTable.id); - * // => true - * ``` - */ - get parentTable(): Table { - return this._parentTable; - } /** - * The name of the field. Can be watched. + * A list of available aggregators given this field's configuration. * * @example * ```js - * console.log(myField.name); - * // => 'Name' + * const fieldAggregators = myField.availableAggregators; * ``` */ - get name(): string { - return this._data.name; + get availableAggregators(): Array { + const airtableInterface = this._sdk.__airtableInterface; + const availableAggregatorKeysSet = new Set( + airtableInterface.aggregators.getAvailableAggregatorKeysForField(this._data), + ); + + const {aggregators} = require('./models'); + return values(aggregators).filter(aggregator => { + return availableAggregatorKeysSet.has(aggregator.key); + }); } /** - * The type of the field. Can be watched. + * Checks if the given aggregator is available for this field. * + * @param aggregator The aggregator object or aggregator key. * @example * ```js - * console.log(myField.type); - * // => 'singleLineText' - * ``` - */ - get type(): FieldType { - const {type} = this._getCachedConfigFromFieldTypeProvider(); - // @ts-ignore - if (type === 'lookup') { - return FieldType.MULTIPLE_LOOKUP_VALUES; - } else { - return type; - } - } - /** - * The configuration options of the field. The structure of the field's - * options depend on the field's type. `null` if the field has no options. - * Can be watched. + * import {aggregators} from '@airtable/blocks/models'; + * const aggregator = aggregators.totalAttachmentSize; * - * @see {@link FieldType} - * @example - * ```js - * import {FieldType} from '@airtable/blocks/models'; + * // Using an aggregator object + * console.log(myAttachmentField.isAggregatorAvailable(aggregator)); + * // => true * - * if (myField.type === FieldType.CURRENCY) { - * console.log(myField.options.symbol); - * // => '$' - * } + * // Using an aggregator key + * console.log(myTextField.isAggregatorAvailable('totalAttachmentSize')); + * // => false * ``` */ - get options(): FieldOptions | null { - const {options} = this._getCachedConfigFromFieldTypeProvider(); - - return options ? cloneDeep(options) : null; - } + isAggregatorAvailable(aggregator: Aggregator | AggregatorKey): boolean { + const aggregatorKey = typeof aggregator === 'string' ? aggregator : aggregator.key; - _getCachedConfigFromFieldTypeProvider(): FieldTypeConfig { - if (this._cachedFieldTypeConfigOrNull !== null) { - return this._cachedFieldTypeConfigOrNull; - } const airtableInterface = this._sdk.__airtableInterface; - const appInterface = this._sdk.__appInterface; - - this._cachedFieldTypeConfigOrNull = airtableInterface.fieldTypeProvider.getConfig( - appInterface, + const availableAggregatorKeys = airtableInterface.aggregators.getAvailableAggregatorKeysForField( this._data, - this.parentTable.__getFieldNamesById(), ); - return this._cachedFieldTypeConfigOrNull; - } - _clearCachedConfig(): void { - this._cachedFieldTypeConfigOrNull = null; - } - - /** - * The type and options of the field to make type narrowing `FieldOptions` easier. - * - * @see {@link FieldConfig} - * @example - * const fieldConfig = field.config; - * if (fieldConfig.type === FieldType.SINGLE_SELECT) { - * return fieldConfig.options.choices; - * } else if (fieldConfig.type === FieldType.MULTIPLE_LOOKUP_VALUES && fieldConfig.options.isValid) { - * if (fieldConfig.options.result.type === FieldType.SINGLE_SELECT) { - * return fieldConfig.options.result.options.choices; - * } - * } - * return DEFAULT_CHOICES; - */ - get config(): FieldConfig { - return { - type: this.type, - options: this.options, - } as FieldConfig; - } - /** - * Checks whether the current user has permission to perform the given options update. - * - * Accepts partial input, in the same format as {@link updateOptionsAsync}. - * - * Returns `{hasPermission: true}` if the current user can update the specified field, - * `{hasPermission: false, reasonDisplayString: string}` otherwise. `reasonDisplayString` may be - * used to display an error message to the user. - * - * @param options new options for the field - * - * @example - * ```js - * const updateFieldCheckResult = field.checkPermissionsForUpdateOptions(); - * - * if (!updateFieldCheckResult.hasPermission) { - * alert(updateFieldCheckResult.reasonDisplayString); - * } - * ``` - */ - checkPermissionsForUpdateOptions(options?: FieldOptions): PermissionCheckResult { - return this._sdk.__mutations.checkPermissionsForMutation({ - type: MutationTypes.UPDATE_SINGLE_FIELD_CONFIG, - tableId: this.parentTable.id, - id: this.id, - config: { - type: this.type, - options: options, - }, - }); - } - - /** - * An alias for `checkPermissionsForUpdateOptions(options).hasPermission`. - * - * Checks whether the current user has permission to perform the options update. - * - * Accepts partial input, in the same format as {@link updateOptionsAsync}. - * - * @param options new options for the field - * - * @example - * ```js - * const canUpdateField = field.hasPermissionToUpdateOptions(); - * - * if (!canUpdateField) { - * alert('not allowed!'); - * } - * ``` - */ - hasPermissionToUpdateOptions(options?: FieldOptions): boolean { - return this.checkPermissionsForUpdateOptions(options).hasPermission; - } - - /** - * Updates the options for this field. - * - * Throws an error if the user does not have permission to update the field, if invalid - * options are provided, if this field has no writable options, or if updates to this field - * type is not supported. - * - * Refer to {@link FieldType} for supported field types, the write format for options, and - * other specifics for certain field types. - * - * This action is asynchronous. Unlike updates to cell values, updates to field options are - * **not** applied optimistically locally. You must `await` the returned promise before - * relying on the change in your extension. - * - * Optionally, you can pass an `opts` object as the second argument. See {@link UpdateFieldOptionsOpts} - * for available options. - * - * @param options new options for the field - * @param opts optional options to affect the behavior of the update - * - * @example - * ```js - * async function addChoiceToSelectField(selectField, nameForNewOption) { - * const updatedOptions = { - * choices: [ - * ...selectField.options.choices, - * {name: nameForNewOption}, - * ] - * }; - * - * if (selectField.hasPermissionToUpdateOptions(updatedOptions)) { - * await selectField.updateOptionsAsync(updatedOptions); - * } - * } - * ``` - */ - async updateOptionsAsync( - options: FieldOptions, - opts: UpdateFieldOptionsOpts = {}, - ): Promise { - await this._sdk.__mutations.applyMutationAsync({ - type: MutationTypes.UPDATE_SINGLE_FIELD_CONFIG, - tableId: this.parentTable.id, - id: this.id, - config: { - type: this.type, - options: options, - }, - opts, - }); + return availableAggregatorKeys.some(key => key === aggregatorKey); } /** @@ -439,164 +231,109 @@ class Field extends AbstractModel { } /** - * `true` if this field is synced, `false` otherwise. A field is - * "synced" if it's source is from another airtable base or external data source - * like Google Calendar, Jira, etc.. + * Checks whether the current user has permission to perform the given options update. * - * @hidden - */ - get isFieldSynced(): boolean { - return this._data.isSynced ?? false; - } - - /** - * `true` if this field is computed, `false` otherwise. A field is - * "computed" if it's value is not set by user input (e.g. autoNumber, formula, - * etc.). Can be watched + * Accepts partial input, in the same format as {@link updateOptionsAsync}. + * + * Returns `{hasPermission: true}` if the current user can update the specified field, + * `{hasPermission: false, reasonDisplayString: string}` otherwise. `reasonDisplayString` may be + * used to display an error message to the user. + * + * @param options new options for the field * * @example * ```js - * console.log(mySingleLineTextField.isComputed); - * // => false - * console.log(myAutoNumberField.isComputed); - * // => true + * const updateFieldCheckResult = field.checkPermissionsForUpdateOptions(); + * + * if (!updateFieldCheckResult.hasPermission) { + * alert(updateFieldCheckResult.reasonDisplayString); + * } * ``` */ - get isComputed(): boolean { - const airtableInterface = this._sdk.__airtableInterface; - return airtableInterface.fieldTypeProvider.isComputed(this._data); - } - /** - * `true` if this field is its parent table's primary field, `false` otherwise. - * Should never change because the primary field of a table cannot change. - */ - get isPrimaryField(): boolean { - return this.id === this.parentTable.primaryField.id; + checkPermissionsForUpdateOptions(options?: FieldOptions): PermissionCheckResult { + return this._sdk.__mutations.checkPermissionsForMutation({ + type: MutationTypes.UPDATE_SINGLE_FIELD_CONFIG, + tableId: this.parentTable.id, + id: this.id, + config: { + type: this.type, + options: options, + }, + }); } /** - * The description of the field, if it has one. Can be watched. + * An alias for `checkPermissionsForUpdateOptions(options).hasPermission`. * - * @example - * ```js - * console.log(myField.description); - * // => 'This is my field' - * ``` - */ - get description(): string | null { - return this._data.description; - } - /** - * A list of available aggregators given this field's configuration. + * Checks whether the current user has permission to perform the options update. + * + * Accepts partial input, in the same format as {@link updateOptionsAsync}. + * + * @param options new options for the field * * @example * ```js - * const fieldAggregators = myField.availableAggregators; + * const canUpdateField = field.hasPermissionToUpdateOptions(); + * + * if (!canUpdateField) { + * alert('not allowed!'); + * } * ``` */ - get availableAggregators(): Array { - const airtableInterface = this._sdk.__airtableInterface; - const availableAggregatorKeysSet = new Set( - airtableInterface.aggregators.getAvailableAggregatorKeysForField(this._data), - ); - - const {aggregators} = require('./models'); - return values(aggregators).filter(aggregator => { - return availableAggregatorKeysSet.has(aggregator.key); - }); + hasPermissionToUpdateOptions(options?: FieldOptions): boolean { + return this.checkPermissionsForUpdateOptions(options).hasPermission; } + /** - * Checks if the given aggregator is available for this field. + * Updates the options for this field. * - * @param aggregator The aggregator object or aggregator key. - * @example - * ```js - * import {aggregators} from '@airtable/blocks/models'; - * const aggregator = aggregators.totalAttachmentSize; + * Throws an error if the user does not have permission to update the field, if invalid + * options are provided, if this field has no writable options, or if updates to this field + * type is not supported. * - * // Using an aggregator object - * console.log(myAttachmentField.isAggregatorAvailable(aggregator)); - * // => true + * Refer to {@link FieldType} for supported field types, the write format for options, and + * other specifics for certain field types. * - * // Using an aggregator key - * console.log(myTextField.isAggregatorAvailable('totalAttachmentSize')); - * // => false - * ``` - */ - isAggregatorAvailable(aggregator: Aggregator | AggregatorKey): boolean { - const aggregatorKey = typeof aggregator === 'string' ? aggregator : aggregator.key; - - const airtableInterface = this._sdk.__airtableInterface; - const availableAggregatorKeys = airtableInterface.aggregators.getAvailableAggregatorKeysForField( - this._data, - ); - - return availableAggregatorKeys.some(key => key === aggregatorKey); - } - /** - * Attempt to parse a given string and return a valid cell value for the field's current config. - * Returns `null` if unable to parse the given string. + * This action is asynchronous. Unlike updates to cell values, updates to field options are + * **not** applied optimistically locally. You must `await` the returned promise before + * relying on the change in your extension. + * + * Optionally, you can pass an `opts` object as the second argument. See {@link UpdateFieldOptionsOpts} + * for available options. + * + * @param options new options for the field + * @param opts optional options to affect the behavior of the update * - * @param string The string to parse. * @example * ```js - * const inputString = '42'; - * const cellValue = myNumberField.convertStringToCellValue(inputString); - * console.log(cellValue === 42); - * // => true + * async function addChoiceToSelectField(selectField, nameForNewOption) { + * const updatedOptions = { + * choices: [ + * ...selectField.options.choices, + * {name: nameForNewOption}, + * ] + * }; + * + * if (selectField.hasPermissionToUpdateOptions(updatedOptions)) { + * await selectField.updateOptionsAsync(updatedOptions); + * } + * } * ``` */ - convertStringToCellValue(string: string): unknown { - const airtableInterface = this._sdk.__airtableInterface; - const appInterface = this._sdk.__appInterface; - - const cellValue = airtableInterface.fieldTypeProvider.convertStringToCellValue( - appInterface, - string, - this._data, - {parseDateCellValueInColumnTimeZone: this.type === FieldType.DATE_TIME}, - ); - - if (this.isComputed) { - return cellValue; - } - - const validationResult = airtableInterface.fieldTypeProvider.validateCellValueForUpdate( - appInterface, - cellValue, - null, - this._data, - ); - - if (validationResult.isValid) { - return cellValue; - } else { - return null; - } - } - /** - * @internal - */ - __triggerOnChangeForDirtyPaths(dirtyPaths: FlowAnyObject) { - this._clearCachedConfig(); - - if (dirtyPaths.name) { - this._onChange(WatchableFieldKeys.name); - } - if (dirtyPaths.type) { - this._onChange(WatchableFieldKeys.type); - - this._onChange(WatchableFieldKeys.isComputed); - } - if (dirtyPaths.typeOptions) { - this._onChange(WatchableFieldKeys.options); - } - if (dirtyPaths.description) { - this._onChange(WatchableFieldKeys.description); - } - if (dirtyPaths.isSynced) { - this._onChange(WatchableFieldKeys.isFieldSynced); - } + async updateOptionsAsync( + options: FieldOptions, + opts: UpdateFieldOptionsOpts = {}, + ): Promise { + await this._sdk.__mutations.applyMutationAsync({ + type: MutationTypes.UPDATE_SINGLE_FIELD_CONFIG, + tableId: this.parentTable.id, + id: this.id, + config: { + type: this.type, + options: options, + }, + opts, + }); } } diff --git a/packages/sdk/src/models/grouped_record_query_result.ts b/packages/sdk/src/base/models/grouped_record_query_result.ts similarity index 97% rename from packages/sdk/src/models/grouped_record_query_result.ts rename to packages/sdk/src/base/models/grouped_record_query_result.ts index 29be26e06..c219492f9 100644 --- a/packages/sdk/src/models/grouped_record_query_result.ts +++ b/packages/sdk/src/base/models/grouped_record_query_result.ts @@ -1,9 +1,8 @@ /** @module @airtable/blocks/models: RecordQueryResult */ /** */ -import {FieldId} from '../types/field'; +import {FieldId, RecordId} from '../../shared/types/hyper_ids'; import Sdk from '../sdk'; -import {FlowAnyFunction, FlowAnyObject, ObjectMap} from '../private_utils'; -import {invariant} from '../error_utils'; -import {RecordId} from '../types/record'; +import {FlowAnyFunction, FlowAnyObject, ObjectMap} from '../../shared/private_utils'; +import {invariant} from '../../shared/error_utils'; import {GroupData} from '../types/view'; import {NormalizedGroupLevel} from '../types/airtable_interface'; import RecordQueryResult, { diff --git a/packages/sdk/src/models/linked_records_query_result.ts b/packages/sdk/src/base/models/linked_records_query_result.ts similarity index 98% rename from packages/sdk/src/models/linked_records_query_result.ts rename to packages/sdk/src/base/models/linked_records_query_result.ts index cd9f5137c..f17005181 100644 --- a/packages/sdk/src/models/linked_records_query_result.ts +++ b/packages/sdk/src/base/models/linked_records_query_result.ts @@ -1,9 +1,9 @@ /** @module @airtable/blocks/models: RecordQueryResult */ /** */ -import {FieldType, FieldId} from '../types/field'; +import {FieldType} from '../../shared/types/field'; import Sdk from '../sdk'; -import {FlowAnyFunction, FlowAnyObject, ObjectMap} from '../private_utils'; -import {invariant} from '../error_utils'; -import {RecordId} from '../types/record'; +import {FlowAnyFunction, FlowAnyObject, ObjectMap} from '../../shared/private_utils'; +import {invariant} from '../../shared/error_utils'; +import {FieldId, RecordId} from '../../shared/types/hyper_ids'; import RecordQueryResult, { WatchableRecordQueryResultKey, NormalizedRecordQueryResultOpts, diff --git a/packages/sdk/src/models/models.ts b/packages/sdk/src/base/models/models.ts similarity index 95% rename from packages/sdk/src/models/models.ts rename to packages/sdk/src/base/models/models.ts index ecddbd4a3..f8dea8bea 100644 --- a/packages/sdk/src/models/models.ts +++ b/packages/sdk/src/base/models/models.ts @@ -1,7 +1,7 @@ /** @ignore */ /** */ import * as recordColoring from './record_coloring'; import createAggregators from './create_aggregators'; -export {FieldType, FieldConfig} from '../types/field'; +export {FieldType, FieldConfig} from '../../shared/types/field'; export {ViewType} from '../types/view'; export {default as Base} from './base'; export {default as Table} from './table'; diff --git a/packages/sdk/src/models/mutations.ts b/packages/sdk/src/base/models/mutations.ts similarity index 84% rename from packages/sdk/src/models/mutations.ts rename to packages/sdk/src/base/models/mutations.ts index 511ae147e..d276a8647 100644 --- a/packages/sdk/src/models/mutations.ts +++ b/packages/sdk/src/base/models/mutations.ts @@ -1,118 +1,24 @@ -import {AirtableInterface, BlockRunContextType} from '../types/airtable_interface'; -import {ModelChange} from '../types/base'; -import {Mutation, PartialMutation, PermissionCheckResult, MutationTypes} from '../types/mutations'; -import {entries, ObjectMap} from '../private_utils'; -import {spawnError, spawnUnknownSwitchCaseError} from '../error_utils'; -import {GlobalConfigUpdate} from '../types/global_config'; -import {FieldId} from '../types/field'; -import Sdk from '../sdk'; -import Session from './session'; -import Base from './base'; -import Field from './field'; -import Table from './table'; +import {BlockRunContextType} from '../types/airtable_interface'; +import {ModelChange} from '../../shared/types/base_core'; +import {Mutation, MutationTypes} from '../types/mutations'; +import {entries, ObjectMap} from '../../shared/private_utils'; +import {spawnError, spawnUnknownSwitchCaseError} from '../../shared/error_utils'; +import {FieldId} from '../../shared/types/hyper_ids'; import { MAX_FIELD_NAME_LENGTH, MAX_FIELD_DESCRIPTION_LENGTH, MAX_TABLE_NAME_LENGTH, MAX_NUM_FIELDS_PER_TABLE, -} from './mutation_constants'; - -const MUTATIONS_MAX_BATCH_SIZE = 50; - -const MUTATIONS_MAX_BODY_SIZE = 1.9 * 1024 * 1024; - -const MUTATION_HOLD_FOR_MS = 100; - -/** @internal */ -class Mutations { - /** @internal */ - _airtableInterface: AirtableInterface; - /** @internal */ - _session: Session; - /** @internal */ - _sdk: Sdk; - /** @internal */ - _base: Base; - /** @internal */ - _applyModelChanges: (arg1: Array) => void; - /** @internal */ - _applyGlobalConfigUpdates: (arg1: ReadonlyArray) => void; - - /** @hidden */ - constructor( - sdk: Sdk, - session: Session, - base: Base, - applyModelChanges: (arg1: ReadonlyArray) => void, - applyGlobalConfigUpdates: (arg1: ReadonlyArray) => void, - ) { - this._airtableInterface = sdk.__airtableInterface; - this._session = session; - this._sdk = sdk; - this._base = base; - this._applyModelChanges = applyModelChanges; - this._applyGlobalConfigUpdates = applyGlobalConfigUpdates; - } - - /** @hidden */ - async applyMutationAsync(mutation: Mutation): Promise { - this._assertMutationIsValid(mutation); - this._assertMutationUnderLimits(mutation); - - const permissionCheck = this.checkPermissionsForMutation(mutation); - if (!permissionCheck.hasPermission) { - throw spawnError( - 'Cannot apply %s mutation: %s', - mutation.type, - permissionCheck.reasonDisplayString, - ); - } - - const didApplyOptimisticUpdates = this._applyOptimisticUpdatesForMutation(mutation); - - try { - await this._airtableInterface.applyMutationAsync(mutation, { - holdForMs: MUTATION_HOLD_FOR_MS, - }); - } catch (err) { - if (didApplyOptimisticUpdates) { - setTimeout(() => { - throw err; - }, 0); - await new Promise(() => {}); - } else { - throw err; - } - } - } - - /** @hidden */ - checkPermissionsForMutation(mutation: PartialMutation): PermissionCheckResult { - return this._airtableInterface.checkPermissionsForMutation( - mutation, - this._base.__getBaseData(), - ); - } - - /** @internal */ - _assertMutationUnderLimits(mutation: Mutation) { - if (encodeURIComponent(JSON.stringify(mutation)).length > MUTATIONS_MAX_BODY_SIZE) { - throw spawnError( - 'Request exceeds maximum size limit of %s bytes', - MUTATIONS_MAX_BODY_SIZE, - ); - } - - if (this._doesMutationExceedBatchSizeLimit(mutation)) { - throw spawnError( - 'Request exceeds maximum batch size limit of %s items', - MUTATIONS_MAX_BATCH_SIZE, - ); - } - } +} from '../../shared/types/mutation_constants'; +import {MutationsCore, MUTATIONS_MAX_BATCH_SIZE} from '../../shared/models/mutations_core'; +import {BaseSdkMode} from '../../sdk_mode'; +import Table from './table'; +import Field from './field'; +/** @hidden */ +class Mutations extends MutationsCore { /** @internal */ - _doesMutationExceedBatchSizeLimit(mutation: Mutation) { + _doesMutationExceedBatchSizeLimit(mutation: Mutation): boolean { switch (mutation.type) { case MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES: case MutationTypes.CREATE_MULTIPLE_RECORDS: @@ -127,40 +33,7 @@ class Mutations { } /** @internal */ - _assertFieldIsValidForMutation(field: Field) { - if (field.isComputed) { - throw spawnError( - "Can't set cell values: Field '%s' is computed and cannot be set", - field.name, - ); - } - } - - /** @internal */ - _assertFieldNameIsValidForMutation(name: string, table: Table) { - if (!name) { - throw spawnError("Can't create or update field: must provide non-empty name"); - } - - if (name.length > MAX_FIELD_NAME_LENGTH) { - throw spawnError( - "Can't create or update field: name '%s' exceeds maximum length of %s characters", - name, - MAX_FIELD_NAME_LENGTH, - ); - } - - const existingLowercaseFieldNames = table.fields.map(field => field.name.toLowerCase()); - if (existingLowercaseFieldNames.includes(name.toLowerCase())) { - throw spawnError( - "Can't create or update field: field with name '%s' already exists", - name, - ); - } - } - - /** @internal */ - _assertMutationIsValid(mutation: Mutation) { + _assertMutationIsValid(mutation: Mutation): void { const appInterface = this._sdk.__appInterface; const billingPlanGrouping = this._base.__billingPlanGrouping; @@ -545,20 +418,36 @@ class Mutations { } /** @internal */ - _applyOptimisticUpdatesForMutation(mutation: Mutation): boolean { - if (mutation.type === MutationTypes.SET_MULTIPLE_GLOBAL_CONFIG_PATHS) { - this._applyGlobalConfigUpdates(mutation.updates); - return true; + _assertFieldIsValidForMutation(field: Field) { + if (field.isComputed) { + throw spawnError( + "Can't set cell values: Field '%s' is computed and cannot be set", + field.name, + ); } + } - const modelChanges = this._getOptimisticModelChangesForMutation(mutation); + /** @internal */ + _assertFieldNameIsValidForMutation(name: string, table: Table) { + if (!name) { + throw spawnError("Can't create or update field: must provide non-empty name"); + } - if (modelChanges.length > 0) { - this._applyModelChanges(modelChanges); - return true; + if (name.length > MAX_FIELD_NAME_LENGTH) { + throw spawnError( + "Can't create or update field: name '%s' exceeds maximum length of %s characters", + name, + MAX_FIELD_NAME_LENGTH, + ); } - return false; + const existingLowercaseFieldNames = table.fields.map(field => field.name.toLowerCase()); + if (existingLowercaseFieldNames.includes(name.toLowerCase())) { + throw spawnError( + "Can't create or update field: field with name '%s' already exists", + name, + ); + } } /** @internal */ diff --git a/packages/sdk/src/models/object_pool.ts b/packages/sdk/src/base/models/object_pool.ts similarity index 97% rename from packages/sdk/src/models/object_pool.ts rename to packages/sdk/src/base/models/object_pool.ts index c92f88a4d..200207023 100644 --- a/packages/sdk/src/models/object_pool.ts +++ b/packages/sdk/src/base/models/object_pool.ts @@ -1,6 +1,6 @@ /** @hidden */ /** */ -import {invariant} from '../error_utils'; -import {TimeoutId} from '../private_utils'; +import {invariant} from '../../shared/error_utils'; +import {TimeoutId} from '../../shared/private_utils'; const WEAK_RETAIN_TIME_MS = 10000; diff --git a/packages/sdk/src/models/record.ts b/packages/sdk/src/base/models/record.ts similarity index 60% rename from packages/sdk/src/models/record.ts rename to packages/sdk/src/base/models/record.ts index 07e36f4de..ed3604180 100644 --- a/packages/sdk/src/models/record.ts +++ b/packages/sdk/src/base/models/record.ts @@ -1,25 +1,23 @@ /** @module @airtable/blocks/models: Record */ /** */ -import {Color} from '../colors'; -import Sdk from '../sdk'; -import {RecordData, RecordId} from '../types/record'; -import {FieldType, FieldId} from '../types/field'; -import {ViewId} from '../types/view'; -import {isEnumValue, cloneDeep, isObjectEmpty, ObjectValues, FlowAnyObject} from '../private_utils'; -import {invariant} from '../error_utils'; -import colorUtils from '../color_utils'; -import AbstractModel from './abstract_model'; -import Field from './field'; +import {Color} from '../../shared/colors'; +import {RecordCore, WatchableRecordKeysCore} from '../../shared/models/record_core'; +import {ViewId, FieldId} from '../../shared/types/hyper_ids'; +import {BaseSdkMode} from '../../sdk_mode'; +import {isEnumValue, ObjectValues, FlowAnyObject, isObjectEmpty} from '../../shared/private_utils'; +import BlockSdk from '../sdk'; +import {invariant} from '../../shared/error_utils'; +import colorUtils from '../../shared/color_utils'; +import LinkedRecordsQueryResult from './linked_records_query_result'; import ObjectPool from './object_pool'; +import RecordStore from './record_store'; +import Field from './field'; import Table from './table'; import View from './view'; import RecordQueryResult, {RecordQueryResultOpts} from './record_query_result'; -import LinkedRecordsQueryResult from './linked_records_query_result'; -import RecordStore from './record_store'; const WatchableRecordKeys = Object.freeze({ - name: 'name' as const, + ...WatchableRecordKeysCore, commentCount: 'commentCount' as const, - cellValues: 'cellValues' as const, }); const WatchableCellValueInFieldKeyPrefix = 'cellValueInField:'; const WatchableColorInViewKeyPrefix = 'colorInView:'; @@ -41,7 +39,7 @@ type WatchableRecordKey = ObjectValues | string; * * @docsPath models/Record */ -class Record extends AbstractModel { +class Record extends RecordCore { /** @internal */ static _className = 'Record'; /** @internal */ @@ -53,10 +51,6 @@ class Record extends AbstractModel { ); } /** @internal */ - _parentRecordStore: RecordStore; - /** @internal */ - _parentTable: Table; - /** @internal */ __linkedRecordsQueryResultPool: ObjectPool< LinkedRecordsQueryResult, typeof LinkedRecordsQueryResult @@ -65,47 +59,15 @@ class Record extends AbstractModel { /** * @internal */ - constructor(sdk: Sdk, parentRecordStore: RecordStore, parentTable: Table, recordId: string) { - super(sdk, recordId); - - this._parentRecordStore = parentRecordStore; - this._parentTable = parentTable; + constructor( + sdk: BlockSdk, + parentRecordStore: RecordStore, + parentTable: Table, + recordId: string, + ) { + super(sdk, parentRecordStore, parentTable, recordId); this.__linkedRecordsQueryResultPool = new ObjectPool(LinkedRecordsQueryResult); } - - /** - * @internal - */ - get _dataOrNullIfDeleted(): RecordData | null { - const tableData = this._baseData.tablesById[this.parentTable.id]; - if (!tableData) { - return null; - } - const recordsById = tableData.recordsById; - invariant(recordsById, 'Record data is not loaded'); - return recordsById[this._id] ?? null; - } - /** - * The table that this record belongs to. Should never change because records aren't moved between tables. - * - * @internal (since we may not be able to return parent model instances in the immutable models world) - * @example - * ```js - * import {useRecords} from '@airtable/blocks/ui'; - * const records = useRecords(myTable); - * console.log(records[0].parentTable.id === myTable.id); - * // => true - * ``` - */ - get parentTable(): Table { - return this._parentTable; - } - /** - * @internal - */ - _getFieldMatching(fieldOrFieldIdOrFieldName: Field | string): Field { - return this.parentTable.__getFieldMatching(fieldOrFieldIdOrFieldName); - } /** * @internal */ @@ -113,80 +75,6 @@ class Record extends AbstractModel { return this.parentTable.__getViewMatching(viewOrViewIdOrViewName); } - /** - * @internal - * - * For use when we need the raw public API cell value. Specifically makes a difference - * for lookup fields, where we translate the format to a blocks-specific format in getCellValue. - * That format is incompatible with fieldTypeProvider methods, which expect the public API - * format - use _getRawCellValue instead. - */ - _getRawCellValue(field: Field): unknown { - invariant( - this._parentRecordStore.areCellValuesLoadedForFieldId(field.id), - 'Cell values for field %s are not loaded', - field.id, - ); - - const {cellValuesByFieldId} = this._data; - if (!cellValuesByFieldId) { - return null; - } - const cellValue = - cellValuesByFieldId[field.id] !== undefined ? cellValuesByFieldId[field.id] : null; - - if (typeof cellValue === 'object' && cellValue !== null) { - return cloneDeep(cellValue); - } else { - return cellValue; - } - } - /** - * Gets the cell value of the given field for this record. - * - * @param fieldOrFieldIdOrFieldName The field (or field ID or field name) whose cell value you'd like to get. - * @example - * ```js - * const cellValue = myRecord.getCellValue(mySingleLineTextField); - * console.log(cellValue); - * // => 'cell value' - * ``` - */ - getCellValue(fieldOrFieldIdOrFieldName: Field | FieldId | string): unknown { - const field = this._getFieldMatching(fieldOrFieldIdOrFieldName); - const cellValue = this._getRawCellValue(field); - - if ( - typeof cellValue === 'object' && - cellValue !== null && - field.type === FieldType.MULTIPLE_LOOKUP_VALUES && - !this._sdk.__airtableInterface.sdkInitData.isUsingNewLookupCellValueFormat - ) { - const cellValueForMigration: Array<{linkedRecordId: RecordId; value: unknown}> = []; - invariant(Array.isArray((cellValue as any).linkedRecordIds), 'linkedRecordIds'); - for (const linkedRecordId of (cellValue as any).linkedRecordIds) { - invariant(typeof linkedRecordId === 'string', 'linkedRecordId'); - const {valuesByLinkedRecordId} = cellValue as any; - - invariant( - valuesByLinkedRecordId && typeof valuesByLinkedRecordId === 'object', - 'valuesByLinkedRecordId', - ); - - const value = valuesByLinkedRecordId[linkedRecordId]; - if (Array.isArray(value)) { - for (const v of value) { - cellValueForMigration.push({linkedRecordId, value: v}); - } - } else { - cellValueForMigration.push({linkedRecordId, value}); - } - } - return cellValueForMigration; - } - - return cellValue; - } /** * Gets the cell value of the given field for this record, formatted as a `string`. * @@ -200,27 +88,22 @@ class Record extends AbstractModel { */ getCellValueAsString(fieldOrFieldIdOrFieldName: Field | FieldId | string): string { const field = this._getFieldMatching(fieldOrFieldIdOrFieldName); - invariant( this._parentRecordStore.areCellValuesLoadedForFieldId(field.id), 'Cell values for field %s are not loaded', field.id, ); - - const cellValue = this._getRawCellValue(field); - - if (cellValue === null || cellValue === undefined) { - return ''; - } else { - const airtableInterface = this._sdk.__airtableInterface; - const appInterface = this._sdk.__appInterface; - return airtableInterface.fieldTypeProvider.convertCellValueToString( - appInterface, - cellValue, - field._data, - ); - } + return super.getCellValueAsString(field.id); + } + _getRawCellValue(field: Field): unknown { + invariant( + this._parentRecordStore.areCellValuesLoadedForFieldId(field.id), + 'Cell values for field %s are not loaded', + field.id, + ); + return super._getRawCellValue(field); } + /** * Returns a URL that is suitable for rendering an attachment on the current client. * The URL that is returned will only work for the current user. @@ -355,18 +238,6 @@ class Record extends AbstractModel { this.parentTable.id, ); } - /** - * The primary cell value in this record, formatted as a `string`. - * - * @example - * ```js - * console.log(myRecord.name); - * // => '42' - * ``` - */ - get name(): string { - return this.getCellValueAsString(this.parentTable.primaryField); - } /** * The number of comments on this record. * @@ -382,38 +253,17 @@ class Record extends AbstractModel { get commentCount(): number { return this._data.commentCount; } - /** - * The created time of this record. - * - * @example - * ```js - * console.log(` - * This record was created at ${myRecord.createdTime.toISOString()} - * `); - * ``` - */ - get createdTime(): Date { - return new Date(this._data.createdTime); - } /** * @internal */ __triggerOnChangeForDirtyPaths(dirtyPaths: FlowAnyObject) { + super.__triggerOnChangeForDirtyPaths(dirtyPaths); const {cellValuesByFieldId, commentCount} = dirtyPaths; - if (cellValuesByFieldId && !isObjectEmpty(cellValuesByFieldId)) { - - this._onChange(WatchableRecordKeys.cellValues, Object.keys(cellValuesByFieldId)); - - if (cellValuesByFieldId[this.parentTable.primaryField.id]) { - this._onChange(WatchableRecordKeys.name); - } - for (const fieldId of Object.keys(cellValuesByFieldId)) { this._onChange(WatchableCellValueInFieldKeyPrefix + fieldId, fieldId); } } - if (commentCount) { this._onChange(WatchableRecordKeys.commentCount); } diff --git a/packages/sdk/src/models/record_coloring.ts b/packages/sdk/src/base/models/record_coloring.ts similarity index 98% rename from packages/sdk/src/models/record_coloring.ts rename to packages/sdk/src/base/models/record_coloring.ts index 35897cca3..c0a70bf06 100644 --- a/packages/sdk/src/models/record_coloring.ts +++ b/packages/sdk/src/base/models/record_coloring.ts @@ -1,5 +1,5 @@ /** @module @airtable/blocks/models: Record Coloring */ /** */ -import {ObjectValues} from '../private_utils'; +import {ObjectValues} from '../../shared/private_utils'; import Field from './field'; import View from './view'; diff --git a/packages/sdk/src/models/record_query_result.ts b/packages/sdk/src/base/models/record_query_result.ts similarity index 99% rename from packages/sdk/src/models/record_query_result.ts rename to packages/sdk/src/base/models/record_query_result.ts index 28eeca6c6..0c640b0d7 100644 --- a/packages/sdk/src/models/record_query_result.ts +++ b/packages/sdk/src/base/models/record_query_result.ts @@ -1,8 +1,8 @@ /** @module @airtable/blocks/models: RecordQueryResult */ /** */ -import Colors, {Color} from '../colors'; +import Colors, {Color} from '../../shared/colors'; import Sdk from '../sdk'; -import {RecordId} from '../types/record'; -import {FieldType, FieldId} from '../types/field'; +import {FieldId, RecordId} from '../../shared/types/hyper_ids'; +import {FieldType} from '../../shared/types/field'; import { isEnumValue, assertEnumValue, @@ -12,9 +12,9 @@ import { cast, FlowAnyFunction, FlowAnyObject, -} from '../private_utils'; -import {spawnUnknownSwitchCaseError, spawnError, invariant} from '../error_utils'; -import Watchable from '../watchable'; +} from '../../shared/private_utils'; +import {spawnUnknownSwitchCaseError, spawnError, invariant} from '../../shared/error_utils'; +import Watchable from '../../shared/watchable'; import {NormalizedGroupLevel} from '../types/airtable_interface'; import AbstractModelWithAsyncData from './abstract_model_with_async_data'; import Table from './table'; diff --git a/packages/sdk/src/models/record_store.ts b/packages/sdk/src/base/models/record_store.ts similarity index 74% rename from packages/sdk/src/models/record_store.ts rename to packages/sdk/src/base/models/record_store.ts index 53658b49c..d506f7b4e 100644 --- a/packages/sdk/src/models/record_store.ts +++ b/packages/sdk/src/base/models/record_store.ts @@ -1,34 +1,36 @@ import { - isEnumValue, fireAndForgetPromise, entries, has, values, - ObjectValues, ObjectMap, FlowAnyFunction, FlowAnyObject, cast, keys as objectKeys, -} from '../private_utils'; -import {invariant, logErrorToSentry} from '../error_utils'; + ObjectValues, + isEnumValue, +} from '../../shared/private_utils'; +import {invariant, logErrorToSentry} from '../../shared/error_utils'; import Sdk from '../sdk'; -import {TableId, TableData} from '../types/table'; -import {FieldId} from '../types/field'; -import {RecordId, RecordData} from '../types/record'; -import {ViewId} from '../types/view'; +import {TableData} from '../types/table'; +import {TableId, FieldId, ViewId, RecordId} from '../../shared/types/hyper_ids'; +import {RecordData} from '../types/record'; import {AirtableInterface} from '../types/airtable_interface'; +import {ChangedPathsForType} from '../../shared/models/base_core'; +import {BaseSdkMode} from '../../sdk_mode'; +import RecordStoreCore, { + WatchableCellValuesInFieldKeyPrefix, + WatchableRecordStoreKeysCore, +} from '../../shared/models/record_store_core'; import AbstractModelWithAsyncData from './abstract_model_with_async_data'; import Record from './record'; import ViewDataStore from './view_data_store'; -import {ChangedPathsForType} from './base'; +import Table from './table'; export const WatchableRecordStoreKeys = Object.freeze({ - records: 'records' as const, - recordIds: 'recordIds' as const, - cellValues: 'cellValues' as const, + ...WatchableRecordStoreKeysCore, }); -const WatchableCellValuesInFieldKeyPrefix = 'cellValuesInField:'; /** * The string case is to accommodate prefix keys @@ -43,7 +45,7 @@ export type WatchableRecordStoreKey = ObjectValues { +class RecordStore extends RecordStoreCore { static _className = 'RecordStore'; static _isWatchableKey(key: string): boolean { return ( @@ -51,31 +53,26 @@ class RecordStore extends AbstractModelWithAsyncData = {}; - readonly _primaryFieldId: FieldId; - readonly _airtableInterface: AirtableInterface; readonly _viewDataStoresByViewId: ObjectMap = {}; + readonly _loader: RecordStoreAsyncLoader; - _areCellValuesLoadedByFieldId: ObjectMap = {}; - _pendingCellValuesLoadPromiseByFieldId: ObjectMap< - FieldId, - Promise> | undefined - > = {}; - _cellValuesRetainCountByFieldId: ObjectMap = {}; + constructor(sdk: Sdk, tableId: TableId) { + super(sdk, tableId); - _timeoutForRemovingFieldIds: NodeJS.Timeout | null = null; + const onChange = this._onChange.bind(this); + const clearRecordModels = () => { + this._recordModelsById = {}; + }; + this._loader = new RecordStoreAsyncLoader(sdk, tableId, onChange, clearRecordModels); + } - constructor(sdk: Sdk, tableId: TableId) { - super(sdk, `${tableId}-RecordStore`); + _constructRecord(recordId: RecordId, parentTable: Table): Record { + return new Record(this._sdk, this, parentTable, recordId); + } - this._airtableInterface = sdk.__airtableInterface; - this.tableId = tableId; - this._primaryFieldId = this._data.primaryFieldId; + get _dataOrNullIfDeleted(): TableData | null { + return this._loader._dataOrNullIfDeleted; } getViewDataStore(viewId: ViewId): ViewDataStore { @@ -94,9 +91,14 @@ class RecordStore extends AbstractModelWithAsyncData { const validKeys = super.watch(keys, callback, context); - const fieldIdsToLoad = this._getFieldIdsToLoadFromWatchableKeys(validKeys); + const fieldIdsToLoad = this._loader._getFieldIdsToLoadFromWatchableKeys(validKeys); if (fieldIdsToLoad.length > 0) { - fireAndForgetPromise(this.loadCellValuesInFieldIdsAsync.bind(this, fieldIdsToLoad)); + fireAndForgetPromise(async () => { + await this._loader.loadCellValuesInFieldIdsAsync( + fieldIdsToLoad, + this._onChange.bind(this), + ); + }); } return validKeys; } @@ -107,51 +109,13 @@ class RecordStore extends AbstractModelWithAsyncData { const validKeys = super.unwatch(keys, callback, context); - const fieldIdsToUnload = this._getFieldIdsToLoadFromWatchableKeys(validKeys); + const fieldIdsToUnload = this._loader._getFieldIdsToLoadFromWatchableKeys(validKeys); if (fieldIdsToUnload.length > 0) { - this.unloadCellValuesInFieldIds(fieldIdsToUnload); + this._loader.unloadCellValuesInFieldIds(fieldIdsToUnload); } return validKeys; } - _getFieldIdsToLoadFromWatchableKeys(keys: Array): Array { - const fieldIdsToLoad = []; - for (const key of keys) { - if (key.startsWith(WatchableCellValuesInFieldKeyPrefix)) { - const fieldId = key.substring(WatchableCellValuesInFieldKeyPrefix.length); - fieldIdsToLoad.push(fieldId); - } else if ( - key === WatchableRecordStoreKeys.records || - key === WatchableRecordStoreKeys.recordIds - ) { - fieldIdsToLoad.push(this._getFieldIdForCausingRecordMetadataToLoad()); - } - } - return fieldIdsToLoad; - } - - get _dataOrNullIfDeleted(): TableData | null { - return this._baseData.tablesById[this.tableId] ?? null; - } - - _onChangeIsDataLoaded() { - } - - /** - * The records in this table. The order is arbitrary since records are - * only ordered in the context of a specific view. - */ - get records(): Array { - const recordsById = this._data.recordsById; - invariant(recordsById, 'Record metadata is not loaded'); - const records = Object.keys(recordsById).map(recordId => { - const record = this.getRecordByIdIfExists(recordId); - invariant(record, 'record'); - return record; - }); - return records; - } - /** * The record IDs in this table. The order is arbitrary since records are * only ordered in the context of a specific view. @@ -162,45 +126,112 @@ class RecordStore extends AbstractModelWithAsyncData 0 + this._loader._cellValuesRetainCountByFieldId[fieldId] && + this._loader._cellValuesRetainCountByFieldId[fieldId] > 0 ) { - this.unloadCellValuesInFieldIds([fieldId]); + this._loader.unloadCellValuesInFieldIds([fieldId]); } } - this._forceUnload(); + this._loader._forceUnload(); for (const viewDataStore of values(this._viewDataStoresByViewId)) { viewDataStore.__onDataDeletion(); } } + get isDeleted(): boolean { + return this._loader.isDeleted || super.isDeleted; + } + get isDataLoaded(): boolean { + return !this.isDeleted && this._loader.isDataLoaded; + } + async loadDataAsync() { + return await this._loader.loadDataAsync(); + } + unloadData() { + return this._loader.unloadData(); + } + get isRecordMetadataLoaded() { + return this._loader.isRecordMetadataLoaded; + } + async loadRecordMetadataAsync() { + return await this._loader.loadRecordMetadataAsync(this._onChange.bind(this)); + } + unloadRecordMetadata() { + return this._loader.unloadRecordMetadata(); + } + areCellValuesLoadedForFieldId(fieldId: FieldId): boolean { + return this._loader.areCellValuesLoadedForFieldId(fieldId); + } + async loadCellValuesInFieldIdsAsync(fieldIds: Array) { + return await this._loader.loadCellValuesInFieldIdsAsync( + fieldIds, + this._onChange.bind(this), + ); + } + unloadCellValuesInFieldIds(fieldIds: Array) { + return this._loader.unloadCellValuesInFieldIds(fieldIds); + } + + triggerOnChangeForDirtyPaths(dirtyPaths: ChangedPathsForType) { + if (this.isRecordMetadataLoaded && dirtyPaths.recordsById) { + super.triggerOnChangeForDirtyPaths(dirtyPaths); + } + + if (dirtyPaths.viewOrder) { + for (const [viewId, viewDataStore] of entries(this._viewDataStoresByViewId)) { + if (viewDataStore.isDeleted) { + viewDataStore.__onDataDeletion(); + delete this._viewDataStoresByViewId[viewId]; + } + } + } + } +} + +/** @internal */ +class RecordStoreAsyncLoader extends AbstractModelWithAsyncData { + static _shouldLoadDataForKey(key: WatchableRecordStoreKey): boolean { + return key === WatchableRecordStoreKeys.cellValues; + } + + readonly tableId: TableId; + readonly _airtableInterface: AirtableInterface; + readonly _primaryFieldId: FieldId; + readonly _onChange: (key: string) => void; + readonly _clearRecordModels: () => void; + + _areCellValuesLoadedByFieldId: ObjectMap = {}; + _pendingCellValuesLoadPromiseByFieldId: ObjectMap< + FieldId, + Promise> | undefined + > = {}; + _cellValuesRetainCountByFieldId: ObjectMap = {}; + + _timeoutForRemovingFieldIds: NodeJS.Timeout | null = null; + + constructor( + sdk: Sdk, + tableId: TableId, + onChange: (key: string) => void, + clearRecordModels: () => void, + ) { + super(sdk, `${tableId}-RecordStore`); + + this._airtableInterface = sdk.__airtableInterface; + this.tableId = tableId; + this._onChange = onChange; + this._clearRecordModels = clearRecordModels; + this._primaryFieldId = this._data.primaryFieldId; + } + + _onChangeIsDataLoaded(): void { + } + /** * Record metadata means record IDs, createdTime, and commentCount are loaded. * Record metadata must be loaded before creating, deleting, or updating records. @@ -209,16 +240,33 @@ class RecordStore extends AbstractModelWithAsyncData void) { + return await this.loadCellValuesInFieldIdsAsync( + [this._getFieldIdForCausingRecordMetadataToLoad()], + onChange, + ); } unloadRecordMetadata() { this.unloadCellValuesInFieldIds([this._getFieldIdForCausingRecordMetadataToLoad()]); } + _getFieldIdsToLoadFromWatchableKeys(keys: Array): Array { + const fieldIdsToLoad = []; + for (const key of keys) { + if (key.startsWith(WatchableCellValuesInFieldKeyPrefix)) { + const fieldId = key.substring(WatchableCellValuesInFieldKeyPrefix.length); + fieldIdsToLoad.push(fieldId); + } else if ( + key === WatchableRecordStoreKeys.records || + key === WatchableRecordStoreKeys.recordIds + ) { + fieldIdsToLoad.push(this._getFieldIdForCausingRecordMetadataToLoad()); + } + } + return fieldIdsToLoad; + } + _getFieldIdForCausingRecordMetadataToLoad(): FieldId { return this._primaryFieldId; } @@ -227,7 +275,10 @@ class RecordStore extends AbstractModelWithAsyncData) { + async loadCellValuesInFieldIdsAsync( + fieldIds: Array, + onChange: (key: WatchableRecordStoreKey) => void, + ) { this._assertNotForceUnloaded(); const fieldIdsWhichAreNotAlreadyLoadedOrLoading: Array = []; const pendingLoadPromises: Array>> = []; @@ -264,7 +315,7 @@ class RecordStore extends AbstractModelWithAsyncData) { - if (this.isRecordMetadataLoaded && dirtyPaths.recordsById) { - const dirtyFieldIdsSet: ObjectMap = {}; - const addedRecordIds: Array = []; - const removedRecordIds: Array = []; - for (const [recordId, dirtyRecordPaths] of entries(dirtyPaths.recordsById) as Array< - [RecordId, ChangedPathsForType] - >) { - if (dirtyRecordPaths && dirtyRecordPaths._isDirty) { - invariant(this._data.recordsById, 'No recordsById'); - - if (has(this._data.recordsById, recordId)) { - addedRecordIds.push(recordId); - } else { - removedRecordIds.push(recordId); - - const recordModel = this._recordModelsById[recordId]; - if (recordModel) { - delete this._recordModelsById[recordId]; - } - } - } else { - const recordModel = this._recordModelsById[recordId]; - if (recordModel) { - recordModel.__triggerOnChangeForDirtyPaths(dirtyRecordPaths); - } - } - - const {cellValuesByFieldId} = dirtyRecordPaths; - if (cellValuesByFieldId) { - for (const fieldId of Object.keys(cellValuesByFieldId)) { - dirtyFieldIdsSet[fieldId] = true; - } - } - } - - if (addedRecordIds.length > 0 || removedRecordIds.length > 0) { - this._onChange(WatchableRecordStoreKeys.records, { - addedRecordIds, - removedRecordIds, - }); - - this._onChange(WatchableRecordStoreKeys.recordIds, { - addedRecordIds, - removedRecordIds, - }); - } - - const fieldIds = Object.freeze(Object.keys(dirtyFieldIdsSet)); - const recordIds = Object.freeze(Object.keys(dirtyPaths.recordsById)); - if (fieldIds.length > 0 && recordIds.length > 0) { - this._onChange(WatchableRecordStoreKeys.cellValues, { - recordIds, - fieldIds, - }); - } - for (const fieldId of fieldIds) { - this._onChange(WatchableCellValuesInFieldKeyPrefix + fieldId, recordIds, fieldId); - } - } - - if (dirtyPaths.viewOrder) { - for (const [viewId, viewDataStore] of entries(this._viewDataStoresByViewId)) { - if (viewDataStore.isDeleted) { - viewDataStore.__onDataDeletion(); - delete this._viewDataStoresByViewId[viewId]; - } - } - } + get _dataOrNullIfDeleted(): TableData | null { + return this._baseData.tablesById[this.tableId] ?? null; } } diff --git a/packages/sdk/src/models/session.ts b/packages/sdk/src/base/models/session.ts similarity index 54% rename from packages/sdk/src/models/session.ts rename to packages/sdk/src/base/models/session.ts index 1fbeba9b4..92193baa0 100644 --- a/packages/sdk/src/models/session.ts +++ b/packages/sdk/src/base/models/session.ts @@ -1,33 +1,7 @@ -/** @module @airtable/blocks/models: Session */ /** */ -import {invariant} from '../error_utils'; -import Sdk from '../sdk'; -import {AirtableInterface} from '../types/airtable_interface'; -import {ModelChange} from '../types/base'; -import {CollaboratorData, UserId} from '../types/collaborator'; -import {PermissionLevel} from '../types/permission_levels'; -import {isEnumValue, entries, ObjectValues, ObjectMap} from '../private_utils'; -import {PermissionCheckResult, MutationTypes} from '../types/mutations'; -import AbstractModel from './abstract_model'; - -/** @hidden */ -interface SessionData { - currentUserId: UserId | null; - permissionLevel: PermissionLevel; - enabledFeatureNames: Array; -} - -const WatchableSessionKeys = Object.freeze({ - permissionLevel: 'permissionLevel' as const, - - currentUser: 'currentUser' as const, -}); - -/** - * Watchable keys in {@link Session}. - * - `currentUser` - * - `permissionLevel` - */ -type WatchableSessionKey = ObjectValues; +import {BaseSdkMode} from '../../sdk_mode'; +import {SessionCore} from '../../shared/models/session_core'; +import {PermissionCheckResult} from '../../shared/types/mutations_core'; +import {MutationTypes} from '../types/mutations'; /** * Model class representing the current user's session. @@ -48,77 +22,9 @@ type WatchableSessionKey = ObjectValues; * ``` * @docsPath models/Session */ -class Session extends AbstractModel { +class Session extends SessionCore { /** @internal */ static _className = 'Session'; - /** @internal */ - static _isWatchableKey(key: string): boolean { - return isEnumValue(WatchableSessionKeys, key); - } - /** @internal */ - _airtableInterface: AirtableInterface; - /** @internal */ - _sessionData: SessionData; - - /** - * @internal - */ - constructor(sdk: Sdk) { - super(sdk, 'session'); - this._airtableInterface = sdk.__airtableInterface; - - const { - permissionLevel, - currentUserId, - enabledFeatureNames, - } = this._airtableInterface.sdkInitData.baseData; - this._sessionData = { - permissionLevel, - currentUserId, - enabledFeatureNames, - }; - - Object.seal(this); - } - - /** - * @internal - */ - get _dataOrNullIfDeleted(): SessionData { - return this._sessionData; - } - - /** - * The current user, or `null` if the extension is running in a publicly shared base. - * - * @example - * ```js - * import {useSession} from '@airtable/blocks/ui'; - * - * function CurrentUser() { - * const session = useSession(); - * - * if (!session.currentUser) { - * return
This extension is being used in a public share.
; - * } - * - * return
    - *
  • ID: {session.currentUser.id}
  • - *
  • E-mail: {session.currentUser.email}
  • - *
  • Name: {session.currentUser.name}
  • - *
; - * } - * ``` - */ - get currentUser(): CollaboratorData | null { - const userId = this._sessionData.currentUserId; - if (!userId) { - return null; - } else { - const {base} = this._sdk; - return base.getCollaboratorByIdIfExists(userId); - } - } /** * Checks whether the current user has permission to update any records in the current base. For * more granular permission checks, see {@link Table.checkPermissionsForUpdateRecords}. @@ -241,62 +147,6 @@ class Session extends AbstractModel { hasPermissionToDeleteRecords(): boolean { return this.checkPermissionsForDeleteRecords().hasPermission; } - /** - * Returns true if `featureName` is enabled and automatically tracks an exposure. - * - * @internal - */ - __isFeatureEnabled(featureName: string): boolean { - this._airtableInterface.trackExposure(featureName); - return this.__peekIfFeatureIsEnabled(featureName); - } - - /** - * Returns true if `featureName` is enabled; does not track an exposure. - * - * @internal - */ - __peekIfFeatureIsEnabled(featureName: string): boolean { - return this._sessionData.enabledFeatureNames.includes(featureName); - } - - /** - * @internal - */ - __applyChangesWithoutTriggeringEvents( - changes: ReadonlyArray, - ): ObjectMap { - const changedKeys = { - [WatchableSessionKeys.permissionLevel]: false, - [WatchableSessionKeys.currentUser]: false, - }; - for (const {path, value} of changes) { - if (path[0] === 'permissionLevel') { - invariant(path.length === 1, 'cannot set within permissionLevel'); - - invariant(typeof value === 'string', 'permissionLevel must be a string'); - - this._sessionData.permissionLevel = value as any; - changedKeys[WatchableSessionKeys.permissionLevel] = true; - } - - if (path[0] === 'collaboratorsById') { - changedKeys[WatchableSessionKeys.currentUser] = true; - } - } - - return changedKeys; - } - /** - * @internal - */ - __triggerOnChangeForChangedKeys(changedKeys: ObjectMap) { - for (const [key, didChange] of entries(changedKeys)) { - if (didChange) { - this._onChange(key); - } - } - } } export default Session; diff --git a/packages/sdk/src/models/table.ts b/packages/sdk/src/base/models/table.ts similarity index 84% rename from packages/sdk/src/models/table.ts rename to packages/sdk/src/base/models/table.ts index c7a3c4571..8cc9fc9b6 100644 --- a/packages/sdk/src/models/table.ts +++ b/packages/sdk/src/base/models/table.ts @@ -1,35 +1,43 @@ /** @module @airtable/blocks/models: Table */ /** */ -import Sdk from '../sdk'; +import {TableCore, WatchableTableKeysCore} from '../../shared/models/table_core'; +import {ViewType} from '../types/view'; +import {spawnError} from '../../shared/error_utils'; +import { + entries, + cast, + ObjectMap, + keys, + isEnumValue, + ObjectValues, +} from '../../shared/private_utils'; +import BlockSdk from '../sdk'; +import {FieldId, ViewId, RecordId} from '../../shared/types/hyper_ids'; import {TableData} from '../types/table'; -import {ViewType, ViewId} from '../types/view'; -import {FieldId, FieldType, FieldOptions} from '../types/field'; -import {RecordId} from '../types/record'; -import {MutationTypes, PermissionCheckResult} from '../types/mutations'; -import {isEnumValue, entries, has, ObjectValues, cast, ObjectMap, keys} from '../private_utils'; -import {spawnError} from '../error_utils'; -import AbstractModel from './abstract_model'; -import View from './view'; +import {FieldType, FieldOptions} from '../../shared/types/field'; +import {MutationTypes} from '../types/mutations'; +import {BaseSdkMode} from '../../sdk_mode'; +import {PermissionCheckResult} from '../../shared/types/mutations_core'; +import {ChangedPathsForType} from '../../shared/models/base_core'; +import RecordStore from './record_store'; +import RecordQueryResult, {RecordQueryResultOpts} from './record_query_result'; import Field from './field'; -import Base, {ChangedPathsForType} from './base'; +import TableOrViewQueryResult from './table_or_view_query_result'; +import View from './view'; import ObjectPool from './object_pool'; +import Base from './base'; import Record from './record'; -import RecordQueryResult, {RecordQueryResultOpts} from './record_query_result'; -import TableOrViewQueryResult from './table_or_view_query_result'; -import RecordStore from './record_store'; export const WatchableTableKeys = Object.freeze({ - name: 'name' as const, - description: 'description' as const, + ...WatchableTableKeysCore, views: 'views' as const, - fields: 'fields' as const, }); /** * A key in {@link Table} that can be watched. * - `name` * - `description` - * - `views` * - `fields` + * - `views` */ export type WatchableTableKey = ObjectValues; @@ -38,7 +46,7 @@ export type WatchableTableKey = ObjectValues; * * @docsPath models/Table */ -class Table extends AbstractModel { +class Table extends TableCore { /** @internal */ static _className = 'Table'; /** @internal */ @@ -46,77 +54,23 @@ class Table extends AbstractModel { return isEnumValue(WatchableTableKeys, key); } /** @internal */ - _parentBase: Base; - /** @internal */ _viewModelsById: {[key: string]: View}; /** @internal */ - _fieldModelsById: {[key: string]: Field}; - /** @internal */ - _cachedFieldNamesById: {[key: string]: string} | null; - /** @internal */ - _recordStore: RecordStore; - /** @internal */ __tableOrViewQueryResultPool: ObjectPool; - /** - * @internal - */ - constructor(parentBase: Base, recordStore: RecordStore, tableId: string, sdk: Sdk) { - super(sdk, tableId); - this._parentBase = parentBase; - this._recordStore = recordStore; - this._viewModelsById = {}; - this._fieldModelsById = {}; - this._cachedFieldNamesById = null; + /** @internal */ + constructor(parentBase: Base, recordStore: RecordStore, tableId: string, sdk: BlockSdk) { + super(parentBase, recordStore, tableId, sdk); + this._viewModelsById = {}; this.__tableOrViewQueryResultPool = new ObjectPool(TableOrViewQueryResult); } - /** - * @internal - */ - get _dataOrNullIfDeleted(): TableData | null { - return this._baseData.tablesById[this._id] ?? null; - } - /** - * The base that this table belongs to. - * - * @internal (since we may not be able to return parent model instances in the immutable models world) - * @example - * ```js - * import {base} from '@airtable/blocks'; - * const table = base.getTableByName('Table 1'); - * console.log(table.parentBase.id === base.id); - * // => true - * ``` - */ - get parentBase(): Base { - return this._parentBase; - } - /** - * The name of the table. Can be watched. - * - * @example - * ```js - * console.log(myTable.name); - * // => 'Table 1' - * ``` - */ - get name(): string { - return this._data.name; - } - /** - * The description of the table, if it has one. Can be watched. - * - * @example - * ```js - * console.log(myTable.description); - * // => 'This is my table' - * ``` - */ - get description(): string | null { - return this._data.description; + /** @internal */ + _constructField(fieldId: FieldId): Field { + return new Field(this.parentBase.__sdk, this, fieldId); } + /** * The URL for the table. You can visit this URL in the browser to be taken to the table in the Airtable UI. * @@ -129,165 +83,21 @@ class Table extends AbstractModel { get url(): string { return this._sdk.__airtableInterface.urlConstructor.getTableUrl(this.id); } - /** - * The table's primary field. Every table has exactly one primary - * field. The primary field of a table will not change. - * - * @example - * ```js - * console.log(myTable.primaryField.name); - * // => 'Name' - * ``` - */ - get primaryField(): Field { - const primaryField = this.getFieldById(this._data.primaryFieldId); - return primaryField; - } - /** - * The fields in this table. The order is arbitrary, since fields are - * only ordered in the context of a specific view. - * - * Can be watched to know when fields are created or deleted. - * - * @example - * ```js - * console.log(`This table has ${myTable.fields.length} fields`); - * ``` - */ - get fields(): Array { - const fields = []; - for (const fieldId of Object.keys(this._data.fieldsById)) { - const field = this.getFieldById(fieldId); - fields.push(field); - } - return fields; - } - /** - * Gets the field matching the given ID, or `null` if that field does not exist in this table. - * @param fieldId The ID of the field. - * @example - * ```js - * const fieldId = 'fldxxxxxxxxxxxxxx'; - * const field = myTable.getFieldByIdIfExists(fieldId); - * if (field !== null) { - * console.log(field.name); - * } else { - * console.log('No field exists with that ID'); - * } - * ``` - */ - getFieldByIdIfExists(fieldId: FieldId): Field | null { - if (!this._data.fieldsById[fieldId]) { - return null; - } else { - if (!this._fieldModelsById[fieldId]) { - this._fieldModelsById[fieldId] = new Field(this._sdk, this, fieldId); - } - return this._fieldModelsById[fieldId]; - } - } /** - * Gets the field matching the given ID. Throws if that field does not exist in this table. Use - * {@link getFieldByIdIfExists} instead if you are unsure whether a field exists with the given - * ID. - * - * @param fieldId The ID of the field. - * @example - * ```js - * const fieldId = 'fldxxxxxxxxxxxxxx'; - * const field = myTable.getFieldById(fieldId); - * console.log(field.name); - * // => 'Name' - * ``` - */ - getFieldById(fieldId: FieldId): Field { - const field = this.getFieldByIdIfExists(fieldId); - if (!field) { - throw spawnError("No field with ID %s in table '%s'", fieldId, this.name); - } - return field; - } - /** - * Gets the field matching the given name, or `null` if no field exists with that name in this - * table. - * - * @param fieldName The name of the field you're looking for. - * @example - * ```js - * const field = myTable.getFieldByNameIfExists('Name'); - * if (field !== null) { - * console.log(field.id); - * } else { - * console.log('No field exists with that name'); - * } - * ``` - */ - getFieldByNameIfExists(fieldName: string): Field | null { - for (const [fieldId, fieldData] of entries(this._data.fieldsById)) { - if (fieldData.name === fieldName) { - return this.getFieldByIdIfExists(fieldId); - } - } - return null; - } - /** - * Gets the field matching the given name. Throws if no field exists with that name in this - * table. Use {@link getFieldByNameIfExists} instead if you are unsure whether a field exists - * with the given name. - * - * @param fieldName The name of the field you're looking for. - * @example - * ```js - * const field = myTable.getFieldByName('Name'); - * console.log(field.id); - * // => 'fldxxxxxxxxxxxxxx' - * ``` - */ - getFieldByName(fieldName: string): Field { - const field = this.getFieldByNameIfExists(fieldName); - if (!field) { - throw spawnError("No field named '%s' in table '%s'", fieldName, this.name); - } - return field; - } - /** - * The field matching the given ID or name. Returns `null` if no matching field exists within - * this table. - * - * This method is convenient when building an extension for a specific base, but for more generic - * extensions the best practice is to use the {@link getFieldByIdIfExists} or - * {@link getFieldByNameIfExists} methods instead. - * - * @param fieldIdOrName The ID or name of the field you're looking for. + * @internal */ - getFieldIfExists(fieldIdOrName: FieldId | string): Field | null { - return ( - this.getFieldByIdIfExists(fieldIdOrName) ?? this.getFieldByNameIfExists(fieldIdOrName) + async getDefaultCellValuesByFieldIdAsync(opts?: { + view?: View | null; + }): Promise<{[key: string]: unknown}> { + const viewId = opts && opts.view ? opts.view.id : null; + const cellValuesByFieldId = await this._sdk.__airtableInterface.fetchDefaultCellValuesByFieldIdAsync( + this._id, + viewId, ); + return cellValuesByFieldId; } - /** - * The field matching the given ID or name. Throws if no matching field exists within this table. - * Use {@link getFieldIfExists} instead if you are unsure whether a field exists with the given - * name/ID. - * - * This method is convenient when building an extension for a specific base, but for more generic - * extensions the best practice is to use the {@link getFieldById} or {@link getFieldByName} methods - * instead. - * - * @param fieldIdOrName The ID or name of the field you're looking for. - */ - getField(fieldIdOrName: FieldId | string): Field { - const field = this.getFieldIfExists(fieldIdOrName); - if (!field) { - throw spawnError( - "No field with ID or name '%s' in table '%s'", - fieldIdOrName, - this.name, - ); - } - return field; - } + /** * The views in this table. Can be watched to know when views are created, * deleted, or reordered. @@ -534,25 +344,195 @@ class Table extends AbstractModel { } } - return ( - this.views.find(view => { - return allowedViewTypes.includes(view.type); - }) ?? null - ); - } - /** - * @internal - */ - async getDefaultCellValuesByFieldIdAsync(opts?: { - view?: View | null; - }): Promise<{[key: string]: unknown}> { - const viewId = opts && opts.view ? opts.view.id : null; - const cellValuesByFieldId = await this._sdk.__airtableInterface.fetchDefaultCellValuesByFieldIdAsync( - this._id, - viewId, - ); - return cellValuesByFieldId; + return ( + this.views.find(view => { + return allowedViewTypes.includes(view.type); + }) ?? null + ); + } + /** + * @internal + */ + __getViewMatching(viewOrViewIdOrViewName: View | string): View { + let view: View | null; + if (viewOrViewIdOrViewName instanceof View) { + if (viewOrViewIdOrViewName.parentTable.id !== this.id) { + throw spawnError( + "View '%s' is from a different table than table '%s'", + viewOrViewIdOrViewName.name, + this.name, + ); + } + view = viewOrViewIdOrViewName; + } else { + view = + this.getViewByIdIfExists(viewOrViewIdOrViewName) || + this.getViewByNameIfExists(viewOrViewIdOrViewName); + + if (view === null) { + throw spawnError( + "View '%s' does not exist in table '%s'", + viewOrViewIdOrViewName, + this.name, + ); + } + } + + if (view.isDeleted) { + throw spawnError("View '%s' was deleted from table '%s'", view.name, this.name); + } + return view; + } + + /** + * Checks whether the current user has permission to create a field in this table. + * + * Accepts partial input, in the same format as {@link createFieldAsync}. + * + * Returns `{hasPermission: true}` if the current user can update the specified record, + * `{hasPermission: false, reasonDisplayString: string}` otherwise. `reasonDisplayString` may be + * used to display an error message to the user. + * + * @param name name for the field. must be case-insensitive unique for the table + * @param type type for the field + * @param options options for the field. omit for fields without writable options + * @param description description for the field. omit to leave blank + * + * @example + * ```js + * const createFieldCheckResult = table.checkPermissionsForCreateField(); + * + * if (!createFieldCheckResult.hasPermission) { + * alert(createFieldCheckResult.reasonDisplayString); + * } + * ``` + */ + checkPermissionsForCreateField( + name?: string, + type?: FieldType, + options?: FieldOptions | null, + description?: string | null, + ): PermissionCheckResult { + return this._sdk.__mutations.checkPermissionsForMutation({ + type: MutationTypes.CREATE_SINGLE_FIELD, + tableId: this.id, + id: undefined, + name, + config: type + ? { + type: type, + ...(options ? {options} : null), + } + : undefined, + description, + }); + } + + /** + * An alias for `checkPermissionsForCreateField(name, type, options, description).hasPermission`. + * + * Checks whether the current user has permission to create a field in this table. + * + * Accepts partial input, in the same format as {@link createFieldAsync}. + * + * @param name name for the field. must be case-insensitive unique for the table + * @param type type for the field + * @param options options for the field. omit for fields without writable options + * @param description description for the field. omit to leave blank + * + * @example + * ```js + * const canCreateField = table.hasPermissionToCreateField(); + * + * if (!canCreateField) { + * alert('not allowed!'); + * } + * ``` + */ + hasPermissionToCreateField( + name?: string, + type?: FieldType, + options?: FieldOptions | null, + description?: string | null, + ): boolean { + return this.checkPermissionsForCreateField(name, type, options, description).hasPermission; + } + + /** + * Creates a new field. + * + * Similar to creating a field from the Airtable UI, the new field will not be visible + * in views that have other hidden fields and views that are publicly shared. + * + * Throws an error if the user does not have permission to create a field, if invalid + * name, type or options are provided, or if creating fields of this type is not supported. + * + * Refer to {@link FieldType} for supported field types, the write format for options, and + * other specifics for certain field types. + * + * This action is asynchronous. Unlike new records, new fields are **not** created + * optimistically locally. You must `await` the returned promise before using the new + * field in your extension. + * + * @param name name for the field. must be case-insensitive unique + * @param type type for the field + * @param options options for the field. omit for fields without writable options + * @param description description for the field. is optional and will be `''` if not specified + * or if specified as `null`. + * + * @example + * ```js + * async function createNewSingleLineTextField(table, name) { + * if (table.hasPermissionToCreateField(name, FieldType.SINGLE_LINE_TEXT)) { + * await table.createFieldAsync(name, FieldType.SINGLE_LINE_TEXT); + * } + * } + * + * async function createNewCheckboxField(table, name) { + * const options = { + * icon: 'check', + * color: 'greenBright', + * }; + * if (table.hasPermissionToCreateField(name, FieldType.CHECKBOX, options)) { + * await table.createFieldAsync(name, FieldType.CHECKBOX, options); + * } + * } + * + * async function createNewDateField(table, name) { + * const options = { + * dateFormat: { + * name: 'iso', + * }, + * }; + * if (table.hasPermissionToCreateField(name, FieldType.DATE, options)) { + * await table.createFieldAsync(name, FieldType.DATE, options); + * } + * } + * ``` + */ + async createFieldAsync( + name: string, + type: FieldType, + options?: FieldOptions | null, + description?: string | null, + ): Promise { + const fieldId = this._sdk.__airtableInterface.idGenerator.generateFieldId(); + + await this._sdk.__mutations.applyMutationAsync({ + type: MutationTypes.CREATE_SINGLE_FIELD, + tableId: this.id, + id: fieldId, + name, + config: { + type: type, + ...(options ? {options} : null), + }, + description: description ?? null, + }); + + return this.getFieldById(fieldId); } + /** * Updates cell values for a record. * @@ -1544,238 +1524,11 @@ class Table extends AbstractModel { ): boolean { return this.checkPermissionsForCreateRecords(records).hasPermission; } - /** @internal */ - _cellValuesByFieldIdOrNameToCellValuesByFieldId( - cellValuesByFieldIdOrName: ObjectMap, - ): ObjectMap { - return Object.fromEntries( - entries(cellValuesByFieldIdOrName).map(([fieldIdOrName, cellValue]) => { - const field = this.__getFieldMatching(fieldIdOrName); - return [field.id, cellValue]; - }), - ); - } - /** - * Checks whether the current user has permission to create a field in this table. - * - * Accepts partial input, in the same format as {@link createFieldAsync}. - * - * Returns `{hasPermission: true}` if the current user can update the specified record, - * `{hasPermission: false, reasonDisplayString: string}` otherwise. `reasonDisplayString` may be - * used to display an error message to the user. - * - * @param name name for the field. must be case-insensitive unique for the table - * @param type type for the field - * @param options options for the field. omit for fields without writable options - * @param description description for the field. omit to leave blank - * - * @example - * ```js - * const createFieldCheckResult = table.checkPermissionsForCreateField(); - * - * if (!createFieldCheckResult.hasPermission) { - * alert(createFieldCheckResult.reasonDisplayString); - * } - * ``` - */ - checkPermissionsForCreateField( - name?: string, - type?: FieldType, - options?: FieldOptions | null, - description?: string | null, - ): PermissionCheckResult { - return this._sdk.__mutations.checkPermissionsForMutation({ - type: MutationTypes.CREATE_SINGLE_FIELD, - tableId: this.id, - id: undefined, - name, - config: type - ? { - type: type, - ...(options ? {options} : null), - } - : undefined, - description, - }); - } - - /** - * An alias for `checkPermissionsForCreateField(name, type, options, description).hasPermission`. - * - * Checks whether the current user has permission to create a field in this table. - * - * Accepts partial input, in the same format as {@link createFieldAsync}. - * - * @param name name for the field. must be case-insensitive unique for the table - * @param type type for the field - * @param options options for the field. omit for fields without writable options - * @param description description for the field. omit to leave blank - * - * @example - * ```js - * const canCreateField = table.hasPermissionToCreateField(); - * - * if (!canCreateField) { - * alert('not allowed!'); - * } - * ``` - */ - hasPermissionToCreateField( - name?: string, - type?: FieldType, - options?: FieldOptions | null, - description?: string | null, - ): boolean { - return this.checkPermissionsForCreateField(name, type, options, description).hasPermission; - } - - /** - * Creates a new field. - * - * Similar to creating a field from the Airtable UI, the new field will not be visible - * in views that have other hidden fields and views that are publicly shared. - * - * Throws an error if the user does not have permission to create a field, if invalid - * name, type or options are provided, or if creating fields of this type is not supported. - * - * Refer to {@link FieldType} for supported field types, the write format for options, and - * other specifics for certain field types. - * - * This action is asynchronous. Unlike new records, new fields are **not** created - * optimistically locally. You must `await` the returned promise before using the new - * field in your extension. - * - * @param name name for the field. must be case-insensitive unique - * @param type type for the field - * @param options options for the field. omit for fields without writable options - * @param description description for the field. is optional and will be `''` if not specified - * or if specified as `null`. - * - * @example - * ```js - * async function createNewSingleLineTextField(table, name) { - * if (table.hasPermissionToCreateField(name, FieldType.SINGLE_LINE_TEXT)) { - * await table.createFieldAsync(name, FieldType.SINGLE_LINE_TEXT); - * } - * } - * - * async function createNewCheckboxField(table, name) { - * const options = { - * icon: 'check', - * color: 'greenBright', - * }; - * if (table.hasPermissionToCreateField(name, FieldType.CHECKBOX, options)) { - * await table.createFieldAsync(name, FieldType.CHECKBOX, options); - * } - * } - * - * async function createNewDateField(table, name) { - * const options = { - * dateFormat: { - * name: 'iso', - * }, - * }; - * if (table.hasPermissionToCreateField(name, FieldType.DATE, options)) { - * await table.createFieldAsync(name, FieldType.DATE, options); - * } - * } - * ``` - */ - async createFieldAsync( - name: string, - type: FieldType, - options?: FieldOptions | null, - description?: string | null, - ): Promise { - const fieldId = this._sdk.__airtableInterface.idGenerator.generateFieldId(); - - await this._sdk.__mutations.applyMutationAsync({ - type: MutationTypes.CREATE_SINGLE_FIELD, - tableId: this.id, - id: fieldId, - name, - config: { - type: type, - ...(options ? {options} : null), - }, - description: description ?? null, - }); - - return this.getFieldById(fieldId); - } - /** - * @internal - */ - __getFieldMatching(fieldOrFieldIdOrFieldName: Field | string): Field { - let field: Field | null; - if (fieldOrFieldIdOrFieldName instanceof Field) { - if (fieldOrFieldIdOrFieldName.parentTable.id !== this.id) { - throw spawnError( - "Field '%s' is from a different table than table '%s'", - fieldOrFieldIdOrFieldName.name, - this.name, - ); - } - field = fieldOrFieldIdOrFieldName; - } else { - field = - this.getFieldByIdIfExists(fieldOrFieldIdOrFieldName) || - this.getFieldByNameIfExists(fieldOrFieldIdOrFieldName); - - if (field === null) { - throw spawnError( - "Field '%s' does not exist in table '%s'", - fieldOrFieldIdOrFieldName, - this.name, - ); - } - } - - if (field.isDeleted) { - throw spawnError("Field '%s' was deleted from table '%s'", field.name, this.name); - } - return field; - } - /** - * @internal - */ - __getViewMatching(viewOrViewIdOrViewName: View | string): View { - let view: View | null; - if (viewOrViewIdOrViewName instanceof View) { - if (viewOrViewIdOrViewName.parentTable.id !== this.id) { - throw spawnError( - "View '%s' is from a different table than table '%s'", - viewOrViewIdOrViewName.name, - this.name, - ); - } - view = viewOrViewIdOrViewName; - } else { - view = - this.getViewByIdIfExists(viewOrViewIdOrViewName) || - this.getViewByNameIfExists(viewOrViewIdOrViewName); - - if (view === null) { - throw spawnError( - "View '%s' does not exist in table '%s'", - viewOrViewIdOrViewName, - this.name, - ); - } - } - if (view.isDeleted) { - throw spawnError("View '%s' was deleted from table '%s'", view.name, this.name); - } - return view; - } - /** - * @internal - */ + /** @internal */ __triggerOnChangeForDirtyPaths(dirtyPaths: ChangedPathsForType): boolean { let didTableSchemaChange = false; - if (dirtyPaths.name) { - this._onChange(WatchableTableKeys.name); + if (super.__triggerOnChangeForDirtyPaths(dirtyPaths)) { didTableSchemaChange = true; } if (dirtyPaths.viewOrder) { @@ -1788,16 +1541,6 @@ class Table extends AbstractModel { } } } - if (dirtyPaths.lock) { - didTableSchemaChange = true; - } - if (dirtyPaths.externalSyncById) { - didTableSchemaChange = true; - } - if (dirtyPaths.description) { - this._onChange(WatchableTableKeys.description); - didTableSchemaChange = true; - } if (dirtyPaths.viewsById) { for (const [viewId, dirtyViewPaths] of entries(dirtyPaths.viewsById)) { const view = this._viewModelsById[viewId]; @@ -1809,56 +1552,9 @@ class Table extends AbstractModel { } } } - if (dirtyPaths.fieldsById) { - didTableSchemaChange = true; - - const addedFieldIds: Array = []; - const removedFieldIds: Array = []; - for (const [fieldId, dirtyFieldPaths] of entries(dirtyPaths.fieldsById)) { - if (dirtyFieldPaths && dirtyFieldPaths._isDirty) { - if (has(this._data.fieldsById, fieldId)) { - addedFieldIds.push(fieldId); - } else { - removedFieldIds.push(fieldId); - - const fieldModel = this._fieldModelsById[fieldId]; - if (fieldModel) { - delete this._fieldModelsById[fieldId]; - } - } - } else { - const field = this._fieldModelsById[fieldId]; - if (field) { - field.__triggerOnChangeForDirtyPaths(dirtyFieldPaths); - } - } - } - - if (addedFieldIds.length > 0 || removedFieldIds.length > 0) { - this._onChange(WatchableTableKeys.fields, { - addedFieldIds, - removedFieldIds, - }); - } - this._cachedFieldNamesById = null; - } - this._recordStore.triggerOnChangeForDirtyPaths(dirtyPaths); return didTableSchemaChange; } - /** - * @internal - */ - __getFieldNamesById(): {[key: string]: string} { - if (!this._cachedFieldNamesById) { - const fieldNamesById: ObjectMap = {}; - for (const [fieldId, fieldData] of entries(this._data.fieldsById)) { - fieldNamesById[fieldId] = fieldData.name; - } - this._cachedFieldNamesById = fieldNamesById; - } - return this._cachedFieldNamesById; - } } export default Table; diff --git a/packages/sdk/src/models/table_or_view_query_result.ts b/packages/sdk/src/base/models/table_or_view_query_result.ts similarity index 99% rename from packages/sdk/src/models/table_or_view_query_result.ts rename to packages/sdk/src/base/models/table_or_view_query_result.ts index fc92e1eac..743af803c 100644 --- a/packages/sdk/src/models/table_or_view_query_result.ts +++ b/packages/sdk/src/base/models/table_or_view_query_result.ts @@ -1,6 +1,6 @@ /** @module @airtable/blocks/models: RecordQueryResult */ /** */ import Sdk from '../sdk'; -import {FieldId} from '../types/field'; +import {FieldId, RecordId} from '../../shared/types/hyper_ids'; import { has, arrayDifference, @@ -8,10 +8,9 @@ import { FlowAnyExistential, FlowAnyFunction, ObjectMap, -} from '../private_utils'; -import {invariant, spawnError} from '../error_utils'; +} from '../../shared/private_utils'; +import {invariant, spawnError} from '../../shared/error_utils'; import {VisList, NormalizedGroupLevel} from '../types/airtable_interface'; -import {RecordId} from '../types/record'; import {GroupLevelData, GroupData} from '../types/view'; import Table, {WatchableTableKeys} from './table'; import View from './view'; diff --git a/packages/sdk/src/models/view.ts b/packages/sdk/src/base/models/view.ts similarity index 96% rename from packages/sdk/src/models/view.ts rename to packages/sdk/src/base/models/view.ts index ac4698ccd..a87a89c8f 100644 --- a/packages/sdk/src/models/view.ts +++ b/packages/sdk/src/base/models/view.ts @@ -1,9 +1,11 @@ /** @module @airtable/blocks/models: View */ /** */ import Sdk from '../sdk'; import {ViewData, ViewType} from '../types/view'; -import {isEnumValue, ObjectValues, FlowAnyObject} from '../private_utils'; -import {MutationTypes, PermissionCheckResult} from '../types/mutations'; -import AbstractModel from './abstract_model'; +import {isEnumValue, ObjectValues, FlowAnyObject} from '../../shared/private_utils'; +import {MutationTypes} from '../types/mutations'; +import AbstractModel from '../../shared/models/abstract_model'; +import {PermissionCheckResult} from '../../shared/types/mutations_core'; +import {BaseSdkMode} from '../../sdk_mode'; import ObjectPool from './object_pool'; import Table from './table'; import RecordQueryResult, { @@ -32,7 +34,7 @@ export type WatchableViewKey = ObjectValues; * * @docsPath models/View */ -class View extends AbstractModel { +class View extends AbstractModel { /** @internal */ static _className = 'View'; /** @internal */ diff --git a/packages/sdk/src/models/view_data_store.ts b/packages/sdk/src/base/models/view_data_store.ts similarity index 97% rename from packages/sdk/src/models/view_data_store.ts rename to packages/sdk/src/base/models/view_data_store.ts index 21e9f295a..f10b2a9eb 100644 --- a/packages/sdk/src/models/view_data_store.ts +++ b/packages/sdk/src/base/models/view_data_store.ts @@ -6,15 +6,14 @@ import { has, ObjectMap, cloneDeep, -} from '../private_utils'; -import {invariant} from '../error_utils'; -import {ModelChange} from '../types/base'; +} from '../../shared/private_utils'; +import {invariant} from '../../shared/error_utils'; import Sdk from '../sdk'; -import {FieldId} from '../types/field'; -import {GroupData, GroupLevelData, ViewData, ViewId} from '../types/view'; -import {RecordId} from '../types/record'; +import {FieldId, ViewId, RecordId} from '../../shared/types/hyper_ids'; +import {GroupData, GroupLevelData, ViewData} from '../types/view'; import {AirtableInterface} from '../types/airtable_interface'; -import {Color} from '../colors'; +import {Color} from '../../shared/colors'; +import {ModelChange} from '../../shared/types/base_core'; import AbstractModelWithAsyncData from './abstract_model_with_async_data'; import RecordStore from './record_store'; import Record from './record'; diff --git a/packages/sdk/src/models/view_metadata_query_result.ts b/packages/sdk/src/base/models/view_metadata_query_result.ts similarity index 97% rename from packages/sdk/src/models/view_metadata_query_result.ts rename to packages/sdk/src/base/models/view_metadata_query_result.ts index 1e56d5ce4..59d6e25e1 100644 --- a/packages/sdk/src/models/view_metadata_query_result.ts +++ b/packages/sdk/src/base/models/view_metadata_query_result.ts @@ -1,9 +1,9 @@ /** @module @airtable/blocks/models: View */ /** */ import Sdk from '../sdk'; -import {FieldId} from '../types/field'; +import {FieldId} from '../../shared/types/hyper_ids'; import {NormalizedGroupLevel} from '../types/airtable_interface'; -import {invariant} from '../error_utils'; -import {isEnumValue, getLocallyUniqueId, ObjectValues} from '../private_utils'; +import {invariant} from '../../shared/error_utils'; +import {isEnumValue, getLocallyUniqueId, ObjectValues} from '../../shared/private_utils'; import AbstractModelWithAsyncData from './abstract_model_with_async_data'; import ViewDataStore from './view_data_store'; import View from './view'; diff --git a/packages/sdk/src/perform_record_action.ts b/packages/sdk/src/base/perform_record_action.ts similarity index 98% rename from packages/sdk/src/perform_record_action.ts rename to packages/sdk/src/base/perform_record_action.ts index 07cac1ce8..7329d4b3e 100644 --- a/packages/sdk/src/perform_record_action.ts +++ b/packages/sdk/src/base/perform_record_action.ts @@ -1,9 +1,9 @@ -import {invariant} from './error_utils'; +import {invariant} from '../shared/error_utils'; +import {isEnumValue, ObjectValues} from '../shared/private_utils'; import {AirtableInterface} from './types/airtable_interface'; import {RecordActionData, RecordActionDataCallback} from './types/record_action_data'; import AbstractModelWithAsyncData from './models/abstract_model_with_async_data'; import Sdk from './sdk'; -import {isEnumValue, ObjectValues} from './private_utils'; /** @hidden */ export const WatchablePerformRecordActionKeys = Object.freeze({ diff --git a/packages/sdk/src/sdk.ts b/packages/sdk/src/base/sdk.ts similarity index 61% rename from packages/sdk/src/sdk.ts rename to packages/sdk/src/base/sdk.ts index 7da831c23..da1111c45 100644 --- a/packages/sdk/src/sdk.ts +++ b/packages/sdk/src/base/sdk.ts @@ -3,44 +3,25 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import {ModelChange} from './types/base'; -import GlobalConfig from './global_config'; -import {GlobalConfigUpdate} from './types/global_config'; +import {ModelChange} from '../shared/types/base_core'; +import {GlobalConfigUpdate} from '../shared/types/global_config'; +import {BlockSdkCore} from '../shared/sdk_core'; +import {BaseSdkMode} from '../sdk_mode'; +import Viewport from './viewport'; import Base from './models/base'; import Session from './models/session'; import Mutations from './models/mutations'; import Cursor from './models/cursor'; -import Viewport from './viewport'; import SettingsButton from './settings_button'; import UndoRedo from './undo_redo'; import {PerformRecordAction} from './perform_record_action'; -import {AirtableInterface, AppInterface, BlockRunContext} from './types/airtable_interface'; +import {AirtableInterface, BlockRunContext} from './types/airtable_interface'; import {RequestJson, ResponseJson} from './types/backend_fetch_types'; if (!(React as any).PropTypes) { (React as any).PropTypes = PropTypes; } -/** - * @hidden - * @example - * ```js - * import {runInfo} from '@airtable/blocks'; - * if (runInfo.isFirstRun) { - * // The current user just installed this block. - * // Take the opportunity to show any onboarding and set - * // sensible defaults if the user has permission. - * // For example, if the block relies on a table, it would - * // make sense to set that to cursor.activeTableId - * } - * ``` - */ -export interface RunInfo { - isFirstRun: boolean; - isDevelopmentMode: boolean; - intentData: unknown; -} - /** @hidden */ type UpdateBatcher = (applyUpdates: () => void) => void; @@ -54,51 +35,12 @@ function defaultUpdateBatcher(applyUpdates: () => void) { * * @hidden */ -export default class BlockSdk { - /** - * This value is used by the blocks-testing library to verify - * compatibility. - * - * @hidden - */ - // @ts-ignore - static VERSION = global.PACKAGE_VERSION; - - /** @internal */ - __airtableInterface: AirtableInterface; - - /** Storage for this block installation's configuration. */ - globalConfig: GlobalConfig; - - /** Represents the current Airtable {@link Base}. */ - base: Base; - - /** Contains information about the current session. */ - session: Session; - - /** @internal */ - __mutations: Mutations; - - /** - * Returns the ID for the current block installation. - * - * @example - * ```js - * import {installationId} from '@airtable/blocks'; - * console.log(installationId); - * // => 'blifDutUr92OKwnUn' - * ``` - */ - installationId: string; - +export default class BaseBlockSdk extends BlockSdkCore { /** Controls the block's viewport. You can fullscreen the block and add size * constrains using `viewport`. */ viewport: Viewport; - /** @hidden */ - runInfo: RunInfo; - /** Returns information about the active table, active view, and selected records. */ cursor: Cursor; @@ -118,42 +60,40 @@ export default class BlockSdk { /** @hidden */ constructor(airtableInterface: AirtableInterface) { - this.__airtableInterface = airtableInterface; - // @ts-ignore - airtableInterface.assertAllowedSdkPackageVersion(global.PACKAGE_NAME, BlockSdk.VERSION); + super(airtableInterface); const sdkInitData = airtableInterface.sdkInitData; - this.globalConfig = new GlobalConfig(sdkInitData.initialKvValuesByKey, this); - this.base = new Base(this); - this.installationId = sdkInitData.blockInstallationId; - this.reload = this.reload.bind(this); this.unstable_fetchAsync = this.unstable_fetchAsync.bind(this); this.viewport = new Viewport(sdkInitData.isFullscreen, airtableInterface); this.cursor = new Cursor(this); - this.session = new Session(this); - this.__mutations = new Mutations( - this, - this.session, - this.base, - changes => this.__applyModelChanges(changes), - updates => this.__applyGlobalConfigUpdates(updates), - ); this.settingsButton = new SettingsButton(airtableInterface); this.undoRedo = new UndoRedo(airtableInterface); this.performRecordAction = new PerformRecordAction(this, airtableInterface); - this.runInfo = Object.freeze({ - isFirstRun: sdkInitData.isFirstRun, - isDevelopmentMode: sdkInitData.isDevelopmentMode, - intentData: sdkInitData.intentData, - }); - this._registerHandlers(); } /** @internal */ + _constructSession(): Session { + return new Session(this); + } + /** @internal */ + _constructBase(): Base { + return new Base(this); + } + /** @internal */ + _constructMutations(): Mutations { + return new Mutations( + this, + this.session, + this.base, + changes => this.__applyModelChanges(changes), + updates => this.__applyGlobalConfigUpdates(updates), + ); + } + /** @internal */ __applyModelChanges(changes: ReadonlyArray) { this._runWithUpdateBatching(() => { const changedBasePaths = this.base.__applyChangesWithoutTriggeringEvents(changes); @@ -205,35 +145,11 @@ export default class BlockSdk { }); }); } - /** - * Call this function to reload your block. - * - * @example - * ```js - * import React from 'react'; - * import {reload} from '@airtable/blocks'; - * import {Button, initializeBlock} from '@airtable/blocks/ui'; - * function MyBlock() { - * return ; - * } - * initializeBlock(() => ); - * ``` - */ - reload() { - this.__airtableInterface.reloadFrame(); - } /** @internal */ __setBatchedUpdatesFn(newUpdateBatcher: UpdateBatcher) { this._runWithUpdateBatching = newUpdateBatcher; } - /** - * @internal - */ - get __appInterface(): AppInterface { - return this.base._baseData.appInterface; - } - /** @hidden */ async unstable_fetchAsync(requestJson: RequestJson): Promise { return await this.__airtableInterface.performBackendFetchAsync(requestJson); diff --git a/packages/sdk/src/settings_button.ts b/packages/sdk/src/base/settings_button.ts similarity index 96% rename from packages/sdk/src/settings_button.ts rename to packages/sdk/src/base/settings_button.ts index 60900bd52..40c343712 100644 --- a/packages/sdk/src/settings_button.ts +++ b/packages/sdk/src/base/settings_button.ts @@ -1,6 +1,6 @@ /** @module @airtable/blocks: settingsButton */ /** */ -import Watchable from './watchable'; -import {isEnumValue, ObjectValues} from './private_utils'; +import Watchable from '../shared/watchable'; +import {isEnumValue, ObjectValues} from '../shared/private_utils'; import {AirtableInterface} from './types/airtable_interface'; const WatchableSettingsButtonKeys = Object.freeze({ diff --git a/packages/sdk/src/types/aggregators.ts b/packages/sdk/src/base/types/aggregators.ts similarity index 100% rename from packages/sdk/src/types/aggregators.ts rename to packages/sdk/src/base/types/aggregators.ts diff --git a/packages/sdk/src/types/airtable_interface.ts b/packages/sdk/src/base/types/airtable_interface.ts similarity index 64% rename from packages/sdk/src/types/airtable_interface.ts rename to packages/sdk/src/base/types/airtable_interface.ts index a451022b3..a1e67d8c4 100644 --- a/packages/sdk/src/types/airtable_interface.ts +++ b/packages/sdk/src/base/types/airtable_interface.ts @@ -1,35 +1,23 @@ -import {ObjectMap} from '../private_utils'; import {NormalizedSortConfig} from '../models/record_query_result'; -import {Stat} from './stat'; +import {TableId, RecordId, ViewId} from '../../shared/types/hyper_ids'; +import { + AirtableInterfaceCore, + AppInterface, + FieldTypeProviderCore, + FieldTypeConfig, + SdkInitDataCore, +} from '../../shared/types/airtable_interface_core'; +import {FieldData} from '../../shared/types/field'; +import {BaseSdkMode} from '../../sdk_mode'; +import {RecordData} from './record'; +import {ViewportSizeConstraint} from './viewport'; import {AggregatorKey} from './aggregators'; -import {BaseData, BasePermissionData, ModelChange} from './base'; -import {BlockInstallationId} from './block'; +import {BaseData} from './base'; import {CursorData} from './cursor'; -import {FieldData, FieldId, FieldType} from './field'; import {RecordActionData, RecordActionDataCallback} from './record_action_data'; -import { - GlobalConfigUpdate, - GlobalConfigData, - GlobalConfigPath, - GlobalConfigPathValidationResult, -} from './global_config'; -import {RecordData, RecordId} from './record'; import {UndoRedoMode} from './undo_redo'; -import {ViewportSizeConstraint} from './viewport'; -import { - Mutation, - PartialMutation, - PermissionCheckResult, - UpdateFieldOptionsOpts, -} from './mutations'; -import {TableId} from './table'; -import { - GroupData, - ViewColorsByRecordIdData, - ViewFieldOrderData, - ViewId, - GroupLevelData, -} from './view'; +import {UpdateFieldOptionsOpts} from './mutations'; +import {GroupData, ViewColorsByRecordIdData, ViewFieldOrderData, GroupLevelData} from './view'; import {RequestJson, ResponseJson} from './backend_fetch_types'; /** @hidden */ @@ -53,6 +41,32 @@ export interface ViewBlockRunContext { /** @hidden */ export type BlockRunContext = BlockInstallationPageBlockRunContext | ViewBlockRunContext; +/** @hidden */ +type FieldConfigValidationResult = {isValid: true} | {isValid: false; reason: string}; +/** @hidden */ +interface FieldUiConfig { + iconName: string; + desiredCellWidthForRecordCard: number; + minimumCellWidthForRecordCard: number; +} +/** @hidden */ +export interface FieldTypeProvider extends FieldTypeProviderCore { + validateConfigForUpdate( + appInterface: AppInterface, + newConfig: FieldTypeConfig, + currentConfig: FieldTypeConfig | null, + fieldData: FieldData | null, + billingPlanGrouping: string, + opts?: UpdateFieldOptionsOpts, + ): FieldConfigValidationResult; + canBePrimary( + appInterface: AppInterface, + config: FieldTypeConfig, + billingPlanGrouping: string, + ): boolean; + getUiConfig: (appInterface: AppInterface, fieldData: FieldData) => FieldUiConfig; +} + /** @hidden */ export interface PartialViewData { visibleRecordIds: Array; @@ -72,16 +86,10 @@ export interface NormalizedViewMetadata { } /** @hidden */ -export interface SdkInitData { - initialKvValuesByKey: GlobalConfigData; - isDevelopmentMode: boolean; +export interface SdkInitData extends SdkInitDataCore { + runContext: BlockRunContext; baseData: BaseData; - blockInstallationId: BlockInstallationId; isFullscreen: boolean; - isFirstRun: boolean; - intentData: unknown; - isUsingNewLookupCellValueFormat?: true | undefined; - runContext: BlockRunContext; locale?: string; defaultLocale?: string; } @@ -131,88 +139,6 @@ export interface Aggregators { getAvailableAggregatorKeysForField(fieldData: FieldData): Array; } -/** @hidden */ -type CellValueValidationResult = {isValid: true} | {isValid: false; reason: string}; -/** @hidden */ -type FieldConfigValidationResult = {isValid: true} | {isValid: false; reason: string}; -/** @hidden */ -export interface FieldTypeConfig { - type: FieldType; - options?: {[key: string]: unknown}; -} -/** @hidden */ -interface FieldUiConfig { - iconName: string; - desiredCellWidthForRecordCard: number; - minimumCellWidthForRecordCard: number; -} - -/** @hidden */ -export interface FieldTypeProvider { - isComputed(fieldData: FieldData): boolean; - validateCellValueForUpdate( - appInterface: AppInterface, - newCellValue: unknown, - currentCellValue: unknown, - fieldData: FieldData, - ): CellValueValidationResult; - getConfig( - appInterface: AppInterface, - fieldData: FieldData, - fieldNamesById: ObjectMap, - ): FieldTypeConfig; - validateConfigForUpdate( - appInterface: AppInterface, - newConfig: FieldTypeConfig, - currentConfig: FieldTypeConfig | null, - fieldData: FieldData | null, - billingPlanGrouping: string, - opts?: UpdateFieldOptionsOpts, - ): FieldConfigValidationResult; - canBePrimary( - appInterface: AppInterface, - config: FieldTypeConfig, - billingPlanGrouping: string, - ): boolean; - convertStringToCellValue( - appInterface: AppInterface, - string: string, - fieldData: FieldData, - opts?: {parseDateCellValueInColumnTimeZone?: boolean}, - ): unknown; - convertCellValueToString( - appInterface: AppInterface, - cellValue: unknown, - fieldData: FieldData, - ): string; - getCellRendererData( - appInterface: AppInterface, - cellValue: unknown, - fieldData: FieldData, - shouldWrap: boolean, - ): {cellValueHtml: string; attributes: {[key: string]: unknown}}; - getUiConfig: (appInterface: AppInterface, fieldData: FieldData) => FieldUiConfig; -} - -/** @hidden */ -export interface GlobalConfigHelpers /**/ { - validatePath(path: GlobalConfigPath, store: GlobalConfigData): GlobalConfigPathValidationResult; - validateAndApplyUpdates( - updates: ReadonlyArray, - store: GlobalConfigData, - ): { - newKvStore: GlobalConfigData; - changedTopLevelKeys: Array; - }; -} - -/** - * AppInterface should never be used directly by the SDK, so we don't describe the type. - * - * @hidden - */ -export type AppInterface = unknown; - /** @hidden */ export interface VisList { removeRecordIds(recordIds: Array): void; @@ -228,15 +154,12 @@ export interface VisList { * * @hidden */ -export interface AirtableInterface { - sdkInitData: SdkInitData; +export interface AirtableInterface extends AirtableInterfaceCore { idGenerator: IdGenerator; urlConstructor: UrlConstructor; aggregators: Aggregators; - fieldTypeProvider: FieldTypeProvider; - globalConfigHelpers: GlobalConfigHelpers; - assertAllowedSdkPackageVersion: (packageName: string, packageVersion: string) => void; + fieldTypeProvider: FieldTypeProvider; /** * table @@ -261,17 +184,6 @@ export interface AirtableInterface { viewId: string | null, ): Promise<{[key: string]: unknown}>; - applyMutationAsync(mutation: Mutation, opts?: {holdForMs?: number}): Promise; - checkPermissionsForMutation( - mutation: PartialMutation, - basePermissionData: BasePermissionData, - ): PermissionCheckResult; - - // frontend only: - subscribeToModelUpdates(callback: (data: {changes: ReadonlyArray}) => void): void; - subscribeToGlobalConfigUpdates( - callback: (data: {updates: ReadonlyArray}) => void, - ): void; subscribeToSettingsButtonClick(callback: () => void): void; subscribeToEnterFullScreen(callback: () => void): void; subscribeToExitFullScreen(callback: () => void): void; @@ -290,7 +202,6 @@ export interface AirtableInterface { fieldIds: Array | null, shouldAllowCreatingRecord: boolean, ): Promise; - reloadFrame(): void; setSettingsButtonVisibility(isVisible: boolean): void; setUndoRedoMode(mode: UndoRedoMode): void; setFullscreenMaxSize(maxFullscreenSize: ViewportSizeConstraint): void; @@ -307,11 +218,4 @@ export interface AirtableInterface { callback: RecordActionDataCallback, ): Promise; performBackendFetchAsync(requestJson: RequestJson): Promise; - - /** - * internal utils - */ - trackEvent(eventSchemaName: string, eventData: {[key: string]: unknown}): void; - trackExposure(featureName: string): void; - sendStat(stat: Stat): void; } diff --git a/packages/sdk/src/types/backend_fetch_types.ts b/packages/sdk/src/base/types/backend_fetch_types.ts similarity index 100% rename from packages/sdk/src/types/backend_fetch_types.ts rename to packages/sdk/src/base/types/backend_fetch_types.ts diff --git a/packages/sdk/src/base/types/base.ts b/packages/sdk/src/base/types/base.ts new file mode 100644 index 000000000..b0aba6abe --- /dev/null +++ b/packages/sdk/src/base/types/base.ts @@ -0,0 +1,11 @@ +import {BaseDataCore} from '../../shared/types/base_core'; +import {TableId} from '../../shared/types/hyper_ids'; +import {TableData} from './table'; +import {CursorData} from './cursor'; + +/** @hidden */ +export interface BaseData extends BaseDataCore { + tableOrder: Array; + activeTableId: TableId | null; + cursorData: CursorData | null; +} diff --git a/packages/sdk/src/types/cursor.ts b/packages/sdk/src/base/types/cursor.ts similarity index 56% rename from packages/sdk/src/types/cursor.ts rename to packages/sdk/src/base/types/cursor.ts index aed9771f5..f46eef602 100644 --- a/packages/sdk/src/types/cursor.ts +++ b/packages/sdk/src/base/types/cursor.ts @@ -1,6 +1,5 @@ -import {ObjectMap} from '../private_utils'; -import {RecordId} from './record'; -import {FieldId} from './field'; +import {ObjectMap} from '../../shared/private_utils'; +import {FieldId, RecordId} from '../../shared/types/hyper_ids'; /** @hidden */ export interface CursorData { diff --git a/packages/sdk/src/types/mutations.ts b/packages/sdk/src/base/types/mutations.ts similarity index 84% rename from packages/sdk/src/types/mutations.ts rename to packages/sdk/src/base/types/mutations.ts index 579e86030..bc5ba3d32 100644 --- a/packages/sdk/src/types/mutations.ts +++ b/packages/sdk/src/base/types/mutations.ts @@ -1,18 +1,20 @@ /** @module @airtable/blocks: mutations */ /** */ -import {ObjectValues, ObjectMap} from '../private_utils'; -import {FieldTypeConfig, NormalizedViewMetadata} from './airtable_interface'; -import {GlobalConfigUpdate, GlobalConfigValue} from './global_config'; -import {TableId} from './table'; -import {FieldId} from './field'; -import {ViewId} from './view'; -import {RecordId} from './record'; +import {ObjectValues, ObjectMap} from '../../shared/private_utils'; +import {TableId, FieldId, ViewId, RecordId} from '../../shared/types/hyper_ids'; +import { + MutationTypesCore, + MutationCore, + PartialMutationCore, +} from '../../shared/types/mutations_core'; +import {FieldTypeConfig} from '../../shared/types/airtable_interface_core'; +import {NormalizedViewMetadata} from './airtable_interface'; /** @hidden */ export const MutationTypes = Object.freeze({ + ...MutationTypesCore, SET_MULTIPLE_RECORDS_CELL_VALUES: 'setMultipleRecordsCellValues' as const, DELETE_MULTIPLE_RECORDS: 'deleteMultipleRecords' as const, CREATE_MULTIPLE_RECORDS: 'createMultipleRecords' as const, - SET_MULTIPLE_GLOBAL_CONFIG_PATHS: 'setMultipleGlobalConfigPaths' as const, CREATE_SINGLE_FIELD: 'createSingleField' as const, UPDATE_SINGLE_FIELD_CONFIG: 'updateSingleFieldConfig' as const, UPDATE_SINGLE_FIELD_DESCRIPTION: 'updateSingleFieldDescription' as const, @@ -109,30 +111,6 @@ export interface PartialCreateMultipleRecordsMutation { readonly opts?: {parseDateCellValueInColumnTimeZone?: boolean}; } -/** - * The Mutation emitted when the App modifies one or more values in the - * {@link GlobalConfig}. - * - * @docsPath testing/mutations/SetMultipleGlobalConfigPathsMutation - */ -export interface SetMultipleGlobalConfigPathsMutation { - /** This Mutation's [discriminant property](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) */ - readonly type: typeof MutationTypes.SET_MULTIPLE_GLOBAL_CONFIG_PATHS; - /** One or more pairs of path and value */ - readonly updates: ReadonlyArray; -} - -/** @hidden */ -export interface PartialSetMultipleGlobalConfigPathsMutation { - readonly type: typeof MutationTypes.SET_MULTIPLE_GLOBAL_CONFIG_PATHS; - readonly updates: - | ReadonlyArray<{ - readonly path: ReadonlyArray | undefined; - readonly value: GlobalConfigValue | undefined | undefined; - }> - | undefined; -} - /** * The Mutation emitted when the App creates a {@link Field}. * @@ -312,10 +290,10 @@ export interface PartialUpdateViewMetadataMutation { /** @hidden */ export type Mutation = + | MutationCore | SetMultipleRecordsCellValuesMutation | DeleteMultipleRecordsMutation | CreateMultipleRecordsMutation - | SetMultipleGlobalConfigPathsMutation | CreateSingleFieldMutation | UpdateSingleFieldConfigMutation | UpdateSingleFieldDescriptionMutation @@ -325,36 +303,13 @@ export type Mutation = /** @hidden */ export type PartialMutation = + | PartialMutationCore | PartialSetMultipleRecordsCellValuesMutation | PartialDeleteMultipleRecordsMutation | PartialCreateMultipleRecordsMutation - | PartialSetMultipleGlobalConfigPathsMutation | PartialCreateSingleFieldMutation | PartialUpdateSingleFieldConfigMutation | PartialUpdateSingleFieldDescriptionMutation | PartialUpdateSingleFieldNameMutation | PartialCreateSingleTableMutation | PartialUpdateViewMetadataMutation; - -/** */ -export interface SuccessfulPermissionCheckResult { - /** */ - hasPermission: true; -} - -/** */ -export interface UnsuccessfulPermissionCheckResult { - /** */ - hasPermission: false; - /** - * A string explaining why the action is not permitted. These strings should only be used to - * show to the user; you should not rely on the format of the string as it may change without - * notice. - */ - reasonDisplayString: string; -} - -/** Indicates whether the user has permission to perform a particular action, and if not, why. */ -export type PermissionCheckResult = - | SuccessfulPermissionCheckResult - | UnsuccessfulPermissionCheckResult; diff --git a/packages/sdk/src/base/types/record.ts b/packages/sdk/src/base/types/record.ts new file mode 100644 index 000000000..61801a7eb --- /dev/null +++ b/packages/sdk/src/base/types/record.ts @@ -0,0 +1,7 @@ +/** @module @airtable/blocks/models: Record */ /** */ +import {RecordDataCore} from '../../shared/types/record'; + +/** @hidden */ +export interface RecordData extends RecordDataCore { + commentCount: number; +} diff --git a/packages/sdk/src/types/record_action_data.ts b/packages/sdk/src/base/types/record_action_data.ts similarity index 90% rename from packages/sdk/src/types/record_action_data.ts rename to packages/sdk/src/base/types/record_action_data.ts index 9b49ed979..9ed88e6ba 100644 --- a/packages/sdk/src/types/record_action_data.ts +++ b/packages/sdk/src/base/types/record_action_data.ts @@ -1,6 +1,4 @@ -import {RecordId} from './record'; -import {ViewId} from './view'; -import {TableId} from './table'; +import {TableId, RecordId, ViewId} from '../../shared/types/hyper_ids'; /** * The data format used by {@link useRecordActionData} and {@link registerRecordActionDataCallback} diff --git a/packages/sdk/src/base/types/table.ts b/packages/sdk/src/base/types/table.ts new file mode 100644 index 000000000..a2e2e6603 --- /dev/null +++ b/packages/sdk/src/base/types/table.ts @@ -0,0 +1,13 @@ +import {TableDataCore} from '../../shared/types/table_core'; +import {RecordId, ViewId} from '../../shared/types/hyper_ids'; +import {ObjectMap} from '../../shared/private_utils'; +import {ViewData} from './view'; +import {RecordData} from './record'; + +/** @hidden */ +export interface TableData extends TableDataCore { + activeViewId: ViewId | null; + viewOrder: Array; + viewsById: ObjectMap; + recordsById?: ObjectMap; +} diff --git a/packages/sdk/src/types/undo_redo.ts b/packages/sdk/src/base/types/undo_redo.ts similarity index 76% rename from packages/sdk/src/types/undo_redo.ts rename to packages/sdk/src/base/types/undo_redo.ts index 95db6b1a2..2bfd336d3 100644 --- a/packages/sdk/src/types/undo_redo.ts +++ b/packages/sdk/src/base/types/undo_redo.ts @@ -1,4 +1,4 @@ -import {ObjectValues} from '../private_utils'; +import {ObjectValues} from '../../shared/private_utils'; export const UndoRedoModes = Object.freeze({ NONE: 'none' as const, diff --git a/packages/sdk/src/types/view.ts b/packages/sdk/src/base/types/view.ts similarity index 89% rename from packages/sdk/src/types/view.ts rename to packages/sdk/src/base/types/view.ts index c3006097c..bf70d4a16 100644 --- a/packages/sdk/src/types/view.ts +++ b/packages/sdk/src/base/types/view.ts @@ -1,11 +1,7 @@ /** @module @airtable/blocks/models: View */ /** */ -import {ObjectMap} from '../private_utils'; -import {Color} from '../colors'; -import {FieldId} from './field'; -import {RecordId} from './record'; - -/** */ -export type ViewId = string; +import {ObjectMap} from '../../shared/private_utils'; +import {Color} from '../../shared/colors'; +import {FieldId, RecordId, ViewId} from '../../shared/types/hyper_ids'; /** * An enum of Airtable's view types diff --git a/packages/sdk/src/types/viewport.ts b/packages/sdk/src/base/types/viewport.ts similarity index 100% rename from packages/sdk/src/types/viewport.ts rename to packages/sdk/src/base/types/viewport.ts diff --git a/packages/sdk/src/ui/base_provider.tsx b/packages/sdk/src/base/ui/base_provider.tsx similarity index 95% rename from packages/sdk/src/ui/base_provider.tsx rename to packages/sdk/src/base/ui/base_provider.tsx index 849c3e302..e1b7db67d 100644 --- a/packages/sdk/src/ui/base_provider.tsx +++ b/packages/sdk/src/base/ui/base_provider.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import Base from '../models/base'; -import {SdkContext} from './sdk_context'; +import {SdkContext} from '../../shared/ui/sdk_context'; /** * Props for the {@link BaseProvider} component. diff --git a/packages/sdk/src/ui/baymax_utils.ts b/packages/sdk/src/base/ui/baymax_utils.ts similarity index 99% rename from packages/sdk/src/ui/baymax_utils.ts rename to packages/sdk/src/base/ui/baymax_utils.ts index ad65659f6..2a0326e04 100644 --- a/packages/sdk/src/ui/baymax_utils.ts +++ b/packages/sdk/src/base/ui/baymax_utils.ts @@ -1,6 +1,6 @@ import {css, keyframes} from 'emotion'; -import {has} from '../private_utils'; -import {spawnError} from '../error_utils'; +import {has} from '../../shared/private_utils'; +import {spawnError} from '../../shared/error_utils'; const bounceIn = keyframes` from, 50%, to { diff --git a/packages/sdk/src/ui/block_wrapper.tsx b/packages/sdk/src/base/ui/block_wrapper.tsx similarity index 95% rename from packages/sdk/src/ui/block_wrapper.tsx rename to packages/sdk/src/base/ui/block_wrapper.tsx index 056752bc9..b9d027279 100644 --- a/packages/sdk/src/ui/block_wrapper.tsx +++ b/packages/sdk/src/base/ui/block_wrapper.tsx @@ -1,14 +1,14 @@ /** @hidden */ /** */ import * as React from 'react'; -import {invariant} from '../error_utils'; +import {invariant} from '../../shared/error_utils'; import Sdk from '../sdk'; -import {globalAlert} from './ui'; +import withHooks from '../../shared/ui/with_hooks'; +import useWatchable from '../../shared/ui/use_watchable'; +import {SdkContext} from '../../shared/ui/sdk_context'; import {baymax} from './baymax_utils'; import Modal from './modal'; import Loader from './loader'; -import withHooks from './with_hooks'; -import useWatchable from './use_watchable'; -import {SdkContext} from './sdk_context'; +import {globalAlert} from './ui'; interface BlockWrapperProps { sdk: Sdk; diff --git a/packages/sdk/src/ui/box.tsx b/packages/sdk/src/base/ui/box.tsx similarity index 100% rename from packages/sdk/src/ui/box.tsx rename to packages/sdk/src/base/ui/box.tsx diff --git a/packages/sdk/src/ui/button.tsx b/packages/sdk/src/base/ui/button.tsx similarity index 96% rename from packages/sdk/src/ui/button.tsx rename to packages/sdk/src/base/ui/button.tsx index 020b9d8c2..388cfb112 100644 --- a/packages/sdk/src/ui/button.tsx +++ b/packages/sdk/src/base/ui/button.tsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import {cx} from 'emotion'; import * as React from 'react'; import {compose} from '@styled-system/core'; -import {createEnum, EnumType, createPropTypeFromEnum} from '../private_utils'; +import {createEnum, EnumType} from '../../shared/private_utils'; import useStyledSystem from './use_styled_system'; import {OptionalResponsiveProp} from './system/utils/types'; import createResponsivePropType from './system/utils/create_responsive_prop_type'; @@ -36,6 +36,7 @@ import {IconName, iconNamePropType} from './icon_config'; import Icon from './icon'; import cssHelpers from './css_helpers'; import Box from './box'; +import {createPropTypeFromEnum} from './system/utils/enum_prop_type_utils'; /** * Style props for the {@link Button} component. Also accepts: @@ -108,7 +109,10 @@ function useButtonVariant(variant: ButtonVariant = ButtonVariant.default): strin * @noInheritDoc * @docsPath UI/components/Button */ -interface ButtonProps extends AriaProps, ButtonStyleProps, TooltipAnchorProps { +export interface ButtonProps + extends AriaProps, + ButtonStyleProps, + TooltipAnchorProps { /** The size of the button. Defaults to `default`. Can be a responsive prop object. */ size?: ControlSizeProp; /** The variant of the button. Defaults to `default`. */ diff --git a/packages/sdk/src/ui/cell_renderer.tsx b/packages/sdk/src/base/ui/cell_renderer.tsx similarity index 95% rename from packages/sdk/src/ui/cell_renderer.tsx rename to packages/sdk/src/base/ui/cell_renderer.tsx index 1e748eb3a..d8eb77993 100644 --- a/packages/sdk/src/ui/cell_renderer.tsx +++ b/packages/sdk/src/base/ui/cell_renderer.tsx @@ -3,15 +3,17 @@ import PropTypes from 'prop-types'; import {cx} from 'emotion'; import * as React from 'react'; import {compose} from '@styled-system/core'; -import {spawnError} from '../error_utils'; +import {spawnError} from '../../shared/error_utils'; import Sdk from '../sdk'; import Record from '../models/record'; import Field from '../models/field'; -import {FieldType} from '../types/field'; -import {RecordId} from '../types/record'; -import {ObjectMap} from '../private_utils'; -import withHooks from './with_hooks'; -import useWatchable from './use_watchable'; +import {FieldType} from '../../shared/types/field'; +import {RecordId} from '../../shared/types/hyper_ids'; +import {ObjectMap} from '../../shared/private_utils'; +import withHooks from '../../shared/ui/with_hooks'; +import useWatchable from '../../shared/ui/use_watchable'; +import {useSdk} from '../../shared/ui/sdk_context'; +import {BaseSdkMode} from '../../sdk_mode'; import { display, displayPropTypes, @@ -38,7 +40,6 @@ import useStyledSystem from './use_styled_system'; import {splitStyleProps} from './with_styled_system'; import {OptionalResponsiveProp} from './system/utils/types'; import {tooltipAnchorPropTypes, TooltipAnchorProps} from './types/tooltip_anchor_props'; -import {useSdk} from './sdk_context'; /** * Style props for the {@link CellRenderer} component. Also accepts: @@ -296,7 +297,7 @@ export default withHooks<{className?: string; sdk: Sdk}, CellRendererProps, Cell styleProps, styleParser, ); - const sdk = useSdk(); + const sdk = useSdk(); useWatchable(props.record, [`cellValueInField:${props.field.id}`]); useWatchable(props.field, ['type', 'options']); return {className: cx(classNameForStyleProps, className), sdk}; diff --git a/packages/sdk/src/ui/choice_token.tsx b/packages/sdk/src/base/ui/choice_token.tsx similarity index 96% rename from packages/sdk/src/ui/choice_token.tsx rename to packages/sdk/src/base/ui/choice_token.tsx index d0ef2dbe3..6957fb72b 100644 --- a/packages/sdk/src/ui/choice_token.tsx +++ b/packages/sdk/src/base/ui/choice_token.tsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import {cx} from 'emotion'; import * as React from 'react'; import {compose} from '@styled-system/core'; -import {Color} from '../colors'; +import {Color} from '../../shared/colors'; import {baymax} from './baymax_utils'; import Box from './box'; import Text from './text'; @@ -59,7 +59,7 @@ interface ChoiceOption { * @docsPath UI/components/ChoiceToken * @noInheritDoc */ -interface ChoiceTokenProps extends ChoiceTokenStyleProps, TooltipAnchorProps { +export interface ChoiceTokenProps extends ChoiceTokenStyleProps, TooltipAnchorProps { /** An object representing a select option. You should not create these objects from scratch, but should instead grab them from base data. */ choice: ChoiceOption; /** Additional styles to apply to the choice token. */ diff --git a/packages/sdk/src/ui/collaborator_token.tsx b/packages/sdk/src/base/ui/collaborator_token.tsx similarity index 95% rename from packages/sdk/src/ui/collaborator_token.tsx rename to packages/sdk/src/base/ui/collaborator_token.tsx index a18d51653..e72b4dec7 100644 --- a/packages/sdk/src/ui/collaborator_token.tsx +++ b/packages/sdk/src/base/ui/collaborator_token.tsx @@ -3,7 +3,8 @@ import PropTypes from 'prop-types'; import {cx} from 'emotion'; import * as React from 'react'; import {compose} from '@styled-system/core'; -import {CollaboratorData} from '../types/collaborator'; +import {CollaboratorData} from '../../shared/types/collaborator'; +import useBase from '../../shared/ui/use_base'; import Box from './box'; import Text from './text'; import {baymax} from './baymax_utils'; @@ -20,7 +21,6 @@ import { MarginProps, } from './system'; import {tooltipAnchorPropTypes, TooltipAnchorProps} from './types/tooltip_anchor_props'; -import useBase from './use_base'; const UNKNOWN_PROFILE_PIC_URL = 'https://static.airtable.com/images/userIcons/user_icon_unknown.png'; @@ -50,7 +50,7 @@ export const collaboratorTokenStylePropTypes = { * @noInheritDoc * @docsPath UI/components/CollaboratorToken */ -interface CollaboratorTokenProps extends CollaboratorTokenStyleProps, TooltipAnchorProps { +export interface CollaboratorTokenProps extends CollaboratorTokenStyleProps, TooltipAnchorProps { /** An object representing a collaborator. You should not create these objects from scratch, but should instead grab them from base data. */ collaborator: Partial; /** Additional class names to apply to the collaborator token. */ diff --git a/packages/sdk/src/ui/color_palette.tsx b/packages/sdk/src/base/ui/color_palette.tsx similarity index 98% rename from packages/sdk/src/ui/color_palette.tsx rename to packages/sdk/src/base/ui/color_palette.tsx index 262ec6a60..28bd5037d 100644 --- a/packages/sdk/src/ui/color_palette.tsx +++ b/packages/sdk/src/base/ui/color_palette.tsx @@ -3,8 +3,8 @@ import PropTypes from 'prop-types'; import {cx} from 'emotion'; import * as React from 'react'; import {compose} from '@styled-system/core'; -import colorUtils from '../color_utils'; -import {invariant} from '../error_utils'; +import {invariant} from '../../shared/error_utils'; +import colorUtils from '../../shared/color_utils'; import {baymax} from './baymax_utils'; import Box from './box'; import Icon from './icon'; @@ -101,7 +101,7 @@ export const sharedColorPalettePropTypes = { * * @docsPath UI/components/ColorPalette */ -interface ColorPaletteProps extends SharedColorPaletteProps { +export interface ColorPaletteProps extends SharedColorPaletteProps { /** The current selected {@link Color} option. */ color?: string | null; } diff --git a/packages/sdk/src/ui/color_palette_synced.tsx b/packages/sdk/src/base/ui/color_palette_synced.tsx similarity index 92% rename from packages/sdk/src/ui/color_palette_synced.tsx rename to packages/sdk/src/base/ui/color_palette_synced.tsx index 0d2b18877..93fd34e9f 100644 --- a/packages/sdk/src/ui/color_palette_synced.tsx +++ b/packages/sdk/src/base/ui/color_palette_synced.tsx @@ -1,14 +1,14 @@ /** @module @airtable/blocks/ui: ColorPalette */ /** */ import * as React from 'react'; -import {spawnError} from '../error_utils'; -import {GlobalConfigKey} from '../types/global_config'; +import {spawnError} from '../../shared/error_utils'; +import {GlobalConfigKey} from '../../shared/types/global_config'; +import globalConfigSyncedComponentHelpers from '../../shared/ui/global_config_synced_component_helpers'; +import Synced from './synced'; import ColorPalette, { colorPaletteStylePropTypes, sharedColorPalettePropTypes, SharedColorPaletteProps, } from './color_palette'; -import Synced from './synced'; -import globalConfigSyncedComponentHelpers from './global_config_synced_component_helpers'; /** * Props for the {@link ColorPaletteSynced} component. Also accepts: diff --git a/packages/sdk/src/ui/confirmation_dialog.tsx b/packages/sdk/src/base/ui/confirmation_dialog.tsx similarity index 100% rename from packages/sdk/src/ui/confirmation_dialog.tsx rename to packages/sdk/src/base/ui/confirmation_dialog.tsx diff --git a/packages/sdk/src/ui/control_sizes.ts b/packages/sdk/src/base/ui/control_sizes.ts similarity index 94% rename from packages/sdk/src/ui/control_sizes.ts rename to packages/sdk/src/base/ui/control_sizes.ts index 96287e2a2..234cb89a0 100644 --- a/packages/sdk/src/ui/control_sizes.ts +++ b/packages/sdk/src/base/ui/control_sizes.ts @@ -1,11 +1,12 @@ /** @module @airtable/blocks/ui/system: Control sizes */ /** */ import {compose, system} from '@styled-system/core'; -import {createEnum, createResponsivePropTypeFromEnum, EnumType} from '../private_utils'; +import {createEnum, EnumType} from '../../shared/private_utils'; import useTheme from './theme/use_theme'; import {ResponsiveProp} from './system/utils/types'; import getStylePropsForResponsiveProp from './system/utils/get_style_props_for_responsive_prop'; import useStyledSystem from './use_styled_system'; -import {allStylesParser} from './system/'; +import {allStylesParser} from './system'; +import {createResponsivePropTypeFromEnum} from './system/utils/enum_prop_type_utils'; /** * Sizes for the {@link Button}, {@link Input}, {@link Select}, {@link SelectButtons}, and {@link Switch} components. diff --git a/packages/sdk/src/ui/create_detect_element_resize.d.ts b/packages/sdk/src/base/ui/create_detect_element_resize.d.ts similarity index 100% rename from packages/sdk/src/ui/create_detect_element_resize.d.ts rename to packages/sdk/src/base/ui/create_detect_element_resize.d.ts diff --git a/packages/sdk/src/ui/create_detect_element_resize.js b/packages/sdk/src/base/ui/create_detect_element_resize.js similarity index 100% rename from packages/sdk/src/ui/create_detect_element_resize.js rename to packages/sdk/src/base/ui/create_detect_element_resize.js diff --git a/packages/sdk/src/ui/css_helpers.ts b/packages/sdk/src/base/ui/css_helpers.ts similarity index 100% rename from packages/sdk/src/ui/css_helpers.ts rename to packages/sdk/src/base/ui/css_helpers.ts diff --git a/packages/sdk/src/ui/dialog.tsx b/packages/sdk/src/base/ui/dialog.tsx similarity index 100% rename from packages/sdk/src/ui/dialog.tsx rename to packages/sdk/src/base/ui/dialog.tsx diff --git a/packages/sdk/src/ui/dialog_close_button.tsx b/packages/sdk/src/base/ui/dialog_close_button.tsx similarity index 100% rename from packages/sdk/src/ui/dialog_close_button.tsx rename to packages/sdk/src/base/ui/dialog_close_button.tsx diff --git a/packages/sdk/src/ui/expand_record.ts b/packages/sdk/src/base/ui/expand_record.ts similarity index 100% rename from packages/sdk/src/ui/expand_record.ts rename to packages/sdk/src/base/ui/expand_record.ts diff --git a/packages/sdk/src/ui/expand_record_list.ts b/packages/sdk/src/base/ui/expand_record_list.ts similarity index 97% rename from packages/sdk/src/ui/expand_record_list.ts rename to packages/sdk/src/base/ui/expand_record_list.ts index 25e2c4bc2..441a0547c 100644 --- a/packages/sdk/src/ui/expand_record_list.ts +++ b/packages/sdk/src/base/ui/expand_record_list.ts @@ -1,5 +1,5 @@ /** @module @airtable/blocks/ui: expandRecordList */ /** */ -import {invariant} from '../error_utils'; +import {invariant} from '../../shared/error_utils'; import Record from '../models/record'; import Field from '../models/field'; diff --git a/packages/sdk/src/ui/expand_record_picker_async.ts b/packages/sdk/src/base/ui/expand_record_picker_async.ts similarity index 98% rename from packages/sdk/src/ui/expand_record_picker_async.ts rename to packages/sdk/src/base/ui/expand_record_picker_async.ts index d62ff7284..d55d34131 100644 --- a/packages/sdk/src/ui/expand_record_picker_async.ts +++ b/packages/sdk/src/base/ui/expand_record_picker_async.ts @@ -1,5 +1,5 @@ /** @module @airtable/blocks/ui: expandRecordPickerAsync */ /** */ -import {invariant} from '../error_utils'; +import {invariant} from '../../shared/error_utils'; import Record from '../models/record'; import Field from '../models/field'; diff --git a/packages/sdk/src/ui/field_icon.tsx b/packages/sdk/src/base/ui/field_icon.tsx similarity index 90% rename from packages/sdk/src/ui/field_icon.tsx rename to packages/sdk/src/base/ui/field_icon.tsx index ed58a7304..18acc4727 100644 --- a/packages/sdk/src/ui/field_icon.tsx +++ b/packages/sdk/src/base/ui/field_icon.tsx @@ -2,9 +2,10 @@ import PropTypes from 'prop-types'; import * as React from 'react'; import Field from '../models/field'; +import {useSdk} from '../../shared/ui/sdk_context'; +import {BaseSdkMode} from '../../sdk_mode'; import Icon, {sharedIconPropTypes, SharedIconProps} from './icon'; import {IconName} from './icon_config'; -import {useSdk} from './sdk_context'; /** * Props for the {@link FieldIcon} component. Also accepts: @@ -28,7 +29,7 @@ interface FieldIconProps extends SharedIconProps { */ const FieldIcon = (props: FieldIconProps) => { const {field, ...restOfProps} = props; - const sdk = useSdk(); + const sdk = useSdk(); const airtableInterface = sdk.__airtableInterface; const appInterface = sdk.__appInterface; diff --git a/packages/sdk/src/ui/field_picker.tsx b/packages/sdk/src/base/ui/field_picker.tsx similarity index 91% rename from packages/sdk/src/ui/field_picker.tsx rename to packages/sdk/src/base/ui/field_picker.tsx index 2abe62037..f0ff5023e 100644 --- a/packages/sdk/src/ui/field_picker.tsx +++ b/packages/sdk/src/base/ui/field_picker.tsx @@ -1,14 +1,15 @@ /** @module @airtable/blocks/ui: FieldPicker */ /** */ import PropTypes from 'prop-types'; import * as React from 'react'; -import {values, ObjectMap, has} from '../private_utils'; +import {values, ObjectMap, has} from '../../shared/private_utils'; import Field from '../models/field'; import Table from '../models/table'; -import {FieldType} from '../types/field'; +import {FieldType} from '../../shared/types/field'; +import useWatchable from '../../shared/ui/use_watchable'; +import {useSdk} from '../../shared/ui/sdk_context'; +import {BaseSdkMode} from '../../sdk_mode'; import {SharedSelectBaseProps, sharedSelectBasePropTypes} from './select'; import ModelPickerSelect from './model_picker_select'; -import useWatchable from './use_watchable'; -import {useSdk} from './sdk_context'; /** * Props shared between the {@link FieldPicker} and {@link FieldPickerSynced} components. @@ -41,7 +42,7 @@ export const sharedFieldPickerPropTypes = { * * @docsPath UI/components/FieldPicker */ -interface FieldPickerProps extends SharedFieldPickerProps { +export interface FieldPickerProps extends SharedFieldPickerProps { /** The selected field model. */ field?: Field | null; } @@ -64,7 +65,7 @@ const FieldPicker = (props: FieldPickerProps, ref: React.Ref) onChange, ...restOfProps } = props; - const sdk = useSdk(); + const sdk = useSdk(); useWatchable(sdk.base, ['tables']); useWatchable(table, ['fields']); diff --git a/packages/sdk/src/ui/field_picker_synced.tsx b/packages/sdk/src/base/ui/field_picker_synced.tsx similarity index 85% rename from packages/sdk/src/ui/field_picker_synced.tsx rename to packages/sdk/src/base/ui/field_picker_synced.tsx index 9e59fa4f8..0454645b9 100644 --- a/packages/sdk/src/ui/field_picker_synced.tsx +++ b/packages/sdk/src/base/ui/field_picker_synced.tsx @@ -1,12 +1,13 @@ /** @module @airtable/blocks/ui: FieldPicker */ /** */ import * as React from 'react'; import Field from '../models/field'; -import {GlobalConfigKey} from '../types/global_config'; -import globalConfigSyncedComponentHelpers from './global_config_synced_component_helpers'; +import {GlobalConfigKey} from '../../shared/types/global_config'; +import useSynced from '../../shared/ui/use_synced'; +import useWatchable from '../../shared/ui/use_watchable'; +import {useSdk} from '../../shared/ui/sdk_context'; +import {BaseSdkMode} from '../../sdk_mode'; +import globalConfigSyncedComponentHelpers from '../../shared/ui/global_config_synced_component_helpers'; import FieldPicker, {sharedFieldPickerPropTypes, SharedFieldPickerProps} from './field_picker'; -import useSynced from './use_synced'; -import useWatchable from './use_watchable'; -import {useSdk} from './sdk_context'; /** * Props for the {@link FieldPickerSynced} component. Also accepts: @@ -32,7 +33,7 @@ interface FieldPickerSyncedProps extends SharedFieldPickerProps { const FieldPickerSynced = (props: FieldPickerSyncedProps, ref: React.Ref) => { const {globalConfigKey, onChange, disabled, table, ...restOfProps} = props; const [fieldId, setFieldId, canSetFieldId] = useSynced(globalConfigKey); - const sdk = useSdk(); + const sdk = useSdk(); useWatchable(sdk.base, ['tables']); useWatchable(table, ['fields']); diff --git a/packages/sdk/src/ui/form_field.tsx b/packages/sdk/src/base/ui/form_field.tsx similarity index 97% rename from packages/sdk/src/ui/form_field.tsx rename to packages/sdk/src/base/ui/form_field.tsx index ef2375982..31b82a884 100644 --- a/packages/sdk/src/ui/form_field.tsx +++ b/packages/sdk/src/base/ui/form_field.tsx @@ -3,7 +3,7 @@ import React, {useState} from 'react'; import PropTypes from 'prop-types'; import {cx} from 'emotion'; import {compose} from '@styled-system/core'; -import {getLocallyUniqueId} from '../private_utils'; +import {getLocallyUniqueId} from '../../shared/private_utils'; import Box from './box'; import Text, {TextSize} from './text'; import Label from './label'; @@ -67,7 +67,7 @@ export const formFieldStylePropTypes = { * @noInheritDoc * @docsPath UI/components/FormField */ -interface FormFieldProps extends FormFieldStyleProps { +export interface FormFieldProps extends FormFieldStyleProps { /** The `id` attribute. */ id?: string; /** Additional class names to apply to the form field. */ diff --git a/packages/sdk/src/ui/geometry/geometry.ts b/packages/sdk/src/base/ui/geometry/geometry.ts similarity index 100% rename from packages/sdk/src/ui/geometry/geometry.ts rename to packages/sdk/src/base/ui/geometry/geometry.ts diff --git a/packages/sdk/src/ui/geometry/point.ts b/packages/sdk/src/base/ui/geometry/point.ts similarity index 100% rename from packages/sdk/src/ui/geometry/point.ts rename to packages/sdk/src/base/ui/geometry/point.ts diff --git a/packages/sdk/src/ui/geometry/rect.ts b/packages/sdk/src/base/ui/geometry/rect.ts similarity index 100% rename from packages/sdk/src/ui/geometry/rect.ts rename to packages/sdk/src/base/ui/geometry/rect.ts diff --git a/packages/sdk/src/ui/geometry/size.ts b/packages/sdk/src/base/ui/geometry/size.ts similarity index 100% rename from packages/sdk/src/ui/geometry/size.ts rename to packages/sdk/src/base/ui/geometry/size.ts diff --git a/packages/sdk/src/ui/global_alert.tsx b/packages/sdk/src/base/ui/global_alert.tsx similarity index 90% rename from packages/sdk/src/ui/global_alert.tsx rename to packages/sdk/src/base/ui/global_alert.tsx index 722e4e143..4bc859fb7 100644 --- a/packages/sdk/src/ui/global_alert.tsx +++ b/packages/sdk/src/base/ui/global_alert.tsx @@ -1,9 +1,9 @@ /** @hidden */ /** */ import * as React from 'react'; -import {isEnumValue, ObjectValues} from '../private_utils'; -import Watchable from '../watchable'; +import {isEnumValue, ObjectValues} from '../../shared/private_utils'; +import Watchable from '../../shared/watchable'; +import {useSdk} from '../../shared/ui/sdk_context'; import {baymax} from './baymax_utils'; -import {useSdk} from './sdk_context'; const WatchableGlobalAlertKeys = Object.freeze({ __alertInfo: '__alertInfo' as const, diff --git a/packages/sdk/src/ui/heading.tsx b/packages/sdk/src/base/ui/heading.tsx similarity index 96% rename from packages/sdk/src/ui/heading.tsx rename to packages/sdk/src/base/ui/heading.tsx index ca23366be..a88be3e4d 100644 --- a/packages/sdk/src/ui/heading.tsx +++ b/packages/sdk/src/base/ui/heading.tsx @@ -2,16 +2,8 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import {cx} from 'emotion'; -import {invariant} from '../error_utils'; -import { - has, - createEnum, - createPropTypeFromEnum, - createResponsivePropTypeFromEnum, - ObjectMap, - keys, - EnumType, -} from '../private_utils'; +import {invariant} from '../../shared/error_utils'; +import {has, createEnum, ObjectMap, keys, EnumType} from '../../shared/private_utils'; import useStyledSystem from './use_styled_system'; import {allStylesPropTypes, AllStylesProps} from './system/index'; import {ResponsiveProp, ResponsiveKey} from './system/utils/types'; @@ -19,6 +11,10 @@ import getStylePropsForResponsiveProp from './system/utils/get_style_props_for_r import useTheme from './theme/use_theme'; import {ariaPropTypes, AriaProps} from './types/aria_props'; import {dataAttributesPropType, DataAttributesProp} from './types/data_attributes_prop'; +import { + createPropTypeFromEnum, + createResponsivePropTypeFromEnum, +} from './system/utils/enum_prop_type_utils'; /** * Sizes for the {@link Heading} component. @@ -94,7 +90,7 @@ function useHeadingStyle(headingSizeProp: HeadingSizeProp, variant: HeadingVaria * @docsPath UI/components/Heading * @noInheritDoc */ -interface HeadingProps extends AllStylesProps, AriaProps { +export interface HeadingProps extends AllStylesProps, AriaProps { /** The `role` attribute. */ role?: string; /** The element that is rendered. Defaults to `h3`. */ diff --git a/packages/sdk/src/ui/icon.tsx b/packages/sdk/src/base/ui/icon.tsx similarity index 98% rename from packages/sdk/src/ui/icon.tsx rename to packages/sdk/src/base/ui/icon.tsx index de63e0531..353fa393e 100644 --- a/packages/sdk/src/ui/icon.tsx +++ b/packages/sdk/src/base/ui/icon.tsx @@ -3,7 +3,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import {compose} from '@styled-system/core'; import {cx} from 'emotion'; -import warning from '../warning'; +import warning from '../../shared/warning'; import useStyledSystem from './use_styled_system'; import { flexItemSet, @@ -83,7 +83,7 @@ export const sharedIconPropTypes = { * Props for the {@link Icon} component. Also accepts: * * {@link IconStyleProps} */ -interface IconProps extends SharedIconProps { +export interface IconProps extends SharedIconProps { /** The name of the icon. For more details, see the {@link IconName|list of supported icons}. */ name: IconName; /** @internal */ diff --git a/packages/sdk/src/ui/icon_config.ts b/packages/sdk/src/base/ui/icon_config.ts similarity index 99% rename from packages/sdk/src/ui/icon_config.ts rename to packages/sdk/src/base/ui/icon_config.ts index df7638217..5407c6577 100644 --- a/packages/sdk/src/ui/icon_config.ts +++ b/packages/sdk/src/base/ui/icon_config.ts @@ -1,4 +1,5 @@ -import {createEnum, EnumType, createPropTypeFromEnum} from '../private_utils'; +import {createEnum, EnumType} from '../../shared/private_utils'; +import {createPropTypeFromEnum} from './system/utils/enum_prop_type_utils'; export const iconNamesArray = [ 'aiAssistant', diff --git a/packages/sdk/src/ui/initialize_block.tsx b/packages/sdk/src/base/ui/initialize_block.tsx similarity index 97% rename from packages/sdk/src/ui/initialize_block.tsx rename to packages/sdk/src/base/ui/initialize_block.tsx index e7e3a2c4b..ce689562b 100644 --- a/packages/sdk/src/ui/initialize_block.tsx +++ b/packages/sdk/src/base/ui/initialize_block.tsx @@ -1,9 +1,9 @@ /** @module @airtable/blocks/ui: initializeBlock */ /** */ import * as React from 'react'; import ReactDOM from 'react-dom'; -import {spawnError} from '../error_utils'; +import {spawnError} from '../../shared/error_utils'; import Sdk from '../sdk'; -import getAirtableInterface from '../injected/airtable_interface'; +import getAirtableInterface from '../../injected/airtable_interface'; import {BlockRunContextType} from '../types/airtable_interface'; import Table from '../models/table'; import View from '../models/view'; diff --git a/packages/sdk/src/ui/input.tsx b/packages/sdk/src/base/ui/input.tsx similarity index 97% rename from packages/sdk/src/ui/input.tsx rename to packages/sdk/src/base/ui/input.tsx index b0d951070..e3ee9d70c 100644 --- a/packages/sdk/src/ui/input.tsx +++ b/packages/sdk/src/base/ui/input.tsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import {cx} from 'emotion'; import * as React from 'react'; import {compose} from '@styled-system/core'; -import {createEnum, EnumType, createPropTypeFromEnum} from '../private_utils'; +import {createEnum, EnumType} from '../../shared/private_utils'; import useTheme from './theme/use_theme'; import useStyledSystem from './use_styled_system'; import useFormField from './use_form_field'; @@ -29,6 +29,7 @@ import { } from './system'; import {tooltipAnchorPropTypes, TooltipAnchorProps} from './types/tooltip_anchor_props'; import {ControlSizeProp, controlSizePropType, ControlSize, useInputSize} from './control_sizes'; +import {createPropTypeFromEnum} from './system/utils/enum_prop_type_utils'; /** @internal */ type InputVariant = EnumType; @@ -198,7 +199,7 @@ export const sharedInputPropTypes = { * * @docsPath UI/components/Input */ -interface InputProps extends SharedInputProps { +export interface InputProps extends SharedInputProps { /** The input's current value. */ value: string; } diff --git a/packages/sdk/src/ui/input_synced.tsx b/packages/sdk/src/base/ui/input_synced.tsx similarity index 89% rename from packages/sdk/src/ui/input_synced.tsx rename to packages/sdk/src/base/ui/input_synced.tsx index b271169b9..6284c3d70 100644 --- a/packages/sdk/src/ui/input_synced.tsx +++ b/packages/sdk/src/base/ui/input_synced.tsx @@ -1,10 +1,10 @@ /** @module @airtable/blocks/ui: Input */ /** */ import * as React from 'react'; -import {spawnError} from '../error_utils'; -import {GlobalConfigKey} from '../types/global_config'; +import {spawnError} from '../../shared/error_utils'; +import {GlobalConfigKey} from '../../shared/types/global_config'; +import useSynced from '../../shared/ui/use_synced'; +import globalConfigSyncedComponentHelpers from '../../shared/ui/global_config_synced_component_helpers'; import Input, {sharedInputPropTypes, SharedInputProps, SupportedInputType} from './input'; -import useSynced from './use_synced'; -import globalConfigSyncedComponentHelpers from './global_config_synced_component_helpers'; /** * Props for the {@link InputSynced} component. Also accepts: diff --git a/packages/sdk/src/ui/key_codes.ts b/packages/sdk/src/base/ui/key_codes.ts similarity index 100% rename from packages/sdk/src/ui/key_codes.ts rename to packages/sdk/src/base/ui/key_codes.ts diff --git a/packages/sdk/src/ui/label.tsx b/packages/sdk/src/base/ui/label.tsx similarity index 98% rename from packages/sdk/src/ui/label.tsx rename to packages/sdk/src/base/ui/label.tsx index 33690ae4d..f34a862a7 100644 --- a/packages/sdk/src/ui/label.tsx +++ b/packages/sdk/src/base/ui/label.tsx @@ -16,7 +16,7 @@ import {dataAttributesPropType, DataAttributesProp} from './types/data_attribute * @noInheritDoc * @docsPath UI/components/Label */ -interface LabelProps extends AllStylesProps, AriaProps { +export interface LabelProps extends AllStylesProps, AriaProps { /** The size of the label. Defaults to `default`. Can be a responsive prop object. */ size?: TextSizeProp; /** The `for` attribute. Should contain the `id` of the input. */ diff --git a/packages/sdk/src/ui/link.tsx b/packages/sdk/src/base/ui/link.tsx similarity index 97% rename from packages/sdk/src/ui/link.tsx rename to packages/sdk/src/base/ui/link.tsx index 4990417c6..a80132cc3 100644 --- a/packages/sdk/src/ui/link.tsx +++ b/packages/sdk/src/base/ui/link.tsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import * as React from 'react'; import {cx} from 'emotion'; import {compose} from '@styled-system/core'; -import {createEnum, EnumType, createPropTypeFromEnum} from '../private_utils'; +import {createEnum, EnumType} from '../../shared/private_utils'; import useStyledSystem from './use_styled_system'; import useTheme from './theme/use_theme'; import {ariaPropTypes, AriaProps} from './types/aria_props'; @@ -38,6 +38,7 @@ import {tooltipAnchorPropTypes, TooltipAnchorProps} from './types/tooltip_anchor import {useTextStyle, TextSize, TextSizeProp, textSizePropType} from './text'; import {IconName, iconNamePropType} from './icon_config'; import Icon from './icon'; +import {createPropTypeFromEnum} from './system/utils/enum_prop_type_utils'; /** * Style props for the {@link Link} component. Also accepts: @@ -118,7 +119,10 @@ function useLinkVariant(variant: LinkVariant = LinkVariant.default): string { * @docsPath UI/components/Link * @noInheritDoc */ -interface LinkProps extends AriaProps, LinkStyleProps, TooltipAnchorProps { +export interface LinkProps + extends AriaProps, + LinkStyleProps, + TooltipAnchorProps { /** The size of the link. Defaults to `default`. Can be a responsive prop object. */ size?: TextSizeProp; /** The variant of the link, which defines the color. Defaults to `default`. */ diff --git a/packages/sdk/src/ui/loader.tsx b/packages/sdk/src/base/ui/loader.tsx similarity index 64% rename from packages/sdk/src/ui/loader.tsx rename to packages/sdk/src/base/ui/loader.tsx index 623b01c6c..15b7f4180 100644 --- a/packages/sdk/src/ui/loader.tsx +++ b/packages/sdk/src/base/ui/loader.tsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import {cx} from 'emotion'; import * as React from 'react'; import {compose} from '@styled-system/core'; +import LoaderCore from '../../shared/ui/loader'; import {baymax} from './baymax_utils'; import useStyledSystem from './use_styled_system'; import { @@ -17,8 +18,6 @@ import { MarginProps, } from './system'; -const ORIGINAL_SIZE = 54; - /** * Style props for the {@link Loader} component. Accepts: * * {@link FlexItemSetProps} @@ -42,7 +41,7 @@ export const loaderStylePropTypes = { * @docsPath UI/components/Loader * @noInheritDoc */ -interface LoaderProps extends LoaderStyleProps { +export interface LoaderProps extends LoaderStyleProps { /** The color of the loading spinner. Defaults to `'#888'` */ fillColor: string; /** A scalar for the loading spinner. Increasing the scale increases the size of the loading spinner. Defaults to `0.3`. */ @@ -64,37 +63,17 @@ interface LoaderProps extends LoaderStyleProps { const Loader = (props: LoaderProps) => { const {fillColor, scale, className, style, ...styleProps} = props; const classNameForStyleProps = useStyledSystem(styleProps, styleParser); - return ( - - - - - - - + style={style} + /> ); }; diff --git a/packages/sdk/src/ui/modal.tsx b/packages/sdk/src/base/ui/modal.tsx similarity index 99% rename from packages/sdk/src/ui/modal.tsx rename to packages/sdk/src/base/ui/modal.tsx index e425f001b..a98af6ca6 100644 --- a/packages/sdk/src/ui/modal.tsx +++ b/packages/sdk/src/base/ui/modal.tsx @@ -4,7 +4,7 @@ import {cx} from 'emotion'; import * as React from 'react'; import ReactDOM from 'react-dom'; import {compose} from '@styled-system/core'; -import {invariant} from '../error_utils'; +import {invariant} from '../../shared/error_utils'; import {baymax} from './baymax_utils'; import withStyledSystem from './with_styled_system'; import { diff --git a/packages/sdk/src/ui/model_picker_select.tsx b/packages/sdk/src/base/ui/model_picker_select.tsx similarity index 96% rename from packages/sdk/src/ui/model_picker_select.tsx rename to packages/sdk/src/base/ui/model_picker_select.tsx index d9fbeb418..0180b026a 100644 --- a/packages/sdk/src/ui/model_picker_select.tsx +++ b/packages/sdk/src/base/ui/model_picker_select.tsx @@ -1,13 +1,13 @@ /** @hidden */ /** */ import * as React from 'react'; import PropTypes from 'prop-types'; -import {invariant} from '../error_utils'; +import {invariant} from '../../shared/error_utils'; import Table from '../models/table'; import View from '../models/view'; import Field from '../models/field'; +import useWatchable from '../../shared/ui/use_watchable'; import Select, {sharedSelectBasePropTypes, SharedSelectBaseProps} from './select'; import {SelectOptionValue} from './select_and_select_buttons_helpers'; -import useWatchable from './use_watchable'; type AnyModel = Table | View | Field; diff --git a/packages/sdk/src/ui/popover.tsx b/packages/sdk/src/base/ui/popover.tsx similarity index 99% rename from packages/sdk/src/ui/popover.tsx rename to packages/sdk/src/base/ui/popover.tsx index 5c19e1e8e..ceaebb19e 100644 --- a/packages/sdk/src/ui/popover.tsx +++ b/packages/sdk/src/base/ui/popover.tsx @@ -3,8 +3,8 @@ import PropTypes from 'prop-types'; import {cx} from 'emotion'; import ReactDOM from 'react-dom'; import * as React from 'react'; -import {values, ObjectValues, FlowAnyObject, FlowAnyFunction} from '../private_utils'; -import {invariant} from '../error_utils'; +import {values, ObjectValues, FlowAnyObject, FlowAnyFunction} from '../../shared/private_utils'; +import {invariant} from '../../shared/error_utils'; import {baymax} from './baymax_utils'; import createDetectElementResize from './create_detect_element_resize'; import * as Geometry from './geometry/geometry'; diff --git a/packages/sdk/src/ui/progress_bar.tsx b/packages/sdk/src/base/ui/progress_bar.tsx similarity index 96% rename from packages/sdk/src/ui/progress_bar.tsx rename to packages/sdk/src/base/ui/progress_bar.tsx index 4dc703be6..ddb7ec4cf 100644 --- a/packages/sdk/src/ui/progress_bar.tsx +++ b/packages/sdk/src/base/ui/progress_bar.tsx @@ -3,8 +3,8 @@ import PropTypes from 'prop-types'; import {cx, css} from 'emotion'; import * as React from 'react'; import {compose} from '@styled-system/core'; -import colors from '../colors'; -import {clamp} from '../private_utils'; +import {clamp} from '../../shared/private_utils'; +import colors from '../../shared/colors'; import {baymax} from './baymax_utils'; import useStyledSystem from './use_styled_system'; import { @@ -97,7 +97,7 @@ export const progressBarStylePropTypes = { * @docsPath UI/components/ProgressBar * @noInheritDoc */ -interface ProgressBarProps extends ProgressBarStyleProps, TooltipAnchorProps { +export interface ProgressBarProps extends ProgressBarStyleProps, TooltipAnchorProps { /** A CSS color, such as `#ff9900`. Defaults to a blue color. */ barColor?: string; /** A number between 0 and 1. 0 is 0% complete, 0.5 is 50% complete, 1 is 100% complete. If you include a number outside of the range, the value will be clamped to be inside of the range. */ diff --git a/packages/sdk/src/ui/record_card.tsx b/packages/sdk/src/base/ui/record_card.tsx similarity index 96% rename from packages/sdk/src/ui/record_card.tsx rename to packages/sdk/src/base/ui/record_card.tsx index bf84c068c..cad04a739 100644 --- a/packages/sdk/src/ui/record_card.tsx +++ b/packages/sdk/src/base/ui/record_card.tsx @@ -3,24 +3,30 @@ import PropTypes from 'prop-types'; import {cx} from 'emotion'; import * as React from 'react'; import {compose} from '@styled-system/core'; -import {isNullOrUndefinedOrEmpty, keyBy, uniqBy, FlowAnyObject, has} from '../private_utils'; -import {invariant, spawnError} from '../error_utils'; -import {AttachmentData} from '../types/attachment'; -import {FieldType} from '../types/field'; -import {RecordDef, RecordId} from '../types/record'; +import { + isNullOrUndefinedOrEmpty, + keyBy, + uniqBy, + FlowAnyObject, + has, +} from '../../shared/private_utils'; +import {invariant, spawnError} from '../../shared/error_utils'; +import {AttachmentData} from '../../shared/types/attachment'; +import {FieldType} from '../../shared/types/field'; +import {RecordDef} from '../../shared/types/record'; import Field from '../models/field'; import Record from '../models/record'; import View from '../models/view'; import ViewMetadataQueryResult from '../models/view_metadata_query_result'; -import colorUtils from '../color_utils'; import Sdk from '../sdk'; +import {RecordId} from '../../shared/types/hyper_ids'; +import useWatchable from '../../shared/ui/use_watchable'; +import withHooks from '../../shared/ui/with_hooks'; +import {useSdk} from '../../shared/ui/sdk_context'; +import {BaseSdkMode} from '../../sdk_mode'; +import colorUtils from '../../shared/color_utils'; import {baymax} from './baymax_utils'; -import expandRecord, {ExpandRecordOpts} from './expand_record'; import Box from './box'; -import CellRenderer from './cell_renderer'; -import useWatchable from './use_watchable'; -import withHooks from './with_hooks'; -import useViewMetadata from './use_view_metadata'; import {isCommandModifierKeyEvent} from './key_codes'; import useStyledSystem from './use_styled_system'; import { @@ -36,7 +42,9 @@ import { } from './system'; import {splitStyleProps} from './with_styled_system'; import {tooltipAnchorPropTypes} from './types/tooltip_anchor_props'; -import {useSdk} from './sdk_context'; +import useViewMetadata from './use_view_metadata'; +import CellRenderer from './cell_renderer'; +import expandRecord, {ExpandRecordOpts} from './expand_record'; const FALLBACK_RECORD_NAME_FOR_DISPLAY = 'Unnamed record'; @@ -124,7 +132,7 @@ CellValueAndFieldLabel.propTypes = { * @noInheritDoc * @docsPath UI/components/RecordCard */ -interface RecordCardProps extends RecordCardStyleProps { +export interface RecordCardProps extends RecordCardStyleProps { /** Record to display in the card. */ record: Record | RecordDef; /** The view model to use for field order and record coloring. */ @@ -671,7 +679,7 @@ export default withHooks< useWatchable(parentTable, ['fields']); const viewMetadata = useViewMetadata(view); - const sdk = useSdk(); + const sdk = useSdk(); return { viewMetadata, diff --git a/packages/sdk/src/ui/record_card_list.tsx b/packages/sdk/src/base/ui/record_card_list.tsx similarity index 98% rename from packages/sdk/src/ui/record_card_list.tsx rename to packages/sdk/src/base/ui/record_card_list.tsx index 5dbd7a1c5..a4be2df2c 100644 --- a/packages/sdk/src/ui/record_card_list.tsx +++ b/packages/sdk/src/base/ui/record_card_list.tsx @@ -3,12 +3,11 @@ import PropTypes from 'prop-types'; import * as React from 'react'; import {FixedSizeList} from 'react-window'; import {compose} from '@styled-system/core'; -import {invariant, spawnError} from '../error_utils'; -import {RecordDef} from '../types/record'; +import {invariant, spawnError} from '../../shared/error_utils'; +import {RecordDef} from '../../shared/types/record'; import Record from '../models/record'; import Field from '../models/field'; import View from '../models/view'; -import RecordCard from './record_card'; import Box from './box'; import createDetectElementResize from './create_detect_element_resize'; import withStyledSystem from './with_styled_system'; @@ -26,6 +25,7 @@ import { marginPropTypes, MarginProps, } from './system'; +import RecordCard from './record_card'; const RECORD_CARD_ROW_HEIGHT = 80; const RECORD_CARD_SPACING = 10; @@ -213,7 +213,7 @@ interface RecordCardListScrollEvent { * @docsPath UI/components/RecordCardList */ -interface RecordCardListProps { +export interface RecordCardListProps { /** Records to display in card list. */ records: Array | Array; /** Scroll event handler for the list window. */ @@ -245,7 +245,7 @@ interface RecordCardListProps { * * @noInheritDoc */ -interface RecordCardListStyleProps +export interface RecordCardListStyleProps extends DimensionsSetProps, FlexItemSetProps, PositionSetProps, diff --git a/packages/sdk/src/ui/select.tsx b/packages/sdk/src/base/ui/select.tsx similarity index 98% rename from packages/sdk/src/ui/select.tsx rename to packages/sdk/src/base/ui/select.tsx index 8d86e5b47..fa883e8a3 100644 --- a/packages/sdk/src/ui/select.tsx +++ b/packages/sdk/src/base/ui/select.tsx @@ -3,8 +3,8 @@ import {cx} from 'emotion'; import * as React from 'react'; import PropTypes from 'prop-types'; import {compose} from '@styled-system/core'; -import {createEnum, EnumType} from '../private_utils'; -import {spawnError} from '../error_utils'; +import {createEnum, EnumType} from '../../shared/private_utils'; +import {spawnError} from '../../shared/error_utils'; import useFormField from './use_form_field'; import { maxWidth, diff --git a/packages/sdk/src/ui/select_and_select_buttons_helpers.ts b/packages/sdk/src/base/ui/select_and_select_buttons_helpers.ts similarity index 100% rename from packages/sdk/src/ui/select_and_select_buttons_helpers.ts rename to packages/sdk/src/base/ui/select_and_select_buttons_helpers.ts diff --git a/packages/sdk/src/ui/select_buttons.tsx b/packages/sdk/src/base/ui/select_buttons.tsx similarity index 97% rename from packages/sdk/src/ui/select_buttons.tsx rename to packages/sdk/src/base/ui/select_buttons.tsx index 31b430539..8b286ab96 100644 --- a/packages/sdk/src/ui/select_buttons.tsx +++ b/packages/sdk/src/base/ui/select_buttons.tsx @@ -3,8 +3,8 @@ import {cx} from 'emotion'; import React, {useEffect, useState} from 'react'; import PropTypes from 'prop-types'; import {compose} from '@styled-system/core'; -import {spawnError} from '../error_utils'; -import {createEnum, EnumType, getLocallyUniqueId} from '../private_utils'; +import {spawnError} from '../../shared/error_utils'; +import {createEnum, EnumType, getLocallyUniqueId} from '../../shared/private_utils'; import { validateOptions, optionValueToString, @@ -135,7 +135,7 @@ export const sharedSelectButtonsPropTypes = { * * @docsPath UI/components/SelectButtons */ -interface SelectButtonsProps extends SharedSelectButtonsProps { +export interface SelectButtonsProps extends SharedSelectButtonsProps { /** The value of the selected option. */ value: SelectOptionValue; } diff --git a/packages/sdk/src/ui/select_buttons_synced.tsx b/packages/sdk/src/base/ui/select_buttons_synced.tsx similarity index 90% rename from packages/sdk/src/ui/select_buttons_synced.tsx rename to packages/sdk/src/base/ui/select_buttons_synced.tsx index 45c3bb22a..385ebb6e4 100644 --- a/packages/sdk/src/ui/select_buttons_synced.tsx +++ b/packages/sdk/src/base/ui/select_buttons_synced.tsx @@ -1,13 +1,13 @@ /** @module @airtable/blocks/ui: SelectButtons */ /** */ import * as React from 'react'; -import {spawnError} from '../error_utils'; -import {GlobalConfigKey} from '../types/global_config'; +import {spawnError} from '../../shared/error_utils'; +import {GlobalConfigKey} from '../../shared/types/global_config'; +import useSynced from '../../shared/ui/use_synced'; +import globalConfigSyncedComponentHelpers from '../../shared/ui/global_config_synced_component_helpers'; import SelectButtons, { sharedSelectButtonsPropTypes, SharedSelectButtonsProps, } from './select_buttons'; -import globalConfigSyncedComponentHelpers from './global_config_synced_component_helpers'; -import useSynced from './use_synced'; /** * Props for the {@link SelectButtonsSynced} component. Also accepts: diff --git a/packages/sdk/src/ui/select_synced.tsx b/packages/sdk/src/base/ui/select_synced.tsx similarity index 89% rename from packages/sdk/src/ui/select_synced.tsx rename to packages/sdk/src/base/ui/select_synced.tsx index 6c0b3da4b..bf1a8c231 100644 --- a/packages/sdk/src/ui/select_synced.tsx +++ b/packages/sdk/src/base/ui/select_synced.tsx @@ -1,10 +1,10 @@ /** @module @airtable/blocks/ui: Select */ /** */ import * as React from 'react'; -import {spawnError} from '../error_utils'; -import {GlobalConfigKey} from '../types/global_config'; -import globalConfigSyncedComponentHelpers from './global_config_synced_component_helpers'; +import {spawnError} from '../../shared/error_utils'; +import {GlobalConfigKey} from '../../shared/types/global_config'; +import useSynced from '../../shared/ui/use_synced'; +import globalConfigSyncedComponentHelpers from '../../shared/ui/global_config_synced_component_helpers'; import Select, {sharedSelectPropTypes, SharedSelectProps} from './select'; -import useSynced from './use_synced'; /** * Props for the {@link SelectSynced} component. Also accepts: diff --git a/packages/sdk/src/ui/switch.tsx b/packages/sdk/src/base/ui/switch.tsx similarity index 97% rename from packages/sdk/src/ui/switch.tsx rename to packages/sdk/src/base/ui/switch.tsx index bd20228cb..b27ff4f63 100644 --- a/packages/sdk/src/ui/switch.tsx +++ b/packages/sdk/src/base/ui/switch.tsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import {cx} from 'emotion'; import React from 'react'; import {compose} from '@styled-system/core'; -import {createEnum, EnumType, createPropTypeFromEnum} from '../private_utils'; +import {createEnum, EnumType} from '../../shared/private_utils'; import useStyledSystem from './use_styled_system'; import { maxWidth, @@ -34,6 +34,7 @@ import {OptionalResponsiveProp} from './system/utils/types'; import {tooltipAnchorPropTypes, TooltipAnchorProps} from './types/tooltip_anchor_props'; import useTheme from './theme/use_theme'; import {useSwitchSize, ControlSizeProp, ControlSize} from './control_sizes'; +import {createPropTypeFromEnum} from './system/utils/enum_prop_type_utils'; /** * Variants for the {@link Switch} component: @@ -94,7 +95,7 @@ export interface SharedSwitchProps extends TooltipAnchorProps, SwitchStyleProps * * @docsPath UI/components/Switch */ -interface SwitchProps extends SharedSwitchProps { +export interface SwitchProps extends SharedSwitchProps { /** If set to `true`, the switch will be switchd on. */ value: boolean; } diff --git a/packages/sdk/src/ui/switch_synced.tsx b/packages/sdk/src/base/ui/switch_synced.tsx similarity index 89% rename from packages/sdk/src/ui/switch_synced.tsx rename to packages/sdk/src/base/ui/switch_synced.tsx index 31b6d29dd..984e7288a 100644 --- a/packages/sdk/src/ui/switch_synced.tsx +++ b/packages/sdk/src/base/ui/switch_synced.tsx @@ -1,9 +1,9 @@ /** @module @airtable/blocks/ui: Switch */ /** */ import * as React from 'react'; -import {GlobalConfigKey} from '../types/global_config'; +import {GlobalConfigKey} from '../../shared/types/global_config'; +import useSynced from '../../shared/ui/use_synced'; +import globalConfigSyncedComponentHelpers from '../../shared/ui/global_config_synced_component_helpers'; import Switch, {sharedSwitchPropTypes, SharedSwitchProps} from './switch'; -import useSynced from './use_synced'; -import globalConfigSyncedComponentHelpers from './global_config_synced_component_helpers'; /** * Props for the {@link SwitchSynced} component. Also accepts: diff --git a/packages/sdk/src/ui/synced.ts b/packages/sdk/src/base/ui/synced.ts similarity index 58% rename from packages/sdk/src/ui/synced.ts rename to packages/sdk/src/base/ui/synced.ts index ae49a2cf1..4b2a7cebb 100644 --- a/packages/sdk/src/ui/synced.ts +++ b/packages/sdk/src/base/ui/synced.ts @@ -1,14 +1,14 @@ /** @hidden */ /** */ import PropTypes from 'prop-types'; import * as React from 'react'; -import {GlobalConfigKey, GlobalConfigValue} from '../types/global_config'; -import Sdk from '../sdk'; -import globalConfigSyncedComponentHelpers from './global_config_synced_component_helpers'; -import withHooks from './with_hooks'; -import {useSdk} from './sdk_context'; +import {GlobalConfigKey, GlobalConfigValue} from '../../shared/types/global_config'; +import {SdkMode} from '../../sdk_mode'; +import withHooks from '../../shared/ui/with_hooks'; +import {useSdk} from '../../shared/ui/sdk_context'; +import globalConfigSyncedComponentHelpers from '../../shared/ui/global_config_synced_component_helpers'; /** @hidden */ -interface SyncedProps { +interface SyncedProps { globalConfigKey: GlobalConfigKey; render: (arg1: { value: unknown; @@ -16,18 +16,18 @@ interface SyncedProps { setValue: (newValue: GlobalConfigValue | undefined) => void; }) => React.ReactElement; /** @internal injected by withHooks */ - sdk: Sdk; + sdk: SdkModeT['SdkT']; } /** @hidden */ -export class Synced extends React.Component { +export class Synced extends React.Component> { /** @hidden */ static propTypes = { globalConfigKey: globalConfigSyncedComponentHelpers.globalConfigKeyPropType, render: PropTypes.func.isRequired, }; /** @hidden */ - constructor(props: SyncedProps) { + constructor(props: SyncedProps) { super(props); this._setValue = this._setValue.bind(this); @@ -49,9 +49,14 @@ export class Synced extends React.Component { } } -export default withHooks<{sdk: Sdk}, SyncedProps, Synced>(Synced, props => { - globalConfigSyncedComponentHelpers.useDefaultWatchesForSyncedComponent(props.globalConfigKey); - return { - sdk: useSdk(), - }; -}); +export default withHooks<{sdk: SdkMode['SdkT']}, SyncedProps, Synced>( + Synced, + props => { + globalConfigSyncedComponentHelpers.useDefaultWatchesForSyncedComponent( + props.globalConfigKey, + ); + return { + sdk: useSdk(), + }; + }, +); diff --git a/packages/sdk/src/ui/system/all_styles_set.ts b/packages/sdk/src/base/ui/system/all_styles_set.ts similarity index 100% rename from packages/sdk/src/ui/system/all_styles_set.ts rename to packages/sdk/src/base/ui/system/all_styles_set.ts diff --git a/packages/sdk/src/ui/system/appearance/appearance_set.ts b/packages/sdk/src/base/ui/system/appearance/appearance_set.ts similarity index 100% rename from packages/sdk/src/ui/system/appearance/appearance_set.ts rename to packages/sdk/src/base/ui/system/appearance/appearance_set.ts diff --git a/packages/sdk/src/ui/system/appearance/background_color.ts b/packages/sdk/src/base/ui/system/appearance/background_color.ts similarity index 100% rename from packages/sdk/src/ui/system/appearance/background_color.ts rename to packages/sdk/src/base/ui/system/appearance/background_color.ts diff --git a/packages/sdk/src/ui/system/appearance/border.ts b/packages/sdk/src/base/ui/system/appearance/border.ts similarity index 100% rename from packages/sdk/src/ui/system/appearance/border.ts rename to packages/sdk/src/base/ui/system/appearance/border.ts diff --git a/packages/sdk/src/ui/system/appearance/border_radius.ts b/packages/sdk/src/base/ui/system/appearance/border_radius.ts similarity index 100% rename from packages/sdk/src/ui/system/appearance/border_radius.ts rename to packages/sdk/src/base/ui/system/appearance/border_radius.ts diff --git a/packages/sdk/src/ui/system/appearance/box_shadow.ts b/packages/sdk/src/base/ui/system/appearance/box_shadow.ts similarity index 100% rename from packages/sdk/src/ui/system/appearance/box_shadow.ts rename to packages/sdk/src/base/ui/system/appearance/box_shadow.ts diff --git a/packages/sdk/src/ui/system/appearance/opacity.ts b/packages/sdk/src/base/ui/system/appearance/opacity.ts similarity index 100% rename from packages/sdk/src/ui/system/appearance/opacity.ts rename to packages/sdk/src/base/ui/system/appearance/opacity.ts diff --git a/packages/sdk/src/ui/system/dimensions/dimensions_set.ts b/packages/sdk/src/base/ui/system/dimensions/dimensions_set.ts similarity index 100% rename from packages/sdk/src/ui/system/dimensions/dimensions_set.ts rename to packages/sdk/src/base/ui/system/dimensions/dimensions_set.ts diff --git a/packages/sdk/src/ui/system/dimensions/height.ts b/packages/sdk/src/base/ui/system/dimensions/height.ts similarity index 100% rename from packages/sdk/src/ui/system/dimensions/height.ts rename to packages/sdk/src/base/ui/system/dimensions/height.ts diff --git a/packages/sdk/src/ui/system/dimensions/max_height.ts b/packages/sdk/src/base/ui/system/dimensions/max_height.ts similarity index 100% rename from packages/sdk/src/ui/system/dimensions/max_height.ts rename to packages/sdk/src/base/ui/system/dimensions/max_height.ts diff --git a/packages/sdk/src/ui/system/dimensions/max_width.ts b/packages/sdk/src/base/ui/system/dimensions/max_width.ts similarity index 100% rename from packages/sdk/src/ui/system/dimensions/max_width.ts rename to packages/sdk/src/base/ui/system/dimensions/max_width.ts diff --git a/packages/sdk/src/ui/system/dimensions/min_height.ts b/packages/sdk/src/base/ui/system/dimensions/min_height.ts similarity index 100% rename from packages/sdk/src/ui/system/dimensions/min_height.ts rename to packages/sdk/src/base/ui/system/dimensions/min_height.ts diff --git a/packages/sdk/src/ui/system/dimensions/min_width.ts b/packages/sdk/src/base/ui/system/dimensions/min_width.ts similarity index 100% rename from packages/sdk/src/ui/system/dimensions/min_width.ts rename to packages/sdk/src/base/ui/system/dimensions/min_width.ts diff --git a/packages/sdk/src/ui/system/dimensions/width.ts b/packages/sdk/src/base/ui/system/dimensions/width.ts similarity index 100% rename from packages/sdk/src/ui/system/dimensions/width.ts rename to packages/sdk/src/base/ui/system/dimensions/width.ts diff --git a/packages/sdk/src/ui/system/display.ts b/packages/sdk/src/base/ui/system/display.ts similarity index 100% rename from packages/sdk/src/ui/system/display.ts rename to packages/sdk/src/base/ui/system/display.ts diff --git a/packages/sdk/src/ui/system/flex_container/align_content.ts b/packages/sdk/src/base/ui/system/flex_container/align_content.ts similarity index 100% rename from packages/sdk/src/ui/system/flex_container/align_content.ts rename to packages/sdk/src/base/ui/system/flex_container/align_content.ts diff --git a/packages/sdk/src/ui/system/flex_container/align_items.ts b/packages/sdk/src/base/ui/system/flex_container/align_items.ts similarity index 100% rename from packages/sdk/src/ui/system/flex_container/align_items.ts rename to packages/sdk/src/base/ui/system/flex_container/align_items.ts diff --git a/packages/sdk/src/ui/system/flex_container/flex_container_set.ts b/packages/sdk/src/base/ui/system/flex_container/flex_container_set.ts similarity index 100% rename from packages/sdk/src/ui/system/flex_container/flex_container_set.ts rename to packages/sdk/src/base/ui/system/flex_container/flex_container_set.ts diff --git a/packages/sdk/src/ui/system/flex_container/flex_direction.ts b/packages/sdk/src/base/ui/system/flex_container/flex_direction.ts similarity index 100% rename from packages/sdk/src/ui/system/flex_container/flex_direction.ts rename to packages/sdk/src/base/ui/system/flex_container/flex_direction.ts diff --git a/packages/sdk/src/ui/system/flex_container/flex_wrap.ts b/packages/sdk/src/base/ui/system/flex_container/flex_wrap.ts similarity index 100% rename from packages/sdk/src/ui/system/flex_container/flex_wrap.ts rename to packages/sdk/src/base/ui/system/flex_container/flex_wrap.ts diff --git a/packages/sdk/src/ui/system/flex_container/justify_content.ts b/packages/sdk/src/base/ui/system/flex_container/justify_content.ts similarity index 100% rename from packages/sdk/src/ui/system/flex_container/justify_content.ts rename to packages/sdk/src/base/ui/system/flex_container/justify_content.ts diff --git a/packages/sdk/src/ui/system/flex_container/justify_items.ts b/packages/sdk/src/base/ui/system/flex_container/justify_items.ts similarity index 100% rename from packages/sdk/src/ui/system/flex_container/justify_items.ts rename to packages/sdk/src/base/ui/system/flex_container/justify_items.ts diff --git a/packages/sdk/src/ui/system/flex_item/align_self.ts b/packages/sdk/src/base/ui/system/flex_item/align_self.ts similarity index 100% rename from packages/sdk/src/ui/system/flex_item/align_self.ts rename to packages/sdk/src/base/ui/system/flex_item/align_self.ts diff --git a/packages/sdk/src/ui/system/flex_item/flex.ts b/packages/sdk/src/base/ui/system/flex_item/flex.ts similarity index 100% rename from packages/sdk/src/ui/system/flex_item/flex.ts rename to packages/sdk/src/base/ui/system/flex_item/flex.ts diff --git a/packages/sdk/src/ui/system/flex_item/flex_basis.ts b/packages/sdk/src/base/ui/system/flex_item/flex_basis.ts similarity index 100% rename from packages/sdk/src/ui/system/flex_item/flex_basis.ts rename to packages/sdk/src/base/ui/system/flex_item/flex_basis.ts diff --git a/packages/sdk/src/ui/system/flex_item/flex_grow.ts b/packages/sdk/src/base/ui/system/flex_item/flex_grow.ts similarity index 100% rename from packages/sdk/src/ui/system/flex_item/flex_grow.ts rename to packages/sdk/src/base/ui/system/flex_item/flex_grow.ts diff --git a/packages/sdk/src/ui/system/flex_item/flex_item_set.ts b/packages/sdk/src/base/ui/system/flex_item/flex_item_set.ts similarity index 100% rename from packages/sdk/src/ui/system/flex_item/flex_item_set.ts rename to packages/sdk/src/base/ui/system/flex_item/flex_item_set.ts diff --git a/packages/sdk/src/ui/system/flex_item/flex_shrink.ts b/packages/sdk/src/base/ui/system/flex_item/flex_shrink.ts similarity index 100% rename from packages/sdk/src/ui/system/flex_item/flex_shrink.ts rename to packages/sdk/src/base/ui/system/flex_item/flex_shrink.ts diff --git a/packages/sdk/src/ui/system/flex_item/justify_self.ts b/packages/sdk/src/base/ui/system/flex_item/justify_self.ts similarity index 100% rename from packages/sdk/src/ui/system/flex_item/justify_self.ts rename to packages/sdk/src/base/ui/system/flex_item/justify_self.ts diff --git a/packages/sdk/src/ui/system/flex_item/order.ts b/packages/sdk/src/base/ui/system/flex_item/order.ts similarity index 100% rename from packages/sdk/src/ui/system/flex_item/order.ts rename to packages/sdk/src/base/ui/system/flex_item/order.ts diff --git a/packages/sdk/src/ui/system/index.ts b/packages/sdk/src/base/ui/system/index.ts similarity index 100% rename from packages/sdk/src/ui/system/index.ts rename to packages/sdk/src/base/ui/system/index.ts diff --git a/packages/sdk/src/ui/system/overflow.ts b/packages/sdk/src/base/ui/system/overflow.ts similarity index 100% rename from packages/sdk/src/ui/system/overflow.ts rename to packages/sdk/src/base/ui/system/overflow.ts diff --git a/packages/sdk/src/ui/system/position/bottom.ts b/packages/sdk/src/base/ui/system/position/bottom.ts similarity index 100% rename from packages/sdk/src/ui/system/position/bottom.ts rename to packages/sdk/src/base/ui/system/position/bottom.ts diff --git a/packages/sdk/src/ui/system/position/left.ts b/packages/sdk/src/base/ui/system/position/left.ts similarity index 100% rename from packages/sdk/src/ui/system/position/left.ts rename to packages/sdk/src/base/ui/system/position/left.ts diff --git a/packages/sdk/src/ui/system/position/position.ts b/packages/sdk/src/base/ui/system/position/position.ts similarity index 100% rename from packages/sdk/src/ui/system/position/position.ts rename to packages/sdk/src/base/ui/system/position/position.ts diff --git a/packages/sdk/src/ui/system/position/position_set.ts b/packages/sdk/src/base/ui/system/position/position_set.ts similarity index 100% rename from packages/sdk/src/ui/system/position/position_set.ts rename to packages/sdk/src/base/ui/system/position/position_set.ts diff --git a/packages/sdk/src/ui/system/position/right.ts b/packages/sdk/src/base/ui/system/position/right.ts similarity index 100% rename from packages/sdk/src/ui/system/position/right.ts rename to packages/sdk/src/base/ui/system/position/right.ts diff --git a/packages/sdk/src/ui/system/position/top.ts b/packages/sdk/src/base/ui/system/position/top.ts similarity index 100% rename from packages/sdk/src/ui/system/position/top.ts rename to packages/sdk/src/base/ui/system/position/top.ts diff --git a/packages/sdk/src/ui/system/position/z_index.ts b/packages/sdk/src/base/ui/system/position/z_index.ts similarity index 100% rename from packages/sdk/src/ui/system/position/z_index.ts rename to packages/sdk/src/base/ui/system/position/z_index.ts diff --git a/packages/sdk/src/ui/system/spacing/margin.ts b/packages/sdk/src/base/ui/system/spacing/margin.ts similarity index 100% rename from packages/sdk/src/ui/system/spacing/margin.ts rename to packages/sdk/src/base/ui/system/spacing/margin.ts diff --git a/packages/sdk/src/ui/system/spacing/padding.ts b/packages/sdk/src/base/ui/system/spacing/padding.ts similarity index 100% rename from packages/sdk/src/ui/system/spacing/padding.ts rename to packages/sdk/src/base/ui/system/spacing/padding.ts diff --git a/packages/sdk/src/ui/system/spacing/spacing_set.ts b/packages/sdk/src/base/ui/system/spacing/spacing_set.ts similarity index 100% rename from packages/sdk/src/ui/system/spacing/spacing_set.ts rename to packages/sdk/src/base/ui/system/spacing/spacing_set.ts diff --git a/packages/sdk/src/ui/system/typography/font_family.ts b/packages/sdk/src/base/ui/system/typography/font_family.ts similarity index 100% rename from packages/sdk/src/ui/system/typography/font_family.ts rename to packages/sdk/src/base/ui/system/typography/font_family.ts diff --git a/packages/sdk/src/ui/system/typography/font_size.ts b/packages/sdk/src/base/ui/system/typography/font_size.ts similarity index 100% rename from packages/sdk/src/ui/system/typography/font_size.ts rename to packages/sdk/src/base/ui/system/typography/font_size.ts diff --git a/packages/sdk/src/ui/system/typography/font_style.ts b/packages/sdk/src/base/ui/system/typography/font_style.ts similarity index 100% rename from packages/sdk/src/ui/system/typography/font_style.ts rename to packages/sdk/src/base/ui/system/typography/font_style.ts diff --git a/packages/sdk/src/ui/system/typography/font_weight.ts b/packages/sdk/src/base/ui/system/typography/font_weight.ts similarity index 100% rename from packages/sdk/src/ui/system/typography/font_weight.ts rename to packages/sdk/src/base/ui/system/typography/font_weight.ts diff --git a/packages/sdk/src/ui/system/typography/letter_spacing.ts b/packages/sdk/src/base/ui/system/typography/letter_spacing.ts similarity index 100% rename from packages/sdk/src/ui/system/typography/letter_spacing.ts rename to packages/sdk/src/base/ui/system/typography/letter_spacing.ts diff --git a/packages/sdk/src/ui/system/typography/line_height.ts b/packages/sdk/src/base/ui/system/typography/line_height.ts similarity index 100% rename from packages/sdk/src/ui/system/typography/line_height.ts rename to packages/sdk/src/base/ui/system/typography/line_height.ts diff --git a/packages/sdk/src/ui/system/typography/text_align.ts b/packages/sdk/src/base/ui/system/typography/text_align.ts similarity index 100% rename from packages/sdk/src/ui/system/typography/text_align.ts rename to packages/sdk/src/base/ui/system/typography/text_align.ts diff --git a/packages/sdk/src/ui/system/typography/text_color.ts b/packages/sdk/src/base/ui/system/typography/text_color.ts similarity index 100% rename from packages/sdk/src/ui/system/typography/text_color.ts rename to packages/sdk/src/base/ui/system/typography/text_color.ts diff --git a/packages/sdk/src/ui/system/typography/text_decoration.ts b/packages/sdk/src/base/ui/system/typography/text_decoration.ts similarity index 100% rename from packages/sdk/src/ui/system/typography/text_decoration.ts rename to packages/sdk/src/base/ui/system/typography/text_decoration.ts diff --git a/packages/sdk/src/ui/system/typography/text_transform.ts b/packages/sdk/src/base/ui/system/typography/text_transform.ts similarity index 100% rename from packages/sdk/src/ui/system/typography/text_transform.ts rename to packages/sdk/src/base/ui/system/typography/text_transform.ts diff --git a/packages/sdk/src/ui/system/typography/typography_set.ts b/packages/sdk/src/base/ui/system/typography/typography_set.ts similarity index 100% rename from packages/sdk/src/ui/system/typography/typography_set.ts rename to packages/sdk/src/base/ui/system/typography/typography_set.ts diff --git a/packages/sdk/src/ui/system/utils/create_responsive_prop_type.ts b/packages/sdk/src/base/ui/system/utils/create_responsive_prop_type.ts similarity index 100% rename from packages/sdk/src/ui/system/utils/create_responsive_prop_type.ts rename to packages/sdk/src/base/ui/system/utils/create_responsive_prop_type.ts diff --git a/packages/sdk/src/ui/system/utils/create_style_prop_types.ts b/packages/sdk/src/base/ui/system/utils/create_style_prop_types.ts similarity index 90% rename from packages/sdk/src/ui/system/utils/create_style_prop_types.ts rename to packages/sdk/src/base/ui/system/utils/create_style_prop_types.ts index 7b3f26ef6..af135349f 100644 --- a/packages/sdk/src/ui/system/utils/create_style_prop_types.ts +++ b/packages/sdk/src/base/ui/system/utils/create_style_prop_types.ts @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import {ObjectMap} from '../../../private_utils'; +import {ObjectMap} from '../../../../shared/private_utils'; import createResponsivePropType from './create_responsive_prop_type'; export const stylePropValue = PropTypes.oneOfType([PropTypes.number, PropTypes.string]); diff --git a/packages/sdk/src/ui/system/utils/csstype.ts b/packages/sdk/src/base/ui/system/utils/csstype.ts similarity index 100% rename from packages/sdk/src/ui/system/utils/csstype.ts rename to packages/sdk/src/base/ui/system/utils/csstype.ts diff --git a/packages/sdk/src/ui/system/utils/ensure_numbers_are_within_scale.ts b/packages/sdk/src/base/ui/system/utils/ensure_numbers_are_within_scale.ts similarity index 96% rename from packages/sdk/src/ui/system/utils/ensure_numbers_are_within_scale.ts rename to packages/sdk/src/base/ui/system/utils/ensure_numbers_are_within_scale.ts index df1b81fcc..c2fb5037d 100644 --- a/packages/sdk/src/ui/system/utils/ensure_numbers_are_within_scale.ts +++ b/packages/sdk/src/base/ui/system/utils/ensure_numbers_are_within_scale.ts @@ -1,5 +1,5 @@ import {get, Scale} from '@styled-system/core'; -import {spawnError} from '../../../error_utils'; +import {spawnError} from '../../../../shared/error_utils'; /** @internal */ function isNumber(n: unknown): n is number { diff --git a/packages/sdk/src/base/ui/system/utils/enum_prop_type_utils.ts b/packages/sdk/src/base/ui/system/utils/enum_prop_type_utils.ts new file mode 100644 index 000000000..da9fd7024 --- /dev/null +++ b/packages/sdk/src/base/ui/system/utils/enum_prop_type_utils.ts @@ -0,0 +1,29 @@ +import PropTypes from 'prop-types'; +import {values} from '../../../../shared/private_utils'; +import createResponsivePropType from './create_responsive_prop_type'; + +/** + * Creates a React propType for a provided enum. + * + * @hidden + */ +export function createPropTypeFromEnum( + enumData: {[K in T]: T}, +): PropTypes.Requireable { + return PropTypes.oneOf(values(enumData)); +} + +/** + * Creates a responsive React propType for a provided enum. + * + * This allows the prop to be either a valid enum property, or a map of viewport sizes to valid enum + * properties. + * + * @hidden + */ +export function createResponsivePropTypeFromEnum( + enumData: {[K in T]: T}, +): PropTypes.Validator { + const propType: PropTypes.Requireable = createPropTypeFromEnum(enumData); + return createResponsivePropType(propType); +} diff --git a/packages/sdk/src/ui/system/utils/get_style_props_for_responsive_prop.ts b/packages/sdk/src/base/ui/system/utils/get_style_props_for_responsive_prop.ts similarity index 93% rename from packages/sdk/src/ui/system/utils/get_style_props_for_responsive_prop.ts rename to packages/sdk/src/base/ui/system/utils/get_style_props_for_responsive_prop.ts index 54b69a283..9a2fbd727 100644 --- a/packages/sdk/src/ui/system/utils/get_style_props_for_responsive_prop.ts +++ b/packages/sdk/src/base/ui/system/utils/get_style_props_for_responsive_prop.ts @@ -1,6 +1,6 @@ -import {invariant} from '../../../error_utils'; -import {values, has, ObjectMap, keys} from '../../../private_utils'; -import {AllStylesProps} from '../'; +import {invariant} from '../../../../shared/error_utils'; +import {values, has, ObjectMap, keys} from '../../../../shared/private_utils'; +import {AllStylesProps} from '..'; import {ResponsivePropObject} from './types'; /** diff --git a/packages/sdk/src/ui/system/utils/types.ts b/packages/sdk/src/base/ui/system/utils/types.ts similarity index 100% rename from packages/sdk/src/ui/system/utils/types.ts rename to packages/sdk/src/base/ui/system/utils/types.ts diff --git a/packages/sdk/src/ui/table_picker.tsx b/packages/sdk/src/base/ui/table_picker.tsx similarity index 91% rename from packages/sdk/src/ui/table_picker.tsx rename to packages/sdk/src/base/ui/table_picker.tsx index 9d5f63f32..f6f1823b0 100644 --- a/packages/sdk/src/ui/table_picker.tsx +++ b/packages/sdk/src/base/ui/table_picker.tsx @@ -2,10 +2,11 @@ import PropTypes from 'prop-types'; import * as React from 'react'; import Table from '../models/table'; +import useWatchable from '../../shared/ui/use_watchable'; +import {useSdk} from '../../shared/ui/sdk_context'; +import {BaseSdkMode} from '../../sdk_mode'; import {sharedSelectBasePropTypes, SharedSelectBaseProps} from './select'; import ModelPickerSelect from './model_picker_select'; -import useWatchable from './use_watchable'; -import {useSdk} from './sdk_context'; /** * Props shared between the {@link TablePicker} and {@link TablePickerSynced} components. @@ -32,7 +33,7 @@ export const sharedTablePickerPropTypes = { * * @docsPath UI/components/TablePicker */ -interface TablePickerProps extends SharedTablePickerProps { +export interface TablePickerProps extends SharedTablePickerProps { /** The selected table model. */ table?: Table | null; } @@ -48,7 +49,7 @@ interface TablePickerProps extends SharedTablePickerProps { const TablePicker = (props: TablePickerProps, ref: React.Ref) => { const {table, shouldAllowPickingNone, placeholder, onChange, ...restOfProps} = props; const selectedTable = table && !table.isDeleted ? table : null; - const sdk = useSdk(); + const sdk = useSdk(); useWatchable(sdk.base, ['tables']); function _onChange(tableId: string | null) { diff --git a/packages/sdk/src/ui/table_picker_synced.tsx b/packages/sdk/src/base/ui/table_picker_synced.tsx similarity index 85% rename from packages/sdk/src/ui/table_picker_synced.tsx rename to packages/sdk/src/base/ui/table_picker_synced.tsx index abca15bfa..1c38e5ba8 100644 --- a/packages/sdk/src/ui/table_picker_synced.tsx +++ b/packages/sdk/src/base/ui/table_picker_synced.tsx @@ -1,13 +1,14 @@ /** @module @airtable/blocks/ui: TablePicker */ /** */ import * as React from 'react'; import Table from '../models/table'; -import {GlobalConfigKey} from '../types/global_config'; +import {GlobalConfigKey} from '../../shared/types/global_config'; import Sdk from '../sdk'; +import useSynced from '../../shared/ui/use_synced'; +import useWatchable from '../../shared/ui/use_watchable'; +import {useSdk} from '../../shared/ui/sdk_context'; +import {BaseSdkMode} from '../../sdk_mode'; +import globalConfigSyncedComponentHelpers from '../../shared/ui/global_config_synced_component_helpers'; import TablePicker, {sharedTablePickerPropTypes, SharedTablePickerProps} from './table_picker'; -import globalConfigSyncedComponentHelpers from './global_config_synced_component_helpers'; -import useSynced from './use_synced'; -import useWatchable from './use_watchable'; -import {useSdk} from './sdk_context'; /** * Props for the {@link TablePickerSynced} component. Also accepts: @@ -38,7 +39,7 @@ function _getTableFromGlobalConfigValue(sdk: Sdk, tableId: unknown): Table | nul const TablePickerSynced = (props: TablePickerSyncedProps, ref: React.Ref) => { const {globalConfigKey, onChange, disabled, ...restOfProps} = props; const [tableId, setTableId, canSetTableId] = useSynced(globalConfigKey); - const sdk = useSdk(); + const sdk = useSdk(); useWatchable(sdk.base, ['tables']); return ( diff --git a/packages/sdk/src/ui/text.tsx b/packages/sdk/src/base/ui/text.tsx similarity index 96% rename from packages/sdk/src/ui/text.tsx rename to packages/sdk/src/base/ui/text.tsx index ced305ce7..3a38508eb 100644 --- a/packages/sdk/src/ui/text.tsx +++ b/packages/sdk/src/base/ui/text.tsx @@ -2,12 +2,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import {cx} from 'emotion'; -import { - createEnum, - EnumType, - createPropTypeFromEnum, - createResponsivePropTypeFromEnum, -} from '../private_utils'; +import {createEnum, EnumType} from '../../shared/private_utils'; import useStyledSystem from './use_styled_system'; import {allStylesPropTypes, AllStylesProps} from './system/index'; import {ResponsiveProp} from './system/utils/types'; @@ -15,6 +10,10 @@ import getStylePropsForResponsiveProp from './system/utils/get_style_props_for_r import useTheme from './theme/use_theme'; import {ariaPropTypes, AriaProps} from './types/aria_props'; import {dataAttributesPropType, DataAttributesProp} from './types/data_attributes_prop'; +import { + createPropTypeFromEnum, + createResponsivePropTypeFromEnum, +} from './system/utils/enum_prop_type_utils'; /** * Variants for the {@link Text} component: @@ -64,7 +63,7 @@ export function useTextStyle( * @noInheritDoc * @docsPath UI/components/Text */ -interface TextProps extends AriaProps, AllStylesProps { +export interface TextProps extends AriaProps, AllStylesProps { /** The element that is rendered. Defaults to `p`. */ as?: | 'p' diff --git a/packages/sdk/src/ui/text_button.tsx b/packages/sdk/src/base/ui/text_button.tsx similarity index 98% rename from packages/sdk/src/ui/text_button.tsx rename to packages/sdk/src/base/ui/text_button.tsx index 29beb41bb..dbcc2ed6a 100644 --- a/packages/sdk/src/ui/text_button.tsx +++ b/packages/sdk/src/base/ui/text_button.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import {cx} from 'emotion'; import {compose} from '@styled-system/core'; -import {createEnum, EnumType, createPropTypeFromEnum} from '../private_utils'; +import {createEnum, EnumType} from '../../shared/private_utils'; import useStyledSystem from './use_styled_system'; import useTheme from './theme/use_theme'; import {ariaPropTypes, AriaProps} from './types/aria_props'; @@ -37,6 +37,7 @@ import {tooltipAnchorPropTypes, TooltipAnchorProps} from './types/tooltip_anchor import cssHelpers from './css_helpers'; import Box from './box'; import {DataAttributesProp} from './types/data_attributes_prop'; +import {createPropTypeFromEnum} from './system/utils/enum_prop_type_utils'; /** * Style props for the {@link TextButton} component. Also accepts: @@ -113,7 +114,7 @@ function useTextButtonVariant(variant: TextButtonVariant = TextButtonVariant.def * @noInheritDoc * @docsPath UI/components/TextButton */ -interface TextButtonProps +export interface TextButtonProps extends TooltipAnchorProps, AriaProps, TextButtonStyleProps { diff --git a/packages/sdk/src/ui/theme/default_theme/button_variants.ts b/packages/sdk/src/base/ui/theme/default_theme/button_variants.ts similarity index 100% rename from packages/sdk/src/ui/theme/default_theme/button_variants.ts rename to packages/sdk/src/base/ui/theme/default_theme/button_variants.ts diff --git a/packages/sdk/src/ui/theme/default_theme/control_sizes.ts b/packages/sdk/src/base/ui/theme/default_theme/control_sizes.ts similarity index 100% rename from packages/sdk/src/ui/theme/default_theme/control_sizes.ts rename to packages/sdk/src/base/ui/theme/default_theme/control_sizes.ts diff --git a/packages/sdk/src/ui/theme/default_theme/heading_styles.ts b/packages/sdk/src/base/ui/theme/default_theme/heading_styles.ts similarity index 97% rename from packages/sdk/src/ui/theme/default_theme/heading_styles.ts rename to packages/sdk/src/base/ui/theme/default_theme/heading_styles.ts index 736788d40..c8a8722c8 100644 --- a/packages/sdk/src/ui/theme/default_theme/heading_styles.ts +++ b/packages/sdk/src/base/ui/theme/default_theme/heading_styles.ts @@ -1,4 +1,4 @@ -import {ObjectMap} from '../../../private_utils'; +import {ObjectMap} from '../../../../shared/private_utils'; import {TypographySetProps, MarginProps} from '../../system'; /** @hidden */ diff --git a/packages/sdk/src/ui/theme/default_theme/index.ts b/packages/sdk/src/base/ui/theme/default_theme/index.ts similarity index 100% rename from packages/sdk/src/ui/theme/default_theme/index.ts rename to packages/sdk/src/base/ui/theme/default_theme/index.ts diff --git a/packages/sdk/src/ui/theme/default_theme/input_variants.ts b/packages/sdk/src/base/ui/theme/default_theme/input_variants.ts similarity index 100% rename from packages/sdk/src/ui/theme/default_theme/input_variants.ts rename to packages/sdk/src/base/ui/theme/default_theme/input_variants.ts diff --git a/packages/sdk/src/ui/theme/default_theme/link_variants.ts b/packages/sdk/src/base/ui/theme/default_theme/link_variants.ts similarity index 100% rename from packages/sdk/src/ui/theme/default_theme/link_variants.ts rename to packages/sdk/src/base/ui/theme/default_theme/link_variants.ts diff --git a/packages/sdk/src/ui/theme/default_theme/select_buttons_variants.ts b/packages/sdk/src/base/ui/theme/default_theme/select_buttons_variants.ts similarity index 100% rename from packages/sdk/src/ui/theme/default_theme/select_buttons_variants.ts rename to packages/sdk/src/base/ui/theme/default_theme/select_buttons_variants.ts diff --git a/packages/sdk/src/ui/theme/default_theme/select_variants.ts b/packages/sdk/src/base/ui/theme/default_theme/select_variants.ts similarity index 100% rename from packages/sdk/src/ui/theme/default_theme/select_variants.ts rename to packages/sdk/src/base/ui/theme/default_theme/select_variants.ts diff --git a/packages/sdk/src/ui/theme/default_theme/switch_variants.ts b/packages/sdk/src/base/ui/theme/default_theme/switch_variants.ts similarity index 100% rename from packages/sdk/src/ui/theme/default_theme/switch_variants.ts rename to packages/sdk/src/base/ui/theme/default_theme/switch_variants.ts diff --git a/packages/sdk/src/ui/theme/default_theme/text_button_variants.ts b/packages/sdk/src/base/ui/theme/default_theme/text_button_variants.ts similarity index 100% rename from packages/sdk/src/ui/theme/default_theme/text_button_variants.ts rename to packages/sdk/src/base/ui/theme/default_theme/text_button_variants.ts diff --git a/packages/sdk/src/ui/theme/default_theme/text_styles.ts b/packages/sdk/src/base/ui/theme/default_theme/text_styles.ts similarity index 100% rename from packages/sdk/src/ui/theme/default_theme/text_styles.ts rename to packages/sdk/src/base/ui/theme/default_theme/text_styles.ts diff --git a/packages/sdk/src/ui/theme/default_theme/tokens.ts b/packages/sdk/src/base/ui/theme/default_theme/tokens.ts similarity index 97% rename from packages/sdk/src/ui/theme/default_theme/tokens.ts rename to packages/sdk/src/base/ui/theme/default_theme/tokens.ts index 3b88f4061..f17ac811c 100644 --- a/packages/sdk/src/ui/theme/default_theme/tokens.ts +++ b/packages/sdk/src/base/ui/theme/default_theme/tokens.ts @@ -1,5 +1,5 @@ -import {ObjectMap} from '../../../private_utils'; -import {Color} from '../../../colors'; +import {ObjectMap} from '../../../../shared/private_utils'; +import {Color} from '../../../../shared/colors'; export const colors = { white: 'hsl(0, 0%, 100%)', diff --git a/packages/sdk/src/ui/theme/theme_context.ts b/packages/sdk/src/base/ui/theme/theme_context.ts similarity index 80% rename from packages/sdk/src/ui/theme/theme_context.ts rename to packages/sdk/src/base/ui/theme/theme_context.ts index 9fa2294ea..c756914d9 100644 --- a/packages/sdk/src/ui/theme/theme_context.ts +++ b/packages/sdk/src/base/ui/theme/theme_context.ts @@ -1,5 +1,5 @@ import {createContext} from 'react'; -import defaultTheme from './default_theme/'; +import defaultTheme from './default_theme'; /** @hidden */ type ThemeSpec = typeof defaultTheme; diff --git a/packages/sdk/src/ui/theme/use_theme.ts b/packages/sdk/src/base/ui/theme/use_theme.ts similarity index 100% rename from packages/sdk/src/ui/theme/use_theme.ts rename to packages/sdk/src/base/ui/theme/use_theme.ts diff --git a/packages/sdk/src/ui/tooltip.tsx b/packages/sdk/src/base/ui/tooltip.tsx similarity index 99% rename from packages/sdk/src/ui/tooltip.tsx rename to packages/sdk/src/base/ui/tooltip.tsx index e89485c60..a71cfbf3f 100644 --- a/packages/sdk/src/ui/tooltip.tsx +++ b/packages/sdk/src/base/ui/tooltip.tsx @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import {cx} from 'emotion'; import * as React from 'react'; -import {values} from '../private_utils'; +import {values} from '../../shared/private_utils'; import {baymax} from './baymax_utils'; import Popover, {PopoverPlacementX, PopoverPlacementY, FitInWindowMode} from './popover'; import Text from './text'; diff --git a/packages/sdk/src/ui/types/aria_props.ts b/packages/sdk/src/base/ui/types/aria_props.ts similarity index 100% rename from packages/sdk/src/ui/types/aria_props.ts rename to packages/sdk/src/base/ui/types/aria_props.ts diff --git a/packages/sdk/src/ui/types/data_attributes_prop.ts b/packages/sdk/src/base/ui/types/data_attributes_prop.ts similarity index 100% rename from packages/sdk/src/ui/types/data_attributes_prop.ts rename to packages/sdk/src/base/ui/types/data_attributes_prop.ts diff --git a/packages/sdk/src/ui/types/tooltip_anchor_props.ts b/packages/sdk/src/base/ui/types/tooltip_anchor_props.ts similarity index 100% rename from packages/sdk/src/ui/types/tooltip_anchor_props.ts rename to packages/sdk/src/base/ui/types/tooltip_anchor_props.ts diff --git a/packages/sdk/src/ui/ui.ts b/packages/sdk/src/base/ui/ui.ts similarity index 81% rename from packages/sdk/src/ui/ui.ts rename to packages/sdk/src/base/ui/ui.ts index e66dedf40..a4e820e6d 100644 --- a/packages/sdk/src/ui/ui.ts +++ b/packages/sdk/src/base/ui/ui.ts @@ -1,10 +1,10 @@ /** @ignore */ import GlobalAlert from './global_alert'; -import '../'; +import '..'; export {default as BaseProvider} from './base_provider'; -export {default as colors} from '../colors'; -export {default as colorUtils} from '../color_utils'; +export {default as colors} from '../../shared/colors'; +export {default as colorUtils} from '../../shared/color_utils'; export {default as Synced} from './synced'; export {default as TablePicker} from './table_picker'; export {default as TablePickerSynced} from './table_picker_synced'; @@ -28,17 +28,17 @@ export {default as SelectButtonsSynced} from './select_buttons_synced'; export {default as SwitchSynced} from './switch_synced'; export {default as ViewportConstraint} from './viewport_constraint'; export {initializeBlock} from './initialize_block'; -export {default as withHooks} from './with_hooks'; -export {default as useLoadable} from './use_loadable'; +export {default as withHooks} from '../../shared/ui/with_hooks'; +export {default as useLoadable} from '../../shared/ui/use_loadable'; export {useRecordIds, useRecords, useRecordById, useRecordQueryResult} from './use_records'; export {default as useBase} from './use_base'; export {default as useCursor} from './use_cursor'; export {default as useSession} from './use_session'; export {default as useSettingsButton} from './use_settings_button'; -export {default as useSynced} from './use_synced'; -export {default as useWatchable} from './use_watchable'; +export {default as useSynced} from '../../shared/ui/use_synced'; +export {default as useWatchable} from '../../shared/ui/use_watchable'; export {default as useViewport} from './use_viewport'; -export {default as useGlobalConfig} from './use_global_config'; +export {default as useGlobalConfig} from '../../shared/ui/use_global_config'; export {default as useViewMetadata} from './use_view_metadata'; export {default as useRecordActionData} from './use_record_action_data'; export {registerRecordActionDataCallback} from '../perform_record_action'; diff --git a/packages/sdk/src/ui/unstable_standalone_ui.ts b/packages/sdk/src/base/ui/unstable_standalone_ui.ts similarity index 89% rename from packages/sdk/src/ui/unstable_standalone_ui.ts rename to packages/sdk/src/base/ui/unstable_standalone_ui.ts index 90d499ff9..308d014a1 100644 --- a/packages/sdk/src/ui/unstable_standalone_ui.ts +++ b/packages/sdk/src/base/ui/unstable_standalone_ui.ts @@ -14,7 +14,11 @@ export {default as Link} from './link'; export {default as Loader} from './loader'; export {default as Modal} from './modal'; export {default as Popover} from './popover'; -export {loadCSSFromString, loadCSSFromURLAsync, loadScriptFromURLAsync} from './remote_utils'; +export { + loadCSSFromString, + loadCSSFromURLAsync, + loadScriptFromURLAsync, +} from '../../shared/ui/remote_utils'; export {default as Select} from './select'; export {default as SelectButtons} from './select_buttons'; export {default as Switch} from './switch'; diff --git a/packages/sdk/src/ui/use_base.ts b/packages/sdk/src/base/ui/use_base.ts similarity index 79% rename from packages/sdk/src/ui/use_base.ts rename to packages/sdk/src/base/ui/use_base.ts index 9544215bb..77f648712 100644 --- a/packages/sdk/src/ui/use_base.ts +++ b/packages/sdk/src/base/ui/use_base.ts @@ -1,7 +1,6 @@ /** @module @airtable/blocks/ui: useBase */ /** */ -import Base from '../models/base'; -import useWatchable from './use_watchable'; -import {useSdk} from './sdk_context'; +import {BaseSdkMode} from '../../sdk_mode'; +import useBaseInternal from '../../shared/ui/use_base'; /** * A hook for connecting a React component to your base's schema. This returns a {@link Base} @@ -33,11 +32,8 @@ import {useSdk} from './sdk_context'; * @docsPath UI/hooks/useBase * @hook */ -const useBase = (): Base => { - const {base, session} = useSdk(); - useWatchable(base, ['schema']); - useWatchable(session, ['permissionLevel']); - return base; -}; +function useBase() { + return useBaseInternal(); +} export default useBase; diff --git a/packages/sdk/src/ui/use_cursor.ts b/packages/sdk/src/base/ui/use_cursor.ts similarity index 86% rename from packages/sdk/src/ui/use_cursor.ts rename to packages/sdk/src/base/ui/use_cursor.ts index 6a89a5f21..3e53bd156 100644 --- a/packages/sdk/src/ui/use_cursor.ts +++ b/packages/sdk/src/base/ui/use_cursor.ts @@ -1,7 +1,8 @@ /** @module @airtable/blocks/ui: useCursor */ /** */ import Cursor from '../models/cursor'; -import useWatchable from './use_watchable'; -import {useSdk} from './sdk_context'; +import useWatchable from '../../shared/ui/use_watchable'; +import {useSdk} from '../../shared/ui/sdk_context'; +import {BaseSdkMode} from '../../sdk_mode'; /** * A hook for connecting a React component to your base's cursor. This returns a {@link Cursor} @@ -32,7 +33,7 @@ import {useSdk} from './sdk_context'; * @hook */ const useCursor = (): Cursor => { - const {cursor} = useSdk(); + const {cursor} = useSdk(); useWatchable(cursor, ['activeTableId', 'activeViewId']); diff --git a/packages/sdk/src/ui/use_form_field.ts b/packages/sdk/src/base/ui/use_form_field.ts similarity index 100% rename from packages/sdk/src/ui/use_form_field.ts rename to packages/sdk/src/base/ui/use_form_field.ts diff --git a/packages/sdk/src/ui/use_record_action_data.ts b/packages/sdk/src/base/ui/use_record_action_data.ts similarity index 90% rename from packages/sdk/src/ui/use_record_action_data.ts rename to packages/sdk/src/base/ui/use_record_action_data.ts index 6b1297cb1..77906709d 100644 --- a/packages/sdk/src/ui/use_record_action_data.ts +++ b/packages/sdk/src/base/ui/use_record_action_data.ts @@ -1,9 +1,10 @@ /** @module @airtable/blocks/ui: useRecordActionData */ /** */ import {RecordActionData} from '../types/record_action_data'; import {WatchablePerformRecordActionKeys} from '../perform_record_action'; -import useLoadable from './use_loadable'; -import useWatchable from './use_watchable'; -import {useSdk} from './sdk_context'; +import useLoadable from '../../shared/ui/use_loadable'; +import useWatchable from '../../shared/ui/use_watchable'; +import {useSdk} from '../../shared/ui/sdk_context'; +import {BaseSdkMode} from '../../sdk_mode'; /** * A hook to watch "open extension" / "perform record action" events (from button field). Returns @@ -57,7 +58,7 @@ import {useSdk} from './sdk_context'; */ export default function useRecordActionData(): RecordActionData | null { - const {performRecordAction} = useSdk(); + const {performRecordAction} = useSdk(); useLoadable(performRecordAction); diff --git a/packages/sdk/src/ui/use_records.ts b/packages/sdk/src/base/ui/use_records.ts similarity index 95% rename from packages/sdk/src/ui/use_records.ts rename to packages/sdk/src/base/ui/use_records.ts index 065e5c1a8..e47dcabc6 100644 --- a/packages/sdk/src/ui/use_records.ts +++ b/packages/sdk/src/base/ui/use_records.ts @@ -1,6 +1,6 @@ /** @module @airtable/blocks/ui: useRecords */ /** */ -import {spawnError} from '../error_utils'; -import {RecordId} from '../types/record'; +import {spawnError} from '../../shared/error_utils'; +import {RecordId} from '../../shared/types/hyper_ids'; import Table from '../models/table'; import TableOrViewQueryResult from '../models/table_or_view_query_result'; import LinkedRecordsQueryResult from '../models/linked_records_query_result'; @@ -12,8 +12,8 @@ import RecordQueryResult, { import Record from '../models/record'; import * as RecordColoring from '../models/record_coloring'; import View from '../models/view'; -import useLoadable from './use_loadable'; -import useWatchable from './use_watchable'; +import useLoadable from '../../shared/ui/use_loadable'; +import useWatchable from '../../shared/ui/use_watchable'; /** */ type AnyQueryResult = TableOrViewQueryResult | LinkedRecordsQueryResult; @@ -29,7 +29,7 @@ type TableOrViewOrQueryResult = Table | View | AnyQueryResult; * @param opts * @internal */ -function _useUnwatchedRecordQueryResult( +function useUnwatchedRecordQueryResult_( tableOrViewOrQueryResult: TableOrViewOrQueryResult | null, functionNameForErrors: string, opts?: RecordQueryResultOpts, @@ -108,7 +108,7 @@ export function useRecordIds( } : opts; - const queryResult = _useUnwatchedRecordQueryResult( + const queryResult = useUnwatchedRecordQueryResult_( tableOrViewOrQueryResult, 'useRecordIds', generatedOpts, @@ -202,7 +202,7 @@ export function useRecords( tableOrViewOrQueryResult: TableOrViewOrQueryResult | null, opts?: RecordQueryResultOpts, ): Array | null { - const queryResult = _useUnwatchedRecordQueryResult( + const queryResult = useUnwatchedRecordQueryResult_( tableOrViewOrQueryResult, 'useRecords', opts, @@ -275,7 +275,7 @@ export function useRecordById( recordId: RecordId, opts?: SingleRecordQueryResultOpts, ): Record | null { - const queryResult = _useUnwatchedRecordQueryResult( + const queryResult = useUnwatchedRecordQueryResult_( tableOrViewOrQueryResult, 'useRecordById', opts, @@ -297,7 +297,7 @@ export function useRecordQueryResult( tableOrViewOrQueryResult: TableOrViewOrQueryResult | null, opts?: RecordQueryResultOpts, ): RecordQueryResult | null { - const queryResult = _useUnwatchedRecordQueryResult( + const queryResult = useUnwatchedRecordQueryResult_( tableOrViewOrQueryResult, 'useRecordQueryResult', opts, diff --git a/packages/sdk/src/ui/use_session.ts b/packages/sdk/src/base/ui/use_session.ts similarity index 78% rename from packages/sdk/src/ui/use_session.ts rename to packages/sdk/src/base/ui/use_session.ts index c398a99e2..4f1cc8ea7 100644 --- a/packages/sdk/src/ui/use_session.ts +++ b/packages/sdk/src/base/ui/use_session.ts @@ -1,7 +1,6 @@ /** @module @airtable/blocks/ui: useSession */ /** */ -import Session from '../models/session'; -import useWatchable from './use_watchable'; -import {useSdk} from './sdk_context'; +import {BaseSdkMode} from '../../sdk_mode'; +import useSessionInternal from '../../shared/ui/use_session'; /** * A hook for connecting a React component to the current session. This returns a {@link Session} @@ -31,11 +30,8 @@ import {useSdk} from './sdk_context'; * @docsPath UI/hooks/useSession * @hook */ -const useSession = (): Session => { - const {session, base} = useSdk(); - useWatchable(session, ['permissionLevel', 'currentUser']); - useWatchable(base, ['schema']); - return session; -}; +function useSession() { + return useSessionInternal(); +} export default useSession; diff --git a/packages/sdk/src/ui/use_settings_button.ts b/packages/sdk/src/base/ui/use_settings_button.ts similarity index 82% rename from packages/sdk/src/ui/use_settings_button.ts rename to packages/sdk/src/base/ui/use_settings_button.ts index 32636d936..988079a8d 100644 --- a/packages/sdk/src/ui/use_settings_button.ts +++ b/packages/sdk/src/base/ui/use_settings_button.ts @@ -1,8 +1,9 @@ /** @module @airtable/blocks/ui: useSettingsButton */ /** */ import {useEffect} from 'react'; -import {FlowAnyFunction} from '../private_utils'; -import useWatchable from './use_watchable'; -import {useSdk} from './sdk_context'; +import {FlowAnyFunction} from '../../shared/private_utils'; +import useWatchable from '../../shared/ui/use_watchable'; +import {useSdk} from '../../shared/ui/sdk_context'; +import {BaseSdkMode} from '../../sdk_mode'; /** * A hook for using the settings button that lives outside the extension's viewport. It will show @@ -32,7 +33,7 @@ import {useSdk} from './sdk_context'; * @hook */ export default function useSettingsButton(onClickCallback: FlowAnyFunction) { - const {settingsButton} = useSdk(); + const {settingsButton} = useSdk(); useEffect(() => { settingsButton.show(); diff --git a/packages/sdk/src/ui/use_styled_system.ts b/packages/sdk/src/base/ui/use_styled_system.ts similarity index 100% rename from packages/sdk/src/ui/use_styled_system.ts rename to packages/sdk/src/base/ui/use_styled_system.ts diff --git a/packages/sdk/src/ui/use_text_color_for_background_color.ts b/packages/sdk/src/base/ui/use_text_color_for_background_color.ts similarity index 85% rename from packages/sdk/src/ui/use_text_color_for_background_color.ts rename to packages/sdk/src/base/ui/use_text_color_for_background_color.ts index bb05814d9..68cd684ff 100644 --- a/packages/sdk/src/ui/use_text_color_for_background_color.ts +++ b/packages/sdk/src/base/ui/use_text_color_for_background_color.ts @@ -1,4 +1,4 @@ -import {Color} from '../colors'; +import {Color} from '../../shared/colors'; import useTheme from './theme/use_theme'; /** @internal */ diff --git a/packages/sdk/src/ui/use_view_metadata.ts b/packages/sdk/src/base/ui/use_view_metadata.ts similarity index 93% rename from packages/sdk/src/ui/use_view_metadata.ts rename to packages/sdk/src/base/ui/use_view_metadata.ts index 4c5fc98fd..ae76fad29 100644 --- a/packages/sdk/src/ui/use_view_metadata.ts +++ b/packages/sdk/src/base/ui/use_view_metadata.ts @@ -1,8 +1,8 @@ /** @module @airtable/blocks/ui: useViewMetadata */ /** */ import ViewMetadataQueryResult from '../models/view_metadata_query_result'; import View from '../models/view'; -import useLoadable from './use_loadable'; -import useWatchable from './use_watchable'; +import useLoadable from '../../shared/ui/use_loadable'; +import useWatchable from '../../shared/ui/use_watchable'; /** */ function useViewMetadata( diff --git a/packages/sdk/src/ui/use_viewport.ts b/packages/sdk/src/base/ui/use_viewport.ts similarity index 83% rename from packages/sdk/src/ui/use_viewport.ts rename to packages/sdk/src/base/ui/use_viewport.ts index e3d97c188..ecc86b143 100644 --- a/packages/sdk/src/ui/use_viewport.ts +++ b/packages/sdk/src/base/ui/use_viewport.ts @@ -1,7 +1,8 @@ /** @module @airtable/blocks/ui: useViewport */ /** */ import Viewport from '../viewport'; -import useWatchable from './use_watchable'; -import {useSdk} from './sdk_context'; +import useWatchable from '../../shared/ui/use_watchable'; +import {useSdk} from '../../shared/ui/sdk_context'; +import {BaseSdkMode} from '../../sdk_mode'; /** * Returns the current {@link Viewport} object and updates whenever the viewport size, constraints, @@ -35,7 +36,7 @@ import {useSdk} from './sdk_context'; * @hook */ export default function useViewport(): Viewport { - const viewport = useSdk().viewport; + const viewport = useSdk().viewport; useWatchable(viewport, ['isFullscreen', 'size', 'minSize', 'maxFullscreenSize']); return viewport; } diff --git a/packages/sdk/src/ui/view_picker.tsx b/packages/sdk/src/base/ui/view_picker.tsx similarity index 92% rename from packages/sdk/src/ui/view_picker.tsx rename to packages/sdk/src/base/ui/view_picker.tsx index 449c3034f..44162aabf 100644 --- a/packages/sdk/src/ui/view_picker.tsx +++ b/packages/sdk/src/base/ui/view_picker.tsx @@ -1,14 +1,15 @@ /** @module @airtable/blocks/ui: ViewPicker */ /** */ import PropTypes from 'prop-types'; import * as React from 'react'; -import {values, ObjectMap} from '../private_utils'; +import {values, ObjectMap} from '../../shared/private_utils'; import View from '../models/view'; import Table from '../models/table'; import {ViewType} from '../types/view'; +import useWatchable from '../../shared/ui/use_watchable'; +import {useSdk} from '../../shared/ui/sdk_context'; +import {BaseSdkMode} from '../../sdk_mode'; import {sharedSelectBasePropTypes, SharedSelectBaseProps} from './select'; import ModelPickerSelect from './model_picker_select'; -import useWatchable from './use_watchable'; -import {useSdk} from './sdk_context'; /** * Props shared between the {@link ViewPicker} and {@link ViewPickerSynced} components. @@ -41,7 +42,7 @@ export const sharedViewPickerPropTypes = { * * @docsPath UI/components/ViewPicker */ -interface ViewPickerProps extends SharedViewPickerProps { +export interface ViewPickerProps extends SharedViewPickerProps { /** The selected view model. */ view?: View | null; } @@ -64,7 +65,7 @@ const ViewPicker = (props: ViewPickerProps, ref: React.Ref) = onChange, ...restOfProps } = props; - const sdk = useSdk(); + const sdk = useSdk(); useWatchable(sdk.base, ['tables']); useWatchable(table, ['views']); diff --git a/packages/sdk/src/ui/view_picker_synced.tsx b/packages/sdk/src/base/ui/view_picker_synced.tsx similarity index 85% rename from packages/sdk/src/ui/view_picker_synced.tsx rename to packages/sdk/src/base/ui/view_picker_synced.tsx index de12ae262..e22ff5f5c 100644 --- a/packages/sdk/src/ui/view_picker_synced.tsx +++ b/packages/sdk/src/base/ui/view_picker_synced.tsx @@ -1,12 +1,13 @@ /** @module @airtable/blocks/ui: ViewPicker */ /** */ import * as React from 'react'; import View from '../models/view'; -import {GlobalConfigKey} from '../types/global_config'; +import {GlobalConfigKey} from '../../shared/types/global_config'; +import useSynced from '../../shared/ui/use_synced'; +import useWatchable from '../../shared/ui/use_watchable'; +import {useSdk} from '../../shared/ui/sdk_context'; +import {BaseSdkMode} from '../../sdk_mode'; +import globalConfigSyncedComponentHelpers from '../../shared/ui/global_config_synced_component_helpers'; import ViewPicker, {sharedViewPickerPropTypes, SharedViewPickerProps} from './view_picker'; -import globalConfigSyncedComponentHelpers from './global_config_synced_component_helpers'; -import useSynced from './use_synced'; -import useWatchable from './use_watchable'; -import {useSdk} from './sdk_context'; /** * Props for the {@link ViewPickerSynced} component. Also accepts: @@ -32,7 +33,7 @@ interface ViewPickerSyncedProps extends SharedViewPickerProps { const ViewPickerSynced = (props: ViewPickerSyncedProps, ref: React.Ref) => { const {globalConfigKey, table, onChange, disabled, ...restOfProps} = props; const [viewId, setViewId, canSetViewId] = useSynced(globalConfigKey); - const sdk = useSdk(); + const sdk = useSdk(); useWatchable(sdk.base, ['tables']); useWatchable(table, ['views']); diff --git a/packages/sdk/src/ui/viewport_constraint.tsx b/packages/sdk/src/base/ui/viewport_constraint.tsx similarity index 96% rename from packages/sdk/src/ui/viewport_constraint.tsx rename to packages/sdk/src/base/ui/viewport_constraint.tsx index 3dd024244..6786458b0 100644 --- a/packages/sdk/src/ui/viewport_constraint.tsx +++ b/packages/sdk/src/base/ui/viewport_constraint.tsx @@ -6,8 +6,9 @@ import PropTypes from 'prop-types'; import * as React from 'react'; import {ViewportSizeConstraint} from '../types/viewport'; import Sdk from '../sdk'; -import {useSdk} from './sdk_context'; -import withHooks from './with_hooks'; +import withHooks from '../../shared/ui/with_hooks'; +import {useSdk} from '../../shared/ui/sdk_context'; +import {BaseSdkMode} from '../../sdk_mode'; /** An object specifying a width and/or height for the block's viewport. */ type ViewportSizeConstraintProp = Partial; @@ -152,7 +153,7 @@ export default withHooks<{sdk: Sdk}, ViewportConstraintProps, ViewportConstraint ViewportConstraint, props => { return { - sdk: useSdk(), + sdk: useSdk(), }; }, ); diff --git a/packages/sdk/src/ui/with_styled_system.tsx b/packages/sdk/src/base/ui/with_styled_system.tsx similarity index 98% rename from packages/sdk/src/ui/with_styled_system.tsx rename to packages/sdk/src/base/ui/with_styled_system.tsx index c931d9d17..455885451 100644 --- a/packages/sdk/src/ui/with_styled_system.tsx +++ b/packages/sdk/src/base/ui/with_styled_system.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import {cx} from 'emotion'; import hoistNonReactStatic from 'hoist-non-react-statics'; import {styleFn} from '@styled-system/core'; -import {cast, keys} from '../private_utils'; +import {cast, keys} from '../../shared/private_utils'; import useStyledSystem from './use_styled_system'; /** diff --git a/packages/sdk/src/undo_redo.ts b/packages/sdk/src/base/undo_redo.ts similarity index 89% rename from packages/sdk/src/undo_redo.ts rename to packages/sdk/src/base/undo_redo.ts index 6eebb3423..49aa4e613 100644 --- a/packages/sdk/src/undo_redo.ts +++ b/packages/sdk/src/base/undo_redo.ts @@ -1,5 +1,5 @@ -import {values} from './private_utils'; -import {spawnError} from './error_utils'; +import {values} from '../shared/private_utils'; +import {spawnError} from '../shared/error_utils'; import {UndoRedoModes, UndoRedoMode} from './types/undo_redo'; import {AirtableInterface} from './types/airtable_interface'; diff --git a/packages/sdk/src/unstable_testing_utils.ts b/packages/sdk/src/base/unstable_testing_utils.ts similarity index 57% rename from packages/sdk/src/unstable_testing_utils.ts rename to packages/sdk/src/base/unstable_testing_utils.ts index 689ddefed..57d5db074 100644 --- a/packages/sdk/src/unstable_testing_utils.ts +++ b/packages/sdk/src/base/unstable_testing_utils.ts @@ -1,10 +1,13 @@ -export {BaseData, ModelChange} from './types/base'; +export {ModelChange} from '../shared/types/base_core'; +export {BaseData} from './types/base'; export {Mutation, MutationTypes} from './types/mutations'; +export {AppInterface, GlobalConfigHelpers} from '../shared/types/airtable_interface_core'; export { - AppInterface, BlockRunContextType, + FieldTypeProvider, + IdGenerator, SdkInitData, PartialViewData, } from './types/airtable_interface'; @@ -13,7 +16,7 @@ export {RecordData} from './types/record'; export {CursorData} from './types/cursor'; -export {FieldData, FieldType} from './types/field'; +export {FieldData, FieldType} from '../shared/types/field'; export {ViewType} from './types/view'; @@ -24,9 +27,9 @@ export { GlobalConfigData, GlobalConfigArray, GlobalConfigObject, -} from './types/global_config'; +} from '../shared/types/global_config'; export {RequestJson, ResponseJson} from './types/backend_fetch_types'; export {default as Sdk} from './sdk'; -export {AbstractMockAirtableInterface} from './testing/abstract_mock_airtable_interface'; +export {AbstractMockAirtableInterface} from '../testing/abstract_mock_airtable_interface'; diff --git a/packages/sdk/src/viewport.ts b/packages/sdk/src/base/viewport.ts similarity index 98% rename from packages/sdk/src/viewport.ts rename to packages/sdk/src/base/viewport.ts index 5fe8a276d..22fd01ea6 100644 --- a/packages/sdk/src/viewport.ts +++ b/packages/sdk/src/base/viewport.ts @@ -1,8 +1,14 @@ /** @module @airtable/blocks: viewport */ /** */ +import Watchable from '../shared/watchable'; +import { + isEnumValue, + debounce, + ObjectValues, + FlowAnyFunction, + FlowAnyObject, +} from '../shared/private_utils'; +import {invariant} from '../shared/error_utils'; import {ViewportSizeConstraint} from './types/viewport'; -import Watchable from './watchable'; -import {isEnumValue, debounce, ObjectValues, FlowAnyFunction, FlowAnyObject} from './private_utils'; -import {invariant} from './error_utils'; import {AirtableInterface} from './types/airtable_interface'; const WatchableViewportKeys = Object.freeze({ diff --git a/packages/sdk/src/injected/airtable_interface.ts b/packages/sdk/src/injected/airtable_interface.ts index 322aae736..59d9b8aed 100644 --- a/packages/sdk/src/injected/airtable_interface.ts +++ b/packages/sdk/src/injected/airtable_interface.ts @@ -1,9 +1,9 @@ -import {spawnError} from '../error_utils'; -import {AirtableInterface} from '../types/airtable_interface'; +import {spawnError} from '../shared/error_utils'; +import {SdkMode} from '../sdk_mode'; const AIRTABLE_INTERFACE_VERSION = 0; -let airtableInterface: AirtableInterface | null = null; +let airtableInterface: SdkMode['AirtableInterfaceT'] | null = null; const missingAirtableInterfaceErrorMessage = [ 'Error: Extension environment misconfigured', @@ -19,9 +19,11 @@ const missingAirtableInterfaceErrorMessage = [ ].join(''); /** @hidden */ -export default function getAirtableInterface(): AirtableInterface { +export default function getAirtableInterface< + SdkModeT extends SdkMode +>(): SdkModeT['AirtableInterfaceT'] { const getAirtableInterfaceAtVersion: - | ((arg1: number) => AirtableInterface) + | ((arg1: number) => SdkModeT['AirtableInterfaceT']) | void = (window as any).__getAirtableInterfaceAtVersion; if (!airtableInterface) { @@ -32,5 +34,5 @@ export default function getAirtableInterface(): AirtableInterface { airtableInterface = getAirtableInterfaceAtVersion(AIRTABLE_INTERFACE_VERSION); } - return airtableInterface; + return airtableInterface as SdkModeT['AirtableInterfaceT']; } diff --git a/packages/sdk/src/interface/index.ts b/packages/sdk/src/interface/index.ts new file mode 100644 index 000000000..e018dd697 --- /dev/null +++ b/packages/sdk/src/interface/index.ts @@ -0,0 +1,25 @@ +import {__injectSdkIntoWarning} from '../shared/warning'; +import getAirtableInterface from '../injected/airtable_interface'; +import {InterfaceSdkMode} from '../sdk_mode'; +import {InterfaceBlockSdk} from './sdk'; +import {__injectSdkIntoInitializeBlock} from './ui/initialize_block'; + +/** @internal */ +export let __sdk: InterfaceBlockSdk; +export let base: InterfaceBlockSdk['base']; +export let globalConfig: InterfaceBlockSdk['globalConfig']; +export let installationId: InterfaceBlockSdk['installationId']; +export let reload: InterfaceBlockSdk['reload']; +export let runInfo: InterfaceBlockSdk['runInfo']; + +/** @internal */ +export function __reset() { + __sdk = new InterfaceBlockSdk(getAirtableInterface()); + + ({base, globalConfig, installationId, reload, runInfo} = __sdk); + + __injectSdkIntoInitializeBlock(__sdk); + __injectSdkIntoWarning(__sdk); +} + +__reset(); diff --git a/packages/sdk/src/interface/models/base.ts b/packages/sdk/src/interface/models/base.ts new file mode 100644 index 000000000..e87c31766 --- /dev/null +++ b/packages/sdk/src/interface/models/base.ts @@ -0,0 +1,32 @@ +import {BaseCore} from '../../shared/models/base_core'; +import {InterfaceSdkMode} from '../../sdk_mode'; +import {TableId} from '../../shared/types/hyper_ids'; +import {InterfaceBlockSdk} from '../sdk'; +import {Table} from './table'; +import {RecordStore} from './record_store'; + +/** + * Model class representing a base. + * + * If you want the base model to automatically recalculate whenever the base schema changes, try the + * {@link useBase} hook. + * + * @docsPath models/Base + */ +export class Base extends BaseCore { + /** @internal */ + _constructTable(tableId: TableId): Table { + const recordStore = this.__getRecordStore(tableId); + return new Table(this, recordStore, tableId, this._sdk); + } + + /** @internal */ + _constructRecordStore(sdk: InterfaceBlockSdk, tableId: TableId): RecordStore { + return new RecordStore(sdk, tableId); + } + + /** @internal */ + _iterateTableIds(): Iterable { + return Object.keys(this._data.tablesById); + } +} diff --git a/packages/sdk/src/interface/models/field.ts b/packages/sdk/src/interface/models/field.ts new file mode 100644 index 000000000..c34ead507 --- /dev/null +++ b/packages/sdk/src/interface/models/field.ts @@ -0,0 +1,23 @@ +import {InterfaceSdkMode} from '../../sdk_mode'; +import {FieldCore} from '../../shared/models/field_core'; + +/** + * Model class representing a field in a table. + * + * @example + * ```js + * import {useBase} from '@airtable/blocks/interface/ui'; + * + * function App() { + * const base = useBase(); + * const table = base.getTableByName('Table 1'); + * const field = table.getFieldByName('Name'); + * console.log('The type of this field is', field.type); + * } + * ``` + * @docsPath models/Field + */ +export class Field extends FieldCore { + /** @internal */ + static _className = 'Field'; +} diff --git a/packages/sdk/src/interface/models/mutations.ts b/packages/sdk/src/interface/models/mutations.ts new file mode 100644 index 000000000..e03810aab --- /dev/null +++ b/packages/sdk/src/interface/models/mutations.ts @@ -0,0 +1,40 @@ +import {InterfaceSdkMode} from '../../sdk_mode'; +import {MUTATIONS_MAX_BATCH_SIZE, MutationsCore} from '../../shared/models/mutations_core'; +import {ModelChange} from '../../shared/types/base_core'; +import {spawnError, spawnUnknownSwitchCaseError} from '../../shared/error_utils'; +import {MutationTypes} from '../types/mutations'; + +/** @hidden */ +export class Mutations extends MutationsCore { + /** @internal */ + _doesMutationExceedBatchSizeLimit(mutation: InterfaceSdkMode['MutationT']): boolean { + switch (mutation.type) { + case MutationTypes.SET_MULTIPLE_GLOBAL_CONFIG_PATHS: + return mutation.updates.length > MUTATIONS_MAX_BATCH_SIZE; + default: + throw spawnUnknownSwitchCaseError('mutation type', mutation.type, 'type'); + } + } + /** @internal */ + _assertMutationIsValid(mutation: InterfaceSdkMode['MutationT']): void { + switch (mutation.type) { + case MutationTypes.SET_MULTIPLE_GLOBAL_CONFIG_PATHS: + return; + default: + throw spawnUnknownSwitchCaseError('mutation type', mutation.type, 'type'); + } + } + /** @internal */ + _getOptimisticModelChangesForMutation( + mutation: InterfaceSdkMode['MutationT'], + ): Array { + switch (mutation.type) { + case MutationTypes.SET_MULTIPLE_GLOBAL_CONFIG_PATHS: + throw spawnError( + 'attempting to generate model updates for SET_MULTIPLE_GLOBAL_CONFIG_PATH', + ); + default: + throw spawnUnknownSwitchCaseError('mutation type', mutation.type, 'type'); + } + } +} diff --git a/packages/sdk/src/interface/models/record.ts b/packages/sdk/src/interface/models/record.ts new file mode 100644 index 000000000..7c8d6c00b --- /dev/null +++ b/packages/sdk/src/interface/models/record.ts @@ -0,0 +1,25 @@ +import {InterfaceSdkMode} from '../../sdk_mode'; +import {RecordCore, WatchableRecordKeysCore} from '../../shared/models/record_core'; +import {ObjectValues} from '../../shared/private_utils'; + +const WatchableRecordKeys = Object.freeze({ + ...WatchableRecordKeysCore, +}); +/** + * Any key within record that can be watched: + * - `'name'` + * - `'cellValues'` + */ +type WatchableRecordKey = ObjectValues | string; + +/** + * Model class representing a record in a table. + * + * Do not instantiate. You can get instances of this class by calling {@link useRecords}. + * + * @docsPath models/Record + */ +export class Record extends RecordCore { + /** @internal */ + static _className = 'Record'; +} diff --git a/packages/sdk/src/interface/models/record_store.ts b/packages/sdk/src/interface/models/record_store.ts new file mode 100644 index 000000000..c314c37b2 --- /dev/null +++ b/packages/sdk/src/interface/models/record_store.ts @@ -0,0 +1,57 @@ +import {RecordId} from '../../shared/types/hyper_ids'; +import {InterfaceSdkMode} from '../../sdk_mode'; +import RecordStoreCore, { + WatchableCellValuesInFieldKeyPrefix, + WatchableRecordStoreKeysCore, +} from '../../shared/models/record_store_core'; +import {TableData} from '../types/table'; +import {ChangedPathsForType} from '../../shared/models/base_core'; +import {isEnumValue, ObjectValues} from '../../shared/private_utils'; +import {Table} from './table'; +import {Record} from './record'; + +const WatchableRecordStoreKeys = Object.freeze({ + ...WatchableRecordStoreKeysCore, + recordOrder: 'recordOrder' as const, +}); + +/** + * The string case is to accommodate prefix keys + * + * @internal + */ +type WatchableRecordStoreKey = ObjectValues | string; + +/** + * One RecordStore exists per table, and contains all the record data associated with that table. + * Table itself is for schema information only, so isn't the appropriate place for this data. + * + * @internal + */ +export class RecordStore extends RecordStoreCore { + static _className = 'RecordStore'; + static _isWatchableKey(key: string): boolean { + return ( + isEnumValue(WatchableRecordStoreKeys, key) || + key.startsWith(WatchableCellValuesInFieldKeyPrefix) + ); + } + + _constructRecord(recordId: RecordId, parentTable: Table): Record { + return new Record(this._sdk, this, parentTable, recordId); + } + + /** + * The record Ids in this table. + */ + get recordIds(): Array { + return this._data.recordOrder; + } + + triggerOnChangeForDirtyPaths(dirtyPaths: ChangedPathsForType) { + super.triggerOnChangeForDirtyPaths(dirtyPaths); + if (dirtyPaths.recordOrder) { + this._onChange(WatchableRecordStoreKeys.recordOrder); + } + } +} diff --git a/packages/sdk/src/interface/models/session.ts b/packages/sdk/src/interface/models/session.ts new file mode 100644 index 000000000..1564e3cf1 --- /dev/null +++ b/packages/sdk/src/interface/models/session.ts @@ -0,0 +1,23 @@ +import {InterfaceSdkMode} from '../../sdk_mode'; +import {SessionCore} from '../../shared/models/session_core'; + +/** + * Model class representing the current user's session. + * + * @example + * ```js + * import {useSession} from '@airtable/blocks/interface/ui'; + * + * function Username() { + * const session = useSession(); + * + * if (session.currentUser !== null) { + * return The current user's name is {session.currentUser.name}; + * } else { + * return This extension is being viewed in a public share; + * } + * } + * ``` + * @docsPath models/Session + */ +export class Session extends SessionCore {} diff --git a/packages/sdk/src/interface/models/table.ts b/packages/sdk/src/interface/models/table.ts new file mode 100644 index 000000000..6e6df8adb --- /dev/null +++ b/packages/sdk/src/interface/models/table.ts @@ -0,0 +1,28 @@ +import {TableCore} from '../../shared/models/table_core'; +import {InterfaceSdkMode} from '../../sdk_mode'; +import {FieldId} from '../../shared/types/hyper_ids'; +import {Field} from './field'; + +/** + * Model class representing a table. Every {@link Base} has one or more tables. + * + * @example + * ```js + * import {useBase} from '@airtable/blocks/interface/ui'; + * + * function App() { + * const base = useBase(); + * const table = base.getTables()[0]; + * if (table) { + * console.log('The name of this table is', table.name); + * } + * } + * ``` + * @docsPath models/Table + */ +export class Table extends TableCore { + /** @internal */ + _constructField(fieldId: FieldId): Field { + return new Field(this.parentBase.__sdk, this, fieldId); + } +} diff --git a/packages/sdk/src/interface/sdk.ts b/packages/sdk/src/interface/sdk.ts new file mode 100644 index 000000000..5c3a6309e --- /dev/null +++ b/packages/sdk/src/interface/sdk.ts @@ -0,0 +1,84 @@ +import {ModelChange} from '../shared/types/base_core'; +import {GlobalConfigUpdate} from '../shared/types/global_config'; +import {BlockSdkCore} from '../shared/sdk_core'; +import {InterfaceSdkMode} from '../sdk_mode'; +import {AppInterface} from '../shared/types/airtable_interface_core'; +import {Session} from './models/session'; +import {Mutations} from './models/mutations'; +import {Base} from './models/base'; +import { + BlockInstallationPageElementCustomPropertyForAirtableInterface, + BlockRunContext, +} from './types/airtable_interface'; + + + +/** @hidden */ +export class InterfaceBlockSdk extends BlockSdkCore { + constructor(airtableInterface: InterfaceSdkMode['AirtableInterfaceT']) { + super(airtableInterface); + + this._registerHandlers(); + } + /** @internal */ + _constructSession(): Session { + return new Session(this); + } + /** @internal */ + _constructBase(): Base { + return new Base(this); + } + /** @internal */ + _constructMutations() { + return new Mutations( + this, + this.session, + this.base, + changes => this.__applyModelChanges(changes), + updates => this.__applyGlobalConfigUpdates(updates), + ); + } + /** @internal */ + _registerHandlers() { + this.__airtableInterface.subscribeToModelUpdates(({changes}) => { + this.__applyModelChanges(changes); + }); + + this.__airtableInterface.subscribeToGlobalConfigUpdates(({updates}) => { + this.__applyGlobalConfigUpdates(updates); + }); + } + /** @internal */ + __applyModelChanges(changes: ReadonlyArray) { + const changedBasePaths = this.base.__applyChangesWithoutTriggeringEvents(changes); + const changedSessionKeys = this.session.__applyChangesWithoutTriggeringEvents(changes); + this.base.__triggerOnChangeForChangedPaths(changedBasePaths); + this.session.__triggerOnChangeForChangedKeys(changedSessionKeys); + } + /** @internal */ + __applyGlobalConfigUpdates(updates: ReadonlyArray) { + this.globalConfig.__setMultipleKvPaths(updates); + } + + + /** + * @internal + */ + get __appInterface(): AppInterface { + return this.base._baseData.appInterface; + } + + /** @hidden */ + getBlockRunContext(): BlockRunContext { + return this.__airtableInterface.sdkInitData.runContext; + } + + /** + * @internal + */ + setCustomPropertiesAsync( + properties: Array, + ): Promise { + return this.__airtableInterface.setCustomPropertiesAsync(properties); + } +} diff --git a/packages/sdk/src/interface/types/airtable_interface.ts b/packages/sdk/src/interface/types/airtable_interface.ts new file mode 100644 index 000000000..6bb77b1f0 --- /dev/null +++ b/packages/sdk/src/interface/types/airtable_interface.ts @@ -0,0 +1,77 @@ +import {AirtableInterfaceCore, SdkInitDataCore} from '../../shared/types/airtable_interface_core'; +import {InterfaceSdkMode} from '../../sdk_mode'; +import {BaseDataCore} from '../../shared/types/base_core'; +import {TableId, PageId, FieldId} from '../../shared/types/hyper_ids'; +import {TableData} from './table'; + +/** @hidden */ +export enum BlockRunContextType { + PAGE_ELEMENT_IN_QUERY_CONTAINER = 'pageElementInQueryContainer', +} + +/** @hidden */ +export interface PageElementInQueryContainerBlockRunContextType { + type: BlockRunContextType.PAGE_ELEMENT_IN_QUERY_CONTAINER; + pageId: PageId; + isPageElementInEditMode: boolean; +} + +/** @hidden */ +export type BlockRunContext = PageElementInQueryContainerBlockRunContextType; + +/** @hidden */ +export interface SdkInitData extends SdkInitDataCore { + runContext: BlockRunContext; + baseData: BaseDataCore; +} + +/** @hidden */ +export enum BlockInstallationPageElementCustomPropertyTypeForAirtableInterface { + BOOLEAN = 'boolean', + STRING = 'string', + ENUM = 'enum', + FIELD_ID = 'fieldId', +} + +/** @hidden */ +export type BlockInstallationPageElementCustomPropertyForAirtableInterface = { + key: string; + label: string; +} & ( + | { + type: BlockInstallationPageElementCustomPropertyTypeForAirtableInterface.BOOLEAN; + defaultValue: boolean; + } + | { + type: BlockInstallationPageElementCustomPropertyTypeForAirtableInterface.STRING; + defaultValue?: string; + } + | { + type: BlockInstallationPageElementCustomPropertyTypeForAirtableInterface.ENUM; + possibleValues: Array<{value: string; label: string}>; + defaultValue?: string; + } + | { + type: BlockInstallationPageElementCustomPropertyTypeForAirtableInterface.FIELD_ID; + tableId: TableId; + possibleValues?: Array; + defaultValue?: FieldId; + } +); + +/** + * AirtableInterface is designed as the communication interface between the + * Block SDK and Airtable. The mechanism through which we communicate with Airtable + * depends on the context in which the block is running (i.e. frontend or backend), + * but the interface should remain consistent. + * + * @hidden + */ +export interface AirtableInterface extends AirtableInterfaceCore { + sdkInitData: SdkInitData; + + expandRecord(tableId: string, recordId: string): void; + setCustomPropertiesAsync( + properties: Array, + ): Promise; +} diff --git a/packages/sdk/src/interface/types/base.ts b/packages/sdk/src/interface/types/base.ts new file mode 100644 index 000000000..4cbeb456f --- /dev/null +++ b/packages/sdk/src/interface/types/base.ts @@ -0,0 +1,5 @@ +import {BaseDataCore} from '../../shared/types/base_core'; +import {TableData} from './table'; + +/** @hidden */ +export interface BaseData extends BaseDataCore {} diff --git a/packages/sdk/src/interface/types/mutations.ts b/packages/sdk/src/interface/types/mutations.ts new file mode 100644 index 000000000..a3ea1d399 --- /dev/null +++ b/packages/sdk/src/interface/types/mutations.ts @@ -0,0 +1,20 @@ +import {ObjectValues} from '../../shared/private_utils'; +import { + MutationCore, + MutationTypesCore, + PartialMutationCore, +} from '../../shared/types/mutations_core'; + +/** @hidden */ +export const MutationTypes = Object.freeze({ + ...MutationTypesCore, +}); + +/** @hidden */ +export type MutationType = ObjectValues; + +/** @hidden */ +export type Mutation = MutationCore; + +/** @hidden */ +export type PartialMutation = PartialMutationCore; diff --git a/packages/sdk/src/interface/types/record.ts b/packages/sdk/src/interface/types/record.ts new file mode 100644 index 000000000..322571699 --- /dev/null +++ b/packages/sdk/src/interface/types/record.ts @@ -0,0 +1,4 @@ +import {RecordDataCore} from '../../shared/types/record'; + +/** @hidden */ +export interface RecordData extends RecordDataCore {} diff --git a/packages/sdk/src/interface/types/table.ts b/packages/sdk/src/interface/types/table.ts new file mode 100644 index 000000000..44863bf84 --- /dev/null +++ b/packages/sdk/src/interface/types/table.ts @@ -0,0 +1,10 @@ +import {TableDataCore} from '../../shared/types/table_core'; +import {RecordId} from '../../shared/types/hyper_ids'; +import {ObjectMap} from '../../shared/private_utils'; +import {RecordData} from './record'; + +/** @hidden */ +export interface TableData extends TableDataCore { + recordsById: ObjectMap; + recordOrder: Array; +} diff --git a/packages/sdk/src/interface/ui/block_wrapper.tsx b/packages/sdk/src/interface/ui/block_wrapper.tsx new file mode 100644 index 000000000..99e6bebf0 --- /dev/null +++ b/packages/sdk/src/interface/ui/block_wrapper.tsx @@ -0,0 +1,72 @@ +/** @hidden */ /** */ +import * as React from 'react'; +import {css, keyframes} from 'emotion'; +import {InterfaceBlockSdk} from '../sdk'; +import Loader from '../../shared/ui/loader'; +import {SdkContext} from '../../shared/ui/sdk_context'; + +interface BlockWrapperProps { + sdk: InterfaceBlockSdk; + children: React.ReactNode; +} + +const suspenseFallbackClassName = css` + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + -ms-grid-row-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; +`; + +const spinScale = keyframes` + 0% { + transform: rotate(0) scale(1); + } + 50% { + transform: rotate(360deg) scale(0.9); + } + 100% { + transform: rotate(720deg) scale(1); + } +`; + +const animateSpinnerClassName = css` + animation-iteration-count: infinite; + animation-name: ${spinScale}; + animation-duration: 1800ms; + animation-timing-function: cubic-bezier(0.785, 0.135, 0.15, 0.86); +`; + +export class BlockWrapper extends React.Component { + /** @internal */ + _minSizeBeforeRender: {width: number | null; height: number | null} | null = null; + /** @hidden */ + render() { + return ( + + + + + } + > + {this.props.children} + + + ); + } +} diff --git a/packages/sdk/src/interface/ui/expand_record.ts b/packages/sdk/src/interface/ui/expand_record.ts new file mode 100644 index 000000000..2664e68db --- /dev/null +++ b/packages/sdk/src/interface/ui/expand_record.ts @@ -0,0 +1,21 @@ +/** @module @airtable/blocks/interface/ui: expandRecord */ /** */ +import {Record} from '../models/record'; + +/** + * Expands the given record in the Airtable UI. + * + * @param record The record to expand. + * + * @example + * ```js + * import {expandRecord} from '@airtable/blocks/interface/ui'; + * expandRecord(record); + * ``` + * @docsPath UI/utils/expandRecord + */ +export function expandRecord(record: Record): void { + record.parentTable.parentBase.__sdk.__airtableInterface.expandRecord( + record.parentTable.id, + record.id, + ); +} diff --git a/packages/sdk/src/interface/ui/initialize_block.tsx b/packages/sdk/src/interface/ui/initialize_block.tsx new file mode 100644 index 000000000..aa81d8ec2 --- /dev/null +++ b/packages/sdk/src/interface/ui/initialize_block.tsx @@ -0,0 +1,92 @@ +/** @module @airtable/blocks/ui: initializeBlock */ /** */ +import * as React from 'react'; +import ReactDOM from 'react-dom'; +import {spawnError} from '../../shared/error_utils'; +import {InterfaceBlockSdk} from '../sdk'; +import getAirtableInterface from '../../injected/airtable_interface'; +import {BlockRunContextType} from '../types/airtable_interface'; +import {BlockWrapper} from './block_wrapper'; + +let hasBeenInitialized = false; + +/** */ +type EntryElementFunction = () => React.ReactNode; +/** @hidden */ +interface EntryPoints { + interface?: EntryElementFunction; +} + +/** + * `initializeBlock` takes the top-level React component in your tree and renders it. It is conceptually similar to `ReactDOM.render`, but takes care of some Extensions-specific things. + * + * @param entryPoints An object with an `interface` property which is a function that returns your React Node. + * + * @example + * ```js + * import {initializeBlock} from '@airtable/blocks/interface/ui'; + * import React from 'react'; + * + * function App() { + * return ( + *
Hello world 🚀
+ * ); + * } + * + * initializeBlock({interface: () => }); + * ``` + * @docsPath UI/utils/initializeBlock + * @internal + */ +export function initializeBlock(entryPoints: EntryPoints) { + const body = typeof document !== 'undefined' ? document.body : null; + if (!body) { + throw spawnError('initializeBlock should only be called from browser environments'); + } + if (hasBeenInitialized) { + throw spawnError('initializeBlock should only be called once'); + } + hasBeenInitialized = true; + + const airtableInterface = getAirtableInterface(); + + let entryElement: React.ReactNode; + const runContext = airtableInterface.sdkInitData.runContext; + switch (runContext.type) { + case BlockRunContextType.PAGE_ELEMENT_IN_QUERY_CONTAINER: { + if (entryPoints.interface === undefined) { + throw spawnError( + 'If running an extension within the interface, it must have a interface initialization function', + ); + } + if (typeof entryPoints.interface !== 'function') { + throw spawnError( + 'initializeBlock must contain a interface function that returns a React element', + ); + } + entryElement = entryPoints.interface(); + break; + } + default: + throw spawnError('Invalid context to run '); + } + + if (!React.isValidElement(entryElement)) { + throw spawnError( + "The first argument to initializeBlock didn't return a valid React element", + ); + } + + const container = document.createElement('div'); + body.appendChild(container); + ReactDOM.render({entryElement}, container); +} + +let sdk: InterfaceBlockSdk; + +export function __injectSdkIntoInitializeBlock(_sdk: InterfaceBlockSdk) { + sdk = _sdk; +} + +export function __resetHasBeenInitialized() { + hasBeenInitialized = false; +} diff --git a/packages/sdk/src/interface/ui/ui.ts b/packages/sdk/src/interface/ui/ui.ts new file mode 100644 index 000000000..6dc684b7b --- /dev/null +++ b/packages/sdk/src/interface/ui/ui.ts @@ -0,0 +1,16 @@ +import '..'; + +export {expandRecord} from './expand_record'; +export {initializeBlock} from './initialize_block'; +export {useBase} from './use_base'; +export {useCustomProperties} from './use_custom_properties'; +export {useRecords} from './use_records'; +export {useRunInfo} from './use_run_info'; +export {useSession} from './use_session'; +export {default as useGlobalConfig} from '../../shared/ui/use_global_config'; +export {default as useLoadable} from '../../shared/ui/use_loadable'; +export {default as useSynced} from '../../shared/ui/use_synced'; +export {default as useWatchable} from '../../shared/ui/use_watchable'; +export {default as withHooks} from '../../shared/ui/with_hooks'; +export {default as colors} from '../../shared/colors'; +export {default as colorUtils} from '../../shared/color_utils'; diff --git a/packages/sdk/src/interface/ui/use_base.ts b/packages/sdk/src/interface/ui/use_base.ts new file mode 100644 index 000000000..a80d864a3 --- /dev/null +++ b/packages/sdk/src/interface/ui/use_base.ts @@ -0,0 +1,38 @@ +/** @module @airtable/blocks/interface/ui: useBase */ /** */ +import {InterfaceSdkMode} from '../../sdk_mode'; +import useBaseInternal from '../../shared/ui/use_base'; +import {Base} from '../models/base'; + +/** + * A hook for connecting a React component to your base's schema. This returns a {@link Base} + * instance and will re-render your component whenever the base's schema changes. That means any + * change to your base like tables being added or removed, fields getting renamed, etc. It excludes + * any change to the actual records in the base. + * + * `useBase` should meet most of your needs for working with base schema. If you need more granular + * control of when your component updates or want to do anything other than re-render, the lower + * level {@link useWatchable} hook might help. + * + * Returns the current base. + * + * @example + * ```js + * import {useBase} from '@airtable/blocks/interface/ui'; + * + * // renders a list of tables and automatically updates + * function TableList() { + * const base = useBase(); + * + * const tables = base.tables.map(table => { + * return
  • {table.name}
  • ; + * }); + * + * return
      {tables}
    ; + * } + * ``` + * @docsPath UI/hooks/useBase + * @hook + */ +export function useBase(): Base { + return useBaseInternal(); +} diff --git a/packages/sdk/src/interface/ui/use_custom_properties.ts b/packages/sdk/src/interface/ui/use_custom_properties.ts new file mode 100644 index 000000000..49e9569ef --- /dev/null +++ b/packages/sdk/src/interface/ui/use_custom_properties.ts @@ -0,0 +1,180 @@ +/** @module @airtable/blocks/interface/ui: useCustomProperties */ /** */ +import {useState, useEffect} from 'react'; +import {InterfaceSdkMode} from '../../sdk_mode'; +import {useSdk} from '../../shared/ui/sdk_context'; +import { + BlockInstallationPageElementCustomPropertyForAirtableInterface, + BlockInstallationPageElementCustomPropertyTypeForAirtableInterface, +} from '../types/airtable_interface'; +import useGlobalConfig from '../../shared/ui/use_global_config'; +import {Table} from '../models/table'; +import GlobalConfig from '../../shared/global_config'; +import {Base} from '../models/base'; +import {spawnUnknownSwitchCaseError} from '../../shared/error_utils'; +import {Field} from '../models/field'; + +/** + * TODO document + * @hidden + */ +type BlockPageElementCustomProperty = {key: string; label: string} & ( + | {type: 'boolean'; defaultValue: boolean} + | {type: 'string'; defaultValue?: string} + | { + type: 'enum'; + possibleValues: Array<{value: string; label: string}>; + defaultValue?: string; + } + | { + type: 'field'; + table: Table; + /** If not provided, all visible fields in the table will be shown in the dropdown. */ + possibleValues?: Array; + defaultValue?: Field; + } +); + +/** + * TODO document. Make sure to describe that getCustomProperties + * should be wrapped in useCallback. + * @hidden + */ +export function useCustomProperties( + getCustomProperties: (base: Base) => Array, +): {customPropertyValueByKey: {[key: string]: unknown}; errorState: {error: Error} | null} { + const sdk = useSdk(); + const [customProperties, setCustomProperties] = useState< + ReadonlyArray + >(() => { + return getCustomProperties(sdk.base); + }); + const globalConfig = useGlobalConfig(); + const [errorState, setErrorState] = useState<{error: Error} | null>(null); + + useEffect(() => { + const base = sdk.base; + const onSchemaChange = (base: Base) => { + const customProperties = getCustomProperties(base); + setCustomProperties(customProperties); + }; + base.watch('schema', onSchemaChange); + return () => { + base.unwatch('schema', onSchemaChange); + }; + }, [sdk, getCustomProperties]); + + const hasError = errorState !== null; + useEffect(() => { + if (hasError) { + return; + } + const customPropertiesForAirtableInterface = customProperties.map( + convertBlockPageElementCustomPropertyToBlockInstallationPageElementCustomPropertyForAirtableInterface, + ); + sdk.setCustomPropertiesAsync(customPropertiesForAirtableInterface).catch(error => { + setErrorState({error}); + }); + }, [sdk, customProperties, hasError]); + + const customPropertyValueByKey = hasError + ? {} + : Object.fromEntries( + customProperties.map(property => [ + property.key, + getCustomPropertyValue(sdk.base, globalConfig, property), + ]), + ); + + return { + customPropertyValueByKey, + errorState, + }; +} + +/** @internal */ +function convertBlockPageElementCustomPropertyToBlockInstallationPageElementCustomPropertyForAirtableInterface( + property: BlockPageElementCustomProperty, +): BlockInstallationPageElementCustomPropertyForAirtableInterface { + switch (property.type) { + case 'boolean': + return { + key: property.key, + label: property.label, + type: BlockInstallationPageElementCustomPropertyTypeForAirtableInterface.BOOLEAN, + defaultValue: property.defaultValue, + }; + case 'string': + return { + key: property.key, + label: property.label, + type: BlockInstallationPageElementCustomPropertyTypeForAirtableInterface.STRING, + defaultValue: property.defaultValue, + }; + case 'enum': + return { + key: property.key, + label: property.label, + type: BlockInstallationPageElementCustomPropertyTypeForAirtableInterface.ENUM, + possibleValues: property.possibleValues, + defaultValue: property.defaultValue, + }; + case 'field': + return { + key: property.key, + label: property.label, + type: BlockInstallationPageElementCustomPropertyTypeForAirtableInterface.FIELD_ID, + tableId: property.table.id, + possibleValues: property.possibleValues?.map(field => field.id), + defaultValue: property.defaultValue?.id, + }; + default: + throw spawnUnknownSwitchCaseError('property type', property, 'type'); + } +} + +/** @internal */ +function getCustomPropertyValue( + base: Base, + globalConfig: GlobalConfig, + property: BlockPageElementCustomProperty, +): unknown { + const defaultValue = 'defaultValue' in property ? property.defaultValue : null; + const rawValue = globalConfig.get(property.key) ?? defaultValue; + + switch (property.type) { + case 'boolean': + if (typeof rawValue === 'boolean') { + return rawValue; + } + return defaultValue; + case 'string': + if (typeof rawValue === 'string') { + return rawValue; + } + return defaultValue; + case 'enum': + if ( + typeof rawValue === 'string' && + property.possibleValues.some(value => value.value === rawValue) + ) { + return rawValue; + } + return defaultValue; + case 'field': + if ( + typeof rawValue === 'string' && + property.table === base.getTableById(property.table.id) + ) { + const fieldModel = property.table.fields.find(field => field.id === rawValue); + if ( + fieldModel && + (!property.possibleValues || property.possibleValues.includes(fieldModel)) + ) { + return fieldModel; + } + } + return defaultValue; + default: + throw spawnUnknownSwitchCaseError('property type', property, 'type'); + } +} diff --git a/packages/sdk/src/interface/ui/use_records.ts b/packages/sdk/src/interface/ui/use_records.ts new file mode 100644 index 000000000..2d42808fd --- /dev/null +++ b/packages/sdk/src/interface/ui/use_records.ts @@ -0,0 +1,50 @@ +/** @module @airtable/blocks/interface/ui: useRecords */ /** */ +import {InterfaceSdkMode} from '../../sdk_mode'; +import {useSdk} from '../../shared/ui/sdk_context'; +import useWatchable from '../../shared/ui/use_watchable'; +import {Record} from '../models/record'; +import {Table} from '../models/table'; + +/** + * A hook for working with all of the records (including cell values) in a + * particular table. Automatically handles loading data and updating + * your component when the underlying data changes. + * + * This hook re-renders when data concerning the records changes (specifically, when cell values + * change and when records are added or removed). + * + * Returns a list of records. + * + * @param table The {@link Table} you want the records from. + * + * @example + * ```js + * import {useBase, useRecords} from '@airtable/blocks/interface/ui'; + * + * function RecordList() { + * const base = useBase(); + * const table = base.tables[0]; + * + * // grab all the records from that table + * const records = useRecords(table); + * + * // render a list of records: + * return ( + *
      + * {records.map(record => { + * return
    • {record.name}
    • ; + * })} + *
    + * ); + * } + * ``` + * @docsPath UI/hooks/useRecords + * @hook + */ +export function useRecords(table: Table): Array { + const {base} = useSdk(); + const recordStore = base.__getRecordStore(table.id); + useWatchable(recordStore, ['records', 'recordIds', 'cellValues', 'recordOrder']); + const records = recordStore.records; + return records; +} diff --git a/packages/sdk/src/interface/ui/use_run_info.ts b/packages/sdk/src/interface/ui/use_run_info.ts new file mode 100644 index 000000000..c3af1e2f2 --- /dev/null +++ b/packages/sdk/src/interface/ui/use_run_info.ts @@ -0,0 +1,37 @@ +/** @module @airtable/blocks/interface/ui: useRunInfo */ /** */ +import {InterfaceSdkMode} from '../../sdk_mode'; +import {useSdk} from '../../shared/ui/sdk_context'; + +/** + * A hook for getting information about the current run context. This can be + * useful if you'd like to display some configuration options when the page + * element is in edit mode. + * + * `useRunInfo` + * + * @example + * ```js + * import {useRunInfo} from '@airtable/blocks/interface/ui'; + * + * // renders a list of tables and automatically updates + * function MyApp() { + * const runInfo = useRunInfo(); + * return ( + *
    + *

    Is development mode: {runInfo.isDevelopmentMode ? 'Yes' : 'No'}

    + *

    Is page element in edit mode: {runInfo.isPageElementInEditMode ? 'Yes' : 'No'}

    + *
    + * ); + * } + * ``` + * @docsPath UI/hooks/useRunInfo + * @hook + */ +export function useRunInfo(): {isDevelopmentMode: boolean; isPageElementInEditMode: boolean} { + const sdk = useSdk(); + const runContext = sdk.getBlockRunContext(); + return { + isDevelopmentMode: sdk.runInfo.isDevelopmentMode, + isPageElementInEditMode: runContext.isPageElementInEditMode, + }; +} diff --git a/packages/sdk/src/interface/ui/use_session.ts b/packages/sdk/src/interface/ui/use_session.ts new file mode 100644 index 000000000..5aceb4f3d --- /dev/null +++ b/packages/sdk/src/interface/ui/use_session.ts @@ -0,0 +1,35 @@ +/** @module @airtable/blocks/ui: useSession */ /** */ +import {InterfaceSdkMode} from '../../sdk_mode'; +import useSessionInternal from '../../shared/ui/use_session'; +import {Session} from '../models/session'; + +/** + * A hook for connecting a React component to the current session. This returns a {@link Session} + * instance and will re-render your component whenever the session changes (e.g. when the current user's + * permissions change or when the current user's name changes). + * + * `useSession` should meet most of your needs for working with {@link Session}. If you need more granular + * control of when your component updates or want to do anything other than re-render, the lower + * level {@link useWatchable} hook might help. + * + * @example + * ```js + * import {useSession} from '@airtable/blocks/interface/ui'; + * + * // Says hello to the current user and updates in realtime if the current user's + * // name or profile pic changes. + * function CurrentUserGreeter() { + * const session = useSession(); + * return ( + * + * Hello {session.currentUser?.name ?? 'stranger'}! + * + * ); + * } + * ``` + * @docsPath UI/hooks/useSession + * @hook + */ +export function useSession(): Session { + return useSessionInternal(); +} diff --git a/packages/sdk/src/sdk_mode.ts b/packages/sdk/src/sdk_mode.ts new file mode 100644 index 000000000..403942cb9 --- /dev/null +++ b/packages/sdk/src/sdk_mode.ts @@ -0,0 +1,91 @@ +import BaseBlockSdk from './base/sdk'; +import {BaseData as BaseDataForBaseSdkMode} from './base/types/base'; +import {TableData as TableDataForBaseSdkMode} from './base/types/table'; +import {RecordData as RecordDataForBaseSdkMode} from './base/types/record'; +import {BaseData as BaseDataForInterfaceSdkMode} from './interface/types/base'; +import {TableData as TableDataForInterfaceSdkMode} from './interface/types/table'; +import {RecordData as RecordDataForInterfaceSdkMode} from './interface/types/record'; +import BaseForBaseSdkMode from './base/models/base'; +import {Base as BaseForInterfaceSdkMode} from './interface/models/base'; +import FieldForBaseSdkMode from './base/models/field'; +import {InterfaceBlockSdk} from './interface/sdk'; +import TableForBaseSdkMode from './base/models/table'; +import {Table as TableForInterfaceSdkMode} from './interface/models/table'; +import {Field as FieldForInterfaceSdkMode} from './interface/models/field'; +import {Record as RecordForInterfaceSdkMode} from './interface/models/record'; +import RecordForBaseSdkMode from './base/models/record'; +import RecordStoreForBaseSdkMode from './base/models/record_store'; +import {RecordStore as RecordStoreForInterfaceSdkMode} from './interface/models/record_store'; +import { + AirtableInterface as AirtableInterfaceForBaseSdkMode, + BlockRunContext as BlockRunContextForBaseSdkMode, + SdkInitData as SdkInitDataForBaseSdkMode, +} from './base/types/airtable_interface'; +import { + AirtableInterface as AirtableInterfaceForInterfaceSdkMode, + BlockRunContext as BlockRunContextForInterfaceSdkMode, + SdkInitData as SdkInitDataForInterfaceSdkMode, +} from './interface/types/airtable_interface'; +import MutationsForBaseSdkMode from './base/models/mutations'; +import {Mutations as MutationsForInterfaceSdkMode} from './interface/models/mutations'; +import { + Mutation as MutationForBaseSdkMode, + PartialMutation as PartialMutationForBaseSdkMode, +} from './base/types/mutations'; +import { + Mutation as MutationForInterfaceSdkMode, + PartialMutation as PartialMutationForInterfaceSdkMode, +} from './interface/types/mutations'; +import SesssionForBaseSdkMode from './base/models/session'; +import {Session as SessionForInterfaceSdkMode} from './interface/models/session'; + +/** @hidden */ +export interface BaseSdkMode { + mode: 'base'; + runContextT: BlockRunContextForBaseSdkMode; + SdkT: BaseBlockSdk; + SdkInitDataT: SdkInitDataForBaseSdkMode; + AirtableInterfaceT: AirtableInterfaceForBaseSdkMode; + SessionT: SesssionForBaseSdkMode; + MutationsModelT: MutationsForBaseSdkMode; + MutationT: MutationForBaseSdkMode; + PartialMutationT: PartialMutationForBaseSdkMode; + + BaseDataT: BaseDataForBaseSdkMode; + TableDataT: TableDataForBaseSdkMode; + RecordDataT: RecordDataForBaseSdkMode; + + BaseT: BaseForBaseSdkMode; + TableT: TableForBaseSdkMode; + FieldT: FieldForBaseSdkMode; + RecordT: RecordForBaseSdkMode; + /** @internal */ + RecordStoreT: RecordStoreForBaseSdkMode; +} + +/** @hidden */ +export interface InterfaceSdkMode { + mode: 'interface'; + runContextT: BlockRunContextForInterfaceSdkMode; + SdkT: InterfaceBlockSdk; + SdkInitDataT: SdkInitDataForInterfaceSdkMode; + AirtableInterfaceT: AirtableInterfaceForInterfaceSdkMode; + SessionT: SessionForInterfaceSdkMode; + MutationsModelT: MutationsForInterfaceSdkMode; + MutationT: MutationForInterfaceSdkMode; + PartialMutationT: PartialMutationForInterfaceSdkMode; + + BaseDataT: BaseDataForInterfaceSdkMode; + TableDataT: TableDataForInterfaceSdkMode; + RecordDataT: RecordDataForInterfaceSdkMode; + + BaseT: BaseForInterfaceSdkMode; + TableT: TableForInterfaceSdkMode; + FieldT: FieldForInterfaceSdkMode; + RecordT: RecordForInterfaceSdkMode; + /** @internal */ + RecordStoreT: RecordStoreForInterfaceSdkMode; +} + +/** @hidden */ +export type SdkMode = BaseSdkMode | InterfaceSdkMode; diff --git a/packages/sdk/src/color_utils.ts b/packages/sdk/src/shared/color_utils.ts similarity index 100% rename from packages/sdk/src/color_utils.ts rename to packages/sdk/src/shared/color_utils.ts diff --git a/packages/sdk/src/colors.ts b/packages/sdk/src/shared/colors.ts similarity index 100% rename from packages/sdk/src/colors.ts rename to packages/sdk/src/shared/colors.ts diff --git a/packages/sdk/src/error_utils.ts b/packages/sdk/src/shared/error_utils.ts similarity index 100% rename from packages/sdk/src/error_utils.ts rename to packages/sdk/src/shared/error_utils.ts diff --git a/packages/sdk/src/event_tracker.ts b/packages/sdk/src/shared/event_tracker.ts similarity index 88% rename from packages/sdk/src/event_tracker.ts rename to packages/sdk/src/shared/event_tracker.ts index 4f450e9d0..6afd955a3 100644 --- a/packages/sdk/src/event_tracker.ts +++ b/packages/sdk/src/shared/event_tracker.ts @@ -1,4 +1,4 @@ -import getAirtableInterface from './injected/airtable_interface'; +import getAirtableInterface from '../injected/airtable_interface'; /** @hidden */ export function trackEvent(eventSchemaName: string, eventData: {[key: string]: unknown} = {}) { getAirtableInterface().trackEvent(eventSchemaName, eventData); diff --git a/packages/sdk/src/global_config.ts b/packages/sdk/src/shared/global_config.ts similarity index 96% rename from packages/sdk/src/global_config.ts rename to packages/sdk/src/shared/global_config.ts index fb0d23787..3fb370691 100644 --- a/packages/sdk/src/global_config.ts +++ b/packages/sdk/src/shared/global_config.ts @@ -1,6 +1,6 @@ /** @module @airtable/blocks: globalConfig */ /** */ +import {SdkMode} from '../sdk_mode'; import Watchable from './watchable'; -import {AirtableInterface} from './types/airtable_interface'; import {spawnError} from './error_utils'; import { GlobalConfigPath, @@ -12,9 +12,10 @@ import { PartialGlobalConfigUpdate, GlobalConfigPathValidationResult, } from './types/global_config'; -import {MutationTypes, PermissionCheckResult} from './types/mutations'; +import {PermissionCheckResult, MutationTypesCore} from './types/mutations_core'; import {getValueAtOwnPath} from './private_utils'; -import Sdk from './sdk'; +import {BlockSdkCore} from './sdk_core'; +import {AirtableInterfaceCore} from './types/airtable_interface_core'; /** * You can watch any top-level key in global config. Use '*' to watch every change. @@ -51,15 +52,15 @@ class GlobalConfig extends Watchable { return true; } /** @internal */ - _sdk: Sdk; + _sdk: BlockSdkCore; /** @internal */ _kvStore: GlobalConfigData; /** @internal */ - _airtableInterface: AirtableInterface; + _airtableInterface: AirtableInterfaceCore; /** * @internal */ - constructor(initialKvValuesByKey: GlobalConfigData, sdk: Sdk) { + constructor(initialKvValuesByKey: GlobalConfigData, sdk: BlockSdkCore) { super(); this._kvStore = initialKvValuesByKey; @@ -265,7 +266,7 @@ class GlobalConfig extends Watchable { updates?: ReadonlyArray, ): PermissionCheckResult { return this._sdk.__mutations.checkPermissionsForMutation({ - type: MutationTypes.SET_MULTIPLE_GLOBAL_CONFIG_PATHS, + type: MutationTypesCore.SET_MULTIPLE_GLOBAL_CONFIG_PATHS, updates: updates ? updates.map(({path, value}) => ({path: path || undefined, value})) : undefined, @@ -350,7 +351,7 @@ class GlobalConfig extends Watchable { } await this._sdk.__mutations.applyMutationAsync({ - type: MutationTypes.SET_MULTIPLE_GLOBAL_CONFIG_PATHS, + type: MutationTypesCore.SET_MULTIPLE_GLOBAL_CONFIG_PATHS, updates, }); } diff --git a/packages/sdk/src/models/abstract_model.ts b/packages/sdk/src/shared/models/abstract_model.ts similarity index 89% rename from packages/sdk/src/models/abstract_model.ts rename to packages/sdk/src/shared/models/abstract_model.ts index ce3612ddf..e9053a66f 100644 --- a/packages/sdk/src/models/abstract_model.ts +++ b/packages/sdk/src/shared/models/abstract_model.ts @@ -1,17 +1,18 @@ /** @module @airtable/blocks/models: Abstract models */ /** */ import {invariant, spawnError} from '../error_utils'; -import Sdk from '../sdk'; -import {BaseData} from '../types/base'; import Watchable from '../watchable'; +import {SdkMode} from '../../sdk_mode'; /** * Abstract superclass for all models. You won't use this class directly. * * @docsPath models/advanced/AbstractModel */ -abstract class AbstractModel extends Watchable< - WatchableKey -> { +abstract class AbstractModel< + SdkModeT extends SdkMode, + DataType, + WatchableKey extends string +> extends Watchable { /** @internal */ static _className = 'AbstractModel'; /** @@ -26,15 +27,15 @@ abstract class AbstractModel extends Watc return false; } /** @internal */ - _baseData: BaseData; + _baseData: SdkModeT['BaseDataT']; /** @internal */ _id: string; /** @internal */ - _sdk: Sdk; + _sdk: SdkModeT['SdkT']; /** * @internal */ - constructor(sdk: Sdk, modelId: string) { + constructor(sdk: SdkModeT['SdkT'], modelId: string) { super(); invariant( diff --git a/packages/sdk/src/models/base.ts b/packages/sdk/src/shared/models/base_core.ts similarity index 58% rename from packages/sdk/src/models/base.ts rename to packages/sdk/src/shared/models/base_core.ts index 2e7243d21..eed7cd235 100644 --- a/packages/sdk/src/models/base.ts +++ b/packages/sdk/src/shared/models/base_core.ts @@ -1,18 +1,13 @@ -/** @module @airtable/blocks/models: Base */ /** */ -import {BaseData, ModelChange} from '../types/base'; -import {CollaboratorData, UserId} from '../types/collaborator'; -import {FieldType} from '../types/field'; -import {MutationTypes, PermissionCheckResult} from '../types/mutations'; -import {TableId} from '../types/table'; -import {isEnumValue, entries, isDeepEqual, ObjectValues, ObjectMap, has} from '../private_utils'; +import {ModelChange} from '../types/base_core'; +import {CollaboratorData} from '../types/collaborator'; +import {TableId, UserId} from '../types/hyper_ids'; +import {isEnumValue, entries, isDeepEqual, ObjectValues, has, ObjectMap} from '../private_utils'; import {spawnError, invariant} from '../error_utils'; -import Sdk from '../sdk'; -import Table from './table'; -import RecordStore from './record_store'; +import {SdkMode} from '../../sdk_mode'; import AbstractModel from './abstract_model'; -const WatchableBaseKeys = Object.freeze({ +export const WatchableBaseKeys = Object.freeze({ name: 'name' as const, tables: 'tables' as const, collaborators: 'collaborators' as const, @@ -40,32 +35,22 @@ export type ChangedPathsForType = T extends string | number | boolean | Reado ? ChangedPathsForObject : never; -/** - * Model class representing a base. - * - * If you want the base model to automatically recalculate whenever the base schema changes, try the - * {@link useBase} hook. Alternatively, you can manually subscribe to changes with - * {@link useWatchable} (recommended) or [Base#watch](/api/models/Base#watch). - * - * @example - * ```js - * import {base} from '@airtable/blocks'; - * - * console.log('The name of your base is', base.name); - * ``` - * @docsPath models/Base - */ -class Base extends AbstractModel { +/** @hidden */ +export abstract class BaseCore extends AbstractModel< + SdkModeT, + SdkModeT['BaseDataT'], + WatchableBaseKey +> { /** @internal */ - static _className = 'Base'; + static _className = 'BaseCore'; /** @internal */ static _isWatchableKey(key: string): boolean { return isEnumValue(WatchableBaseKeys, key); } /** @internal */ - _tableModelsById: {[key: string]: Table}; + _tableModelsById: {[key: string]: SdkModeT['TableT']}; /** @internal */ - _tableRecordStoresByTableId: ObjectMap = {}; + _tableRecordStoresByTableId: ObjectMap = {}; /** @internal */ __billingPlanGrouping: string; /** @internal */ @@ -73,7 +58,7 @@ class Base extends AbstractModel { /** * @internal */ - constructor(sdk: Sdk) { + constructor(sdk: SdkModeT['SdkT']) { super(sdk, sdk.__airtableInterface.sdkInitData.baseData.id); this._tableModelsById = {}; this.__billingPlanGrouping = @@ -85,14 +70,14 @@ class Base extends AbstractModel { * * @internal */ - get __sdk(): Sdk { + get __sdk(): SdkModeT['SdkT'] { return this._sdk; } /** * @internal */ - get _dataOrNullIfDeleted(): BaseData | null { + get _dataOrNullIfDeleted(): SdkModeT['BaseDataT'] | null { return this._baseData; } /** @@ -144,16 +129,27 @@ class Base extends AbstractModel { * console.log(`You have ${base.tables.length} tables`); * ``` */ - get tables(): Array
    { - const tables: Array
    = []; - this._data.tableOrder.forEach(tableId => { + get tables(): Array { + const tables: Array = []; + for (const tableId of this._iterateTableIds()) { const table = this.getTableByIdIfExists(tableId); if (table) { tables.push(table); } - }); + } return tables; } + + /** @internal */ + abstract _constructTable(tableId: TableId): SdkModeT['TableT']; + /** @internal */ + abstract _constructRecordStore( + sdk: SdkModeT['SdkT'], + tableId: TableId, + ): SdkModeT['RecordStoreT']; + /** @internal */ + abstract _iterateTableIds(): Iterable; + /** * The users who have access to this base. * @@ -253,19 +249,7 @@ class Base extends AbstractModel { /** * @internal */ - __getRecordStore(tableId: TableId): RecordStore { - if (has(this._tableRecordStoresByTableId, tableId)) { - return this._tableRecordStoresByTableId[tableId]; - } - invariant(this._data.tablesById[tableId], 'table must exist'); - const newRecordStore = new RecordStore(this._sdk, tableId); - this._tableRecordStoresByTableId[tableId] = newRecordStore; - return newRecordStore; - } - /** - * @internal - */ - __getBaseData(): BaseData { + __getBaseData(): SdkModeT['BaseDataT'] { return this._data; } /** @@ -273,17 +257,12 @@ class Base extends AbstractModel { * * @param tableId The ID of the table. */ - getTableByIdIfExists(tableId: string): Table | null { + getTableByIdIfExists(tableId: string): SdkModeT['TableT'] | null { if (!this._data.tablesById[tableId]) { return null; } else { if (!this._tableModelsById[tableId]) { - this._tableModelsById[tableId] = new Table( - this, - this.__getRecordStore(tableId), - tableId, - this._sdk, - ); + this._tableModelsById[tableId] = this._constructTable(tableId); } return this._tableModelsById[tableId]; } @@ -295,7 +274,7 @@ class Base extends AbstractModel { * * @param tableId The ID of the table. */ - getTableById(tableId: string): Table { + getTableById(tableId: string): SdkModeT['TableT'] { const table = this.getTableByIdIfExists(tableId); if (!table) { throw spawnError("No table with ID %s in base '%s'", tableId, this.name); @@ -307,7 +286,7 @@ class Base extends AbstractModel { * * @param tableName The name of the table you're looking for. */ - getTableByNameIfExists(tableName: string): Table | null { + getTableByNameIfExists(tableName: string): SdkModeT['TableT'] | null { for (const [tableId, tableData] of entries(this._data.tablesById)) { if (tableData.name === tableName) { return this.getTableByIdIfExists(tableId); @@ -322,7 +301,7 @@ class Base extends AbstractModel { * * @param tableName The name of the table you're looking for. */ - getTableByName(tableName: string): Table { + getTableByName(tableName: string): SdkModeT['TableT'] { const table = this.getTableByNameIfExists(tableName); if (!table) { throw spawnError("No table named '%s' in base '%s'", tableName, this.name); @@ -339,7 +318,7 @@ class Base extends AbstractModel { * * @param tableIdOrName The ID or name of the table you're looking for. */ - getTableIfExists(tableIdOrName: TableId | string): Table | null { + getTableIfExists(tableIdOrName: TableId | string): SdkModeT['TableT'] | null { return ( this.getTableByIdIfExists(tableIdOrName) ?? this.getTableByNameIfExists(tableIdOrName) ); @@ -355,7 +334,7 @@ class Base extends AbstractModel { * * @param tableIdOrName The ID or name of the table you're looking for. */ - getTable(tableIdOrName: TableId | string): Table { + getTable(tableIdOrName: TableId | string): SdkModeT['TableT'] { const table = this.getTableIfExists(tableIdOrName); if (!table) { throw spawnError( @@ -368,179 +347,31 @@ class Base extends AbstractModel { } /** - * Checks whether the current user has permission to create a table. - * - * Accepts partial input, in the same format as {@link createTableAsync}. - * - * Returns `{hasPermission: true}` if the current user can update the specified record, - * `{hasPermission: false, reasonDisplayString: string}` otherwise. `reasonDisplayString` may be - * used to display an error message to the user. - * - * @param name name for the table. must be case-insensitive unique - * @param fields array of fields to create in the table - * - * @example - * ```js - * const createTableCheckResult = base.checkPermissionsForCreateTable(); - * - * if (!createTableCheckResult.hasPermission) { - * alert(createTableCheckResult.reasonDisplayString); - * } - * ``` - */ - checkPermissionsForCreateTable( - name?: string, - fields?: Array<{ - name?: string; - type?: FieldType; - options?: {[key: string]: unknown} | null; - description?: string | null; - }>, - ): PermissionCheckResult { - return this._sdk.__mutations.checkPermissionsForMutation({ - type: MutationTypes.CREATE_SINGLE_TABLE, - id: undefined, - name: name, - fields: fields?.map(field => { - return { - name: field.name, - config: field.type - ? { - type: field.type, - ...(field.options ? {options: field.options} : null), - } - : undefined, - description: field.description, - }; - }), - }); - } - - /** - * An alias for `checkPermissionsForCreateTable(name, fields).hasPermission`. - * - * Checks whether the current user has permission to create a table. - * - * Accepts partial input, in the same format as {@link createTableAsync}. - * - * @param name name for the table. must be case-insensitive unique - * @param fields array of fields to create in the table - * - * @example - * ```js - * const canCreateTable = table.hasPermissionToCreateTable(); - * - * if (!canCreateTable) { - * alert('not allowed!'); - * } - * ``` - */ - hasPermissionToCreateTable( - name?: string, - fields?: Array<{ - name?: string; - type?: FieldType; - options?: {[key: string]: unknown} | null; - description?: string | null; - }>, - ): boolean { - return this.checkPermissionsForCreateTable(name, fields).hasPermission; - } - - /** - * Creates a new table. - * - * Throws an error if the user does not have permission to create a table, if an invalid - * table name is provided, or if invalid fields are provided (invalid name, type, options or - * description). - * - * Refer to {@link FieldType} for supported field types, the write format for field options, and - * other specifics for certain field types. - * - * At least one field must be specified. The first field in the `fields` array will be used as - * the table's [primary field](https://support.airtable.com/hc/en-us/articles/202624179-The-Name-Field) - * and must be a supported primary field type. Fields must have case-insensitive unique names - * within the table. - * - * A default grid view will be created with all fields visible. - * - * This action is asynchronous. Unlike new records, new tables are **not** created - * optimistically locally. You must `await` the returned promise before using the new - * table in your extension. - * - * @param name name for the table. must be case-insensitive unique - * @param fields array of fields to create in the table: see below for an example. `name` and - * `type` must be specified for all fields, while `options` is only required for fields that - * have field options. `description` is optional and will be `''` if not specified or if - * specified as `null`. - * - * @example - * ```js - * async function createNewTable() { - * const name = 'My new table'; - * const fields = [ - * // Name will be the primary field of the table. - * {name: 'Name', type: FieldType.SINGLE_LINE_TEXT, description: 'This is the primary field'}, - * {name: 'Notes', type: FieldType.RICH_TEXT}, - * {name: 'Attachments', type: FieldType.MULTIPLE_ATTACHMENTS}, - * {name: 'Number', type: FieldType.NUMBER, options: { - * precision: 8, - * }}, - * {name: 'Select', type: FieldType.SINGLE_SELECT, options: { - * choices: [ - * {name: 'A'}, - * {name: 'B'}, - * ], - * }}, - * ]; - * - * if (base.hasPermissionToCreateTable(name, fields)) { - * await base.createTableAsync(name, fields); - * } - * } - * ``` + * Returns the maximum number of records allowed in each table of this base. */ - async createTableAsync( - name: string, - fields: Array<{ - name: string; - type: FieldType; - options?: {[key: string]: unknown} | null; - description?: string | null; - }>, - ): Promise
    { - const tableId = this._sdk.__airtableInterface.idGenerator.generateTableId(); - - await this._sdk.__mutations.applyMutationAsync({ - id: tableId, - type: MutationTypes.CREATE_SINGLE_TABLE, - name, - fields: fields.map(field => { - return { - name: field.name, - config: { - type: field.type, - ...(field.options ? {options: field.options} : null), - }, - description: field.description ?? null, - }; - }), - }); - - return this.getTableById(tableId); + getMaxRecordsPerTable(): number { + return this._data.maxRowsPerTable ?? 100000; } /** - * Returns the maximum number of records allowed in each table of this base. + * @internal */ - getMaxRecordsPerTable(): number { - return this._data.maxRowsPerTable ?? 100000; + __getRecordStore(tableId: TableId): SdkModeT['RecordStoreT'] { + if (has(this._tableRecordStoresByTableId, tableId)) { + return this._tableRecordStoresByTableId[tableId]; + } + invariant(this._data.tablesById[tableId], 'table must exist'); + const newRecordStore = this._constructRecordStore(this._sdk, tableId); + this._tableRecordStoresByTableId[tableId] = newRecordStore; + return newRecordStore; } /** * @internal */ - __triggerOnChangeForChangedPaths(changedPaths: ChangedPathsForType) { + __triggerOnChangeForChangedPaths( + changedPaths: ChangedPathsForType, + ): void { let didSchemaChange = false; if (changedPaths.name) { this._onChange(WatchableBaseKeys.name); @@ -550,32 +381,20 @@ class Base extends AbstractModel { this._onChange(WatchableBaseKeys.color); didSchemaChange = true; } - if (changedPaths.tableOrder) { - this._onChange(WatchableBaseKeys.tables); - didSchemaChange = true; - - for (const [tableId, tableModel] of entries(this._tableModelsById)) { - if (tableModel.isDeleted) { - delete this._tableModelsById[tableId]; - } - } - for (const [tableId, recordStore] of entries(this._tableRecordStoresByTableId)) { - if (recordStore && recordStore.isDeleted) { - recordStore.__onDataDeletion(); - delete this._tableRecordStoresByTableId[tableId]; - } - } - } const {tablesById} = changedPaths; if (tablesById) { - for (const [tableId, dirtyTablePaths] of entries(tablesById)) { - const table = this.getTableByIdIfExists(tableId); - if (table && dirtyTablePaths) { - const didTableSchemaChange = table.__triggerOnChangeForDirtyPaths( - dirtyTablePaths, - ); - if (didTableSchemaChange) { - didSchemaChange = true; + if (isDeepEqual(tablesById, {_isDirty: true})) { + didSchemaChange = true; + } else { + for (const [tableId, dirtyTablePaths] of entries(tablesById)) { + const table = this.getTableByIdIfExists(tableId); + if (table && dirtyTablePaths) { + const didTableSchemaChange = table.__triggerOnChangeForDirtyPaths( + dirtyTablePaths, + ); + if (didTableSchemaChange) { + didSchemaChange = true; + } } } } @@ -595,8 +414,8 @@ class Base extends AbstractModel { */ __applyChangesWithoutTriggeringEvents( changes: ReadonlyArray, - ): ChangedPathsForType { - const changedPaths = {}; + ): ChangedPathsForType { + const changedPaths = {} as ChangedPathsForType; for (const change of changes) { this._applyChange(change.path, change.value, changedPaths); } @@ -608,7 +427,7 @@ class Base extends AbstractModel { _applyChange( path: Array, value: unknown, - changedPathsByRef: ChangedPathsForType, + changedPathsByRef: ChangedPathsForType, ) { let dataSubtree = this._data as any; let dirtySubtree = changedPathsByRef as any; @@ -643,5 +462,3 @@ class Base extends AbstractModel { } } } - -export default Base; diff --git a/packages/sdk/src/shared/models/field_core.ts b/packages/sdk/src/shared/models/field_core.ts new file mode 100644 index 000000000..fd55f67ad --- /dev/null +++ b/packages/sdk/src/shared/models/field_core.ts @@ -0,0 +1,279 @@ +import {FieldData, FieldType, FieldOptions, FieldConfig} from '../types/field'; +import {isEnumValue, cloneDeep, ObjectValues, FlowAnyObject} from '../private_utils'; +import {SdkMode} from '../../sdk_mode'; +import {FieldTypeConfig} from '../types/airtable_interface_core'; +import AbstractModel from './abstract_model'; + +const WatchableFieldKeys = Object.freeze({ + name: 'name' as const, + type: 'type' as const, + options: 'options' as const, + isComputed: 'isComputed' as const, + description: 'description' as const, + isFieldSynced: 'isFieldSynced' as const, +}); + +/** + * All the watchable keys in a field. + * - `name` + * - `type` + * - `options` + * - `isComputed` + * - `description` + */ +export type WatchableFieldKey = ObjectValues; + +/** @hidden */ +export abstract class FieldCore extends AbstractModel< + SdkModeT, + FieldData, + WatchableFieldKey +> { + /** @internal */ + static _className = 'FieldCore'; + /** @internal */ + static _isWatchableKey(key: string) { + return isEnumValue(WatchableFieldKeys, key); + } + /** @internal */ + _parentTable: SdkModeT['TableT']; + /** @internal */ + _cachedFieldTypeConfigOrNull: FieldTypeConfig | null; + /** + * @internal + */ + constructor(sdk: SdkModeT['SdkT'], parentTable: SdkModeT['TableT'], fieldId: string) { + super(sdk, fieldId); + + this._parentTable = parentTable; + this._cachedFieldTypeConfigOrNull = null; + } + + /** + * @internal + */ + get _dataOrNullIfDeleted(): FieldData | null { + const tableData = this._baseData.tablesById[this.parentTable.id]; + return tableData?.fieldsById[this._id] ?? null; + } + /** + * The table that this field belongs to. Should never change because fields aren't moved between tables. + * + * @internal (since we may not be able to return parent model instances in the immutable models world) + * @example + * ```js + * const field = myTable.getFieldByName('Name'); + * console.log(field.parentTable.id === myTable.id); + * // => true + * ``` + */ + get parentTable(): SdkModeT['TableT'] { + return this._parentTable; + } + /** + * The name of the field. Can be watched. + * + * @example + * ```js + * console.log(myField.name); + * // => 'Name' + * ``` + */ + get name(): string { + return this._data.name; + } + /** + * The type of the field. Can be watched. + * + * @example + * ```js + * console.log(myField.type); + * // => 'singleLineText' + * ``` + */ + get type(): FieldType { + const {type} = this._getCachedConfigFromFieldTypeProvider(); + // @ts-ignore + if (type === 'lookup') { + return FieldType.MULTIPLE_LOOKUP_VALUES; + } else { + return type; + } + } + /** + * The configuration options of the field. The structure of the field's + * options depend on the field's type. `null` if the field has no options. + * Can be watched. + * + * @see {@link FieldType} + * @example + * ```js + * import {FieldType} from '@airtable/blocks/models'; + * + * if (myField.type === FieldType.CURRENCY) { + * console.log(myField.options.symbol); + * // => '$' + * } + * ``` + */ + get options(): FieldOptions | null { + const {options} = this._getCachedConfigFromFieldTypeProvider(); + + return options ? cloneDeep(options) : null; + } + + _getCachedConfigFromFieldTypeProvider(): FieldTypeConfig { + if (this._cachedFieldTypeConfigOrNull !== null) { + return this._cachedFieldTypeConfigOrNull; + } + const airtableInterface = this._sdk.__airtableInterface; + const appInterface = this._sdk.__appInterface; + + this._cachedFieldTypeConfigOrNull = airtableInterface.fieldTypeProvider.getConfig( + appInterface, + this._data, + this.parentTable.__getFieldNamesById(), + ); + + return this._cachedFieldTypeConfigOrNull; + } + _clearCachedConfig(): void { + this._cachedFieldTypeConfigOrNull = null; + } + + /** + * The type and options of the field to make type narrowing `FieldOptions` easier. + * + * @see {@link FieldConfig} + * @example + * const fieldConfig = field.config; + * if (fieldConfig.type === FieldType.SINGLE_SELECT) { + * return fieldConfig.options.choices; + * } else if (fieldConfig.type === FieldType.MULTIPLE_LOOKUP_VALUES && fieldConfig.options.isValid) { + * if (fieldConfig.options.result.type === FieldType.SINGLE_SELECT) { + * return fieldConfig.options.result.options.choices; + * } + * } + * return DEFAULT_CHOICES; + */ + get config(): FieldConfig { + return { + type: this.type, + options: this.options, + } as FieldConfig; + } + + /** + * `true` if this field is synced, `false` otherwise. A field is + * "synced" if it's source is from another airtable base or external data source + * like Google Calendar, Jira, etc.. + * + * @hidden + */ + get isFieldSynced(): boolean { + return this._data.isSynced ?? false; + } + + /** + * `true` if this field is computed, `false` otherwise. A field is + * "computed" if it's value is not set by user input (e.g. autoNumber, formula, + * etc.). Can be watched + * + * @example + * ```js + * console.log(mySingleLineTextField.isComputed); + * // => false + * console.log(myAutoNumberField.isComputed); + * // => true + * ``` + */ + get isComputed(): boolean { + const airtableInterface = this._sdk.__airtableInterface; + return airtableInterface.fieldTypeProvider.isComputed(this._data); + } + /** + * `true` if this field is its parent table's primary field, `false` otherwise. + * Should never change because the primary field of a table cannot change. + */ + get isPrimaryField(): boolean { + return this.id === this.parentTable.primaryField.id; + } + + /** + * The description of the field, if it has one. Can be watched. + * + * @example + * ```js + * console.log(myField.description); + * // => 'This is my field' + * ``` + */ + get description(): string | null { + return this._data.description; + } + /** + * Attempt to parse a given string and return a valid cell value for the field's current config. + * Returns `null` if unable to parse the given string. + * + * @param string The string to parse. + * @example + * ```js + * const inputString = '42'; + * const cellValue = myNumberField.convertStringToCellValue(inputString); + * console.log(cellValue === 42); + * // => true + * ``` + */ + convertStringToCellValue(string: string): unknown { + const airtableInterface = this._sdk.__airtableInterface; + const appInterface = this._sdk.__appInterface; + + const cellValue = airtableInterface.fieldTypeProvider.convertStringToCellValue( + appInterface, + string, + this._data, + {parseDateCellValueInColumnTimeZone: this.type === FieldType.DATE_TIME}, + ); + + if (this.isComputed) { + return cellValue; + } + + const validationResult = airtableInterface.fieldTypeProvider.validateCellValueForUpdate( + appInterface, + cellValue, + null, + this._data, + ); + + if (validationResult.isValid) { + return cellValue; + } else { + return null; + } + } + /** + * @internal + */ + __triggerOnChangeForDirtyPaths(dirtyPaths: FlowAnyObject) { + this._clearCachedConfig(); + + if (dirtyPaths.name) { + this._onChange(WatchableFieldKeys.name); + } + if (dirtyPaths.type) { + this._onChange(WatchableFieldKeys.type); + + this._onChange(WatchableFieldKeys.isComputed); + } + if (dirtyPaths.typeOptions) { + this._onChange(WatchableFieldKeys.options); + } + if (dirtyPaths.description) { + this._onChange(WatchableFieldKeys.description); + } + if (dirtyPaths.isSynced) { + this._onChange(WatchableFieldKeys.isFieldSynced); + } + } +} diff --git a/packages/sdk/src/shared/models/mutations_core.ts b/packages/sdk/src/shared/models/mutations_core.ts new file mode 100644 index 000000000..cff149129 --- /dev/null +++ b/packages/sdk/src/shared/models/mutations_core.ts @@ -0,0 +1,137 @@ +import {SdkMode} from '../../sdk_mode'; +import {ModelChange} from '../types/base_core'; +import {GlobalConfigUpdate} from '../types/global_config'; +import { + PermissionCheckResult, + MutationTypesCore, + SetMultipleGlobalConfigPathsMutation, +} from '../types/mutations_core'; +import {spawnError} from '../error_utils'; +import {AirtableInterfaceCore} from '../types/airtable_interface_core'; + +export const MUTATIONS_MAX_BATCH_SIZE = 50; + +const MUTATIONS_MAX_BODY_SIZE = 1.9 * 1024 * 1024; + +const MUTATION_HOLD_FOR_MS = 100; + +/** @hidden */ +export abstract class MutationsCore { + /** @internal */ + _airtableInterface: SdkModeT['AirtableInterfaceT']; + /** @internal */ + _session: SdkModeT['SessionT']; + /** @internal */ + _sdk: SdkModeT['SdkT']; + /** @internal */ + _base: SdkModeT['BaseT']; + /** @internal */ + _applyModelChanges: (arg1: Array) => void; + /** @internal */ + _applyGlobalConfigUpdates: (arg1: ReadonlyArray) => void; + + /** @hidden */ + constructor( + sdk: SdkModeT['SdkT'], + session: SdkModeT['SessionT'], + base: SdkModeT['BaseT'], + applyModelChanges: (arg1: ReadonlyArray) => void, + applyGlobalConfigUpdates: (arg1: ReadonlyArray) => void, + ) { + this._airtableInterface = sdk.__airtableInterface; + this._session = session; + this._sdk = sdk; + this._base = base; + this._applyModelChanges = applyModelChanges; + this._applyGlobalConfigUpdates = applyGlobalConfigUpdates; + } + + /** @hidden */ + async applyMutationAsync(mutation: SdkModeT['MutationT']): Promise { + this._assertMutationIsValid(mutation); + this._assertMutationUnderLimits(mutation); + + const permissionCheck = this.checkPermissionsForMutation(mutation); + if (!permissionCheck.hasPermission) { + throw spawnError( + 'Cannot apply %s mutation: %s', + mutation.type, + permissionCheck.reasonDisplayString, + ); + } + + const didApplyOptimisticUpdates = this._applyOptimisticUpdatesForMutation(mutation); + + try { + await this._getAirtableInterfaceAsAirtableInterfaceCore().applyMutationAsync(mutation, { + holdForMs: MUTATION_HOLD_FOR_MS, + }); + } catch (err) { + if (didApplyOptimisticUpdates) { + setTimeout(() => { + throw err; + }, 0); + await new Promise(() => {}); + } else { + throw err; + } + } + } + + /** @hidden */ + checkPermissionsForMutation(mutation: SdkModeT['PartialMutationT']): PermissionCheckResult { + return this._getAirtableInterfaceAsAirtableInterfaceCore().checkPermissionsForMutation( + mutation, + this._base.__getBaseData(), + ); + } + + /** @hidden */ + private _getAirtableInterfaceAsAirtableInterfaceCore(): AirtableInterfaceCore { + return this._airtableInterface as AirtableInterfaceCore; + } + + /** @internal */ + _assertMutationUnderLimits(mutation: SdkModeT['MutationT']) { + if (encodeURIComponent(JSON.stringify(mutation)).length > MUTATIONS_MAX_BODY_SIZE) { + throw spawnError( + 'Request exceeds maximum size limit of %s bytes', + MUTATIONS_MAX_BODY_SIZE, + ); + } + + if (this._doesMutationExceedBatchSizeLimit(mutation)) { + throw spawnError( + 'Request exceeds maximum batch size limit of %s items', + MUTATIONS_MAX_BATCH_SIZE, + ); + } + } + + /** @internal */ + _applyOptimisticUpdatesForMutation(mutation: SdkModeT['MutationT']): boolean { + if (mutation.type === MutationTypesCore.SET_MULTIPLE_GLOBAL_CONFIG_PATHS) { + const _mutation = mutation as SetMultipleGlobalConfigPathsMutation; + this._applyGlobalConfigUpdates(_mutation.updates); + return true; + } + + const modelChanges = this._getOptimisticModelChangesForMutation(mutation); + + if (modelChanges.length > 0) { + this._applyModelChanges(modelChanges); + return true; + } + + return false; + } + + /** @internal */ + abstract _doesMutationExceedBatchSizeLimit(mutation: SdkModeT['MutationT']): boolean; + /** @internal */ + abstract _assertMutationIsValid(mutation: SdkModeT['MutationT']): void; + /** @internal */ + abstract _getOptimisticModelChangesForMutation( + mutation: SdkModeT['MutationT'], + ): Array; +} diff --git a/packages/sdk/src/shared/models/record_core.ts b/packages/sdk/src/shared/models/record_core.ts new file mode 100644 index 000000000..1dfea992f --- /dev/null +++ b/packages/sdk/src/shared/models/record_core.ts @@ -0,0 +1,222 @@ +import {SdkMode} from '../../sdk_mode'; +import {cloneDeep, FlowAnyObject, isEnumValue, isObjectEmpty, ObjectValues} from '../private_utils'; +import {invariant} from '../error_utils'; +import {FieldId, RecordId} from '../types/hyper_ids'; +import {FieldType} from '../types/field'; +import AbstractModel from './abstract_model'; +import {FieldCore} from './field_core'; + +export const WatchableRecordKeysCore = Object.freeze({ + name: 'name' as const, + cellValues: 'cellValues' as const, +}); + +/** @hidden */ +type WatchableRecordKeyCore = ObjectValues; + +/** @hidden */ +export abstract class RecordCore< + SdkModeT extends SdkMode, + WatchableKeys extends string = WatchableRecordKeyCore +> extends AbstractModel { + /** @internal */ + static _className = 'RecordCore'; + /** @internal */ + static _isWatchableKey(key: string): boolean { + return isEnumValue(WatchableRecordKeysCore, key); + } + /** @internal */ + _parentRecordStore: SdkModeT['RecordStoreT']; + /** @internal */ + _parentTable: SdkModeT['TableT']; + + /** + * @internal + */ + constructor( + sdk: SdkModeT['SdkT'], + parentRecordStore: SdkModeT['RecordStoreT'], + parentTable: SdkModeT['TableT'], + recordId: string, + ) { + super(sdk, recordId); + + this._parentRecordStore = parentRecordStore; + this._parentTable = parentTable; + } + + /** + * @internal + */ + get _dataOrNullIfDeleted(): SdkModeT['RecordDataT'] | null { + const tableData = this._baseData.tablesById[this.parentTable.id]; + if (!tableData) { + return null; + } + const recordsById = tableData.recordsById; + invariant(recordsById, 'Record data is not loaded'); + return recordsById[this._id] ?? null; + } + + /** + * The table that this record belongs to. Should never change because records aren't moved between tables. + * + * @internal (since we may not be able to return parent model instances in the immutable models world) + * @example + * ```js + * import {useRecords} from '@airtable/blocks/ui'; + * const records = useRecords(myTable); + * console.log(records[0].parentTable.id === myTable.id); + * // => true + * ``` + */ + get parentTable(): SdkModeT['TableT'] { + return this._parentTable; + } + /** + * @internal + */ + _getFieldMatching(fieldOrFieldIdOrFieldName: SdkModeT['FieldT'] | string): SdkModeT['FieldT'] { + if (fieldOrFieldIdOrFieldName instanceof FieldCore) { + return this._parentTable.__getFieldMatching(fieldOrFieldIdOrFieldName.id); + } + return this.parentTable.__getFieldMatching(fieldOrFieldIdOrFieldName); + } + + /** + * @internal + * + * For use when we need the raw public API cell value. Specifically makes a difference + * for lookup fields, where we translate the format to a blocks-specific format in getCellValue. + * That format is incompatible with fieldTypeProvider methods, which expect the public API + * format - use _getRawCellValue instead. + */ + _getRawCellValue(field: SdkModeT['FieldT']): unknown { + const {cellValuesByFieldId} = this._data; + if (!cellValuesByFieldId) { + return null; + } + const cellValue = + cellValuesByFieldId[field.id] !== undefined ? cellValuesByFieldId[field.id] : null; + + if (typeof cellValue === 'object' && cellValue !== null) { + return cloneDeep(cellValue); + } else { + return cellValue; + } + } + /** + * Gets the cell value of the given field for this record. + * + * @param fieldOrFieldIdOrFieldName The field (or field ID or field name) whose cell value you'd like to get. + * @example + * ```js + * const cellValue = myRecord.getCellValue(mySingleLineTextField); + * console.log(cellValue); + * // => 'cell value' + * ``` + */ + getCellValue(fieldOrFieldIdOrFieldName: SdkModeT['FieldT'] | FieldId | string): unknown { + const field = this._getFieldMatching(fieldOrFieldIdOrFieldName); + const cellValue = this._getRawCellValue(field); + + if ( + typeof cellValue === 'object' && + cellValue !== null && + field.type === FieldType.MULTIPLE_LOOKUP_VALUES && + !this._sdk.__airtableInterface.sdkInitData.isUsingNewLookupCellValueFormat + ) { + const cellValueForMigration: Array<{linkedRecordId: RecordId; value: unknown}> = []; + invariant(Array.isArray((cellValue as any).linkedRecordIds), 'linkedRecordIds'); + for (const linkedRecordId of (cellValue as any).linkedRecordIds) { + invariant(typeof linkedRecordId === 'string', 'linkedRecordId'); + const {valuesByLinkedRecordId} = cellValue as any; + + invariant( + valuesByLinkedRecordId && typeof valuesByLinkedRecordId === 'object', + 'valuesByLinkedRecordId', + ); + + const value = valuesByLinkedRecordId[linkedRecordId]; + if (Array.isArray(value)) { + for (const v of value) { + cellValueForMigration.push({linkedRecordId, value: v}); + } + } else { + cellValueForMigration.push({linkedRecordId, value}); + } + } + return cellValueForMigration; + } + + return cellValue; + } + /** + * Gets the cell value of the given field for this record, formatted as a `string`. + * + * @param fieldOrFieldIdOrFieldName The field (or field ID or field name) whose cell value you'd like to get. + * @example + * ```js + * const stringValue = myRecord.getCellValueAsString(myNumberField); + * console.log(stringValue); + * // => '42' + * ``` + */ + getCellValueAsString(fieldOrFieldIdOrFieldName: SdkModeT['FieldT'] | FieldId | string): string { + const field = this._getFieldMatching(fieldOrFieldIdOrFieldName); + + const cellValue = this._getRawCellValue(field); + + if (cellValue === null || cellValue === undefined) { + return ''; + } else { + const airtableInterface = this._sdk.__airtableInterface; + const appInterface = this._sdk.__appInterface; + return airtableInterface.fieldTypeProvider.convertCellValueToString( + appInterface, + cellValue, + field._data, + ); + } + } + /** + * The primary cell value in this record, formatted as a `string`. + * + * @example + * ```js + * console.log(myRecord.name); + * // => '42' + * ``` + */ + get name(): string { + return this.getCellValueAsString(this.parentTable.primaryField); + } + /** + * The created time of this record. + * + * @example + * ```js + * console.log(` + * This record was created at ${myRecord.createdTime.toISOString()} + * `); + * ``` + */ + get createdTime(): Date { + return new Date(this._data.createdTime); + } + /** + * @internal + */ + __triggerOnChangeForDirtyPaths(dirtyPaths: FlowAnyObject) { + const {cellValuesByFieldId} = dirtyPaths; + + if (cellValuesByFieldId && !isObjectEmpty(cellValuesByFieldId)) { + + this._onChange(WatchableRecordKeysCore.cellValues, Object.keys(cellValuesByFieldId)); + + if (cellValuesByFieldId[this.parentTable.primaryField.id]) { + this._onChange(WatchableRecordKeysCore.name); + } + } + } +} diff --git a/packages/sdk/src/shared/models/record_store_core.ts b/packages/sdk/src/shared/models/record_store_core.ts new file mode 100644 index 000000000..bb6021acb --- /dev/null +++ b/packages/sdk/src/shared/models/record_store_core.ts @@ -0,0 +1,190 @@ +import { + isEnumValue, + entries, + has, + ObjectValues, + ObjectMap, + FlowAnyFunction, + FlowAnyObject, +} from '../../shared/private_utils'; +import {invariant} from '../../shared/error_utils'; +import {TableId, FieldId, RecordId} from '../../shared/types/hyper_ids'; +import {ChangedPathsForType} from '../../shared/models/base_core'; +import AbstractModel from '../../shared/models/abstract_model'; +import {SdkMode} from '../../sdk_mode'; + +export const WatchableRecordStoreKeysCore = Object.freeze({ + records: 'records' as const, + recordIds: 'recordIds' as const, + cellValues: 'cellValues' as const, +}); +export const WatchableCellValuesInFieldKeyPrefix = 'cellValuesInField:'; + +/** + * The string case is to accommodate prefix keys + * + * @internal + */ +type WatchableRecordStoreKeyCore = ObjectValues | string; + +/** + * One RecordStore exists per table, and contains all the record data associated with that table. + * Table itself is for schema information only, so isn't the appropriate place for this data. + * + * @internal + */ +abstract class RecordStoreCore< + SdkModeT extends SdkMode, + WatchableKeys extends string = WatchableRecordStoreKeyCore +> extends AbstractModel< + SdkModeT, + SdkModeT['TableDataT'], + WatchableRecordStoreKeyCore | WatchableKeys +> { + static _className = 'RecordStoreCore'; + static _isWatchableKey(key: string): boolean { + return ( + isEnumValue(WatchableRecordStoreKeysCore, key) || + key.startsWith(WatchableCellValuesInFieldKeyPrefix) + ); + } + + readonly tableId: TableId; + _recordModelsById: ObjectMap = {}; + + constructor(sdk: SdkModeT['SdkT'], tableId: TableId) { + super(sdk, `${tableId}-RecordStore`); + this.tableId = tableId; + } + + abstract _constructRecord( + recordId: RecordId, + parentTable: SdkModeT['TableT'], + ): SdkModeT['RecordT']; + + get _dataOrNullIfDeleted(): SdkModeT['TableDataT'] | null { + return this._baseData.tablesById[this.tableId] ?? null; + } + + watch( + keys: WatchableRecordStoreKeyCore | ReadonlyArray, + callback: FlowAnyFunction, + context?: FlowAnyObject | null, + ): Array { + const validKeys = super.watch(keys, callback, context); + return validKeys; + } + + unwatch( + keys: WatchableRecordStoreKeyCore | ReadonlyArray, + callback: FlowAnyFunction, + context?: FlowAnyObject | null, + ): Array { + const validKeys = super.unwatch(keys, callback, context); + return validKeys; + } + + /** + * The records in this table. + */ + get records(): Array { + const recordsById = this._data.recordsById; + invariant(recordsById, 'Record metadata is not loaded'); + const records = this.recordIds.map(recordId => { + const record = this.getRecordByIdIfExists(recordId); + invariant(record, 'record'); + return record; + }); + return records; + } + + /** + * The record IDs in this table. + */ + abstract get recordIds(): Array; + + getRecordByIdIfExists(recordId: string): SdkModeT['RecordT'] | null { + const recordsById = this._data.recordsById; + invariant(recordsById, 'Record metadata is not loaded'); + invariant(typeof recordId === 'string', 'getRecordById expects a string'); + + if (!recordsById[recordId]) { + return null; + } else { + if (this._recordModelsById[recordId]) { + return this._recordModelsById[recordId]; + } + const newRecord = this._constructRecord( + recordId, + this._sdk.base.getTableById(this.tableId), + ); + this._recordModelsById[recordId] = newRecord; + return newRecord; + } + } + + triggerOnChangeForDirtyPaths(dirtyPaths: ChangedPathsForType) { + if (dirtyPaths.recordsById) { + const dirtyFieldIdsSet: ObjectMap = {}; + const addedRecordIds: Array = []; + const removedRecordIds: Array = []; + for (const [recordId, dirtyRecordPaths] of entries(dirtyPaths.recordsById) as Array< + [RecordId, ChangedPathsForType] + >) { + if (dirtyRecordPaths && dirtyRecordPaths._isDirty) { + invariant(this._data.recordsById, 'No recordsById'); + + if (has(this._data.recordsById, recordId)) { + addedRecordIds.push(recordId); + } else { + removedRecordIds.push(recordId); + + const recordModel = this._recordModelsById[recordId]; + if (recordModel) { + delete this._recordModelsById[recordId]; + } + } + } else { + const recordModel = this._recordModelsById[recordId]; + if (recordModel) { + recordModel.__triggerOnChangeForDirtyPaths(dirtyRecordPaths); + } + } + + const {cellValuesByFieldId} = dirtyRecordPaths; + if (cellValuesByFieldId) { + for (const fieldId of Object.keys(cellValuesByFieldId)) { + dirtyFieldIdsSet[fieldId] = true; + } + } + } + + if (addedRecordIds.length > 0 || removedRecordIds.length > 0) { + this._onChange(WatchableRecordStoreKeysCore.records, { + addedRecordIds, + removedRecordIds, + }); + + this._onChange(WatchableRecordStoreKeysCore.recordIds, { + addedRecordIds, + removedRecordIds, + }); + } + + const fieldIds = Object.freeze(Object.keys(dirtyFieldIdsSet)); + const recordIds = Object.freeze(Object.keys(dirtyPaths.recordsById)); + if (fieldIds.length > 0 && recordIds.length > 0) { + this._onChange(WatchableRecordStoreKeysCore.cellValues, { + recordIds, + fieldIds, + }); + } + for (const fieldId of fieldIds) { + this._onChange(WatchableCellValuesInFieldKeyPrefix + fieldId, recordIds, fieldId); + } + } + } +} + +/** @internal */ +export default RecordStoreCore; diff --git a/packages/sdk/src/shared/models/session_core.ts b/packages/sdk/src/shared/models/session_core.ts new file mode 100644 index 000000000..a1b7a07be --- /dev/null +++ b/packages/sdk/src/shared/models/session_core.ts @@ -0,0 +1,163 @@ +/** @module @airtable/blocks/models: Session */ /** */ +import {invariant} from '../error_utils'; +import {CollaboratorData} from '../types/collaborator'; +import {PermissionLevel} from '../types/permission_levels'; +import {isEnumValue, entries, ObjectValues, ObjectMap} from '../private_utils'; +import {UserId} from '../types/hyper_ids'; +import {SdkMode} from '../../sdk_mode'; +import {ModelChange} from '../types/base_core'; +import AbstractModel from './abstract_model'; + +/** @hidden */ +interface SessionData { + currentUserId: UserId | null; + permissionLevel: PermissionLevel; + enabledFeatureNames: Array; +} + +const WatchableSessionKeys = Object.freeze({ + permissionLevel: 'permissionLevel' as const, + + currentUser: 'currentUser' as const, +}); + +/** + * Watchable keys in {@link Session}. + * - `currentUser` + * - `permissionLevel` + */ +type WatchableSessionKey = ObjectValues; + +/** @hidden */ +export abstract class SessionCore extends AbstractModel< + SdkModeT, + SessionData, + WatchableSessionKey +> { + /** @internal */ + static _className = 'SessionCore'; + /** @internal */ + static _isWatchableKey(key: string): boolean { + return isEnumValue(WatchableSessionKeys, key); + } + /** @internal */ + _airtableInterface: SdkModeT['AirtableInterfaceT']; + /** @internal */ + _sessionData: SessionData; + + /** + * @internal + */ + constructor(sdk: SdkModeT['SdkT']) { + super(sdk, 'session'); + this._airtableInterface = sdk.__airtableInterface; + + const { + permissionLevel, + currentUserId, + enabledFeatureNames, + } = this._airtableInterface.sdkInitData.baseData; + this._sessionData = { + permissionLevel, + currentUserId, + enabledFeatureNames, + }; + + Object.seal(this); + } + + /** + * @internal + */ + get _dataOrNullIfDeleted(): SessionData { + return this._sessionData; + } + + /** + * The current user, or `null` if the extension is running in a publicly shared base. + * + * @example + * ```js + * import {useSession} from '@airtable/blocks/ui'; + * + * function CurrentUser() { + * const session = useSession(); + * + * if (!session.currentUser) { + * return
    This extension is being used in a public share.
    ; + * } + * + * return
      + *
    • ID: {session.currentUser.id}
    • + *
    • E-mail: {session.currentUser.email}
    • + *
    • Name: {session.currentUser.name}
    • + *
    ; + * } + * ``` + */ + get currentUser(): CollaboratorData | null { + const userId = this._sessionData.currentUserId; + if (!userId) { + return null; + } else { + const {base} = this._sdk; + return base.getCollaboratorByIdIfExists(userId); + } + } + /** + * Returns true if `featureName` is enabled and automatically tracks an exposure. + * + * @internal + */ + __isFeatureEnabled(featureName: string): boolean { + this._airtableInterface.trackExposure(featureName); + return this.__peekIfFeatureIsEnabled(featureName); + } + + /** + * Returns true if `featureName` is enabled; does not track an exposure. + * + * @internal + */ + __peekIfFeatureIsEnabled(featureName: string): boolean { + return this._sessionData.enabledFeatureNames.includes(featureName); + } + + /** + * @internal + */ + __applyChangesWithoutTriggeringEvents( + changes: ReadonlyArray, + ): ObjectMap { + const changedKeys = { + [WatchableSessionKeys.permissionLevel]: false, + [WatchableSessionKeys.currentUser]: false, + }; + for (const {path, value} of changes) { + if (path[0] === 'permissionLevel') { + invariant(path.length === 1, 'cannot set within permissionLevel'); + + invariant(typeof value === 'string', 'permissionLevel must be a string'); + + this._sessionData.permissionLevel = value as any; + changedKeys[WatchableSessionKeys.permissionLevel] = true; + } + + if (path[0] === 'collaboratorsById') { + changedKeys[WatchableSessionKeys.currentUser] = true; + } + } + + return changedKeys; + } + /** + * @internal + */ + __triggerOnChangeForChangedKeys(changedKeys: ObjectMap) { + for (const [key, didChange] of entries(changedKeys)) { + if (didChange) { + this._onChange(key); + } + } + } +} diff --git a/packages/sdk/src/shared/models/table_core.ts b/packages/sdk/src/shared/models/table_core.ts new file mode 100644 index 000000000..5f57c44e2 --- /dev/null +++ b/packages/sdk/src/shared/models/table_core.ts @@ -0,0 +1,377 @@ +import {FieldId} from '../types/hyper_ids'; +import {isEnumValue, entries, has, ObjectValues, ObjectMap} from '../private_utils'; +import {spawnError} from '../error_utils'; +import {SdkMode} from '../../sdk_mode'; +import AbstractModel from './abstract_model'; +import {ChangedPathsForType} from './base_core'; +import {FieldCore} from './field_core'; + +export const WatchableTableKeysCore = Object.freeze({ + name: 'name' as const, + description: 'description' as const, + fields: 'fields' as const, +}); + +/** @hidden */ +type WatchableTableKeyCore = ObjectValues; + +/** @hidden */ +export abstract class TableCore< + SdkModeT extends SdkMode, + WatchableKeys extends string = WatchableTableKeyCore +> extends AbstractModel { + /** @internal */ + static _className = 'TableCore'; + /** @internal */ + static _isWatchableKey(key: string): boolean { + return isEnumValue(WatchableTableKeysCore, key); + } + /** @internal */ + _parentBase: SdkModeT['BaseT']; + /** @internal */ + _recordStore: SdkModeT['RecordStoreT']; + /** @internal */ + _fieldModelsById: {[key: string]: SdkModeT['FieldT']}; + /** @internal */ + _cachedFieldNamesById: {[key: string]: string} | null; + + /** + * @internal + */ + constructor( + parentBase: SdkModeT['BaseT'], + recordStore: SdkModeT['RecordStoreT'], + tableId: string, + sdk: SdkModeT['SdkT'], + ) { + super(sdk, tableId); + this._parentBase = parentBase; + this._recordStore = recordStore; + this._fieldModelsById = {}; + this._cachedFieldNamesById = null; + } + + /** + * @internal + */ + get _dataOrNullIfDeleted(): SdkModeT['TableDataT'] | null { + return this._baseData.tablesById[this._id] ?? null; + } + /** + * The base that this table belongs to. + * + * @internal (since we may not be able to return parent model instances in the immutable models world) + * @example + * ```js + * import {base} from '@airtable/blocks'; + * const table = base.getTableByName('Table 1'); + * console.log(table.parentBase.id === base.id); + * // => true + * ``` + */ + get parentBase(): SdkModeT['BaseT'] { + return this._parentBase; + } + /** + * The name of the table. Can be watched. + * + * @example + * ```js + * console.log(myTable.name); + * // => 'Table 1' + * ``` + */ + get name(): string { + return this._data.name; + } + /** + * The description of the table, if it has one. Can be watched. + * + * @example + * ```js + * console.log(myTable.description); + * // => 'This is my table' + * ``` + */ + get description(): string | null { + return this._data.description; + } + /** + * The table's primary field. Every table has exactly one primary + * field. The primary field of a table will not change. + * + * @example + * ```js + * console.log(myTable.primaryField.name); + * // => 'Name' + * ``` + */ + get primaryField(): SdkModeT['FieldT'] { + const primaryField = this.getFieldById(this._data.primaryFieldId); + return primaryField; + } + /** + * The fields in this table. The order is arbitrary, since fields are + * only ordered in the context of a specific view. + * + * Can be watched to know when fields are created or deleted. + * + * @example + * ```js + * console.log(`This table has ${myTable.fields.length} fields`); + * ``` + */ + get fields(): Array { + const fields = []; + for (const fieldId of Object.keys(this._data.fieldsById)) { + const field = this.getFieldById(fieldId); + fields.push(field); + } + return fields; + } + /** + * Gets the field matching the given ID, or `null` if that field does not exist in this table. + + * @param fieldId The ID of the field. + * @example + * ```js + * const fieldId = 'fldxxxxxxxxxxxxxx'; + * const field = myTable.getFieldByIdIfExists(fieldId); + * if (field !== null) { + * console.log(field.name); + * } else { + * console.log('No field exists with that ID'); + * } + * ``` + */ + getFieldByIdIfExists(fieldId: FieldId): SdkModeT['FieldT'] | null { + if (!this._data.fieldsById[fieldId]) { + return null; + } else { + if (!this._fieldModelsById[fieldId]) { + this._fieldModelsById[fieldId] = this._constructField(fieldId); + } + return this._fieldModelsById[fieldId]; + } + } + /** @internal */ + abstract _constructField(fieldId: FieldId): SdkModeT['FieldT']; + /** + * Gets the field matching the given ID. Throws if that field does not exist in this table. Use + * {@link getFieldByIdIfExists} instead if you are unsure whether a field exists with the given + * ID. + * + * @param fieldId The ID of the field. + * @example + * ```js + * const fieldId = 'fldxxxxxxxxxxxxxx'; + * const field = myTable.getFieldById(fieldId); + * console.log(field.name); + * // => 'Name' + * ``` + */ + getFieldById(fieldId: FieldId): SdkModeT['FieldT'] { + const field = this.getFieldByIdIfExists(fieldId); + if (!field) { + throw spawnError("No field with ID %s in table '%s'", fieldId, this.name); + } + return field; + } + /** + * Gets the field matching the given name, or `null` if no field exists with that name in this + * table. + * + * @param fieldName The name of the field you're looking for. + * @example + * ```js + * const field = myTable.getFieldByNameIfExists('Name'); + * if (field !== null) { + * console.log(field.id); + * } else { + * console.log('No field exists with that name'); + * } + * ``` + */ + getFieldByNameIfExists(fieldName: string): SdkModeT['FieldT'] | null { + for (const [fieldId, fieldData] of entries(this._data.fieldsById)) { + if (fieldData.name === fieldName) { + return this.getFieldByIdIfExists(fieldId); + } + } + return null; + } + /** + * Gets the field matching the given name. Throws if no field exists with that name in this + * table. Use {@link getFieldByNameIfExists} instead if you are unsure whether a field exists + * with the given name. + * + * @param fieldName The name of the field you're looking for. + * @example + * ```js + * const field = myTable.getFieldByName('Name'); + * console.log(field.id); + * // => 'fldxxxxxxxxxxxxxx' + * ``` + */ + getFieldByName(fieldName: string): SdkModeT['FieldT'] { + const field = this.getFieldByNameIfExists(fieldName); + if (!field) { + throw spawnError("No field named '%s' in table '%s'", fieldName, this.name); + } + return field; + } + /** + * The field matching the given ID or name. Returns `null` if no matching field exists within + * this table. + * + * This method is convenient when building an extension for a specific base, but for more generic + * extensions the best practice is to use the {@link getFieldByIdIfExists} or + * {@link getFieldByNameIfExists} methods instead. + * + * @param fieldIdOrName The ID or name of the field you're looking for. + */ + getFieldIfExists(fieldIdOrName: FieldId | string): SdkModeT['FieldT'] | null { + return ( + this.getFieldByIdIfExists(fieldIdOrName) ?? this.getFieldByNameIfExists(fieldIdOrName) + ); + } + /** + * The field matching the given ID or name. Throws if no matching field exists within this table. + * Use {@link getFieldIfExists} instead if you are unsure whether a field exists with the given + * name/ID. + * + * This method is convenient when building an extension for a specific base, but for more generic + * extensions the best practice is to use the {@link getFieldById} or {@link getFieldByName} methods + * instead. + * + * @param fieldIdOrName The ID or name of the field you're looking for. + */ + getField(fieldIdOrName: FieldId | string): SdkModeT['FieldT'] { + const field = this.getFieldIfExists(fieldIdOrName); + if (!field) { + throw spawnError( + "No field with ID or name '%s' in table '%s'", + fieldIdOrName, + this.name, + ); + } + return field; + } + /** @internal */ + _cellValuesByFieldIdOrNameToCellValuesByFieldId( + cellValuesByFieldIdOrName: ObjectMap, + ): ObjectMap { + return Object.fromEntries( + entries(cellValuesByFieldIdOrName).map(([fieldIdOrName, cellValue]) => { + const field = this.__getFieldMatching(fieldIdOrName); + return [field.id, cellValue]; + }), + ); + } + /** + * @internal + */ + __getFieldMatching(fieldOrFieldIdOrFieldName: SdkModeT['FieldT'] | string): SdkModeT['FieldT'] { + let field: SdkModeT['FieldT'] | null; + if (fieldOrFieldIdOrFieldName instanceof FieldCore) { + if (fieldOrFieldIdOrFieldName.parentTable.id !== this.id) { + throw spawnError( + "Field '%s' is from a different table than table '%s'", + fieldOrFieldIdOrFieldName.name, + this.name, + ); + } + field = fieldOrFieldIdOrFieldName; + } else { + field = + this.getFieldByIdIfExists(fieldOrFieldIdOrFieldName) || + this.getFieldByNameIfExists(fieldOrFieldIdOrFieldName); + + if (field === null) { + throw spawnError( + "Field '%s' does not exist in table '%s'", + fieldOrFieldIdOrFieldName, + this.name, + ); + } + } + + if (field.isDeleted) { + throw spawnError("Field '%s' was deleted from table '%s'", field.name, this.name); + } + return field; + } + /** + * @internal + */ + __triggerOnChangeForDirtyPaths( + dirtyPaths: ChangedPathsForType, + ): boolean { + let didTableSchemaChange = false; + if (dirtyPaths.name) { + this._onChange(WatchableTableKeysCore.name); + didTableSchemaChange = true; + } + if (dirtyPaths.lock) { + didTableSchemaChange = true; + } + if (dirtyPaths.externalSyncById) { + didTableSchemaChange = true; + } + if (dirtyPaths.description) { + this._onChange(WatchableTableKeysCore.description); + didTableSchemaChange = true; + } + if (dirtyPaths.fieldsById) { + didTableSchemaChange = true; + + const addedFieldIds: Array = []; + const removedFieldIds: Array = []; + for (const [fieldId, dirtyFieldPaths] of entries(dirtyPaths.fieldsById)) { + if (dirtyFieldPaths && dirtyFieldPaths._isDirty) { + if (has(this._data.fieldsById, fieldId)) { + addedFieldIds.push(fieldId); + } else { + removedFieldIds.push(fieldId); + + const fieldModel = this._fieldModelsById[fieldId]; + if (fieldModel) { + delete this._fieldModelsById[fieldId]; + } + } + } else { + const field = this._fieldModelsById[fieldId]; + if (field) { + field.__triggerOnChangeForDirtyPaths(dirtyFieldPaths); + } + } + } + + if (addedFieldIds.length > 0 || removedFieldIds.length > 0) { + this._onChange(WatchableTableKeysCore.fields, { + addedFieldIds, + removedFieldIds, + }); + } + + this._cachedFieldNamesById = null; + } + + this._recordStore.triggerOnChangeForDirtyPaths(dirtyPaths); + + return didTableSchemaChange; + } + /** + * @internal + */ + __getFieldNamesById(): {[key: string]: string} { + if (!this._cachedFieldNamesById) { + const fieldNamesById: ObjectMap = {}; + for (const [fieldId, fieldData] of entries(this._data.fieldsById)) { + fieldNamesById[fieldId] = fieldData.name; + } + this._cachedFieldNamesById = fieldNamesById; + } + return this._cachedFieldNamesById; + } +} diff --git a/packages/sdk/src/private_utils.ts b/packages/sdk/src/shared/private_utils.ts similarity index 90% rename from packages/sdk/src/private_utils.ts rename to packages/sdk/src/shared/private_utils.ts index 15b493970..190633ff0 100644 --- a/packages/sdk/src/private_utils.ts +++ b/packages/sdk/src/shared/private_utils.ts @@ -1,8 +1,6 @@ import * as React from 'react'; -import PropTypes from 'prop-types'; -import getAirtableInterface from './injected/airtable_interface'; +import getAirtableInterface from '../injected/airtable_interface'; import {spawnError} from './error_utils'; -import createResponsivePropType from './ui/system/utils/create_responsive_prop_type'; export {default as isDeepEqual} from 'fast-deep-equal'; @@ -74,32 +72,6 @@ export function createEnum(...enumValues: Array): {[K in T] return Object.freeze(spec); } -/** - * Creates a React propType for a provided enum. - * - * @hidden - */ -export function createPropTypeFromEnum( - enumData: {[K in T]: T}, -): PropTypes.Requireable { - return PropTypes.oneOf(values(enumData)); -} - -/** - * Creates a responsive React propType for a provided enum. - * - * This allows the prop to be either a valid enum property, or a map of viewport sizes to valid enum - * properties. - * - * @hidden - */ -export function createResponsivePropTypeFromEnum( - enumData: {[K in T]: T}, -): PropTypes.Validator { - const propType: PropTypes.Requireable = createPropTypeFromEnum(enumData); - return createResponsivePropType(propType); -} - /** * Creates a Type for an enum created using `createEnum`. * @@ -406,7 +378,7 @@ export function isBlockDevelopmentRestrictionEnabled(): boolean { export function getLocaleAndDefaultLocale(): {locale?: string; defaultLocale?: string} { const sdkInitData = getAirtableInterface().sdkInitData; return { - locale: sdkInitData.locale, - defaultLocale: sdkInitData.defaultLocale, + locale: 'locale' in sdkInitData ? sdkInitData.locale : undefined, + defaultLocale: 'defaultLocale' in sdkInitData ? sdkInitData.defaultLocale : undefined, }; } diff --git a/packages/sdk/src/shared/sdk_core.ts b/packages/sdk/src/shared/sdk_core.ts new file mode 100644 index 000000000..fad0d5770 --- /dev/null +++ b/packages/sdk/src/shared/sdk_core.ts @@ -0,0 +1,121 @@ +import {SdkMode} from '../sdk_mode'; +import GlobalConfig from './global_config'; +import {AppInterface} from './types/airtable_interface_core'; +import {BlockInstallationId} from './types/hyper_ids'; + +/** + * @hidden + * @example + * ```js + * import {runInfo} from '@airtable/blocks'; + * if (runInfo.isFirstRun) { + * // The current user just installed this block. + * // Take the opportunity to show any onboarding and set + * // sensible defaults if the user has permission. + * // For example, if the block relies on a table, it would + * // make sense to set that to cursor.activeTableId + * } + * ``` + */ +export interface RunInfo { + isFirstRun: boolean; + isDevelopmentMode: boolean; + intentData: unknown; +} + +/** @hidden */ +export abstract class BlockSdkCore { + /** + * This value is used by the blocks-testing library to verify + * compatibility. + * + * @hidden + */ + // @ts-ignore + static VERSION = global.PACKAGE_VERSION; + + /** Storage for this block installation's configuration. */ + globalConfig: GlobalConfig; + + /** Contains information about the current session. */ + session: SdkModeT['SessionT']; + + /** Represents the current Airtable {@link Base}. */ + base: SdkModeT['BaseT']; + + /** + * Returns the ID for the current block installation. + * + * @example + * ```js + * import {installationId} from '@airtable/blocks'; + * console.log(installationId); + * // => 'blifDutUr92OKwnUn' + * ``` + */ + installationId: BlockInstallationId; + + /** @hidden */ + runInfo: RunInfo; + + /** @internal */ + __airtableInterface: SdkModeT['AirtableInterfaceT']; + + /** @internal */ + __mutations: SdkModeT['MutationsModelT']; + + /** @hidden */ + constructor(airtableInterface: SdkModeT['AirtableInterfaceT']) { + this.__airtableInterface = airtableInterface; + + // @ts-ignore + airtableInterface.assertAllowedSdkPackageVersion(global.PACKAGE_NAME, BlockSdkCore.VERSION); + + const sdkInitData = airtableInterface.sdkInitData; + this.globalConfig = new GlobalConfig(sdkInitData.initialKvValuesByKey, this); + this.installationId = sdkInitData.blockInstallationId; + this.runInfo = Object.freeze({ + isFirstRun: sdkInitData.isFirstRun, + isDevelopmentMode: sdkInitData.isDevelopmentMode, + intentData: sdkInitData.intentData, + }); + + this.session = this._constructSession(); + this.base = this._constructBase(); + this.__mutations = this._constructMutations(); + + this.reload = this.reload.bind(this); + } + + /** @internal */ + abstract _constructSession(): SdkModeT['SessionT']; + /** @internal */ + abstract _constructBase(): SdkModeT['BaseT']; + /** @internal */ + abstract _constructMutations(): SdkModeT['MutationsModelT']; + + /** + * Call this function to reload your block. + * + * @example + * ```js + * import React from 'react'; + * import {reload} from '@airtable/blocks'; + * import {Button, initializeBlock} from '@airtable/blocks/ui'; + * function MyBlock() { + * return ; + * } + * initializeBlock(() => ); + * ``` + */ + reload() { + this.__airtableInterface.reloadFrame(); + } + + /** + * @internal + */ + get __appInterface(): AppInterface { + return this.base._baseData.appInterface; + } +} diff --git a/packages/sdk/src/shared/types/airtable_interface_core.ts b/packages/sdk/src/shared/types/airtable_interface_core.ts new file mode 100644 index 000000000..c9037104f --- /dev/null +++ b/packages/sdk/src/shared/types/airtable_interface_core.ts @@ -0,0 +1,112 @@ +import {ObjectMap} from '../private_utils'; +import {SdkMode} from '../../sdk_mode'; +import {Stat} from './stat'; +import {FieldId, BlockInstallationId} from './hyper_ids'; +import {FieldType, FieldData} from './field'; +import { + GlobalConfigUpdate, + GlobalConfigData, + GlobalConfigPath, + GlobalConfigPathValidationResult, +} from './global_config'; +import {BaseDataCore, ModelChange, BasePermissionData} from './base_core'; +import {TableDataCore} from './table_core'; +import {PermissionCheckResult} from './mutations_core'; + +/** @hidden */ +export interface SdkInitDataCore { + initialKvValuesByKey: GlobalConfigData; + isDevelopmentMode: boolean; + baseData: BaseDataCore; + blockInstallationId: BlockInstallationId; + isFirstRun: boolean; + intentData: unknown; + isUsingNewLookupCellValueFormat?: true | undefined; +} + +/** @hidden */ +type CellValueValidationResult = {isValid: true} | {isValid: false; reason: string}; +/** @hidden */ +export interface FieldTypeConfig { + type: FieldType; + options?: {[key: string]: unknown}; +} +/** @hidden */ +export interface FieldTypeProviderCore { + isComputed(fieldData: FieldData): boolean; + validateCellValueForUpdate( + appInterface: AppInterface, + newCellValue: unknown, + currentCellValue: unknown, + fieldData: FieldData, + ): CellValueValidationResult; + getConfig( + appInterface: AppInterface, + fieldData: FieldData, + fieldNamesById: ObjectMap, + ): FieldTypeConfig; + convertStringToCellValue( + appInterface: AppInterface, + string: string, + fieldData: FieldData, + opts?: {parseDateCellValueInColumnTimeZone?: boolean}, + ): unknown; + convertCellValueToString( + appInterface: AppInterface, + cellValue: unknown, + fieldData: FieldData, + ): string; + getCellRendererData( + appInterface: AppInterface, + cellValue: unknown, + fieldData: FieldData, + shouldWrap: boolean, + ): {cellValueHtml: string; attributes: {[key: string]: unknown}}; +} +/** @hidden */ +export interface GlobalConfigHelpers /**/ { + validatePath(path: GlobalConfigPath, store: GlobalConfigData): GlobalConfigPathValidationResult; + validateAndApplyUpdates( + updates: ReadonlyArray, + store: GlobalConfigData, + ): { + newKvStore: GlobalConfigData; + changedTopLevelKeys: Array; + }; +} + +/** + * AppInterface should never be used directly by the SDK, so we don't describe the type. + * + * @hidden + */ +export type AppInterface = unknown; + +/** @hidden */ +export interface AirtableInterfaceCore { + sdkInitData: SdkModeT['SdkInitDataT']; + fieldTypeProvider: FieldTypeProviderCore; + globalConfigHelpers: GlobalConfigHelpers; + + assertAllowedSdkPackageVersion: (packageName: string, packageVersion: string) => void; + + reloadFrame(): void; + + subscribeToModelUpdates(callback: (data: {changes: ReadonlyArray}) => void): void; + subscribeToGlobalConfigUpdates( + callback: (data: {updates: ReadonlyArray}) => void, + ): void; + + applyMutationAsync(mutation: SdkModeT['MutationT'], opts?: {holdForMs?: number}): Promise; + checkPermissionsForMutation( + mutation: SdkModeT['PartialMutationT'], + basePermissionData: BasePermissionData, + ): PermissionCheckResult; + + /** + * internal utils + */ + trackEvent(eventSchemaName: string, eventData: {[key: string]: unknown}): void; + trackExposure(featureName: string): void; + sendStat(stat: Stat): void; +} diff --git a/packages/sdk/src/types/attachment.ts b/packages/sdk/src/shared/types/attachment.ts similarity index 90% rename from packages/sdk/src/types/attachment.ts rename to packages/sdk/src/shared/types/attachment.ts index f4fc16eab..518ef36c8 100644 --- a/packages/sdk/src/types/attachment.ts +++ b/packages/sdk/src/shared/types/attachment.ts @@ -1,5 +1,4 @@ -/** @hidden */ -export type AttachmentId = string; +import {AttachmentId} from './hyper_ids'; /** @hidden */ export interface AttachmentData { diff --git a/packages/sdk/src/types/base.ts b/packages/sdk/src/shared/types/base_core.ts similarity index 67% rename from packages/sdk/src/types/base.ts rename to packages/sdk/src/shared/types/base_core.ts index 880372269..1c6e8c406 100644 --- a/packages/sdk/src/types/base.ts +++ b/packages/sdk/src/shared/types/base_core.ts @@ -1,13 +1,10 @@ /** @module @airtable/blocks/models: Base */ /** */ import {ObjectMap} from '../private_utils'; -import {AppInterface} from './airtable_interface'; +import {AppInterface} from './airtable_interface_core'; import {PermissionLevel} from './permission_levels'; -import {TableData, TablePermissionData, TableId} from './table'; -import {CursorData} from './cursor'; -import {CollaboratorData, UserId} from './collaborator'; - -/** */ -export type BaseId = string; +import {TableDataCore, TablePermissionData} from './table_core'; +import {CollaboratorData} from './collaborator'; +import {TableId, UserId, BaseId} from './hyper_ids'; /** @hidden */ export interface ModelChange { @@ -16,13 +13,11 @@ export interface ModelChange { } /** @hidden */ -export interface BaseData { +export interface BaseDataCore { id: BaseId; name: string; color: string; - tableOrder: Array; - activeTableId: TableId | null; - tablesById: ObjectMap; + tablesById: ObjectMap; appInterface: AppInterface; @@ -33,7 +28,6 @@ export interface BaseData { currentUserId: UserId | null; permissionLevel: PermissionLevel; enabledFeatureNames: Array; - cursorData: CursorData | null; billingPlanGrouping: string; isBlockDevelopmentRestrictionEnabled: boolean; diff --git a/packages/sdk/src/types/collaborator.ts b/packages/sdk/src/shared/types/collaborator.ts similarity index 93% rename from packages/sdk/src/types/collaborator.ts rename to packages/sdk/src/shared/types/collaborator.ts index c4f4b9b30..ad5d99877 100644 --- a/packages/sdk/src/types/collaborator.ts +++ b/packages/sdk/src/shared/types/collaborator.ts @@ -1,7 +1,5 @@ /** @module @airtable/blocks/models: Base */ /** */ - -/** */ -export type UserId = string; +import {UserId} from './hyper_ids'; /** * An object representing a collaborator. You should not create these objects from scratch, but diff --git a/packages/sdk/src/types/field.ts b/packages/sdk/src/shared/types/field.ts similarity index 99% rename from packages/sdk/src/types/field.ts rename to packages/sdk/src/shared/types/field.ts index 81d80db21..af4818123 100644 --- a/packages/sdk/src/types/field.ts +++ b/packages/sdk/src/shared/types/field.ts @@ -1,10 +1,7 @@ /** @module @airtable/blocks/models: Field */ /** */ import {Color} from '../colors'; -import {TableId} from './table'; -import {ViewId} from './view'; +import {TableId, FieldId, ViewId} from './hyper_ids'; -/** */ -export type FieldId = string; /** @hidden */ export type PrivateColumnType = string; diff --git a/packages/sdk/src/types/global_config.ts b/packages/sdk/src/shared/types/global_config.ts similarity index 100% rename from packages/sdk/src/types/global_config.ts rename to packages/sdk/src/shared/types/global_config.ts diff --git a/packages/sdk/src/shared/types/hyper_ids.ts b/packages/sdk/src/shared/types/hyper_ids.ts new file mode 100644 index 000000000..388dc29ff --- /dev/null +++ b/packages/sdk/src/shared/types/hyper_ids.ts @@ -0,0 +1,18 @@ +/** */ +export type BaseId = string; +/** */ +export type TableId = string; +/** */ +export type FieldId = string; +/** */ +export type ViewId = string; +/** */ +export type RecordId = string; +/** */ +export type PageId = string; +/** @hidden */ +export type BlockInstallationId = string; +/** @hidden */ +export type AttachmentId = string; +/** */ +export type UserId = string; diff --git a/packages/sdk/src/models/mutation_constants.ts b/packages/sdk/src/shared/types/mutation_constants.ts similarity index 100% rename from packages/sdk/src/models/mutation_constants.ts rename to packages/sdk/src/shared/types/mutation_constants.ts diff --git a/packages/sdk/src/shared/types/mutations_core.ts b/packages/sdk/src/shared/types/mutations_core.ts new file mode 100644 index 000000000..4ec5d05b0 --- /dev/null +++ b/packages/sdk/src/shared/types/mutations_core.ts @@ -0,0 +1,61 @@ +/** @module @airtable/blocks: mutations */ /** */ +import {GlobalConfigUpdate, GlobalConfigValue} from './global_config'; + +/** @hidden */ +export const MutationTypesCore = Object.freeze({ + SET_MULTIPLE_GLOBAL_CONFIG_PATHS: 'setMultipleGlobalConfigPaths' as const, +}); + + +/** + * The Mutation emitted when the App modifies one or more values in the + * {@link GlobalConfig}. + * + * @docsPath testing/mutations/SetMultipleGlobalConfigPathsMutation + */ +export interface SetMultipleGlobalConfigPathsMutation { + /** This Mutation's [discriminant property](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) */ + readonly type: typeof MutationTypesCore.SET_MULTIPLE_GLOBAL_CONFIG_PATHS; + /** One or more pairs of path and value */ + readonly updates: ReadonlyArray; +} + +/** @hidden */ +export interface PartialSetMultipleGlobalConfigPathsMutation { + readonly type: typeof MutationTypesCore.SET_MULTIPLE_GLOBAL_CONFIG_PATHS; + readonly updates: + | ReadonlyArray<{ + readonly path: ReadonlyArray | undefined; + readonly value: GlobalConfigValue | undefined | undefined; + }> + | undefined; +} + +/** @hidden */ +export type MutationCore = SetMultipleGlobalConfigPathsMutation; + +/** @hidden */ +export type PartialMutationCore = PartialSetMultipleGlobalConfigPathsMutation; + +/** */ +export interface SuccessfulPermissionCheckResult { + /** */ + hasPermission: true; +} + +/** */ +export interface UnsuccessfulPermissionCheckResult { + /** */ + hasPermission: false; + /** + * A string explaining why the action is not permitted. These strings should only be used to + * show to the user; you should not rely on the format of the string as it may change without + * notice. + */ + reasonDisplayString: string; +} + +/** Indicates whether the user has permission to perform a particular action, and if not, why. */ +export type PermissionCheckResult = + | SuccessfulPermissionCheckResult + | UnsuccessfulPermissionCheckResult; diff --git a/packages/sdk/src/types/permission_levels.ts b/packages/sdk/src/shared/types/permission_levels.ts similarity index 100% rename from packages/sdk/src/types/permission_levels.ts rename to packages/sdk/src/shared/types/permission_levels.ts diff --git a/packages/sdk/src/types/record.ts b/packages/sdk/src/shared/types/record.ts similarity index 68% rename from packages/sdk/src/types/record.ts rename to packages/sdk/src/shared/types/record.ts index 422ca2596..cc7c8e03c 100644 --- a/packages/sdk/src/types/record.ts +++ b/packages/sdk/src/shared/types/record.ts @@ -1,17 +1,13 @@ /** @module @airtable/blocks/models: Record */ /** */ import {ObjectMap} from '../private_utils'; -import {FieldId} from './field'; - -/** */ -export type RecordId = string; +import {FieldId, RecordId} from './hyper_ids'; /** */ export type RecordDef = ObjectMap; /** @hidden */ -export interface RecordData { +export interface RecordDataCore { id: RecordId; cellValuesByFieldId: RecordDef | null | undefined; - commentCount: number; createdTime: string; } diff --git a/packages/sdk/src/types/stat.ts b/packages/sdk/src/shared/types/stat.ts similarity index 100% rename from packages/sdk/src/types/stat.ts rename to packages/sdk/src/shared/types/stat.ts diff --git a/packages/sdk/src/types/table.ts b/packages/sdk/src/shared/types/table_core.ts similarity index 63% rename from packages/sdk/src/types/table.ts rename to packages/sdk/src/shared/types/table_core.ts index b4a7d051c..2b43457bb 100644 --- a/packages/sdk/src/types/table.ts +++ b/packages/sdk/src/shared/types/table_core.ts @@ -1,26 +1,19 @@ /** @module @airtable/blocks/models: Table */ /** */ import {ObjectMap} from '../private_utils'; -import {FieldData, FieldPermissionData, FieldId} from './field'; -import {ViewData, ViewId} from './view'; -import {RecordData, RecordId} from './record'; +import {FieldData, FieldPermissionData} from './field'; +import {TableId, FieldId} from './hyper_ids'; -/** */ -export type TableId = string; /** @hidden */ export type TableLock = unknown; /** @hidden */ export type ExternalSyncById = unknown; /** @hidden */ -export interface TableData { +export interface TableDataCore { id: TableId; name: string; primaryFieldId: string; fieldsById: ObjectMap; - activeViewId: ViewId | null; - viewOrder: Array; - viewsById: ObjectMap; - recordsById?: ObjectMap; description: string | null; lock: TableLock | null; externalSyncById: ExternalSyncById | null; diff --git a/packages/sdk/src/ui/global_config_synced_component_helpers.ts b/packages/sdk/src/shared/ui/global_config_synced_component_helpers.ts similarity index 78% rename from packages/sdk/src/ui/global_config_synced_component_helpers.ts rename to packages/sdk/src/shared/ui/global_config_synced_component_helpers.ts index df1e7b3a8..264a4de1b 100644 --- a/packages/sdk/src/ui/global_config_synced_component_helpers.ts +++ b/packages/sdk/src/shared/ui/global_config_synced_component_helpers.ts @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; -import {BlockRunContextType} from '../types/airtable_interface'; +import {BlockRunContextType} from '../../base/types/airtable_interface'; import {GlobalConfigKey} from '../types/global_config'; +import {BaseSdkMode} from '../../sdk_mode'; import useWatchable from './use_watchable'; import {useSdk} from './sdk_context'; @@ -19,7 +20,9 @@ const globalConfigSyncedComponentHelpers = { const viewIfInViewContext = runContext.type === BlockRunContextType.VIEW - ? sdk.base.getTableById(runContext.tableId).getViewById(runContext.viewId) + ? (sdk as BaseSdkMode['SdkT']).base + .getTableById(runContext.tableId) + .getViewById(runContext.viewId) : null; useWatchable(viewIfInViewContext, ['isLockedView']); }, diff --git a/packages/sdk/src/shared/ui/loader.tsx b/packages/sdk/src/shared/ui/loader.tsx new file mode 100644 index 000000000..212a3f34e --- /dev/null +++ b/packages/sdk/src/shared/ui/loader.tsx @@ -0,0 +1,67 @@ +import PropTypes from 'prop-types'; +import * as React from 'react'; + +const ORIGINAL_SIZE = 54; + +/** + * @internal + */ +export interface LoaderProps { + /** The color of the loading spinner. Defaults to `'#888'` */ + fillColor: string; + /** A scalar for the loading spinner. Increasing the scale increases the size of the loading spinner. Defaults to `0.3`. */ + scale: number; + /** Additional class names to apply to the loading spinner. */ + className?: string; + /** Additional styles to apply to the loading spinner. */ + style?: React.CSSProperties; +} + +/** + * @internal + */ +const Loader = (props: LoaderProps) => { + const {fillColor, scale, className, style} = props; + + return ( + + + + + + + + ); +}; + +Loader.propTypes = { + fillColor: PropTypes.string.isRequired, + scale: PropTypes.number.isRequired, + className: PropTypes.string, + style: PropTypes.object, +}; + +Loader.defaultProps = { + fillColor: '#888', + scale: 0.3, +}; + +export default Loader; diff --git a/packages/sdk/src/ui/remote_utils.ts b/packages/sdk/src/shared/ui/remote_utils.ts similarity index 100% rename from packages/sdk/src/ui/remote_utils.ts rename to packages/sdk/src/shared/ui/remote_utils.ts diff --git a/packages/sdk/src/ui/sdk_context.ts b/packages/sdk/src/shared/ui/sdk_context.ts similarity index 61% rename from packages/sdk/src/ui/sdk_context.ts rename to packages/sdk/src/shared/ui/sdk_context.ts index e4df9380f..1aa7e2377 100644 --- a/packages/sdk/src/ui/sdk_context.ts +++ b/packages/sdk/src/shared/ui/sdk_context.ts @@ -1,9 +1,9 @@ import * as React from 'react'; -import Sdk from '../sdk'; import {invariant} from '../error_utils'; +import {SdkMode} from '../../sdk_mode'; -export const SdkContext = React.createContext(null); -export const useSdk = () => { +export const SdkContext = React.createContext(null); +export const useSdk = (): SdkModeT['SdkT'] => { const sdk = React.useContext(SdkContext); invariant( sdk, diff --git a/packages/sdk/src/ui/use_array_identity.ts b/packages/sdk/src/shared/ui/use_array_identity.ts similarity index 100% rename from packages/sdk/src/ui/use_array_identity.ts rename to packages/sdk/src/shared/ui/use_array_identity.ts diff --git a/packages/sdk/src/shared/ui/use_base.ts b/packages/sdk/src/shared/ui/use_base.ts new file mode 100644 index 000000000..794fd94aa --- /dev/null +++ b/packages/sdk/src/shared/ui/use_base.ts @@ -0,0 +1,15 @@ +import {SdkMode} from '../../sdk_mode'; +import useWatchable from './use_watchable'; +import {useSdk} from './sdk_context'; + +/** + * @hidden + */ +const useBase = (): SdkModeT['BaseT'] => { + const {base, session} = useSdk(); + useWatchable(base, ['schema']); + useWatchable(session, ['permissionLevel']); + return base; +}; + +export default useBase; diff --git a/packages/sdk/src/ui/use_global_config.ts b/packages/sdk/src/shared/ui/use_global_config.ts similarity index 100% rename from packages/sdk/src/ui/use_global_config.ts rename to packages/sdk/src/shared/ui/use_global_config.ts diff --git a/packages/sdk/src/ui/use_loadable.ts b/packages/sdk/src/shared/ui/use_loadable.ts similarity index 100% rename from packages/sdk/src/ui/use_loadable.ts rename to packages/sdk/src/shared/ui/use_loadable.ts diff --git a/packages/sdk/src/shared/ui/use_session.ts b/packages/sdk/src/shared/ui/use_session.ts new file mode 100644 index 000000000..559e8cc02 --- /dev/null +++ b/packages/sdk/src/shared/ui/use_session.ts @@ -0,0 +1,13 @@ +import {SdkMode} from '../../sdk_mode'; +import useWatchable from './use_watchable'; +import {useSdk} from './sdk_context'; + +/** @internal */ +const useSession = (): SdkModeT['SessionT'] => { + const {session, base} = useSdk(); + useWatchable(session, ['permissionLevel', 'currentUser']); + useWatchable(base, ['schema']); + return session; +}; + +export default useSession; diff --git a/packages/sdk/src/ui/use_synced.ts b/packages/sdk/src/shared/ui/use_synced.ts similarity index 100% rename from packages/sdk/src/ui/use_synced.ts rename to packages/sdk/src/shared/ui/use_synced.ts diff --git a/packages/sdk/src/ui/use_watchable.ts b/packages/sdk/src/shared/ui/use_watchable.ts similarity index 100% rename from packages/sdk/src/ui/use_watchable.ts rename to packages/sdk/src/shared/ui/use_watchable.ts diff --git a/packages/sdk/src/ui/with_hooks.tsx b/packages/sdk/src/shared/ui/with_hooks.tsx similarity index 100% rename from packages/sdk/src/ui/with_hooks.tsx rename to packages/sdk/src/shared/ui/with_hooks.tsx diff --git a/packages/sdk/src/unstable_private_utils.ts b/packages/sdk/src/shared/unstable_private_utils.ts similarity index 71% rename from packages/sdk/src/unstable_private_utils.ts rename to packages/sdk/src/shared/unstable_private_utils.ts index 86eb6117d..673d10728 100644 --- a/packages/sdk/src/unstable_private_utils.ts +++ b/packages/sdk/src/shared/unstable_private_utils.ts @@ -1,4 +1,4 @@ export * from './private_utils'; export * from './error_utils'; export * from './event_tracker'; -export * from './stats/block_stats'; +export * from '../stats/block_stats'; diff --git a/packages/sdk/src/warning.ts b/packages/sdk/src/shared/warning.ts similarity index 73% rename from packages/sdk/src/warning.ts rename to packages/sdk/src/shared/warning.ts index 91f90531d..76fcb393e 100644 --- a/packages/sdk/src/warning.ts +++ b/packages/sdk/src/shared/warning.ts @@ -1,5 +1,6 @@ +import {SdkMode} from '../sdk_mode'; import {ObjectMap} from './private_utils'; -import Sdk from './sdk'; +import {BlockSdkCore} from './sdk_core'; const usedWarnings: ObjectMap = {}; @@ -14,8 +15,8 @@ export default (msgLines: string | Array) => { } }; -let sdk: Sdk; +let sdk: BlockSdkCore; -export function __injectSdkIntoWarning(_sdk: Sdk) { +export function __injectSdkIntoWarning(_sdk: BlockSdkCore) { sdk = _sdk; } diff --git a/packages/sdk/src/watchable.ts b/packages/sdk/src/shared/watchable.ts similarity index 100% rename from packages/sdk/src/watchable.ts rename to packages/sdk/src/shared/watchable.ts diff --git a/packages/sdk/src/testing/abstract_mock_airtable_interface.ts b/packages/sdk/src/testing/abstract_mock_airtable_interface.ts index c255a7082..0ce9809c6 100644 --- a/packages/sdk/src/testing/abstract_mock_airtable_interface.ts +++ b/packages/sdk/src/testing/abstract_mock_airtable_interface.ts @@ -1,31 +1,33 @@ -import {AggregatorKey} from '../types/aggregators'; +import {AggregatorKey} from '../base/types/aggregators'; +import { + AppInterface, + FieldTypeConfig, + GlobalConfigHelpers, +} from '../shared/types/airtable_interface_core'; import { AggregatorConfig, Aggregators, AirtableInterface, - AppInterface, - FieldTypeConfig, FieldTypeProvider, SdkInitData, UrlConstructor, - GlobalConfigHelpers, PartialViewData, IdGenerator, VisList, -} from '../types/airtable_interface'; -import {cloneDeep, ObjectMap} from '../private_utils'; -import {spawnError} from '../error_utils'; -import {FieldData, FieldId} from '../types/field'; -import {ModelChange} from '../types/base'; -import {RecordData, RecordId} from '../types/record'; -import {TableId} from '../types/table'; -import {ViewId} from '../types/view'; -import {ViewportSizeConstraint} from '../types/viewport'; -import {Mutation, PermissionCheckResult} from '../types/mutations'; -import {NormalizedSortConfig} from '../models/record_query_result'; -import {RequestJson, ResponseJson} from '../types/backend_fetch_types'; -import {CursorData} from '../types/cursor'; -import {RecordActionData} from '../types/record_action_data'; +} from '../base/types/airtable_interface'; +import {TableId, FieldId, ViewId, RecordId} from '../shared/types/hyper_ids'; +import {cloneDeep, ObjectMap} from '../shared/private_utils'; +import {spawnError} from '../shared/error_utils'; +import {FieldData} from '../shared/types/field'; +import {ModelChange} from '../shared/types/base_core'; +import {RecordData} from '../base/types/record'; +import {ViewportSizeConstraint} from '../base/types/viewport'; +import {PermissionCheckResult} from '../shared/types/mutations_core'; +import {Mutation} from '../base/types/mutations'; +import {NormalizedSortConfig} from '../base/models/record_query_result'; +import {RequestJson, ResponseJson} from '../base/types/backend_fetch_types'; +import {CursorData} from '../base/types/cursor'; +import {RecordActionData} from '../base/types/record_action_data'; const EventEmitter = require('events'); /** @internal */ diff --git a/packages/sdk/src/types/block.ts b/packages/sdk/src/types/block.ts deleted file mode 100644 index 8add3ba45..000000000 --- a/packages/sdk/src/types/block.ts +++ /dev/null @@ -1,2 +0,0 @@ -/** @hidden */ -export type BlockInstallationId = string; diff --git a/packages/sdk/stories/all_controls.stories.tsx b/packages/sdk/stories/all_controls.stories.tsx index 865b2fc0b..2bde0483c 100644 --- a/packages/sdk/stories/all_controls.stories.tsx +++ b/packages/sdk/stories/all_controls.stories.tsx @@ -1,15 +1,19 @@ // @flow import React, {useState} from 'react'; -import {storiesOf} from '@storybook/react'; -import {ControlSize} from '../src/ui/control_sizes'; -import Box from '../src/ui/box'; -import Button from '../src/ui/button'; -import Input from '../src/ui/input'; -import Select from '../src/ui/select'; -import SelectButtons from '../src/ui/select_buttons'; -import Switch from '../src/ui/switch'; +import {ControlSize} from '../src/base/ui/control_sizes'; +import Box from '../src/base/ui/box'; +import Button from '../src/base/ui/button'; +import Input from '../src/base/ui/input'; +import Select from '../src/base/ui/select'; +import SelectButtons from '../src/base/ui/select_buttons'; +import Switch from '../src/base/ui/switch'; +import {SelectOptionValue} from '../src/base/ui/select_and_select_buttons_helpers'; + +export default { + title: 'All controls', + component: Box, +}; -const stories = storiesOf('All controls', module); const sizes = [ControlSize.small, ControlSize.default, ControlSize.large]; const options = ['Apple', 'Pear', 'Banana'].map(value => ({value, label: value})); @@ -17,57 +21,62 @@ function capitalize(str: string) { return str[0].toUpperCase() + str.slice(1); } -stories.add('sizes', () => { - const [value, setValue] = useState(''); - const [isChecked, setIsChecked] = useState(true); - const [selectValue, setSelectValue] = useState(null); - return ( - - {sizes.map(size => ( - - setValue(e.target.value)} - placeholder={`${capitalize(size)} input`} - /> - setValue(e.target.value)} + placeholder={`${capitalize(size)} input`} + /> + setValue(newValue as string)} + {...values} + width={CONTROL_WIDTH} + /> + ); + }} + + ); +} + +export const _FieldPickerExample = { + render: () => , +}; + +function FieldPickerSyncedExample() { + const [value, setValue] = useState(null); + return ( + { + const props = createJsxPropsStringFromValuesMap(values); + + return ` + import React from 'react'; + import {FieldPickerSynced, useBase} from '@airtable/blocks/ui'; + + const FieldPickerSyncedExample = () => { + const base = useBase(); + const table = table.getTableByNameIfExists("Tasks"); + // If table is null or undefined, the FieldPickerSynced will not render. + + return + }; + `; + }} + > + {values => { + const placeholder = values.shouldAllowPickingNone ? 'None' : 'Pick a field...'; + + return ( + setValue(e.target.value)} /> - - ); - }), -); - -stories.add('select field', () => - React.createElement(() => { - const [value, setValue] = useState('Option 1'); - const options = [ - {value: 'Option 1', label: 'Option 1'}, - {value: 'Option 2', label: 'Option 2'}, - {value: 'Option 3', label: 'Option 3'}, - ]; - return ( - - setValue(e.target.value)} /> - - ); - }), -); - -stories.add('custom className', () => - React.createElement(() => { - const [value, setValue] = useState(''); - return ( - - setValue(e.target.value)} /> - - ); - }), -); - -stories.add('custom id', () => - React.createElement(() => { - const [value, setValue] = useState(''); - return ( - - setValue(e.target.value)} /> - - ); - }), -); - -stories.add('forwarded ref', () => - React.createElement(() => { - return ( - { - // eslint-disable-next-line no-console - console.log(node); - }} - label="Look in console to see ref" - /> - ); - }), -); +export const TextInputField = { + render: () => + React.createElement(() => { + const [value, setValue] = useState(''); + return ( + + setValue(e.target.value)} /> + + ); + }), +}; -stories.add('multiple formfields', () => - React.createElement(() => { - const [textValue, setTextValue] = useState(''); - const [selectValue, setSelectValue] = useState('Option 1'); - const [color, setColor] = useState(null); - const [coolLevel, setCoolLevel] = useState('Kinda cool'); +export const SelectField = { + render: () => + React.createElement(() => { + const [value, setValue] = useState('Option 1'); + const options = [ + {value: 'Option 1', label: 'Option 1'}, + {value: 'Option 2', label: 'Option 2'}, + {value: 'Option 3', label: 'Option 3'}, + ]; + return ( + + setTextValue(e.target.value)} /> - - - setValue(e.target.value)} /> - - setCoolLevel(val as string)} - /> + ); + }), +}; + +export const CustomClassName = { + render: () => + React.createElement(() => { + const [value, setValue] = useState(''); + return ( + + setValue(e.target.value)} /> - - ); - }), -); + ); + }), +}; -stories.add('width 100% inside container', () => - React.createElement(() => { - const [value, setValue] = useState(''); - return ( - - +export const CustomId = { + render: () => + React.createElement(() => { + const [value, setValue] = useState(''); + return ( + setValue(e.target.value)} /> - - ); - }), -); + ); + }), +}; + +export const ForwardedRef = { + render: () => + React.createElement(() => { + return ( + { + // eslint-disable-next-line no-console + console.log(node); + }} + label="Look in console to see ref" + /> + ); + }), +}; + +export const MultipleFormfields = { + render: () => + React.createElement(() => { + const [textValue, setTextValue] = useState(''); + const [selectValue, setSelectValue] = useState('Option 1'); + const [color, setColor] = useState(null); + const [coolLevel, setCoolLevel] = useState('Kinda cool'); + + const selectOptions = [ + {value: 'Option 1', label: 'Option 1'}, + {value: 'Option 2', label: 'Option 2'}, + {value: 'Option 3', label: 'Option 3'}, + ]; + const allowedColors = objectValues(colors).slice(0, 12); + const selectButtonsOptions = [ + { + value: 'Kinda cool', + label: 'Kinda cool', + }, + { + value: 'Pretty cool', + label: 'Pretty cool', + }, + { + value: 'So cool', + label: 'So cool', + }, + ]; + return ( +
    + + setTextValue(e.target.value)} /> + + + setValue(e.target.value)} /> + + + ); + }), +}; diff --git a/packages/sdk/stories/heading.size.stories.tsx b/packages/sdk/stories/heading.size.stories.tsx index 80a2f60eb..433396d3c 100644 --- a/packages/sdk/stories/heading.size.stories.tsx +++ b/packages/sdk/stories/heading.size.stories.tsx @@ -1,66 +1,77 @@ import React from 'react'; -import {storiesOf} from '@storybook/react'; -import Heading from '../src/ui/heading'; -import Text from '../src/ui/text'; -import theme from '../src/ui/theme/default_theme'; -import {keys} from '../src/private_utils'; +import Heading from '../src/base/ui/heading'; +import Text from '../src/base/ui/text'; +import theme from '../src/base/ui/theme/default_theme'; +import {keys} from '../src/shared/private_utils'; -const stories = storiesOf('Heading/size', module); +export default { + component: Heading, + title: 'Heading/size', +}; -stories.add('default sizes', () => ( - - {keys(theme.headingStyles.default).map(size => ( - - +export const DefaultSizes = { + render: () => ( + + {keys(theme.headingStyles.default).map(size => ( + + + The brown fox jumped over the lazy dog + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, + quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. + + + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore + eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, + sunt in culpa qui officia deserunt mollit anim id est laborum. + + + ))} + + ), +}; + +export const CapsSizes = { + render: () => ( + + {keys(theme.headingStyles.caps).map(size => ( + The brown fox jumped over the lazy dog - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor - incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis - nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. - - - Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu - fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in - culpa qui officia deserunt mollit anim id est laborum. - - - ))} - -)); + ))} + + ), +}; -stories.add('caps sizes', () => ( - - {keys(theme.headingStyles.caps).map(size => ( - +export const CapsSizeOutOfRangeDefaultFallbackAndWarning = { + render: () => ( + + The brown fox jumped over the lazy dog - ))} - -)); - -stories.add('caps size out of range default fallback and warning', () => ( - - - The brown fox jumped over the lazy dog - - -)); + + ), +}; -stories.add('responsive size', () => ( - - Breakpoints:
    {JSON.stringify(theme.breakpoints, null, 4)}
    - - Resize to see size change - -
    -)); +export const ResponsiveSize = { + render: () => ( + + Breakpoints:
    {JSON.stringify(theme.breakpoints, null, 4)}
    + + Resize to see size change + +
    + ), +}; diff --git a/packages/sdk/stories/heading.stories.tsx b/packages/sdk/stories/heading.stories.tsx index 692fce117..020912029 100644 --- a/packages/sdk/stories/heading.stories.tsx +++ b/packages/sdk/stories/heading.stories.tsx @@ -1,13 +1,14 @@ import React from 'react'; -import {storiesOf} from '@storybook/react'; -import Heading from '../src/ui/heading'; -import theme from '../src/ui/theme/default_theme'; -import {keys, has} from '../src/private_utils'; -import {allStylesPropTypes} from '../src/ui/system'; +import Heading from '../src/base/ui/heading'; +import theme from '../src/base/ui/theme/default_theme'; +import {keys, has} from '../src/shared/private_utils'; +import {allStylesPropTypes} from '../src/base/ui/system'; import Example from './helpers/example'; import {createJsxPropsStringFromValuesMap} from './helpers/code_utils'; -const stories = storiesOf('Heading', module); +export default { + component: Heading, +}; function HeadingExample() { return ( @@ -42,7 +43,7 @@ function HeadingExample() { } return ` import {Heading} from '@airtable/blocks/ui'; - + ${sizeOutOfBoundsComment} const headingExample = ( The brown fox jumped over the lazy dog @@ -55,94 +56,112 @@ function HeadingExample() { ); } -stories.add('example', () => ); +export const _Example = { + render: () => , +}; -stories.add('as', () => ( - - {[ - 'h1' as const, - 'h2' as const, - 'h3' as const, - 'h4' as const, - 'h5' as const, - 'h6' as const, - ].map(as => ( - - {as} - - ))} - -)); +export const As = { + render: () => ( + + {[ + 'h1' as const, + 'h2' as const, + 'h3' as const, + 'h4' as const, + 'h5' as const, + 'h6' as const, + ].map(as => ( + + {as} + + ))} + + ), +}; -stories.add('ref', () => ( - - { - // eslint-disable-next-line no-console - console.log(node); - }} - > - Look into your console to see the ref - - -)); +export const Ref = { + render: () => ( + + { + // eslint-disable-next-line no-console + console.log(node); + }} + > + Look into your console to see the ref + + + ), +}; -stories.add('custom className', () => ( - - Inspect element to see class name - -)); +export const CustomClassName = { + render: () => ( + + Inspect element to see class name + + ), +}; -stories.add('id attribute', () => ( - - Inspect element to see id - -)); +export const IdAttribute = { + render: () => ( + + Inspect element to see id + + ), +}; -stories.add('style attribute', () => ( - - - Inspect element to see style attribute - - -)); +export const StyleAttribute = { + render: () => ( + + + Inspect element to see style attribute + + + ), +}; -stories.add('data attributes', () => ( - - - Inspect element to see data attributes - - -)); +export const DataAttributes = { + render: () => ( + + + Inspect element to see data attributes + + + ), +}; -stories.add('role attribute', () => ( - - Inspect element to see role attribute - -)); +export const RoleAttribute = { + render: () => ( + + Inspect element to see role attribute + + ), +}; -stories.add('aria attributes', () => ( - - - Inspect element to see aria attributes - - -)); +export const AriaAttributes = { + render: () => ( + + + Inspect element to see aria attributes + + + ), +}; diff --git a/packages/sdk/stories/helpers/categorize_style_props.ts b/packages/sdk/stories/helpers/categorize_style_props.ts index e1addfa07..55ffac351 100644 --- a/packages/sdk/stories/helpers/categorize_style_props.ts +++ b/packages/sdk/stories/helpers/categorize_style_props.ts @@ -1,4 +1,4 @@ -import {entries} from '../../src/private_utils'; +import {entries} from '../../src/shared/private_utils'; import { backgroundColor, boxShadow, @@ -14,7 +14,7 @@ import { typographySet, display, overflow, -} from '../../src/ui/system/'; +} from '../../src/base/ui/system'; const categories = { Appearance: [ diff --git a/packages/sdk/stories/helpers/code_utils.ts b/packages/sdk/stories/helpers/code_utils.ts index e7c6a40cc..a13458a9b 100644 --- a/packages/sdk/stories/helpers/code_utils.ts +++ b/packages/sdk/stories/helpers/code_utils.ts @@ -1,4 +1,4 @@ -import {has} from '../../src/private_utils'; +import {has} from '../../src/shared/private_utils'; /** * Helper function to turn an object keyed by prop name into JSX properties. diff --git a/packages/sdk/stories/helpers/example.tsx b/packages/sdk/stories/helpers/example.tsx index 864622f5f..0e4b028cd 100644 --- a/packages/sdk/stories/helpers/example.tsx +++ b/packages/sdk/stories/helpers/example.tsx @@ -1,18 +1,18 @@ import React, {useState} from 'react'; import {injectGlobal} from 'emotion'; import capitalize from 'lodash/capitalize'; -import Select from '../../src/ui/select'; -import SelectButtons from '../../src/ui/select_buttons'; -import Switch from '../../src/ui/switch'; -import Box from '../../src/ui/box'; -import Text from '../../src/ui/text'; -import Heading from '../../src/ui/heading'; -import FormField from '../../src/ui/form_field'; -import {spawnUnknownSwitchCaseError} from '../../src/error_utils'; +import Select from '../../src/base/ui/select'; +import SelectButtons from '../../src/base/ui/select_buttons'; +import Switch from '../../src/base/ui/switch'; +import Box from '../../src/base/ui/box'; +import Text from '../../src/base/ui/text'; +import Heading from '../../src/base/ui/heading'; +import FormField from '../../src/base/ui/form_field'; +import {spawnUnknownSwitchCaseError} from '../../src/shared/error_utils'; import ExampleCodePanel from './example_code_panel'; import categorizeStyleProps from './categorize_style_props'; import StylePropList from './style_prop_list'; -import {SelectOptionValue} from '../../src/ui/select_and_select_buttons_helpers'; +import {SelectOptionValue} from '../../src/base/ui/select_and_select_buttons_helpers'; injectGlobal(` html { diff --git a/packages/sdk/stories/helpers/example_code_panel.tsx b/packages/sdk/stories/helpers/example_code_panel.tsx index 26a5239d6..5689cf874 100644 --- a/packages/sdk/stories/helpers/example_code_panel.tsx +++ b/packages/sdk/stories/helpers/example_code_panel.tsx @@ -1,7 +1,7 @@ import React, {useEffect, useMemo} from 'react'; import prettier from 'prettier/standalone'; import parserBabel from 'prettier/parser-babylon'; -import Box from '../../src/ui/box'; +import Box from '../../src/base/ui/box'; import CodeBlock from './code_block'; import useResizablePanel from './use_resizable_panel'; diff --git a/packages/sdk/stories/helpers/fake_cell_renderer.tsx b/packages/sdk/stories/helpers/fake_cell_renderer.tsx index 5ee189c09..93c5f387f 100644 --- a/packages/sdk/stories/helpers/fake_cell_renderer.tsx +++ b/packages/sdk/stories/helpers/fake_cell_renderer.tsx @@ -1,15 +1,15 @@ import React from 'react'; -import CollaboratorToken from '../../src/ui/collaborator_token'; -import ChoiceToken from '../../src/ui/choice_token'; -import Text from '../../src/ui/text'; -import Box from '../../src/ui/box'; -import Button from '../../src/ui/button'; -import Icon from '../../src/ui/icon'; +import CollaboratorToken from '../../src/base/ui/collaborator_token'; +import ChoiceToken from '../../src/base/ui/choice_token'; +import Text from '../../src/base/ui/text'; +import Box from '../../src/base/ui/box'; +import Button from '../../src/base/ui/button'; +import Icon from '../../src/base/ui/icon'; import {CONTROL_WIDTH} from './code_utils'; import choiceOptions from './choice_options'; import syncSourceOptions from './sync_source_options'; import collaboratorOptions from './collaborator_options'; -import {FieldType} from '../../src/types/field'; +import {FieldType} from '../../src/shared/types/field'; import attachments from './attachments'; import FakeForeignRecord from './fake_foreign_record'; diff --git a/packages/sdk/stories/helpers/fake_foreign_record.tsx b/packages/sdk/stories/helpers/fake_foreign_record.tsx index 1a07551fa..2b27a362c 100644 --- a/packages/sdk/stories/helpers/fake_foreign_record.tsx +++ b/packages/sdk/stories/helpers/fake_foreign_record.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import Text from '../../src/ui/text'; +import Text from '../../src/base/ui/text'; export default function FakeForeignRecord({children}: {children: React.ReactNode}) { return ( diff --git a/packages/sdk/stories/helpers/fake_record_card.tsx b/packages/sdk/stories/helpers/fake_record_card.tsx index 73b319677..1d628a714 100644 --- a/packages/sdk/stories/helpers/fake_record_card.tsx +++ b/packages/sdk/stories/helpers/fake_record_card.tsx @@ -1,12 +1,12 @@ import React, {useState} from 'react'; import {css} from 'emotion'; -import Box from '../../src/ui/box'; -import Text from '../../src/ui/text'; -import Heading from '../../src/ui/heading'; -import Dialog from '../../src/ui/dialog'; -import Button from '../../src/ui/button'; -import {FieldType} from '../../src/types/field'; +import Box from '../../src/base/ui/box'; +import Text from '../../src/base/ui/text'; +import Heading from '../../src/base/ui/heading'; +import Dialog from '../../src/base/ui/dialog'; +import Button from '../../src/base/ui/button'; +import {FieldType} from '../../src/shared/types/field'; import FakeCellRenderer from './fake_cell_renderer'; import {recordCardAttachment} from './attachments'; import {CONTROL_WIDTH} from './code_utils'; diff --git a/packages/sdk/stories/helpers/field_type.ts b/packages/sdk/stories/helpers/field_type.ts index c997dd18b..f81613101 100644 --- a/packages/sdk/stories/helpers/field_type.ts +++ b/packages/sdk/stories/helpers/field_type.ts @@ -1,6 +1,6 @@ -import {ObjectMap} from '../../src/private_utils'; -import {IconName} from '../../src/ui/icon_config'; -import {FieldType} from '../../src/types/field'; +import {ObjectMap} from '../../src/shared/private_utils'; +import {IconName} from '../../src/base/ui/icon_config'; +import {FieldType} from '../../src/shared/types/field'; export const ReadableFieldTypes: ObjectMap = { [FieldType.SINGLE_LINE_TEXT]: 'Single line text', diff --git a/packages/sdk/stories/helpers/style_prop_list.tsx b/packages/sdk/stories/helpers/style_prop_list.tsx index a9b39148b..c25ebbf48 100644 --- a/packages/sdk/stories/helpers/style_prop_list.tsx +++ b/packages/sdk/stories/helpers/style_prop_list.tsx @@ -1,8 +1,8 @@ import React, {useState} from 'react'; -import Text from '../../src/ui/text'; -import TextButton from '../../src/ui/text_button'; -import Box from '../../src/ui/box'; -import Heading from '../../src/ui/heading'; +import Text from '../../src/base/ui/text'; +import TextButton from '../../src/base/ui/text_button'; +import Box from '../../src/base/ui/box'; +import Heading from '../../src/base/ui/heading'; interface StylePropListProps { stylePropsByCategory: {[category: string]: Array}; diff --git a/packages/sdk/stories/icon.stories.tsx b/packages/sdk/stories/icon.stories.tsx index 7b079f524..fc2db4db5 100644 --- a/packages/sdk/stories/icon.stories.tsx +++ b/packages/sdk/stories/icon.stories.tsx @@ -1,34 +1,44 @@ import React from 'react'; -import {storiesOf} from '@storybook/react'; -import {values as objectValues} from '../src/private_utils'; -import {iconNames} from '../src/ui/icon_config'; -import Icon from '../src/ui/icon'; -import Text from '../src/ui/text'; -import Box from '../src/ui/box'; +import {values as objectValues} from '../src/shared/private_utils'; +import {iconNames} from '../src/base/ui/icon_config'; +import Icon from '../src/base/ui/icon'; +import Text from '../src/base/ui/text'; +import Box from '../src/base/ui/box'; import IconExample from './icon_example'; -const stories = storiesOf('Icon', module); +export default { + component: Icon, +}; -stories.add('example', () => ); +export const Example = { + render: () => , +}; -stories.add('standalone', () => ); -stories.add('small size will render micro icon', () => ); +export const Standalone = { + render: () => , +}; -stories.add('all icons', () => ( - - {objectValues(iconNames).map(iconName => { - return ( - - - - {iconName} - - - - {iconName}Micro +export const SmallSizeWillRenderMicroIcon = { + render: () => , +}; + +export const AllIcons = { + render: () => ( + + {objectValues(iconNames).map(iconName => { + return ( + + + + {iconName} + + + + {iconName}Micro + - - ); - })} - -)); + ); + })} + + ), +}; diff --git a/packages/sdk/stories/icon_example.tsx b/packages/sdk/stories/icon_example.tsx index c46a0b301..6f97e8e7c 100644 --- a/packages/sdk/stories/icon_example.tsx +++ b/packages/sdk/stories/icon_example.tsx @@ -1,14 +1,14 @@ import React, {useState, useEffect, useRef} from 'react'; import {css} from 'emotion'; -import {values as objectValues} from '../src/private_utils'; -import {iconNames, IconName, deprecatedIconNameToReplacementName} from '../src/ui/icon_config'; -import Icon, {iconStylePropTypes} from '../src/ui/icon'; -import theme from '../src/ui/theme/default_theme'; -import Text from '../src/ui/text'; -import Box from '../src/ui/box'; +import {values as objectValues} from '../src/shared/private_utils'; +import {iconNames, IconName, deprecatedIconNameToReplacementName} from '../src/base/ui/icon_config'; +import Icon, {iconStylePropTypes} from '../src/base/ui/icon'; +import theme from '../src/base/ui/theme/default_theme'; +import Text from '../src/base/ui/text'; +import Box from '../src/base/ui/box'; import Example from './helpers/example'; -import Input from '../src/ui/input'; -import cssHelpers from '../src/ui/css_helpers'; +import Input from '../src/base/ui/input'; +import cssHelpers from '../src/base/ui/css_helpers'; const iconNamesArray = objectValues(iconNames); diff --git a/packages/sdk/stories/input.stories.tsx b/packages/sdk/stories/input.stories.tsx index 763f716e7..0ed5cfe85 100644 --- a/packages/sdk/stories/input.stories.tsx +++ b/packages/sdk/stories/input.stories.tsx @@ -1,15 +1,16 @@ /* eslint-disable no-console */ import React, {useState} from 'react'; -import {storiesOf} from '@storybook/react'; -import FormField from '../src/ui/form_field'; -import Input, {inputStylePropTypes} from '../src/ui/input'; -import Box from '../src/ui/text'; -import theme from '../src/ui/theme/default_theme'; -import {keys} from '../src/private_utils'; +import FormField from '../src/base/ui/form_field'; +import Input, {inputStylePropTypes} from '../src/base/ui/input'; +import Box from '../src/base/ui/text'; +import theme from '../src/base/ui/theme/default_theme'; +import {keys} from '../src/shared/private_utils'; import Example from './helpers/example'; import {createJsxPropsStringFromValuesMap, CONTROL_WIDTH} from './helpers/code_utils'; -const stories = storiesOf('Input', module); +export default { + component: Input, +}; const sharedExampleProps = { options: { @@ -70,7 +71,9 @@ function InputExample() { ); } -stories.add('example', () => ); +export const _Example = { + render: () => , +}; function InputSyncedExample() { const [value, setValue] = useState(''); @@ -112,97 +115,108 @@ function InputSyncedExample() { ); } -stories.add('example synced', () => ); - -stories.add('sizes', () => - React.createElement(() => { - const [value, setValue] = React.useState(''); - return ( - - setValue(e.target.value)} - margin={2} - /> - setValue(e.target.value)} - margin={2} - /> - setValue(e.target.value)} - margin={2} - /> - - ); - }), -); - -stories.add('inside form field', () => - React.createElement(() => { - const [value, setValue] = React.useState(''); - return ( - - - setValue(e.target.value)} /> - - - ); - }), -); - -stories.add('with ref', () => - React.createElement(() => { - const [value, setValue] = React.useState('Check the console'); - return ( - - console.log(node)} - value={value} - size={'small'} - onChange={e => setValue(e.target.value)} - /> - - ); - }), -); - -stories.add('responsive sizing', () => - React.createElement(() => { - const [value, setValue] = React.useState('Resize the window'); - return ( - - console.log(node)} - value={value} - size={{ - xsmallViewport: 'small', - mediumViewport: 'default', - largeViewport: 'large', - }} - onChange={e => setValue(e.target.value)} - /> - - ); - }), -); - -stories.add('disabled', () => - React.createElement(() => { - const [value, setValue] = React.useState("I'm disabled"); - return ( - - console.log(node)} - value={value} - disabled={true} - onChange={e => setValue(e.target.value)} - /> - - ); - }), -); +export const ExampleSynced = { + render: () => , +}; + +export const Sizes = { + render: () => + React.createElement(() => { + const [value, setValue] = React.useState(''); + return ( + + setValue(e.target.value)} + margin={2} + /> + setValue(e.target.value)} + margin={2} + /> + setValue(e.target.value)} + margin={2} + /> + + ); + }), +}; + +export const InsideFormField = { + render: () => + React.createElement(() => { + const [value, setValue] = React.useState(''); + return ( + + + setValue(e.target.value)} + /> + + + ); + }), +}; + +export const WithRef = { + render: () => + React.createElement(() => { + const [value, setValue] = React.useState('Check the console'); + return ( + + console.log(node)} + value={value} + size={'small'} + onChange={e => setValue(e.target.value)} + /> + + ); + }), +}; + +export const ResponsiveSizing = { + render: () => + React.createElement(() => { + const [value, setValue] = React.useState('Resize the window'); + return ( + + console.log(node)} + value={value} + size={{ + xsmallViewport: 'small', + mediumViewport: 'default', + largeViewport: 'large', + }} + onChange={e => setValue(e.target.value)} + /> + + ); + }), +}; + +export const Disabled = { + render: () => + React.createElement(() => { + const [value, setValue] = React.useState("I'm disabled"); + return ( + + console.log(node)} + value={value} + disabled={true} + onChange={e => setValue(e.target.value)} + /> + + ); + }), +}; diff --git a/packages/sdk/stories/label.stories.tsx b/packages/sdk/stories/label.stories.tsx index ec5a6382b..72adde36d 100644 --- a/packages/sdk/stories/label.stories.tsx +++ b/packages/sdk/stories/label.stories.tsx @@ -1,16 +1,17 @@ import React from 'react'; -import {storiesOf} from '@storybook/react'; -import {keys} from '../src/private_utils'; -import Box from '../src/ui/box'; -import Input from '../src/ui/input'; -import Select from '../src/ui/select'; -import Label from '../src/ui/label'; -import {allStylesPropTypes} from '../src/ui/system'; -import theme from '../src/ui/theme/default_theme'; +import {keys} from '../src/shared/private_utils'; +import Box from '../src/base/ui/box'; +import Input from '../src/base/ui/input'; +import Select from '../src/base/ui/select'; +import Label from '../src/base/ui/label'; +import {allStylesPropTypes} from '../src/base/ui/system'; +import theme from '../src/base/ui/theme/default_theme'; import Example from './helpers/example'; import {createJsxPropsStringFromValuesMap, CONTROL_WIDTH} from './helpers/code_utils'; -const stories = storiesOf('Label', module); +export default { + component: Label, +}; function LabelExample() { const [value, setValue] = React.useState(''); @@ -60,95 +61,115 @@ function LabelExample() { ); } -stories.add('example', () => ); +export const _Example = { + render: () => , +}; -stories.add('with input', () => ( - - - - {}} value="" /> - - -)); +export const WithInput = { + render: () => ( + + + + {}} value="" /> + + + ), +}; -stories.add('with select', () => ( - - - - {}} options={[]} value="" /> + + + ), +}; -stories.add('ref', () => ( - - - -)); +export const Ref = { + render: () => ( + + + + ), +}; -stories.add('custom className', () => ( - - - -)); +export const CustomClassName = { + render: () => ( + + + + ), +}; -stories.add('id attribute', () => ( - - - -)); +export const IdAttribute = { + render: () => ( + + + + ), +}; -stories.add('style attribute', () => ( - - - -)); +export const StyleAttribute = { + render: () => ( + + + + ), +}; -stories.add('data attributes', () => ( - - - -)); +export const DataAttributes = { + render: () => ( + + + + ), +}; -stories.add('role attribute', () => ( - - - -)); +export const RoleAttribute = { + render: () => ( + + + + ), +}; -stories.add('aria attributes', () => ( - - - -)); +export const AriaAttributes = { + render: () => ( + + + + ), +}; diff --git a/packages/sdk/stories/link.stories.tsx b/packages/sdk/stories/link.stories.tsx index 79ed17396..debe6c3e8 100644 --- a/packages/sdk/stories/link.stories.tsx +++ b/packages/sdk/stories/link.stories.tsx @@ -1,16 +1,17 @@ import React from 'react'; -import {storiesOf} from '@storybook/react'; -import {values, keys} from '../src/private_utils'; -import Box from '../src/ui/box'; -import {iconNames} from '../src/ui/icon_config'; -import Link, {linkStylePropTypes} from '../src/ui/link'; -import Text from '../src/ui/text'; -import Tooltip from '../src/ui/tooltip'; -import useTheme from '../src/ui/theme/use_theme'; -import Example from './helpers/example'; +import {values, keys} from '../src/shared/private_utils'; +import Box from '../src/base/ui/box'; +import {iconNames} from '../src/base/ui/icon_config'; +import Link, {linkStylePropTypes} from '../src/base/ui/link'; +import Text from '../src/base/ui/text'; +import Tooltip from '../src/base/ui/tooltip'; +import useTheme from '../src/base/ui/theme/use_theme'; import {createJsxPropsStringFromValuesMap, createJsxComponentString} from './helpers/code_utils'; +import Example from './helpers/example'; -const stories = storiesOf('Link', module); +export default { + component: Link, +}; function LinkExample() { const {linkVariants, textStyles} = useTheme(); @@ -69,7 +70,7 @@ function LinkExample() { return ` import {Link} from '@airtable/blocks/ui'; - + ${ariaLabelComment} const linkExample = ( ${buttonComponentString} @@ -92,154 +93,182 @@ function LinkExample() { ); } -stories.add('example', () => ); +export const _Example = { + render: () => , +}; -stories.add('variants', () => ( - - - Default - - - Dark - - - Light - - -)); - -stories.add('inside of text', () => ( - - Some text with a{' '} - - link - - -)); - -stories.add('inside of paragraph', () => ( - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut - labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco{' '} - laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor - in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur - sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est - laborum. mollit anim id est laborum - -)); - -stories.add('inside of paragraph with underline', () => ( - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut - labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco{' '} - - laboris nisi ut aliquip - {' '} - ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse - cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt - in culpa qui officia deserunt mollit anim id est laborum.{' '} - - mollit anim id est laborum - - -)); +export const Variants = { + render: () => ( + + + Default + + + Dark + + + Light + + + ), +}; -stories.add('with icon', () => ( - - - Community forum - - -)); - -stories.add('with all icons', () => ( - - {values(iconNames).map(iconName => ( - - {}}> - {iconName.substr(0, 1).toUpperCase()} - {iconName.substr(1)} - - - ))} - -)); - -stories.add('responsive size with icon', () => ( - - Responsive text button - -)); - -stories.add('flex', () => ( - - - Link - - -)); +export const InsideOfText = { + render: () => ( + + Some text with a{' '} + + link + + + ), +}; -stories.add('fontWeight', () => ( - - - Strong link - - -)); +export const InsideOfParagraph = { + render: () => ( + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor + incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud + exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore + eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa + qui officia deserunt mollit anim id est laborum.{' '} + mollit anim id est laborum + + ), +}; -stories.add('custom icon', () => ( - +export const InsideOfParagraphWithUnderline = { + render: () => ( + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor + incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud + exercitation ullamco{' '} + + laboris nisi ut aliquip + {' '} + ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse + cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, + sunt in culpa qui officia deserunt mollit anim id est laborum.{' '} + + mollit anim id est laborum + + + ), +}; + +export const WithIcon = { + render: () => ( + + + Community forum + + + ), +}; + +export const WithAllIcons = { + render: () => ( + + {values(iconNames).map(iconName => ( + + {}}> + {iconName.substr(0, 1).toUpperCase()} + {iconName.substr(1)} + + + ))} + + ), +}; + +export const ResponsiveSizeWithIcon = { + render: () => ( - - - } - size="large" - paddingX={1} - marginX={-1} + icon="cube" + size={{ + xsmallViewport: 'small', + smallViewport: 'small', + mediumViewport: 'default', + largeViewport: 'large', + }} > - Custom icon + Responsive text button + + ), +}; + +export const Flex = { + render: () => ( + + + Link + + + ), +}; + +export const FontWeight = { + render: () => ( + + + Strong link + + + ), +}; + +export const CustomIcon = { + render: () => ( + + + + + } + size="large" + paddingX={1} + marginX={-1} + > + Custom icon + + + ), +}; + +export const WrappingAnImage = { + render: () => ( + + shapes - -)); - -stories.add('wrapping an image', () => ( - - shapes - -)); - -stories.add('with tooltip', () => ( - - Link with tooltip - -)); + ), +}; + +export const WithTooltip = { + render: () => ( + + Link with tooltip + + ), +}; diff --git a/packages/sdk/stories/loader.stories.tsx b/packages/sdk/stories/loader.stories.tsx index 831e1a78d..2715b8b38 100644 --- a/packages/sdk/stories/loader.stories.tsx +++ b/packages/sdk/stories/loader.stories.tsx @@ -1,12 +1,13 @@ /* eslint-disable no-console */ // @flow import React from 'react'; -import {storiesOf} from '@storybook/react'; -import Loader, {loaderStylePropTypes} from '../src/ui/loader'; -import Example from './helpers/example'; +import Loader, {loaderStylePropTypes} from '../src/base/ui/loader'; import {createJsxPropsStringFromValuesMap} from './helpers/code_utils'; +import Example from './helpers/example'; -const stories = storiesOf('Loader', module); +export default { + component: Loader, +}; function LoaderExample() { return ( @@ -38,4 +39,6 @@ function LoaderExample() { ); } -stories.add('example', () => ); +export const _Example = { + render: () => , +}; diff --git a/packages/sdk/stories/model_pickers.stories.tsx b/packages/sdk/stories/model_pickers.stories.tsx deleted file mode 100644 index 9cac56abe..000000000 --- a/packages/sdk/stories/model_pickers.stories.tsx +++ /dev/null @@ -1,328 +0,0 @@ -import React, {useState} from 'react'; -import {storiesOf} from '@storybook/react'; -import Select, {selectStylePropTypes} from '../src/ui/select'; -import {keys} from '../src/private_utils'; -import theme from '../src/ui/theme/default_theme'; -import Example from './helpers/example'; -import {createJsxPropsStringFromValuesMap, CONTROL_WIDTH} from './helpers/code_utils'; - -const stories = storiesOf('ModelPickers', module); - -const viewOptions = ['All tasks', 'Grouped by status', 'Incomplete tasks'].map(value => ({ - value, - label: value, -})); -const tableOptions = ['Tasks', 'Projects', 'Teams'].map(value => ({ - value, - label: value, -})); -const fieldOptions = ['Name', 'Notes', 'Attachments'].map(value => ({ - value, - label: value, -})); - -const sharedModelPickerExampleProps = { - options: { - size: { - type: 'selectButtons', - label: 'Size', - options: keys(theme.selectSizes), - defaultValue: 'default', - }, - disabled: { - type: 'switch', - label: 'Disabled', - defaultValue: false, - }, - shouldAllowPickingNone: { - type: 'switch', - label: 'Allow empty selection', - defaultValue: false, - }, - }, - styleProps: Object.keys(selectStylePropTypes), -} as const; - -function ViewPickerExample() { - const [value, setValue] = useState(null); - return ( - { - const props = createJsxPropsStringFromValuesMap(values); - - return ` - import React, {useState} from 'react'; - import {ViewPicker, useBase} from '@airtable/blocks/ui'; - - const ViewPickerExample = () => { - const [view, setView] = useState(null); - const base = useBase(); - const table = base.getTableByNameIfExists('Tasks'); - // If table is null or undefined, the ViewPicker will not render. - - return setView(newView)} ${props} width="${CONTROL_WIDTH}"/> - }; - `; - }} - > - {values => { - const placeholder = values.shouldAllowPickingNone ? 'None' : 'Pick a view...'; - - return ( - setValue(newValue as string)} - {...values} - width={CONTROL_WIDTH} - /> - ); - }} - - ); -} - -stories.add('ViewPickerSynced example', () => ); - -function TablePickerExample() { - const [value, setValue] = useState(null); - return ( - { - const props = createJsxPropsStringFromValuesMap(values); - - return ` - import React, {useState} from 'react'; - import {TablePicker} from '@airtable/blocks/ui'; - - const TablePickerExample = () => { - const [table, setTable] = useState(null); - - return setTable(newTable)} ${props} width="${CONTROL_WIDTH}"/> - }; - `; - }} - > - {values => { - const placeholder = values.shouldAllowPickingNone ? 'None' : 'Pick a table...'; - - return ( - setValue(newValue as string)} - {...values} - width={CONTROL_WIDTH} - /> - ); - }} - - ); -} - -stories.add('TablePickerSynced example', () => ); - -function FieldPickerExample() { - const [value, setValue] = useState(null); - return ( - { - const props = createJsxPropsStringFromValuesMap(values); - - return ` - import React, {useState} from 'react'; - import {FieldPicker, useBase} from '@airtable/blocks/ui'; - - const FieldPickerExample = () => { - const [field, setField] = useState(null); - const base = useBase(); - const table = base.getTableByNameIfExists("Tasks"); - // If table is null or undefined, the FieldPicker will not render. - - return setField(newField)} ${props} width="${CONTROL_WIDTH}"/> - }; - `; - }} - > - {values => { - const placeholder = values.shouldAllowPickingNone ? 'None' : 'Pick a field...'; - - return ( - setValue(newValue as string)} - {...values} - width={CONTROL_WIDTH} - /> - ); - }} - - ); -} - -stories.add('FieldPickerSynced example', () => ); diff --git a/packages/sdk/stories/progress_bar.stories.tsx b/packages/sdk/stories/progress_bar.stories.tsx index a7e34d95c..489449501 100644 --- a/packages/sdk/stories/progress_bar.stories.tsx +++ b/packages/sdk/stories/progress_bar.stories.tsx @@ -1,13 +1,13 @@ /* eslint-disable no-console */ // @flow -import React, {useState} from 'react'; -import {storiesOf} from '@storybook/react'; -import ProgressBar, {progressBarStylePropTypes} from '../src/ui/progress_bar'; -import {keys} from '../src/private_utils'; +import React from 'react'; +import ProgressBar, {progressBarStylePropTypes} from '../src/base/ui/progress_bar'; import Example from './helpers/example'; import {createJsxPropsStringFromValuesMap, CONTROL_WIDTH} from './helpers/code_utils'; -const stories = storiesOf('ProgressBar', module); +export default { + component: ProgressBar, +}; function ProgressBarExample() { return ( @@ -40,4 +40,6 @@ function ProgressBarExample() { ); } -stories.add('example', () => ); +export const _Example = { + render: () => , +}; diff --git a/packages/sdk/stories/record_card.stories.tsx b/packages/sdk/stories/record_card.stories.tsx index 1eb472abc..769f9272d 100644 --- a/packages/sdk/stories/record_card.stories.tsx +++ b/packages/sdk/stories/record_card.stories.tsx @@ -1,11 +1,13 @@ /* eslint-disable no-console */ import React from 'react'; -import {storiesOf} from '@storybook/react'; import Example from './helpers/example'; -import {recordCardStylePropTypes} from '../src/ui/record_card'; +import {recordCardStylePropTypes} from '../src/base/ui/record_card'; import FakeRecordCard from './helpers/fake_record_card'; +import {RecordCard} from '../src/base/ui/ui'; -const stories = storiesOf('RecordCard', module); +export default { + component: RecordCard, +}; function RecordCardExample() { return ( @@ -14,13 +16,13 @@ function RecordCardExample() { renderCodeFn={() => { return ` import {RecordCard, useBase, useRecords} from '@airtable/blocks/ui'; - + const RecordCardExample = () => { const base = useBase(); const table = base.getTableByName('Programmers'); const queryResult = table.selectRecords(); const records = useRecords(queryResult); - + // Specify which fields are shown with the \`fields\` prop return ( @@ -36,4 +38,6 @@ function RecordCardExample() { ); } -stories.add('example', () => ); +export const _Example = { + render: () => , +}; diff --git a/packages/sdk/stories/record_card_list.stories.tsx b/packages/sdk/stories/record_card_list.stories.tsx index 48f43ea1a..e1361f401 100644 --- a/packages/sdk/stories/record_card_list.stories.tsx +++ b/packages/sdk/stories/record_card_list.stories.tsx @@ -1,13 +1,15 @@ /* eslint-disable no-console */ import React from 'react'; -import {storiesOf} from '@storybook/react'; import Example from './helpers/example'; -import {recordCardListStylePropTypes} from '../src/ui/record_card_list'; -import Box from '../src/ui/box'; -import {FieldType} from '../src/types/field'; +import {recordCardListStylePropTypes} from '../src/base/ui/record_card_list'; +import Box from '../src/base/ui/box'; +import {FieldType} from '../src/shared/types/field'; import FakeRecordCard from './helpers/fake_record_card'; +import {RecordCardList} from '../src/base/ui/ui'; -const stories = storiesOf('RecordCardList', module); +export default { + component: RecordCardList, +}; function RecordCardListExample() { const fieldTypes = [ @@ -51,7 +53,7 @@ function RecordCardListExample() { renderCodeFn={() => { return ` import {RecordCardList, useBase, useRecords} from '@airtable/blocks/ui'; - + const RecordCardListExample = () => { const base = useBase(); const table = base.getTableByName('Programmers'); @@ -99,4 +101,6 @@ function RecordCardListExample() { ); } -stories.add('example', () => ); +export const _Example = { + render: () => , +}; diff --git a/packages/sdk/stories/select.stories.tsx b/packages/sdk/stories/select.stories.tsx index 4f7c6ea7b..267376a1a 100644 --- a/packages/sdk/stories/select.stories.tsx +++ b/packages/sdk/stories/select.stories.tsx @@ -1,15 +1,16 @@ import React, {useState} from 'react'; -import {storiesOf} from '@storybook/react'; -import Box from '../src/ui/box'; -import Select, {selectStylePropTypes} from '../src/ui/select'; -import Tooltip from '../src/ui/tooltip'; -import FormField from '../src/ui/form_field'; -import theme from '../src/ui/theme/default_theme'; -import {keys} from '../src/private_utils'; +import Box from '../src/base/ui/box'; +import Select, {selectStylePropTypes} from '../src/base/ui/select'; +import Tooltip from '../src/base/ui/tooltip'; +import FormField from '../src/base/ui/form_field'; +import theme from '../src/base/ui/theme/default_theme'; +import {keys} from '../src/shared/private_utils'; import Example from './helpers/example'; import {createJsxPropsStringFromValuesMap, CONTROL_WIDTH} from './helpers/code_utils'; -const stories = storiesOf('Select', module); +export default { + component: Select, +}; const options = ['Apple', 'Pear', 'Banana'].map(value => ({value, label: value})); const longOptions = [ @@ -45,7 +46,7 @@ function SelectExample() { return ` import React, {useState} from 'react'; import {Select} from '@airtable/blocks/ui'; - + const options = ${JSON.stringify(options)}; const SelectExample = () => { @@ -71,7 +72,9 @@ function SelectExample() { ); } -stories.add('example', () => ); +export const _Example = { + render: () => , +}; function SelectSyncedExample() { const [value, setValue] = useState(options[0].value); @@ -107,68 +110,97 @@ function SelectSyncedExample() { ); } -stories.add('synced example', () => ); - -stories.add('sizes', () => ( - - - - + + + + + - + ), +}; + +export const LabelOverflow = { + render: () => ( + - -)); - -stories.add('label overflow', () => ( - - - { - // eslint-disable-next-line no-console - console.log(node); - }} - options={options} - value={options[0].value} - /> - -)); - -stories.add('inside form field', () => ( - - - - - -)); + ), +}; + +export const Ref = { + render: () => ( + + + + + ), +}; + +export const WithTooltip = { + render: () => ( + + + setFood(e.target.value)} /> - + })); + return ( + setVeggie(val as string)} - options={veggies} - marginBottom={2} + size={{ + xsmallViewport: 'small', + mediumViewport: 'default', + largeViewport: 'large', + }} + value={value} + onChange={val => setValue(val as string)} + options={options} + marginBottom={3} /> - - - + + ); + }), +}; + +export const TruncatedText = { + render: () => + React.createElement(() => { + const [value, setValue] = React.useState('neat'); + const options = ['Some really long text that just keeps going', 'neat', 'cool'].map( + _value => ({ + value: _value, + label: _value, + }), + ); + return ( + setJunkfood(val as string)} - options={junkfoods} - marginBottom={2} + value={value} + onChange={val => setValue(val as string)} + options={options} + marginBottom={3} /> - - ); - }), -); + ); + }), +}; + +export const TabBehaviorForManyInputs = { + render: () => + React.createElement(() => { + const [food, setFood] = React.useState(''); + const [veggie, setVeggie] = React.useState('Squash'); + const [fruit, setFruit] = React.useState('Apple'); + const [junkfood, setJunkfood] = React.useState('Candy'); + const veggies = ['Bok choy', 'Squash', 'Carrot', 'Broccoli'].map(value => ({ + value, + label: value, + })); + const fruits = ['Banana', 'Apple', 'Orange'].map(value => ({ + value, + label: value, + })); + const junkfoods = ['Pizza', 'Milkshake', 'Burger', 'Candy', 'Soda'].map(value => ({ + value, + label: value, + })); + return ( + + + + setFood(e.target.value)} /> + + setVeggie(val as string)} + options={veggies} + marginBottom={2} + /> + + + + setJunkfood(val as string)} + options={junkfoods} + marginBottom={2} + /> + + + ); + }), +}; diff --git a/packages/sdk/stories/switch.stories.tsx b/packages/sdk/stories/switch.stories.tsx index 45b1411cd..707e5a0cd 100644 --- a/packages/sdk/stories/switch.stories.tsx +++ b/packages/sdk/stories/switch.stories.tsx @@ -1,15 +1,16 @@ /* eslint-disable no-console */ // @flow import React, {useState} from 'react'; -import {storiesOf} from '@storybook/react'; -import Box from '../src/ui/box'; -import Switch, {switchStylePropTypes} from '../src/ui/switch'; -import theme from '../src/ui/theme/default_theme'; -import {keys} from '../src/private_utils'; +import Box from '../src/base/ui/box'; +import Switch, {switchStylePropTypes} from '../src/base/ui/switch'; +import theme from '../src/base/ui/theme/default_theme'; +import {keys} from '../src/shared/private_utils'; import Example from './helpers/example'; import {createJsxPropsStringFromValuesMap, CONTROL_WIDTH} from './helpers/code_utils'; -const stories = storiesOf('Switch', module); +export default { + component: Switch, +}; const sharedExampleProps = { options: { @@ -83,7 +84,9 @@ function SwitchExample() { ); } -stories.add('example', () => ); +export const _Example = { + render: () => , +}; function SwitchSyncedExample() { const [isEnabled, setIsEnabled] = useState(true); @@ -128,227 +131,241 @@ function SwitchSyncedExample() { ); } -stories.add('example synced', () => ); +export const ExampleSynced = { + render: () => , +}; -stories.add('sizes', () => - React.createElement(() => { - const [isChecked, setIsChecked] = useState(true); - return ( - - - - - - ); - }), -); +export const Sizes = { + render: () => + React.createElement(() => { + const [isChecked, setIsChecked] = useState(true); + return ( + + + + + + ); + }), +}; -stories.add('variants', () => - React.createElement(() => { - const [isChecked, setIsChecked] = useState(true); - return ( - - - - - ); - }), -); +export const Variants = { + render: () => + React.createElement(() => { + const [isChecked, setIsChecked] = useState(true); + return ( + + + + + ); + }), +}; -stories.add('disabled', () => - React.createElement(() => { - const [isChecked, setIsChecked] = useState(true); - return ( - - - - ); - }), -); +export const Disabled = { + render: () => + React.createElement(() => { + const [isChecked, setIsChecked] = useState(true); + return ( + + + + ); + }), +}; -stories.add('override backgroundColor', () => - React.createElement(() => { - const [isChecked, setIsChecked] = useState(true); - return ( - - - - ); - }), -); +export const OverrideBackgroundColor = { + render: () => + React.createElement(() => { + const [isChecked, setIsChecked] = useState(true); + return ( + + + + ); + }), +}; -stories.add('override width', () => - React.createElement(() => { - const [isChecked, setIsChecked] = useState(true); - return ( - - - - - ); - }), -); +export const OverrideWidth = { + render: () => + React.createElement(() => { + const [isChecked, setIsChecked] = useState(true); + return ( + + + + + ); + }), +}; -stories.add('forwarded ref', () => - React.createElement(() => { - const [isChecked, setIsChecked] = useState(true); - return ( - - console.log(node)} - value={isChecked} - onChange={setIsChecked} - label="Check the console" - /> - - ); - }), -); +export const ForwardedRef = { + render: () => + React.createElement(() => { + const [isChecked, setIsChecked] = useState(true); + return ( + + console.log(node)} + value={isChecked} + onChange={setIsChecked} + label="Check the console" + /> + + ); + }), +}; -stories.add('responsive size', () => - React.createElement(() => { - const [isChecked, setIsChecked] = useState(true); - return ( - - - - ); - }), -); +export const ResponsiveSize = { + render: () => + React.createElement(() => { + const [isChecked, setIsChecked] = useState(true); + return ( + + + + ); + }), +}; -stories.add('custom classname', () => - React.createElement(() => { - const [isChecked, setIsChecked] = useState(true); - return ( - - - - ); - }), -); +export const CustomClassname = { + render: () => + React.createElement(() => { + const [isChecked, setIsChecked] = useState(true); + return ( + + + + ); + }), +}; -stories.add('id attribute', () => - React.createElement(() => { - const [isChecked, setIsChecked] = useState(true); - return ( - - - - ); - }), -); +export const IdAttribute = { + render: () => + React.createElement(() => { + const [isChecked, setIsChecked] = useState(true); + return ( + + + + ); + }), +}; -stories.add('style attribute', () => - React.createElement(() => { - const [isChecked, setIsChecked] = useState(true); - return ( - - - - ); - }), -); +export const StyleAttribute = { + render: () => + React.createElement(() => { + const [isChecked, setIsChecked] = useState(true); + return ( + + + + ); + }), +}; -stories.add('errors with no label', () => - React.createElement(() => { - const [isChecked, setIsChecked] = useState(true); - return ( - - Check the console - - - ); - }), -); +export const ErrorsWithNoLabel = { + render: () => + React.createElement(() => { + const [isChecked, setIsChecked] = useState(true); + return ( + + Check the console + + + ); + }), +}; -stories.add('truncate', () => - React.createElement(() => { - const [isChecked, setIsChecked] = useState(true); - return ( - - - - ); - }), -); +export const Truncate = { + render: () => + React.createElement(() => { + const [isChecked, setIsChecked] = useState(true); + return ( + + + + ); + }), +}; diff --git a/packages/sdk/stories/table_picker.stories.tsx b/packages/sdk/stories/table_picker.stories.tsx new file mode 100644 index 000000000..d6c80266a --- /dev/null +++ b/packages/sdk/stories/table_picker.stories.tsx @@ -0,0 +1,136 @@ +import React, {useState} from 'react'; +import Select, {selectStylePropTypes} from '../src/base/ui/select'; +import {keys} from '../src/shared/private_utils'; +import theme from '../src/base/ui/theme/default_theme'; +import Example from './helpers/example'; +import {createJsxPropsStringFromValuesMap, CONTROL_WIDTH} from './helpers/code_utils'; +import {TablePicker} from '../src/base/ui/ui'; + +export default { + component: TablePicker, +}; + +const tableOptions = ['Tasks', 'Projects', 'Teams'].map(value => ({ + value, + label: value, +})); +const fieldOptions = ['Name', 'Notes', 'Attachments'].map(value => ({ + value, + label: value, +})); + +const sharedModelPickerExampleProps = { + options: { + size: { + type: 'selectButtons', + label: 'Size', + options: keys(theme.selectSizes), + defaultValue: 'default', + }, + disabled: { + type: 'switch', + label: 'Disabled', + defaultValue: false, + }, + shouldAllowPickingNone: { + type: 'switch', + label: 'Allow empty selection', + defaultValue: false, + }, + }, + styleProps: Object.keys(selectStylePropTypes), +} as const; + +function TablePickerExample() { + const [value, setValue] = useState(null); + return ( + { + const props = createJsxPropsStringFromValuesMap(values); + + return ` + import React, {useState} from 'react'; + import {TablePicker} from '@airtable/blocks/ui'; + + const TablePickerExample = () => { + const [table, setTable] = useState(null); + + return setTable(newTable)} ${props} width="${CONTROL_WIDTH}"/> + }; + `; + }} + > + {values => { + const placeholder = values.shouldAllowPickingNone ? 'None' : 'Pick a table...'; + + return ( + setValue(newValue as string)} + {...values} + width={CONTROL_WIDTH} + /> + ); + }} + + ); +} + +export const _TablePickerSyncedExample = { + render: () => , +}; diff --git a/packages/sdk/stories/text.size.stories.tsx b/packages/sdk/stories/text.size.stories.tsx index 7a729defc..cd40beef7 100644 --- a/packages/sdk/stories/text.size.stories.tsx +++ b/packages/sdk/stories/text.size.stories.tsx @@ -1,55 +1,64 @@ import React from 'react'; -import {storiesOf} from '@storybook/react'; -import Box from '../src/ui/box'; -import Text from '../src/ui/text'; -import theme from '../src/ui/theme/default_theme'; -import {keys} from '../src/private_utils'; +import Box from '../src/base/ui/box'; +import Text from '../src/base/ui/text'; +import theme from '../src/base/ui/theme/default_theme'; +import {keys} from '../src/shared/private_utils'; -const stories = storiesOf('Text/size', module); +export default { + component: Text, + title: 'Text/size', +}; -stories.add('default sizes', () => ( - - {keys(theme.textStyles.default).map(textSize => ( - - The brown fox jumped over the lazy dog - - ))} - -)); - -stories.add('paragraph sizes', () => ( - - {keys(theme.textStyles.paragraph).map(textSize => ( - - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor - incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis - nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. - - - Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu - fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in - culpa qui officia deserunt mollit anim id est laborum. +export const DefaultSizes = { + render: () => ( + + {keys(theme.textStyles.default).map(textSize => ( + + The brown fox jumped over the lazy dog - - ))} - -)); + ))} + + ), +}; -stories.add('responsive size', () => ( - - Breakpoints:
    {JSON.stringify(theme.breakpoints, null, 4)}
    - - Resize to see size change - -
    -)); +export const ParagraphSizes = { + render: () => ( + + {keys(theme.textStyles.paragraph).map(textSize => ( + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, + quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. + + + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore + eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, + sunt in culpa qui officia deserunt mollit anim id est laborum. + + + ))} + + ), +}; + +export const ResponsiveSize = { + render: () => ( + + Breakpoints:
    {JSON.stringify(theme.breakpoints, null, 4)}
    + + Resize to see size change + +
    + ), +}; diff --git a/packages/sdk/stories/text.stories.tsx b/packages/sdk/stories/text.stories.tsx index fced8178e..4d1223302 100644 --- a/packages/sdk/stories/text.stories.tsx +++ b/packages/sdk/stories/text.stories.tsx @@ -1,13 +1,14 @@ import React from 'react'; -import {storiesOf} from '@storybook/react'; -import Text from '../src/ui/text'; -import theme from '../src/ui/theme/default_theme'; -import {keys} from '../src/private_utils'; -import {allStylesPropTypes} from '../src/ui/system'; +import Text from '../src/base/ui/text'; +import theme from '../src/base/ui/theme/default_theme'; +import {keys} from '../src/shared/private_utils'; +import {allStylesPropTypes} from '../src/base/ui/system'; import Example from './helpers/example'; import {createJsxPropsStringFromValuesMap} from './helpers/code_utils'; -const stories = storiesOf('Text', module); +export default { + component: Text, +}; const childrenForVariant = { default: 'The brown fox jumped over the lazy dog', @@ -60,117 +61,137 @@ function TextExample() { ); } -stories.add('example', () => ); +export const _Example = { + render: () => , +}; -stories.add('as', () => ( - - {[ - 'p' as const, - 'h1' as const, - 'h2' as const, - 'h3' as const, - 'h4' as const, - 'h5' as const, - 'h6' as const, - 'span' as const, - 'li' as const, - 'em' as const, - 'strong' as const, - 'kbd' as const, - 'mark' as const, - 'q' as const, - 's' as const, - 'samp' as const, - 'small' as const, - 'sub' as const, - 'sup' as const, - 'time' as const, - 'var' as const, - 'blockquote' as const, - ].map(as => ( - - {as} - - ))} - -)); +export const As = { + render: () => ( + + {[ + 'p' as const, + 'h1' as const, + 'h2' as const, + 'h3' as const, + 'h4' as const, + 'h5' as const, + 'h6' as const, + 'span' as const, + 'li' as const, + 'em' as const, + 'strong' as const, + 'kbd' as const, + 'mark' as const, + 'q' as const, + 's' as const, + 'samp' as const, + 'small' as const, + 'sub' as const, + 'sup' as const, + 'time' as const, + 'var' as const, + 'blockquote' as const, + ].map(as => ( + + {as} + + ))} + + ), +}; -stories.add('textColor', () => ( - - (light) The brown fox jumped over the lazy dog - (default = dark) The brown fox jumped over the lazy dog - -)); +export const TextColor = { + render: () => ( + + (light) The brown fox jumped over the lazy dog + (default = dark) The brown fox jumped over the lazy dog + + ), +}; -stories.add('ref', () => ( - - { - // eslint-disable-next-line no-console - console.log(node); - }} - > - Look into your console to see the ref - - -)); +export const Ref = { + render: () => ( + + { + // eslint-disable-next-line no-console + console.log(node); + }} + > + Look into your console to see the ref + + + ), +}; -stories.add('custom className', () => ( - - Inspect element to see class name - -)); +export const CustomClassName = { + render: () => ( + + Inspect element to see class name + + ), +}; -stories.add('id attribute', () => ( - - Inspect element to see id - -)); +export const IdAttribute = { + render: () => ( + + Inspect element to see id + + ), +}; -stories.add('style attribute', () => ( - - - Inspect element to see style attribute - - -)); +export const StyleAttribute = { + render: () => ( + + + Inspect element to see style attribute + + + ), +}; -stories.add('data attributes', () => ( - - - Inspect element to see data attributes - - -)); +export const DataAttributes = { + render: () => ( + + + Inspect element to see data attributes + + + ), +}; -stories.add('role attribute', () => ( - - Inspect element to see role attribute - -)); +export const RoleAttribute = { + render: () => ( + + Inspect element to see role attribute + + ), +}; -stories.add('aria attributes', () => ( - - - Inspect element to see aria attributes - - -)); +export const AriaAttributes = { + render: () => ( + + + Inspect element to see aria attributes + + + ), +}; diff --git a/packages/sdk/stories/text_button.stories.tsx b/packages/sdk/stories/text_button.stories.tsx index 3fcc97df7..9bac60d82 100644 --- a/packages/sdk/stories/text_button.stories.tsx +++ b/packages/sdk/stories/text_button.stories.tsx @@ -1,18 +1,19 @@ /* eslint-disable no-console */ import React from 'react'; -import {storiesOf} from '@storybook/react'; import {action} from '@storybook/addon-actions'; -import {values, keys} from '../src/private_utils'; -import Box from '../src/ui/box'; -import {iconNames} from '../src/ui/icon_config'; -import TextButton, {textButtonStylePropTypes} from '../src/ui/text_button'; -import Text from '../src/ui/text'; -import Tooltip from '../src/ui/tooltip'; -import useTheme from '../src/ui/theme/use_theme'; -import Example from './helpers/example'; +import {values, keys} from '../src/shared/private_utils'; +import Box from '../src/base/ui/box'; +import {iconNames} from '../src/base/ui/icon_config'; +import TextButton, {textButtonStylePropTypes} from '../src/base/ui/text_button'; +import Text from '../src/base/ui/text'; +import Tooltip from '../src/base/ui/tooltip'; +import useTheme from '../src/base/ui/theme/use_theme'; import {createJsxPropsStringFromValuesMap, createJsxComponentString} from './helpers/code_utils'; +import Example from './helpers/example'; -const stories = storiesOf('TextButton', module); +export default { + component: TextButton, +}; function TextButtonExample() { const {textButtonVariants, textStyles} = useTheme(); @@ -88,142 +89,168 @@ function TextButtonExample() { ); } -stories.add('example', () => ); +export const _Example = { + render: () => , +}; -stories.add('variants', () => ( - - - Default - - - Dark - - - Light - - -)); +export const Variants = { + render: () => ( + + + Default + + + Dark + + + Light + + + ), +}; -stories.add('sizes', () => ( - - - Small - - - Default - - - Large - - - Xlarge - - -)); - -stories.add('inside of text', () => ( - - - Some text with a{' '} - - text button +export const Sizes = { + render: () => ( + + + Small - - -)); - -stories.add('with icon', () => ( - - - Import - - - Export - - -)); - -stories.add('with all icons', () => ( - - {values(iconNames).map(iconName => ( - - {}}> - {iconName.substr(0, 1).toUpperCase()} - {iconName.substr(1)} + + Default + + + Large + + + Xlarge + + + ), +}; + +export const InsideOfText = { + render: () => ( + + + Some text with a{' '} + + text button - - ))} - -)); - -stories.add('with icon, no children', () => ); - -stories.add('disabled', () => Disabled); - -stories.add('truncate', () => ( - - Lorem ipsum dolar set amet discitupus - -)); - -stories.add('responsive size with icon', () => ( - - Responsive text button - -)); - -stories.add('flex', () => ( - - - - Text button + + + ), +}; + +export const WithIcon = { + render: () => ( + + + Import - - -)); - -stories.add('custom icon', () => ( - - - - - - } - size="large" - paddingX={1} - marginX={-1} - > - Custom icon + + Export - - -)); - -stories.add('with tooltip', () => ( - - Text button with tooltip - -)); + + ), +}; + +export const WithAllIcons = { + render: () => ( + + {values(iconNames).map(iconName => ( + + {}}> + {iconName.substr(0, 1).toUpperCase()} + {iconName.substr(1)} + + + ))} + + ), +}; + +export const WithIconNoChildren = { + render: () => , +}; + +export const Disabled = { + render: () => Disabled, +}; + +export const Truncate = { + render: () => ( + + Lorem ipsum dolar set amet discitupus + + ), +}; + +export const ResponsiveSizeWithIcon = { + render: () => ( + + Responsive text button + + ), +}; + +export const Flex = { + render: () => ( + + + + Text button + + + + ), +}; + +export const CustomIcon = { + render: () => ( + + + + + + } + size="large" + paddingX={1} + marginX={-1} + > + Custom icon + + + + ), +}; + +export const WithTooltip = { + render: () => ( + + Text button with tooltip + + ), +}; diff --git a/packages/sdk/stories/tooltip.stories.tsx b/packages/sdk/stories/tooltip.stories.tsx index 5b714be08..97e3d88f2 100644 --- a/packages/sdk/stories/tooltip.stories.tsx +++ b/packages/sdk/stories/tooltip.stories.tsx @@ -1,12 +1,13 @@ import React from 'react'; -import {storiesOf} from '@storybook/react'; -import Tooltip from '../src/ui/tooltip'; -import Button from '../src/ui/button'; -import Example from './helpers/example'; +import Tooltip from '../src/base/ui/tooltip'; +import Button from '../src/base/ui/button'; import {createJsxPropsStringFromValuesMap} from './helpers/code_utils'; import {injectGlobal} from 'emotion'; +import Example from './helpers/example'; -const stories = storiesOf('Tooltip', module); +export default { + component: Tooltip, +}; injectGlobal` @keyframes opacityFadeIn { @@ -68,4 +69,6 @@ function TextExample() { ); } -stories.add('example', () => ); +export const _Example = { + render: () => , +}; diff --git a/packages/sdk/stories/view_picker.stories.tsx b/packages/sdk/stories/view_picker.stories.tsx new file mode 100644 index 000000000..110522f72 --- /dev/null +++ b/packages/sdk/stories/view_picker.stories.tsx @@ -0,0 +1,139 @@ +import React, {useState} from 'react'; +import Select, {selectStylePropTypes} from '../src/base/ui/select'; +import {keys} from '../src/shared/private_utils'; +import theme from '../src/base/ui/theme/default_theme'; +import Example from './helpers/example'; +import {createJsxPropsStringFromValuesMap, CONTROL_WIDTH} from './helpers/code_utils'; +import {ViewPicker} from '../src/base/ui/ui'; + +export default { + component: ViewPicker, +}; + +const viewOptions = ['All tasks', 'Grouped by status', 'Incomplete tasks'].map(value => ({ + value, + label: value, +})); + +const sharedModelPickerExampleProps = { + options: { + size: { + type: 'selectButtons', + label: 'Size', + options: keys(theme.selectSizes), + defaultValue: 'default', + }, + disabled: { + type: 'switch', + label: 'Disabled', + defaultValue: false, + }, + shouldAllowPickingNone: { + type: 'switch', + label: 'Allow empty selection', + defaultValue: false, + }, + }, + styleProps: Object.keys(selectStylePropTypes), +} as const; + +function ViewPickerExample() { + const [value, setValue] = useState(null); + return ( + { + const props = createJsxPropsStringFromValuesMap(values); + + return ` + import React, {useState} from 'react'; + import {ViewPicker, useBase} from '@airtable/blocks/ui'; + + const ViewPickerExample = () => { + const [view, setView] = useState(null); + const base = useBase(); + const table = base.getTableByNameIfExists('Tasks'); + // If table is null or undefined, the ViewPicker will not render. + + return setView(newView)} ${props} width="${CONTROL_WIDTH}"/> + }; + `; + }} + > + {values => { + const placeholder = values.shouldAllowPickingNone ? 'None' : 'Pick a view...'; + + return ( + setValue(newValue as string)} + {...values} + width={CONTROL_WIDTH} + /> + ); + }} + + ); +} + +export const _ViewPickerSyncedExample = { + render: () => , +}; diff --git a/packages/sdk/test/airtable_interface_mocks/fixture_data.ts b/packages/sdk/test/airtable_interface_mocks/fixture_data.ts index 7535408d1..9e8e0af88 100644 --- a/packages/sdk/test/airtable_interface_mocks/fixture_data.ts +++ b/packages/sdk/test/airtable_interface_mocks/fixture_data.ts @@ -1,12 +1,11 @@ -import {BaseId} from '../../src/types/base'; -import {TableId, TableData} from '../../src/types/table'; -import {FieldId, FieldType, FieldData} from '../../src/types/field'; -import {ViewData, ViewId, ViewType} from '../../src/types/view'; -import {RecordId} from '../../src/types/record'; -import {CollaboratorData} from '../../src/types/collaborator'; -import {Color} from '../../src/colors'; -import {ObjectMap, keyBy, getId} from '../../src/private_utils'; -import {BlockRunContextType, SdkInitData} from '../../src/types/airtable_interface'; +import {BaseId, TableId, FieldId, ViewId, RecordId} from '../../src/shared/types/hyper_ids'; +import {TableData} from '../../src/base/types/table'; +import {FieldType, FieldData} from '../../src/shared/types/field'; +import {ViewData, ViewType} from '../../src/base/types/view'; +import {CollaboratorData} from '../../src/shared/types/collaborator'; +import {Color} from '../../src/shared/colors'; +import {ObjectMap, keyBy, getId} from '../../src/shared/private_utils'; +import {BlockRunContextType, SdkInitData} from '../../src/base/types/airtable_interface'; const MOCK_BLOCK_INSTALLATION_ID = 'blicPfOILwejF6HL2'; const MOCK_BLOCK_RUN_CONTEXT_TYPE = BlockRunContextType.DASHBOARD_APP; diff --git a/packages/sdk/test/airtable_interface_mocks/linked_records.ts b/packages/sdk/test/airtable_interface_mocks/linked_records.ts index a49311dc5..cea3a94fe 100644 --- a/packages/sdk/test/airtable_interface_mocks/linked_records.ts +++ b/packages/sdk/test/airtable_interface_mocks/linked_records.ts @@ -1,5 +1,5 @@ -import {ViewType} from '../../src/types/view'; -import {FieldType} from '../../src/types/field'; +import {ViewType} from '../../src/base/types/view'; +import {FieldType} from '../../src/shared/types/field'; import {FixtureData} from './fixture_data'; const linkedRecords: FixtureData = { diff --git a/packages/sdk/test/airtable_interface_mocks/mock_airtable_interface.ts b/packages/sdk/test/airtable_interface_mocks/mock_airtable_interface.ts index 61541a0d2..bd5982325 100644 --- a/packages/sdk/test/airtable_interface_mocks/mock_airtable_interface.ts +++ b/packages/sdk/test/airtable_interface_mocks/mock_airtable_interface.ts @@ -2,19 +2,18 @@ import { Aggregators, FieldTypeProvider, UrlConstructor, - GlobalConfigHelpers, IdGenerator, VisList, PartialViewData, -} from '../../src/types/airtable_interface'; +} from '../../src/base/types/airtable_interface'; import {AbstractMockAirtableInterface} from '../../src/testing/abstract_mock_airtable_interface'; -import {TableId} from '../../src/types/table'; -import {FieldId} from '../../src/types/field'; -import {spawnError} from '../../src/error_utils'; -import {CursorData} from '../../src/types/cursor'; -import {RecordData} from '../../src/types/record'; -import {RecordActionData} from '../../src/types/record_action_data'; -import {RequestJson, ResponseJson} from '../../src/types/backend_fetch_types'; +import {TableId, FieldId} from '../../src/shared/types/hyper_ids'; +import {spawnError} from '../../src/shared/error_utils'; +import {CursorData} from '../../src/base/types/cursor'; +import {RecordData} from '../../src/base/types/record'; +import {RecordActionData} from '../../src/base/types/record_action_data'; +import {RequestJson, ResponseJson} from '../../src/base/types/backend_fetch_types'; +import {GlobalConfigHelpers} from '../../src/shared/types/airtable_interface_core'; import projectTrackerData from './project_tracker'; import linkedRecordsData from './linked_records'; import {FixtureData, convertFixtureDataToSdkInitData} from './fixture_data'; diff --git a/packages/sdk/test/airtable_interface_mocks/project_tracker.tsx b/packages/sdk/test/airtable_interface_mocks/project_tracker.tsx index c2606e4bf..6058b15f4 100644 --- a/packages/sdk/test/airtable_interface_mocks/project_tracker.tsx +++ b/packages/sdk/test/airtable_interface_mocks/project_tracker.tsx @@ -1,5 +1,5 @@ -import {ViewType} from '../../src/types/view'; -import {FieldType} from '../../src/types/field'; +import {ViewType} from '../../src/base/types/view'; +import {FieldType} from '../../src/shared/types/field'; import {FixtureData} from './fixture_data'; const projectTracker: FixtureData = { diff --git a/packages/sdk/test/error_utils.test.ts b/packages/sdk/test/error_utils.test.ts index 8a6e5a06c..615fa86eb 100644 --- a/packages/sdk/test/error_utils.test.ts +++ b/packages/sdk/test/error_utils.test.ts @@ -3,7 +3,7 @@ import { invariant, spawnAbstractMethodError, spawnUnknownSwitchCaseError, -} from '../src/error_utils'; +} from '../src/shared/error_utils'; describe('spawnError', () => { it('returns an error with message set to the first argument', () => { diff --git a/packages/sdk/test/get_style_props_for_responsive_prop.test.ts b/packages/sdk/test/get_style_props_for_responsive_prop.test.ts index aafe1a853..911b37c37 100644 --- a/packages/sdk/test/get_style_props_for_responsive_prop.test.ts +++ b/packages/sdk/test/get_style_props_for_responsive_prop.test.ts @@ -1,4 +1,4 @@ -import getStylePropsForResponsiveProp from '../src/ui/system/utils/get_style_props_for_responsive_prop'; +import getStylePropsForResponsiveProp from '../src/base/ui/system/utils/get_style_props_for_responsive_prop'; describe('getStylePropForResponsiveProp', () => { it('returns responsive style props for scale', () => { diff --git a/packages/sdk/test/index.test.ts b/packages/sdk/test/index.test.ts index 021f0ee4e..c015ed6d6 100644 --- a/packages/sdk/test/index.test.ts +++ b/packages/sdk/test/index.test.ts @@ -1,9 +1,7 @@ // eslint-disable-next-line import/order import {MockAirtableInterface} from './airtable_interface_mocks/mock_airtable_interface'; -import * as sdk from '../src/index'; -import * as models from '../src/models/models'; -import * as UI from '../src/ui/ui'; - +import * as sdk from '../src/base/index'; +import * as UI from '../src/base/ui/ui'; let mockAirtableInterface: jest.Mocked; jest.mock('../src/injected/airtable_interface', () => ({ __esModule: true, @@ -52,16 +50,6 @@ describe('index', () => { }); }); - describe('legacy `models` property', () => { - test('value', () => { - expect((sdk as any).models).toBe(models); - }); - - test('enumerability', () => { - expect(Object.keys(sdk).some(key => key === 'models')).toBe(true); - }); - }); - describe('internal `undoRedo` property', () => { test('value', () => { expect((sdk as any).undoRedo).toBe(sdk.__sdk.undoRedo); diff --git a/packages/sdk/test/models/base.test.ts b/packages/sdk/test/models/base.test.ts index db7f666f5..94259c128 100644 --- a/packages/sdk/test/models/base.test.ts +++ b/packages/sdk/test/models/base.test.ts @@ -1,9 +1,9 @@ import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {FieldType} from '../../src/types/field'; -import {MutationTypes} from '../../src/types/mutations'; -import Base from '../../src/models/base'; -import Sdk from '../../src/sdk'; -import Table from '../../src/models/table'; +import {FieldType} from '../../src/shared/types/field'; +import {MutationTypes} from '../../src/base/types/mutations'; +import Base from '../../src/base/models/base'; +import Sdk from '../../src/base/sdk'; +import Table from '../../src/base/models/table'; const mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/models/cursor.test.ts b/packages/sdk/test/models/cursor.test.ts index c14d84a7e..e1ae3582b 100644 --- a/packages/sdk/test/models/cursor.test.ts +++ b/packages/sdk/test/models/cursor.test.ts @@ -1,6 +1,6 @@ import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {__reset, __sdk} from '../../src'; -import Cursor from '../../src/models/cursor'; +import {__reset, __sdk} from '../../src/base'; +import Cursor from '../../src/base/models/cursor'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/models/field.test.ts b/packages/sdk/test/models/field.test.ts index 0577abf8b..2652e0c20 100644 --- a/packages/sdk/test/models/field.test.ts +++ b/packages/sdk/test/models/field.test.ts @@ -1,8 +1,8 @@ import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import Field from '../../src/models/field'; -import {FieldType} from '../../src/types/field'; -import {__reset, __sdk as sdk} from '../../src'; -import {MutationTypes} from '../../src/types/mutations'; +import Field from '../../src/base/models/field'; +import {FieldType} from '../../src/shared/types/field'; +import {__reset, __sdk as sdk} from '../../src/base'; +import {MutationTypes} from '../../src/base/types/mutations'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/models/linked_records_query_result.test.ts b/packages/sdk/test/models/linked_records_query_result.test.ts index 29fbb9728..a332bf278 100644 --- a/packages/sdk/test/models/linked_records_query_result.test.ts +++ b/packages/sdk/test/models/linked_records_query_result.test.ts @@ -1,8 +1,8 @@ import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {__reset, __sdk as sdk} from '../../src'; -import Record from '../../src/models/record'; -import RecordQueryResult from '../../src/models/record_query_result'; -import LinkedRecordsQueryResult from '../../src/models/linked_records_query_result'; +import {__reset, __sdk as sdk} from '../../src/base'; +import Record from '../../src/base/models/record'; +import RecordQueryResult from '../../src/base/models/record_query_result'; +import LinkedRecordsQueryResult from '../../src/base/models/linked_records_query_result'; import {simulateTimersAndClearAfterEachTest, waitForWatchKeyAsync} from '../test_helpers'; let mockAirtableInterface: jest.Mocked; diff --git a/packages/sdk/test/models/mutations.test.ts b/packages/sdk/test/models/mutations.test.ts index f76a6b602..88e49d3fc 100644 --- a/packages/sdk/test/models/mutations.test.ts +++ b/packages/sdk/test/models/mutations.test.ts @@ -1,12 +1,13 @@ import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import Base from '../../src/models/base'; -import Mutations from '../../src/models/mutations'; -import Sdk from '../../src/sdk'; -import Session from '../../src/models/session'; -import {ModelChange} from '../../src/types/base'; -import {FieldType} from '../../src/types/field'; -import {MutationTypes} from '../../src/types/mutations'; -import {BlockRunContextType, FieldTypeConfig} from '../../src/types/airtable_interface'; +import Base from '../../src/base/models/base'; +import Mutations from '../../src/base/models/mutations'; +import Sdk from '../../src/base/sdk'; +import Session from '../../src/base/models/session'; +import {ModelChange} from '../../src/shared/types/base_core'; +import {FieldType} from '../../src/shared/types/field'; +import {MutationTypes} from '../../src/base/types/mutations'; +import {BlockRunContextType} from '../../src/base/types/airtable_interface'; +import {FieldTypeConfig} from '../../src/shared/types/airtable_interface_core'; const mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); jest.mock('../../src/injected/airtable_interface', () => ({ @@ -14,7 +15,7 @@ jest.mock('../../src/injected/airtable_interface', () => ({ default: () => mockAirtableInterface, })); -jest.mock('../../src/models/mutation_constants', () => ({ +jest.mock('../../src/shared/types/mutation_constants', () => ({ MAX_NUM_FIELDS_PER_TABLE: 10, MAX_FIELD_NAME_LENGTH: 20, MAX_FIELD_DESCRIPTION_LENGTH: 50, diff --git a/packages/sdk/test/models/object_pool.test.ts b/packages/sdk/test/models/object_pool.test.ts index 6acec3b7f..6d7134ebb 100644 --- a/packages/sdk/test/models/object_pool.test.ts +++ b/packages/sdk/test/models/object_pool.test.ts @@ -1,4 +1,4 @@ -import ObjectPool, {Poolable} from '../../src/models/object_pool'; +import ObjectPool, {Poolable} from '../../src/base/models/object_pool'; class TestPoolable implements Poolable { __poolKey: string; diff --git a/packages/sdk/test/models/record.test.ts b/packages/sdk/test/models/record.test.ts index 96d1c5212..d82ae87d8 100644 --- a/packages/sdk/test/models/record.test.ts +++ b/packages/sdk/test/models/record.test.ts @@ -1,14 +1,14 @@ /* eslint-disable no-unused-expressions */ import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import Sdk from '../../src/sdk'; -import AbstractModel from '../../src/models/abstract_model'; -import Base from '../../src/models/base'; -import Field from '../../src/models/field'; -import LinkedRecordsQueryResult from '../../src/models/linked_records_query_result'; -import Record from '../../src/models/record'; -import RecordQueryResult from '../../src/models/record_query_result'; -import Table from '../../src/models/table'; -import View from '../../src/models/view'; +import Sdk from '../../src/base/sdk'; +import AbstractModel from '../../src/shared/models/abstract_model'; +import Base from '../../src/base/models/base'; +import Field from '../../src/base/models/field'; +import LinkedRecordsQueryResult from '../../src/base/models/linked_records_query_result'; +import Record from '../../src/base/models/record'; +import RecordQueryResult from '../../src/base/models/record_query_result'; +import Table from '../../src/base/models/table'; +import View from '../../src/base/models/view'; const mockAirtableInterface = MockAirtableInterface.linkedRecordsExample(); jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/models/session.test.ts b/packages/sdk/test/models/session.test.ts index 7915c47c5..0e1f66692 100644 --- a/packages/sdk/test/models/session.test.ts +++ b/packages/sdk/test/models/session.test.ts @@ -1,13 +1,13 @@ import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {UserId} from '../../src/types/collaborator'; -import {PermissionLevel, PermissionLevels} from '../../src/types/permission_levels'; +import {UserId} from '../../src/shared/types/hyper_ids'; +import {PermissionLevel, PermissionLevels} from '../../src/shared/types/permission_levels'; import { MutationTypes, DeleteMultipleRecordsMutation, CreateMultipleRecordsMutation, SetMultipleRecordsCellValuesMutation, -} from '../../src/types/mutations'; -import {__reset, __sdk as sdk} from '../../src'; +} from '../../src/base/types/mutations'; +import {__reset, __sdk as sdk} from '../../src/base'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/models/table.test.ts b/packages/sdk/test/models/table.test.ts index 39a1a7e6b..eaeb99db1 100644 --- a/packages/sdk/test/models/table.test.ts +++ b/packages/sdk/test/models/table.test.ts @@ -1,15 +1,14 @@ /* eslint-disable no-unused-expressions */ import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import Base from '../../src/models/base'; -import Table from '../../src/models/table'; -import Field from '../../src/models/field'; -import View from '../../src/models/view'; -import {TableId} from '../../src/types/table'; -import {RecordId} from '../../src/types/record'; -import {ViewId, ViewType} from '../../src/types/view'; -import {FieldId, FieldType} from '../../src/types/field'; -import {MutationTypes} from '../../src/types/mutations'; -import Sdk from '../../src/sdk'; +import Base from '../../src/base/models/base'; +import Table from '../../src/base/models/table'; +import Field from '../../src/base/models/field'; +import View from '../../src/base/models/view'; +import {TableId, FieldId, ViewId, RecordId} from '../../src/shared/types/hyper_ids'; +import {ViewType} from '../../src/base/types/view'; +import {FieldType} from '../../src/shared/types/field'; +import {MutationTypes} from '../../src/base/types/mutations'; +import Sdk from '../../src/base/sdk'; const mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/models/table_mutations.test.ts b/packages/sdk/test/models/table_mutations.test.ts index bf8422dc1..5c3279a0c 100644 --- a/packages/sdk/test/models/table_mutations.test.ts +++ b/packages/sdk/test/models/table_mutations.test.ts @@ -1,8 +1,8 @@ import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import Base from '../../src/models/base'; -import {MutationTypes} from '../../src/types/mutations'; -import warning from '../../src/warning'; -import Sdk from '../../src/sdk'; +import Base from '../../src/base/models/base'; +import {MutationTypes} from '../../src/base/types/mutations'; +import warning from '../../src/shared/warning'; +import Sdk from '../../src/base/sdk'; const mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); jest.mock('../../src/injected/airtable_interface', () => ({ @@ -10,7 +10,7 @@ jest.mock('../../src/injected/airtable_interface', () => ({ default: () => mockAirtableInterface, })); -jest.mock('../../src/warning', () => jest.fn()); +jest.mock('../../src/shared/warning', () => jest.fn()); describe('Table', () => { let base: Base; diff --git a/packages/sdk/test/models/table_or_view_query_result.test.ts b/packages/sdk/test/models/table_or_view_query_result.test.ts index 8c230d157..f5c4a9031 100644 --- a/packages/sdk/test/models/table_or_view_query_result.test.ts +++ b/packages/sdk/test/models/table_or_view_query_result.test.ts @@ -1,14 +1,14 @@ import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import Base from '../../src/models/base'; +import Base from '../../src/base/models/base'; import {waitForWatchKeyAsync} from '../test_helpers'; -import {__reset, __sdk as sdk} from '../../src'; -import {modes as recordColorModes} from '../../src/models/record_coloring'; -import {FieldType} from '../../src/types/field'; -import {RecordData} from '../../src/types/record'; -import Table from '../../src/models/table'; -import Field from '../../src/models/field'; -import View from '../../src/models/view'; -import {GroupData, GroupLevelData} from '../../src/types/view'; +import {__reset, __sdk as sdk} from '../../src/base'; +import {modes as recordColorModes} from '../../src/base/models/record_coloring'; +import {FieldType} from '../../src/shared/types/field'; +import {RecordData} from '../../src/base/types/record'; +import Table from '../../src/base/models/table'; +import Field from '../../src/base/models/field'; +import View from '../../src/base/models/view'; +import {GroupData, GroupLevelData} from '../../src/base/types/view'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/models/view.test.ts b/packages/sdk/test/models/view.test.ts index f479a98b5..a145b82d1 100644 --- a/packages/sdk/test/models/view.test.ts +++ b/packages/sdk/test/models/view.test.ts @@ -1,14 +1,14 @@ /* eslint-disable no-unused-expressions */ import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {ViewType} from '../../src/types/view'; -import {__reset, __sdk as sdk} from '../../src'; -import AbstractModel from '../../src/models/abstract_model'; -import Base from '../../src/models/base'; -import * as RecordColoring from '../../src/models/record_coloring'; -import Table from '../../src/models/table'; -import View from '../../src/models/view'; -import {MutationTypes} from '../../src/types/mutations'; -import {BlockRunContextType} from '../../src/types/airtable_interface'; +import {ViewType} from '../../src/base/types/view'; +import {__reset, __sdk as sdk} from '../../src/base'; +import AbstractModel from '../../src/shared/models/abstract_model'; +import Base from '../../src/base/models/base'; +import * as RecordColoring from '../../src/base/models/record_coloring'; +import Table from '../../src/base/models/table'; +import View from '../../src/base/models/view'; +import {MutationTypes} from '../../src/base/types/mutations'; +import {BlockRunContextType} from '../../src/base/types/airtable_interface'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/models/view_metadata_query_result.test.ts b/packages/sdk/test/models/view_metadata_query_result.test.ts index 52be75f51..abc1f8074 100644 --- a/packages/sdk/test/models/view_metadata_query_result.test.ts +++ b/packages/sdk/test/models/view_metadata_query_result.test.ts @@ -1,11 +1,11 @@ import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {__reset, __sdk as sdk} from '../../src'; -import {FieldId} from '../../src/types/field'; -import View from '../../src/models/view'; -import ViewMetadataQueryResult from '../../src/models/view_metadata_query_result'; +import {__reset, __sdk as sdk} from '../../src/base'; +import {FieldId} from '../../src/shared/types/hyper_ids'; +import View from '../../src/base/models/view'; +import ViewMetadataQueryResult from '../../src/base/models/view_metadata_query_result'; import {waitForWatchKeyAsync} from '../test_helpers'; -import {NormalizedGroupLevel} from '../../src/types/airtable_interface'; +import {NormalizedGroupLevel} from '../../src/base/types/airtable_interface'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/private_utils.test.ts b/packages/sdk/test/private_utils.test.ts index f25cb7399..4527db51f 100644 --- a/packages/sdk/test/private_utils.test.ts +++ b/packages/sdk/test/private_utils.test.ts @@ -11,7 +11,7 @@ import { arrayDifference, debounce, cast, -} from '../src/private_utils'; +} from '../src/shared/private_utils'; import {flowTest} from './test_helpers'; jest.useFakeTimers(); diff --git a/packages/sdk/test/sdk.test.ts b/packages/sdk/test/sdk.test.ts index 503f0977b..6d63ef58c 100644 --- a/packages/sdk/test/sdk.test.ts +++ b/packages/sdk/test/sdk.test.ts @@ -1,11 +1,11 @@ // eslint-disable-next-line import/order import {MockAirtableInterface} from './airtable_interface_mocks/mock_airtable_interface'; -import Table from '../src/models/table'; -import View from '../src/models/view'; -import AbstractModelWithAsyncData from '../src/models/abstract_model_with_async_data'; -import Sdk from '../src/sdk'; -import {__reset, __sdk as sdk} from '../src'; -import {RequestJson} from '../src/types/backend_fetch_types'; +import Table from '../src/base/models/table'; +import View from '../src/base/models/view'; +import AbstractModelWithAsyncData from '../src/base/models/abstract_model_with_async_data'; +import Sdk from '../src/base/sdk'; +import {__reset, __sdk as sdk} from '../src/base'; +import {RequestJson} from '../src/base/types/backend_fetch_types'; let mockAirtableInterface: jest.Mocked; jest.mock('../src/injected/airtable_interface', () => ({ @@ -312,9 +312,9 @@ describe('sdk', () => { expect(() => newQuery.records).not.toThrow(); - expect(query._recordStore._isDataLoaded).toBe(true); + expect(query._recordStore._loader._isDataLoaded).toBe(true); jest.advanceTimersByTime(AbstractModelWithAsyncData.__DATA_UNLOAD_DELAY_MS); - expect(query._recordStore._isDataLoaded).toBe(false); + expect(query._recordStore._loader._isDataLoaded).toBe(false); }); }); diff --git a/packages/sdk/test/test_helpers.ts b/packages/sdk/test/test_helpers.ts index 9037d06c0..63d666303 100644 --- a/packages/sdk/test/test_helpers.ts +++ b/packages/sdk/test/test_helpers.ts @@ -1,5 +1,5 @@ import {ReactWrapper} from 'enzyme'; -import Watchable from '../src/watchable'; +import Watchable from '../src/shared/watchable'; /** * include a section of code that must pass flow but shouldn't actually be executed. Use it along diff --git a/packages/sdk/test/ui/base_provider.test.tsx b/packages/sdk/test/ui/base_provider.test.tsx index c919d78e4..2e4d139e9 100644 --- a/packages/sdk/test/ui/base_provider.test.tsx +++ b/packages/sdk/test/ui/base_provider.test.tsx @@ -1,8 +1,8 @@ import React from 'react'; import {mount} from 'enzyme'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {BaseProvider, useBase} from '../../src/ui/ui'; -import {__sdk as sdk} from '../../src'; +import {BaseProvider, useBase} from '../../src/base/ui/ui'; +import {__sdk as sdk} from '../../src/base'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/ui/block_wrapper.test.tsx b/packages/sdk/test/ui/block_wrapper.test.tsx index 801dbcc16..a5e0427dd 100644 --- a/packages/sdk/test/ui/block_wrapper.test.tsx +++ b/packages/sdk/test/ui/block_wrapper.test.tsx @@ -2,9 +2,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; import {mount} from 'enzyme'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import BlockWrapper from '../../src/ui/block_wrapper'; -import Sdk from '../../src/sdk'; -import {__reset, __sdk as sdk} from '../../src'; +import BlockWrapper from '../../src/base/ui/block_wrapper'; +import Sdk from '../../src/base/sdk'; +import {__reset, __sdk as sdk} from '../../src/base'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/ui/box.test.tsx b/packages/sdk/test/ui/box.test.tsx index e082b6389..13832c242 100644 --- a/packages/sdk/test/ui/box.test.tsx +++ b/packages/sdk/test/ui/box.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {mount} from 'enzyme'; -import {Box} from '../../src/ui/unstable_standalone_ui'; +import {Box} from '../../src/base/ui/unstable_standalone_ui'; describe('Box', () => { it('renders outside of a blocks context', () => { diff --git a/packages/sdk/test/ui/button.test.tsx b/packages/sdk/test/ui/button.test.tsx index ce80986aa..f22db0415 100644 --- a/packages/sdk/test/ui/button.test.tsx +++ b/packages/sdk/test/ui/button.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {mount} from 'enzyme'; -import {Button} from '../../src/ui/unstable_standalone_ui'; +import {Button} from '../../src/base/ui/unstable_standalone_ui'; describe('Button', () => { it('renders outside of a blocks context', () => { diff --git a/packages/sdk/test/ui/cell_renderer.test.tsx b/packages/sdk/test/ui/cell_renderer.test.tsx index 929aed092..b134c283c 100644 --- a/packages/sdk/test/ui/cell_renderer.test.tsx +++ b/packages/sdk/test/ui/cell_renderer.test.tsx @@ -1,9 +1,9 @@ import React from 'react'; import {mount} from 'enzyme'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {CellRenderer} from '../../src/ui/ui'; -import {SdkContext} from '../../src/ui/sdk_context'; -import {__sdk as sdk} from '../../src'; +import {CellRenderer} from '../../src/base/ui/ui'; +import {SdkContext} from '../../src/shared/ui/sdk_context'; +import {__sdk as sdk} from '../../src/base'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/ui/choice_token.test.tsx b/packages/sdk/test/ui/choice_token.test.tsx index 36e6a0fb6..d1b99b299 100644 --- a/packages/sdk/test/ui/choice_token.test.tsx +++ b/packages/sdk/test/ui/choice_token.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {mount} from 'enzyme'; -import {ChoiceToken} from '../../src/ui/unstable_standalone_ui'; +import {ChoiceToken} from '../../src/base/ui/unstable_standalone_ui'; describe('ChoiceToken', () => { it('renders outside of a blocks context', () => { diff --git a/packages/sdk/test/ui/collaborator_token.test.tsx b/packages/sdk/test/ui/collaborator_token.test.tsx index afa487872..cd0e9ebac 100644 --- a/packages/sdk/test/ui/collaborator_token.test.tsx +++ b/packages/sdk/test/ui/collaborator_token.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {mount} from 'enzyme'; -import {CollaboratorToken} from '../../src/ui/unstable_standalone_ui'; +import {CollaboratorToken} from '../../src/base/ui/unstable_standalone_ui'; describe('StaticCollaboratorToken', () => { it('renders outside of a blocks context', () => { diff --git a/packages/sdk/test/ui/color_palette.test.tsx b/packages/sdk/test/ui/color_palette.test.tsx index 4a0eafc18..90b74aa58 100644 --- a/packages/sdk/test/ui/color_palette.test.tsx +++ b/packages/sdk/test/ui/color_palette.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {mount} from 'enzyme'; -import {ColorPalette} from '../../src/ui/unstable_standalone_ui'; +import {ColorPalette} from '../../src/base/ui/unstable_standalone_ui'; describe('ColorPalette', () => { it('renders outside of a blocks context', () => { diff --git a/packages/sdk/test/ui/color_palette_synced.test.tsx b/packages/sdk/test/ui/color_palette_synced.test.tsx index 5a33ab27a..16c464042 100644 --- a/packages/sdk/test/ui/color_palette_synced.test.tsx +++ b/packages/sdk/test/ui/color_palette_synced.test.tsx @@ -1,9 +1,9 @@ import React from 'react'; import {mount} from 'enzyme'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {ColorPaletteSynced} from '../../src/ui/ui'; -import {SdkContext} from '../../src/ui/sdk_context'; -import {__sdk as sdk} from '../../src'; +import {ColorPaletteSynced} from '../../src/base/ui/ui'; +import {SdkContext} from '../../src/shared/ui/sdk_context'; +import {__sdk as sdk} from '../../src/base'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/ui/confirmation_dialog.test.tsx b/packages/sdk/test/ui/confirmation_dialog.test.tsx index abf205d4a..65d6790bf 100644 --- a/packages/sdk/test/ui/confirmation_dialog.test.tsx +++ b/packages/sdk/test/ui/confirmation_dialog.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {mount} from 'enzyme'; -import {ConfirmationDialog} from '../../src/ui/unstable_standalone_ui'; +import {ConfirmationDialog} from '../../src/base/ui/unstable_standalone_ui'; describe('ConfirmationDialog', () => { it('renders outside of a blocks context', () => { diff --git a/packages/sdk/test/ui/dialog.test.tsx b/packages/sdk/test/ui/dialog.test.tsx index 7f18fdaac..097f49d3a 100644 --- a/packages/sdk/test/ui/dialog.test.tsx +++ b/packages/sdk/test/ui/dialog.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {mount} from 'enzyme'; -import {Dialog} from '../../src/ui/unstable_standalone_ui'; +import {Dialog} from '../../src/base/ui/unstable_standalone_ui'; describe('Dialog', () => { it('renders outside of a blocks context', () => { diff --git a/packages/sdk/test/ui/expand_record.test.tsx b/packages/sdk/test/ui/expand_record.test.tsx index c90447114..a02ad7e5b 100644 --- a/packages/sdk/test/ui/expand_record.test.tsx +++ b/packages/sdk/test/ui/expand_record.test.tsx @@ -1,9 +1,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {expandRecord, useBase, useRecords} from '../../src/ui/ui'; -import {SdkContext} from '../../src/ui/sdk_context'; -import {__sdk as sdk} from '../../src'; +import {expandRecord, useBase, useRecords} from '../../src/base/ui/ui'; +import {SdkContext} from '../../src/shared/ui/sdk_context'; +import {__sdk as sdk} from '../../src/base'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/ui/expand_record_list.test.tsx b/packages/sdk/test/ui/expand_record_list.test.tsx index 3c49ba898..d1579833a 100644 --- a/packages/sdk/test/ui/expand_record_list.test.tsx +++ b/packages/sdk/test/ui/expand_record_list.test.tsx @@ -1,9 +1,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {expandRecordList, useBase, useRecords} from '../../src/ui/ui'; -import {SdkContext} from '../../src/ui/sdk_context'; -import {__sdk as sdk} from '../../src'; +import {expandRecordList, useBase, useRecords} from '../../src/base/ui/ui'; +import {SdkContext} from '../../src/shared/ui/sdk_context'; +import {__sdk as sdk} from '../../src/base'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/ui/expand_record_picker_async.test.tsx b/packages/sdk/test/ui/expand_record_picker_async.test.tsx index de6e7fc38..d5fc15a69 100644 --- a/packages/sdk/test/ui/expand_record_picker_async.test.tsx +++ b/packages/sdk/test/ui/expand_record_picker_async.test.tsx @@ -1,10 +1,10 @@ import React from 'react'; import ReactDOM from 'react-dom'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {expandRecordPickerAsync, useBase, useRecords} from '../../src/ui/ui'; -import {Record} from '../../src/models/models'; -import {SdkContext} from '../../src/ui/sdk_context'; -import {__sdk as sdk} from '../../src'; +import {expandRecordPickerAsync, useBase, useRecords} from '../../src/base/ui/ui'; +import {Record} from '../../src/base/models/models'; +import {SdkContext} from '../../src/shared/ui/sdk_context'; +import {__sdk as sdk} from '../../src/base'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/ui/field_icon.test.tsx b/packages/sdk/test/ui/field_icon.test.tsx index 78775ea8e..e3b423e39 100644 --- a/packages/sdk/test/ui/field_icon.test.tsx +++ b/packages/sdk/test/ui/field_icon.test.tsx @@ -1,9 +1,9 @@ import React from 'react'; import {mount} from 'enzyme'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {FieldIcon} from '../../src/ui/ui'; -import {SdkContext} from '../../src/ui/sdk_context'; -import {__sdk as sdk} from '../../src'; +import {FieldIcon} from '../../src/base/ui/ui'; +import {SdkContext} from '../../src/shared/ui/sdk_context'; +import {__sdk as sdk} from '../../src/base'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/ui/field_picker.test.tsx b/packages/sdk/test/ui/field_picker.test.tsx index 4f10ec179..4ee89d380 100644 --- a/packages/sdk/test/ui/field_picker.test.tsx +++ b/packages/sdk/test/ui/field_picker.test.tsx @@ -1,9 +1,9 @@ import React from 'react'; import {mount} from 'enzyme'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {FieldPicker} from '../../src/ui/ui'; -import {SdkContext} from '../../src/ui/sdk_context'; -import {__sdk as sdk} from '../../src'; +import {FieldPicker} from '../../src/base/ui/ui'; +import {SdkContext} from '../../src/shared/ui/sdk_context'; +import {__sdk as sdk} from '../../src/base'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/ui/field_picker_synced.test.tsx b/packages/sdk/test/ui/field_picker_synced.test.tsx index 40d0b4f49..7bf8bec81 100644 --- a/packages/sdk/test/ui/field_picker_synced.test.tsx +++ b/packages/sdk/test/ui/field_picker_synced.test.tsx @@ -1,9 +1,9 @@ import React from 'react'; import {mount} from 'enzyme'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {FieldPickerSynced} from '../../src/ui/ui'; -import {SdkContext} from '../../src/ui/sdk_context'; -import {__sdk as sdk} from '../../src'; +import {FieldPickerSynced} from '../../src/base/ui/ui'; +import {SdkContext} from '../../src/shared/ui/sdk_context'; +import {__sdk as sdk} from '../../src/base'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/ui/form_field.test.tsx b/packages/sdk/test/ui/form_field.test.tsx index 12b58a339..9d592d0ca 100644 --- a/packages/sdk/test/ui/form_field.test.tsx +++ b/packages/sdk/test/ui/form_field.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {mount, render} from 'enzyme'; -import {FormField, Select, Input} from '../../src/ui/unstable_standalone_ui'; +import {FormField, Select, Input} from '../../src/base/ui/unstable_standalone_ui'; describe('FormField', () => { it('renders outside of a blocks context', () => { diff --git a/packages/sdk/test/ui/global_alert.test.tsx b/packages/sdk/test/ui/global_alert.test.tsx index 59c234362..52cd0ae38 100644 --- a/packages/sdk/test/ui/global_alert.test.tsx +++ b/packages/sdk/test/ui/global_alert.test.tsx @@ -1,9 +1,9 @@ import React from 'react'; import {mount} from 'enzyme'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {GlobalAlert} from '../../src/ui/ui'; -import {SdkContext} from '../../src/ui/sdk_context'; -import {__sdk as sdk} from '../../src'; +import {GlobalAlert} from '../../src/base/ui/ui'; +import {SdkContext} from '../../src/shared/ui/sdk_context'; +import {__sdk as sdk} from '../../src/base'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/ui/heading.test.tsx b/packages/sdk/test/ui/heading.test.tsx index eaa9d3b91..ca8505f9f 100644 --- a/packages/sdk/test/ui/heading.test.tsx +++ b/packages/sdk/test/ui/heading.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {mount} from 'enzyme'; -import {Heading} from '../../src/ui/unstable_standalone_ui'; +import {Heading} from '../../src/base/ui/unstable_standalone_ui'; describe('Heading', () => { it('renders outside of a blocks context', () => { diff --git a/packages/sdk/test/ui/icon.test.tsx b/packages/sdk/test/ui/icon.test.tsx index 32d553abb..621ddfa38 100644 --- a/packages/sdk/test/ui/icon.test.tsx +++ b/packages/sdk/test/ui/icon.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {mount} from 'enzyme'; -import {Icon} from '../../src/ui/unstable_standalone_ui'; +import {Icon} from '../../src/base/ui/unstable_standalone_ui'; describe('Icon', () => { it('renders outside of a blocks context', () => { diff --git a/packages/sdk/test/ui/initialize_block.test.tsx b/packages/sdk/test/ui/initialize_block.test.tsx index 858f97f68..c5eab835b 100644 --- a/packages/sdk/test/ui/initialize_block.test.tsx +++ b/packages/sdk/test/ui/initialize_block.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {initializeBlock} from '../../src/ui/ui'; -import {__resetHasBeenInitialized} from '../../src/ui/initialize_block'; +import {initializeBlock} from '../../src/base/ui/ui'; +import {__resetHasBeenInitialized} from '../../src/base/ui/initialize_block'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/ui/input.test.tsx b/packages/sdk/test/ui/input.test.tsx index b3593d916..df6af88d5 100644 --- a/packages/sdk/test/ui/input.test.tsx +++ b/packages/sdk/test/ui/input.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {mount} from 'enzyme'; -import {Input} from '../../src/ui/unstable_standalone_ui'; +import {Input} from '../../src/base/ui/unstable_standalone_ui'; describe('Input', () => { it('renders outside of a blocks context', () => { diff --git a/packages/sdk/test/ui/input_synced.test.tsx b/packages/sdk/test/ui/input_synced.test.tsx index 3ebabc156..508f5435a 100644 --- a/packages/sdk/test/ui/input_synced.test.tsx +++ b/packages/sdk/test/ui/input_synced.test.tsx @@ -1,9 +1,9 @@ import React from 'react'; import {mount} from 'enzyme'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {InputSynced} from '../../src/ui/ui'; -import {SdkContext} from '../../src/ui/sdk_context'; -import {__sdk as sdk} from '../../src'; +import {InputSynced} from '../../src/base/ui/ui'; +import {SdkContext} from '../../src/shared/ui/sdk_context'; +import {__sdk as sdk} from '../../src/base'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/ui/label.test.tsx b/packages/sdk/test/ui/label.test.tsx index 72aebddd6..225790e5a 100644 --- a/packages/sdk/test/ui/label.test.tsx +++ b/packages/sdk/test/ui/label.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {mount} from 'enzyme'; -import {Label} from '../../src/ui/unstable_standalone_ui'; +import {Label} from '../../src/base/ui/unstable_standalone_ui'; describe('Label', () => { it('renders outside of a blocks context', () => { diff --git a/packages/sdk/test/ui/link.test.tsx b/packages/sdk/test/ui/link.test.tsx index 87040a692..6d239247a 100644 --- a/packages/sdk/test/ui/link.test.tsx +++ b/packages/sdk/test/ui/link.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {mount} from 'enzyme'; -import {Link} from '../../src/ui/unstable_standalone_ui'; +import {Link} from '../../src/base/ui/unstable_standalone_ui'; describe('Link', () => { it('renders outside of a blocks context', () => { diff --git a/packages/sdk/test/ui/loader.test.tsx b/packages/sdk/test/ui/loader.test.tsx index 329118b3f..6498e7543 100644 --- a/packages/sdk/test/ui/loader.test.tsx +++ b/packages/sdk/test/ui/loader.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {mount} from 'enzyme'; -import {Loader} from '../../src/ui/unstable_standalone_ui'; +import {Loader} from '../../src/base/ui/unstable_standalone_ui'; describe('Loader', () => { it('renders outside of a blocks context', () => { diff --git a/packages/sdk/test/ui/modal.test.tsx b/packages/sdk/test/ui/modal.test.tsx index a6b765608..1b672b151 100644 --- a/packages/sdk/test/ui/modal.test.tsx +++ b/packages/sdk/test/ui/modal.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {mount} from 'enzyme'; -import {Modal} from '../../src/ui/unstable_standalone_ui'; +import {Modal} from '../../src/base/ui/unstable_standalone_ui'; describe('Modal', () => { it('renders outside of a blocks context', () => { diff --git a/packages/sdk/test/ui/popover.test.tsx b/packages/sdk/test/ui/popover.test.tsx index d1a226256..5f432217a 100644 --- a/packages/sdk/test/ui/popover.test.tsx +++ b/packages/sdk/test/ui/popover.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {mount} from 'enzyme'; -import {Popover} from '../../src/ui/unstable_standalone_ui'; +import {Popover} from '../../src/base/ui/unstable_standalone_ui'; describe('Popover', () => { it('renders outside of a blocks context', () => { diff --git a/packages/sdk/test/ui/progress_bar.test.tsx b/packages/sdk/test/ui/progress_bar.test.tsx index de567fa94..458d67801 100644 --- a/packages/sdk/test/ui/progress_bar.test.tsx +++ b/packages/sdk/test/ui/progress_bar.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {mount} from 'enzyme'; -import ProgressBar from '../../src/ui/progress_bar'; +import ProgressBar from '../../src/base/ui/progress_bar'; import {getComputedStylePropValue} from '../test_helpers'; describe('ProgressBar', () => { diff --git a/packages/sdk/test/ui/record_card.test.tsx b/packages/sdk/test/ui/record_card.test.tsx index f471090e1..27d0c7feb 100644 --- a/packages/sdk/test/ui/record_card.test.tsx +++ b/packages/sdk/test/ui/record_card.test.tsx @@ -1,9 +1,9 @@ import React from 'react'; import {mount} from 'enzyme'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {RecordCard} from '../../src/ui/ui'; -import {SdkContext} from '../../src/ui/sdk_context'; -import {__sdk as sdk} from '../../src'; +import {RecordCard} from '../../src/base/ui/ui'; +import {SdkContext} from '../../src/shared/ui/sdk_context'; +import {__sdk as sdk} from '../../src/base'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/ui/record_card_list.test.tsx b/packages/sdk/test/ui/record_card_list.test.tsx index 236f902f7..4cb72989c 100644 --- a/packages/sdk/test/ui/record_card_list.test.tsx +++ b/packages/sdk/test/ui/record_card_list.test.tsx @@ -1,9 +1,9 @@ import React from 'react'; import {mount} from 'enzyme'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {RecordCardList} from '../../src/ui/ui'; -import {SdkContext} from '../../src/ui/sdk_context'; -import {__sdk as sdk} from '../../src'; +import {RecordCardList} from '../../src/base/ui/ui'; +import {SdkContext} from '../../src/shared/ui/sdk_context'; +import {__sdk as sdk} from '../../src/base'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/ui/remote_utils.test.ts b/packages/sdk/test/ui/remote_utils.test.ts index 1f0634bb9..8ec8e2e19 100644 --- a/packages/sdk/test/ui/remote_utils.test.ts +++ b/packages/sdk/test/ui/remote_utils.test.ts @@ -1,4 +1,4 @@ -import * as remoteUtils from '../../src/ui/remote_utils'; +import * as remoteUtils from '../../src/shared/ui/remote_utils'; describe('remoteUtils', () => { describe('loadCSSFromString', () => { diff --git a/packages/sdk/test/ui/select.test.tsx b/packages/sdk/test/ui/select.test.tsx index b6511bf6c..33dc7b993 100644 --- a/packages/sdk/test/ui/select.test.tsx +++ b/packages/sdk/test/ui/select.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {mount} from 'enzyme'; -import {Select} from '../../src/ui/unstable_standalone_ui'; +import {Select} from '../../src/base/ui/unstable_standalone_ui'; describe('Select', () => { it('renders outside of a blocks context', () => { diff --git a/packages/sdk/test/ui/select_buttons.test.tsx b/packages/sdk/test/ui/select_buttons.test.tsx index b5856fb7a..5e04f9695 100644 --- a/packages/sdk/test/ui/select_buttons.test.tsx +++ b/packages/sdk/test/ui/select_buttons.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {mount} from 'enzyme'; -import {SelectButtons} from '../../src/ui/unstable_standalone_ui'; +import {SelectButtons} from '../../src/base/ui/unstable_standalone_ui'; describe('SelectButtons', () => { it('renders outside of a blocks context', () => { diff --git a/packages/sdk/test/ui/select_buttons_synced.test.tsx b/packages/sdk/test/ui/select_buttons_synced.test.tsx index c898e5a82..b1ed5ff36 100644 --- a/packages/sdk/test/ui/select_buttons_synced.test.tsx +++ b/packages/sdk/test/ui/select_buttons_synced.test.tsx @@ -1,9 +1,9 @@ import React from 'react'; import {mount} from 'enzyme'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {SelectButtonsSynced} from '../../src/ui/ui'; -import {SdkContext} from '../../src/ui/sdk_context'; -import {__sdk as sdk} from '../../src'; +import {SelectButtonsSynced} from '../../src/base/ui/ui'; +import {SdkContext} from '../../src/shared/ui/sdk_context'; +import {__sdk as sdk} from '../../src/base'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/ui/select_synced.test.tsx b/packages/sdk/test/ui/select_synced.test.tsx index d767de015..436eee370 100644 --- a/packages/sdk/test/ui/select_synced.test.tsx +++ b/packages/sdk/test/ui/select_synced.test.tsx @@ -1,9 +1,9 @@ import React from 'react'; import {mount} from 'enzyme'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {SelectSynced} from '../../src/ui/ui'; -import {SdkContext} from '../../src/ui/sdk_context'; -import {__sdk as sdk} from '../../src'; +import {SelectSynced} from '../../src/base/ui/ui'; +import {SdkContext} from '../../src/shared/ui/sdk_context'; +import {__sdk as sdk} from '../../src/base'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/ui/switch.test.tsx b/packages/sdk/test/ui/switch.test.tsx index 6dc255906..66704f63c 100644 --- a/packages/sdk/test/ui/switch.test.tsx +++ b/packages/sdk/test/ui/switch.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {mount} from 'enzyme'; -import {Switch} from '../../src/ui/unstable_standalone_ui'; +import {Switch} from '../../src/base/ui/unstable_standalone_ui'; describe('Switch', () => { it('renders outside of a blocks context', () => { diff --git a/packages/sdk/test/ui/switch_synced.test.tsx b/packages/sdk/test/ui/switch_synced.test.tsx index 671dc86a4..56a06af7a 100644 --- a/packages/sdk/test/ui/switch_synced.test.tsx +++ b/packages/sdk/test/ui/switch_synced.test.tsx @@ -1,9 +1,9 @@ import React from 'react'; import {mount} from 'enzyme'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {SwitchSynced} from '../../src/ui/ui'; -import {SdkContext} from '../../src/ui/sdk_context'; -import {__sdk as sdk} from '../../src'; +import {SwitchSynced} from '../../src/base/ui/ui'; +import {SdkContext} from '../../src/shared/ui/sdk_context'; +import {__sdk as sdk} from '../../src/base'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/ui/synced.test.tsx b/packages/sdk/test/ui/synced.test.tsx index 7c551baeb..9088794cb 100644 --- a/packages/sdk/test/ui/synced.test.tsx +++ b/packages/sdk/test/ui/synced.test.tsx @@ -1,10 +1,10 @@ import React from 'react'; import {mount} from 'enzyme'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {Synced} from '../../src/ui/ui'; -import {SdkContext} from '../../src/ui/sdk_context'; -import {GlobalConfigUpdate} from '../../src/types/global_config'; -import {__sdk as sdk} from '../../src'; +import {Synced} from '../../src/base/ui/ui'; +import {SdkContext} from '../../src/shared/ui/sdk_context'; +import {GlobalConfigUpdate} from '../../src/shared/types/global_config'; +import {__sdk as sdk} from '../../src/base'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/ui/table_picker.test.tsx b/packages/sdk/test/ui/table_picker.test.tsx index 084f50387..c4bb324a8 100644 --- a/packages/sdk/test/ui/table_picker.test.tsx +++ b/packages/sdk/test/ui/table_picker.test.tsx @@ -1,9 +1,9 @@ import React from 'react'; import {mount} from 'enzyme'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {TablePicker} from '../../src/ui/ui'; -import {SdkContext} from '../../src/ui/sdk_context'; -import {__sdk as sdk} from '../../src'; +import {TablePicker} from '../../src/base/ui/ui'; +import {SdkContext} from '../../src/shared/ui/sdk_context'; +import {__sdk as sdk} from '../../src/base'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/ui/table_picker_synced.test.tsx b/packages/sdk/test/ui/table_picker_synced.test.tsx index db63d5816..db6baddca 100644 --- a/packages/sdk/test/ui/table_picker_synced.test.tsx +++ b/packages/sdk/test/ui/table_picker_synced.test.tsx @@ -1,9 +1,9 @@ import React from 'react'; import {mount} from 'enzyme'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {TablePickerSynced} from '../../src/ui/ui'; -import {SdkContext} from '../../src/ui/sdk_context'; -import {__sdk as sdk} from '../../src'; +import {TablePickerSynced} from '../../src/base/ui/ui'; +import {SdkContext} from '../../src/shared/ui/sdk_context'; +import {__sdk as sdk} from '../../src/base'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/ui/text.test.tsx b/packages/sdk/test/ui/text.test.tsx index 0da9bb8e0..dbfa1b3e3 100644 --- a/packages/sdk/test/ui/text.test.tsx +++ b/packages/sdk/test/ui/text.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {mount} from 'enzyme'; -import {Text} from '../../src/ui/unstable_standalone_ui'; +import {Text} from '../../src/base/ui/unstable_standalone_ui'; describe('Text', () => { it('renders outside of a blocks context', () => { diff --git a/packages/sdk/test/ui/text_button.test.tsx b/packages/sdk/test/ui/text_button.test.tsx index 0efde987f..da89049bd 100644 --- a/packages/sdk/test/ui/text_button.test.tsx +++ b/packages/sdk/test/ui/text_button.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {mount} from 'enzyme'; -import {TextButton} from '../../src/ui/unstable_standalone_ui'; +import {TextButton} from '../../src/base/ui/unstable_standalone_ui'; describe('TextButton', () => { it('renders outside of a blocks context', () => { diff --git a/packages/sdk/test/ui/tooltip.test.tsx b/packages/sdk/test/ui/tooltip.test.tsx index 9207cc9da..9a3b412bc 100644 --- a/packages/sdk/test/ui/tooltip.test.tsx +++ b/packages/sdk/test/ui/tooltip.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {mount} from 'enzyme'; -import {Tooltip, Text} from '../../src/ui/unstable_standalone_ui'; +import {Tooltip, Text} from '../../src/base/ui/unstable_standalone_ui'; describe('Tooltip', () => { it('renders outside of a blocks context', () => { diff --git a/packages/sdk/test/ui/ui.test.tsx b/packages/sdk/test/ui/ui.test.tsx index c3c6bbd53..f24f650f5 100644 --- a/packages/sdk/test/ui/ui.test.tsx +++ b/packages/sdk/test/ui/ui.test.tsx @@ -8,11 +8,11 @@ jest.mock('../../src/injected/airtable_interface', () => ({ })); const run = (bindingIdentifier: string) => { - const exported = require('../../src/ui/ui')[bindingIdentifier]; + const exported = require('../../src/base/ui/ui')[bindingIdentifier]; expect(exported).toBeTruthy(); - expect(exported).toBe(require('../../src/ui/unstable_standalone_ui')[bindingIdentifier]); + expect(exported).toBe(require('../../src/base/ui/unstable_standalone_ui')[bindingIdentifier]); }; describe('ui entry point', () => { diff --git a/packages/sdk/test/ui/unstable_standalone_ui.test.tsx b/packages/sdk/test/ui/unstable_standalone_ui.test.tsx index a2b3d8613..a9d2f3751 100644 --- a/packages/sdk/test/ui/unstable_standalone_ui.test.tsx +++ b/packages/sdk/test/ui/unstable_standalone_ui.test.tsx @@ -1,5 +1,5 @@ describe('unstable_standalone_ui', () => { it('can be imported outside of a blocks context', () => { - require('../../src/ui/unstable_standalone_ui'); + require('../../src/base/ui/unstable_standalone_ui'); }); }); diff --git a/packages/sdk/test/ui/use_array_identity.test.tsx b/packages/sdk/test/ui/use_array_identity.test.tsx index 4b21daa86..868ed2677 100644 --- a/packages/sdk/test/ui/use_array_identity.test.tsx +++ b/packages/sdk/test/ui/use_array_identity.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {mount} from 'enzyme'; import {act} from 'react-dom/test-utils'; -import useArrayIdentity from '../../src/ui/use_array_identity'; +import useArrayIdentity from '../../src/shared/ui/use_array_identity'; describe('useArrayIdentity', () => { it("returns the same array instance as long as it's passed a shallow-equal array", async () => { diff --git a/packages/sdk/test/ui/use_loadable.test.tsx b/packages/sdk/test/ui/use_loadable.test.tsx index a4c0fa5fa..5d96a4fb8 100644 --- a/packages/sdk/test/ui/use_loadable.test.tsx +++ b/packages/sdk/test/ui/use_loadable.test.tsx @@ -2,10 +2,10 @@ import React, {Suspense} from 'react'; import ReactDOM from 'react-dom'; import {mount} from 'enzyme'; import {act} from 'react-dom/test-utils'; -import AbstractModelWithAsyncData from '../../src/models/abstract_model_with_async_data'; -import useLoadable from '../../src/ui/use_loadable'; +import AbstractModelWithAsyncData from '../../src/base/models/abstract_model_with_async_data'; +import useLoadable from '../../src/shared/ui/use_loadable'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import Sdk from '../../src/sdk'; +import Sdk from '../../src/base/sdk'; jest.useFakeTimers(); diff --git a/packages/sdk/test/ui/use_record_action_data.test.tsx b/packages/sdk/test/ui/use_record_action_data.test.tsx index 387ffaa31..9743a47e5 100644 --- a/packages/sdk/test/ui/use_record_action_data.test.tsx +++ b/packages/sdk/test/ui/use_record_action_data.test.tsx @@ -3,10 +3,10 @@ import ReactDOM from 'react-dom'; import {act} from 'react-dom/test-utils'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import Sdk from '../../src/sdk'; -import useRecordActionData from '../../src/ui/use_record_action_data'; -import {RecordActionData} from '../../src/types/record_action_data'; -import {SdkContext} from '../../src/ui/sdk_context'; +import Sdk from '../../src/base/sdk'; +import useRecordActionData from '../../src/base/ui/use_record_action_data'; +import {RecordActionData} from '../../src/base/types/record_action_data'; +import {SdkContext} from '../../src/shared/ui/sdk_context'; describe('useRecordActionData', () => { let sdk: Sdk; diff --git a/packages/sdk/test/ui/use_records.test.tsx b/packages/sdk/test/ui/use_records.test.tsx index 9dd15d635..da45fd057 100644 --- a/packages/sdk/test/ui/use_records.test.tsx +++ b/packages/sdk/test/ui/use_records.test.tsx @@ -1,13 +1,13 @@ import React, {Suspense} from 'react'; import ReactDOM from 'react-dom'; import {act} from 'react-dom/test-utils'; -import {useRecords} from '../../src/ui/use_records'; +import {useRecords} from '../../src/base/ui/use_records'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import Sdk from '../../src/sdk'; -import Record from '../../src/models/record'; -import Table from '../../src/models/table'; -import {TableId} from '../../src/types/table'; +import Sdk from '../../src/base/sdk'; +import Record from '../../src/base/models/record'; +import Table from '../../src/base/models/table'; +import {TableId} from '../../src/shared/types/hyper_ids'; const mockAirtableInterface = MockAirtableInterface.linkedRecordsExample(); jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/ui/use_view_metadata.test.tsx b/packages/sdk/test/ui/use_view_metadata.test.tsx index dcd3861c6..cce784f6c 100644 --- a/packages/sdk/test/ui/use_view_metadata.test.tsx +++ b/packages/sdk/test/ui/use_view_metadata.test.tsx @@ -1,12 +1,12 @@ import React, {Suspense} from 'react'; import ReactDOM from 'react-dom'; import {act} from 'react-dom/test-utils'; -import useViewMetadata from '../../src/ui/use_view_metadata'; +import useViewMetadata from '../../src/base/ui/use_view_metadata'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import Sdk from '../../src/sdk'; -import ViewMetadataQueryResult from '../../src/models/view_metadata_query_result'; -import Table from '../../src/models/table'; +import Sdk from '../../src/base/sdk'; +import ViewMetadataQueryResult from '../../src/base/models/view_metadata_query_result'; +import Table from '../../src/base/models/table'; const mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/ui/use_watchable.test.tsx b/packages/sdk/test/ui/use_watchable.test.tsx index 3218b7902..5d8da8ce0 100644 --- a/packages/sdk/test/ui/use_watchable.test.tsx +++ b/packages/sdk/test/ui/use_watchable.test.tsx @@ -1,8 +1,8 @@ import React from 'react'; import {mount} from 'enzyme'; import {act} from 'react-dom/test-utils'; -import Watchable from '../../src/watchable'; -import useWatchable from '../../src/ui/use_watchable'; +import Watchable from '../../src/shared/watchable'; +import useWatchable from '../../src/shared/ui/use_watchable'; jest.useFakeTimers(); diff --git a/packages/sdk/test/ui/view_picker.test.tsx b/packages/sdk/test/ui/view_picker.test.tsx index a4755cc0f..0d4b2a18b 100644 --- a/packages/sdk/test/ui/view_picker.test.tsx +++ b/packages/sdk/test/ui/view_picker.test.tsx @@ -1,9 +1,9 @@ import React from 'react'; import {mount} from 'enzyme'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {ViewPicker} from '../../src/ui/ui'; -import {SdkContext} from '../../src/ui/sdk_context'; -import {__sdk as sdk} from '../../src'; +import {ViewPicker} from '../../src/base/ui/ui'; +import {SdkContext} from '../../src/shared/ui/sdk_context'; +import {__sdk as sdk} from '../../src/base'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/ui/view_picker_synced.test.tsx b/packages/sdk/test/ui/view_picker_synced.test.tsx index 0d71f748f..cbeedcbfc 100644 --- a/packages/sdk/test/ui/view_picker_synced.test.tsx +++ b/packages/sdk/test/ui/view_picker_synced.test.tsx @@ -1,9 +1,9 @@ import React from 'react'; import {mount} from 'enzyme'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {ViewPickerSynced} from '../../src/ui/ui'; -import {SdkContext} from '../../src/ui/sdk_context'; -import {__sdk as sdk} from '../../src'; +import {ViewPickerSynced} from '../../src/base/ui/ui'; +import {SdkContext} from '../../src/shared/ui/sdk_context'; +import {__sdk as sdk} from '../../src/base'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/ui/viewport_constraint.test.tsx b/packages/sdk/test/ui/viewport_constraint.test.tsx index 2558f40df..ce196f46f 100644 --- a/packages/sdk/test/ui/viewport_constraint.test.tsx +++ b/packages/sdk/test/ui/viewport_constraint.test.tsx @@ -1,9 +1,9 @@ import React from 'react'; import {mount} from 'enzyme'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {ViewportConstraint} from '../../src/ui/ui'; -import {SdkContext} from '../../src/ui/sdk_context'; -import {__sdk as sdk} from '../../src'; +import {ViewportConstraint} from '../../src/base/ui/ui'; +import {SdkContext} from '../../src/shared/ui/sdk_context'; +import {__sdk as sdk} from '../../src/base'; let mockAirtableInterface: jest.Mocked; jest.mock('../../src/injected/airtable_interface', () => ({ diff --git a/packages/sdk/test/unstable_private_utils.test.ts b/packages/sdk/test/unstable_private_utils.test.ts index 96ab172b2..c98b93534 100644 --- a/packages/sdk/test/unstable_private_utils.test.ts +++ b/packages/sdk/test/unstable_private_utils.test.ts @@ -1,5 +1,5 @@ describe('unstable_private_utils', () => { it('can be imported outside of a blocks context', () => { - require('../src/unstable_private_utils'); + require('../src/shared/unstable_private_utils'); }); }); diff --git a/packages/sdk/types.d.ts b/packages/sdk/types.d.ts index e114b1a85..2c4695776 100644 --- a/packages/sdk/types.d.ts +++ b/packages/sdk/types.d.ts @@ -1,15 +1,18 @@ -export {AttachmentId} from './dist/types/src/types/attachment'; -export {BaseId} from './dist/types/src/types/base'; -export {BlockInstallationId} from './dist/types/src/types/block'; -export {UserId, CollaboratorData} from './dist/types/src/types/collaborator'; -export {FieldId} from './dist/types/src/types/field'; -export {PermissionCheckResult} from './dist/types/src/types/mutations'; -export {RecordId} from './dist/types/src/types/record'; -export {TableId} from './dist/types/src/types/table'; -export {ViewId} from './dist/types/src/types/view'; -export {GlobalConfigValue} from './dist/types/src/types/global_config'; -export {default as GlobalConfig} from './dist/types/src/global_config'; -export {Color} from './dist/types/src/colors'; -export {RecordActionData} from './dist/types/src/types/record_action_data'; -export {default as Watchable} from './dist/types/src/watchable'; -export {default as Viewport} from './dist/types/src/viewport'; +export { + AttachmentId, + BaseId, + BlockInstallationId, + UserId, + FieldId, + RecordId, + TableId, + ViewId, +} from './dist/types/src/shared/types/hyper_ids'; +export {CollaboratorData} from './dist/types/src/shared/types/collaborator'; +export {PermissionCheckResult} from './dist/types/src/shared/types/mutations_core'; +export {GlobalConfigValue} from './dist/types/src/shared/types/global_config'; +export {default as GlobalConfig} from './dist/types/src/shared/global_config'; +export {Color} from './dist/types/src/shared/colors'; +export {RecordActionData} from './dist/types/src/base/types/record_action_data'; +export {default as Watchable} from './dist/types/src/shared/watchable'; +export {default as Viewport} from './dist/types/src/base/viewport'; diff --git a/packages/sdk/ui.d.ts b/packages/sdk/ui.d.ts index a9f450521..ee2aad69c 100644 --- a/packages/sdk/ui.d.ts +++ b/packages/sdk/ui.d.ts @@ -1 +1 @@ -export * from './dist/types/src/ui/ui'; +export * from './dist/types/src/base/ui/ui'; diff --git a/packages/sdk/ui.js b/packages/sdk/ui.js index 38ba19199..b69ed851d 100644 --- a/packages/sdk/ui.js +++ b/packages/sdk/ui.js @@ -1 +1 @@ -module.exports = require('./dist/cjs/ui/ui'); +module.exports = require('./dist/cjs/base/ui/ui'); diff --git a/packages/sdk/unstable_private_utils.d.ts b/packages/sdk/unstable_private_utils.d.ts index 96b35a0d2..9df599283 100644 --- a/packages/sdk/unstable_private_utils.d.ts +++ b/packages/sdk/unstable_private_utils.d.ts @@ -1 +1 @@ -export * from './dist/types/src/unstable_private_utils'; +export * from './dist/types/src/shared/unstable_private_utils'; diff --git a/packages/sdk/unstable_private_utils.js b/packages/sdk/unstable_private_utils.js index ae334f801..ac4f56a29 100644 --- a/packages/sdk/unstable_private_utils.js +++ b/packages/sdk/unstable_private_utils.js @@ -1 +1 @@ -module.exports = require('./dist/cjs/unstable_private_utils'); +module.exports = require('./dist/cjs/shared/unstable_private_utils'); diff --git a/packages/sdk/unstable_standalone_ui.d.ts b/packages/sdk/unstable_standalone_ui.d.ts index 7c58289ed..434e5432b 100644 --- a/packages/sdk/unstable_standalone_ui.d.ts +++ b/packages/sdk/unstable_standalone_ui.d.ts @@ -1 +1 @@ -export * from './dist/types/src/ui/unstable_standalone_ui'; +export * from './dist/types/src/base/ui/unstable_standalone_ui'; diff --git a/packages/sdk/unstable_standalone_ui.js b/packages/sdk/unstable_standalone_ui.js index fe2e95e74..12ca04efb 100644 --- a/packages/sdk/unstable_standalone_ui.js +++ b/packages/sdk/unstable_standalone_ui.js @@ -1 +1 @@ -module.exports = require('./dist/cjs/ui/unstable_standalone_ui'); +module.exports = require('./dist/cjs/base/ui/unstable_standalone_ui'); diff --git a/packages/sdk/unstable_testing_utils.d.ts b/packages/sdk/unstable_testing_utils.d.ts index 5790d04f9..fe483d6b4 100644 --- a/packages/sdk/unstable_testing_utils.d.ts +++ b/packages/sdk/unstable_testing_utils.d.ts @@ -1 +1 @@ -export * from './dist/types/src/unstable_testing_utils'; +export * from './dist/types/src/base/unstable_testing_utils'; diff --git a/packages/sdk/unstable_testing_utils.js b/packages/sdk/unstable_testing_utils.js index d4ff1ab02..eeedbf9de 100644 --- a/packages/sdk/unstable_testing_utils.js +++ b/packages/sdk/unstable_testing_utils.js @@ -1 +1 @@ -module.exports = require('./dist/cjs/unstable_testing_utils'); +module.exports = require('./dist/cjs/base/unstable_testing_utils'); diff --git a/yarn.lock b/yarn.lock index d83aa7810..7197e4319 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8562,6 +8562,11 @@ eslint-plugin-react-hooks@^4.6.2: resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz#c829eb06c0e6f484b3fbb85a97e57784f328c596" integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ== +eslint-plugin-react-hooks@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz#1be0080901e6ac31ce7971beed3d3ec0a423d9e3" + integrity sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg== + eslint-plugin-react@^7.34.2: version "7.34.3" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.34.3.tgz#9965f27bd1250a787b5d4cfcc765e5a5d58dcb7b" @@ -17843,9 +17848,9 @@ tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== tar-fs@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" - integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== + version "2.1.2" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.2.tgz#425f154f3404cb16cb8ff6e671d45ab2ed9596c5" + integrity sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA== dependencies: chownr "^1.1.1" mkdirp-classic "^0.5.2" @@ -18172,10 +18177,10 @@ trim-trailing-lines@^1.0.0: resolved "https://registry.yarnpkg.com/trim-trailing-lines/-/trim-trailing-lines-1.1.4.tgz#bd4abbec7cc880462f10b2c8b5ce1d8d1ec7c2c0" integrity sha512-rjUWSqnfTNrjbB9NQWfPMH/xRK1deHeGsHoVfpxJ++XeYXE0d6B1En37AHfw3jtfTU7dzMzZL2jjpe8Qb5gLIQ== -trim@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd" - integrity sha512-YzQV+TZg4AxpKxaTHK3c3D+kRDCGVEE7LemdlQZoQXn0iennk10RsIoY6ikzAqJTc9Xjl9C1/waHom/J86ziAQ== +trim@0.0.1, trim@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/trim/-/trim-1.0.1.tgz#68e78f6178ccab9687a610752f4f5e5a7022ee8c" + integrity sha512-3JVP2YVqITUisXblCDq/Bi4P9457G/sdEamInkyvCsjbTcXLXIiG7XCb4kGMFWh6JGXesS3TKxOPtrncN/xe8w== trough@^1.0.0: version "1.0.5" From 5634d60a5fbe0029e7c52b43fd5bb42a4b6efe91 Mon Sep 17 00:00:00 2001 From: Airtable Date: Thu, 1 May 2025 21:43:20 +0000 Subject: [PATCH 02/14] @airtable/blocks@0.0.0-experimental-d5ee5e823-20250501 --- .prettierignore | 3 +- packages/sdk/package.json | 26 +++--- ...heck_typescript_when_installed_in_block.sh | 9 +- packages/sdk/src/base/models/base.ts | 2 +- .../sdk/src/base/models/create_aggregators.ts | 2 +- packages/sdk/src/base/models/cursor.ts | 4 +- packages/sdk/src/base/models/field.ts | 4 +- .../sdk/src/base/models/record_coloring.ts | 4 +- .../src/base/models/record_query_result.ts | 2 +- packages/sdk/src/base/models/session.ts | 8 +- packages/sdk/src/base/models/table.ts | 4 +- packages/sdk/src/base/models/view.ts | 2 +- .../sdk/src/base/perform_record_action.ts | 2 +- packages/sdk/src/base/settings_button.ts | 4 +- packages/sdk/src/base/types/view.ts | 2 +- packages/sdk/src/base/ui/base_provider.tsx | 2 +- packages/sdk/src/base/ui/expand_record.ts | 2 +- .../sdk/src/base/ui/expand_record_list.ts | 2 +- .../src/base/ui/expand_record_picker_async.ts | 2 +- packages/sdk/src/base/ui/global_alert.tsx | 2 +- packages/sdk/src/base/ui/initialize_block.tsx | 14 ++- packages/sdk/src/base/ui/progress_bar.tsx | 2 +- .../src/base/ui/types/tooltip_anchor_props.ts | 1 + packages/sdk/src/base/ui/ui.ts | 2 +- packages/sdk/src/base/ui/use_base.ts | 2 +- packages/sdk/src/base/ui/use_cursor.ts | 2 +- .../src/{shared => base}/ui/use_loadable.ts | 12 +-- .../sdk/src/base/ui/use_record_action_data.ts | 4 +- packages/sdk/src/base/ui/use_records.ts | 10 +-- packages/sdk/src/base/ui/use_session.ts | 2 +- .../sdk/src/base/ui/use_settings_button.ts | 2 +- packages/sdk/src/base/ui/use_view_metadata.ts | 4 +- packages/sdk/src/base/ui/use_viewport.ts | 2 +- .../sdk/src/base/ui/viewport_constraint.tsx | 4 +- packages/sdk/src/base/viewport.ts | 2 +- packages/sdk/src/interface/models/models.ts | 7 ++ packages/sdk/src/interface/sdk.ts | 3 - .../sdk/src/interface/ui/expand_record.ts | 3 +- .../sdk/src/interface/ui/initialize_block.tsx | 9 +- packages/sdk/src/interface/ui/ui.ts | 6 +- .../src/interface/ui/use_custom_properties.ts | 57 +++++++++++-- packages/sdk/src/shared/color_utils.ts | 6 +- packages/sdk/src/shared/colors.ts | 4 +- packages/sdk/src/shared/global_config.ts | 85 ++++++++++--------- packages/sdk/src/shared/models/base_core.ts | 45 +++++++--- packages/sdk/src/shared/models/field_core.ts | 2 +- packages/sdk/src/shared/models/record_core.ts | 2 +- .../sdk/src/shared/models/session_core.ts | 2 +- packages/sdk/src/shared/models/table_core.ts | 2 +- packages/sdk/src/shared/private_utils.ts | 2 +- packages/sdk/src/shared/sdk_core.ts | 8 +- packages/sdk/src/shared/types/field.ts | 2 +- packages/sdk/src/shared/ui/remote_utils.ts | 6 +- .../sdk/src/shared/ui/use_global_config.ts | 21 +++-- packages/sdk/src/shared/ui/use_synced.ts | 2 +- packages/sdk/src/shared/ui/use_watchable.ts | 25 +----- packages/sdk/src/shared/ui/with_hooks.tsx | 6 +- packages/sdk/stories/box/box.stories.tsx | 2 +- packages/sdk/stories/button.stories.tsx | 2 +- .../sdk/stories/cell_renderer.stories.tsx | 2 +- packages/sdk/stories/choice_token.stories.tsx | 2 +- .../stories/collaborator_token.stories.tsx | 2 +- .../sdk/stories/color_palette.stories.tsx | 4 +- .../stories/confirmation_dialog.stories.tsx | 2 +- packages/sdk/stories/dialog.stories.tsx | 2 +- packages/sdk/stories/field_icon.stories.tsx | 2 +- packages/sdk/stories/field_picker.stories.tsx | 4 +- packages/sdk/stories/form_field.stories.tsx | 2 +- packages/sdk/stories/heading.stories.tsx | 2 +- packages/sdk/stories/icon_example.tsx | 2 +- packages/sdk/stories/input.stories.tsx | 4 +- packages/sdk/stories/label.stories.tsx | 2 +- packages/sdk/stories/link.stories.tsx | 2 +- packages/sdk/stories/loader.stories.tsx | 2 +- packages/sdk/stories/progress_bar.stories.tsx | 2 +- packages/sdk/stories/record_card.stories.tsx | 2 +- .../sdk/stories/record_card_list.stories.tsx | 2 +- packages/sdk/stories/select.stories.tsx | 4 +- .../sdk/stories/select_buttons.stories.tsx | 4 +- packages/sdk/stories/switch.stories.tsx | 4 +- packages/sdk/stories/table_picker.stories.tsx | 4 +- packages/sdk/stories/text.stories.tsx | 2 +- packages/sdk/stories/text_button.stories.tsx | 2 +- packages/sdk/stories/tooltip.stories.tsx | 2 +- packages/sdk/stories/view_picker.stories.tsx | 4 +- packages/sdk/test/ui/use_loadable.test.tsx | 2 +- packages/sdk/ui.d.ts | 1 - packages/sdk/ui.js | 1 - packages/sdk/unstable_private_utils.d.ts | 1 - packages/sdk/unstable_private_utils.js | 1 - packages/sdk/unstable_standalone_ui.d.ts | 1 - packages/sdk/unstable_standalone_ui.js | 1 - packages/sdk/unstable_testing_utils.d.ts | 1 - packages/sdk/unstable_testing_utils.js | 1 - yarn.lock | 4 +- 95 files changed, 313 insertions(+), 231 deletions(-) rename packages/sdk/src/{shared => base}/ui/use_loadable.ts (95%) create mode 100644 packages/sdk/src/interface/models/models.ts delete mode 100644 packages/sdk/ui.d.ts delete mode 100644 packages/sdk/ui.js delete mode 100644 packages/sdk/unstable_private_utils.d.ts delete mode 100644 packages/sdk/unstable_private_utils.js delete mode 100644 packages/sdk/unstable_standalone_ui.d.ts delete mode 100644 packages/sdk/unstable_standalone_ui.js delete mode 100644 packages/sdk/unstable_testing_utils.d.ts delete mode 100644 packages/sdk/unstable_testing_utils.js diff --git a/.prettierignore b/.prettierignore index 322f73c6d..50da5e4ba 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,6 +6,7 @@ /docs/.cache/ /docs/public/ /examples/*/build -/packages/blocks-docs/docs.json +/packages/blocks-docs/docs-base-blocks.json +/packages/blocks-docs/docs-interface-blocks.json /packages/cli-next/test/fixtures/ /packages/webpack-bundler/test/fixtures/ diff --git a/packages/sdk/package.json b/packages/sdk/package.json index a3458b377..c11116017 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -12,15 +12,15 @@ "types": "./dist/types/src/shared/unstable_private_utils.d.ts", "default": "./dist/cjs/shared/unstable_private_utils.js" }, - "./unstable_standalone_ui": { - "types": "./dist/types/src/base/ui/unstable_standalone_ui.d.ts", - "default": "./dist/cjs/base/ui/unstable_standalone_ui.js" - }, "./unstable_testing_utils": { "types": "./dist/types/src/base/unstable_testing_utils.d.ts", "default": "./dist/cjs/base/unstable_testing_utils.js" }, - "./types": { + "./base/unstable_standalone_ui": { + "types": "./dist/types/src/base/ui/unstable_standalone_ui.d.ts", + "default": "./dist/cjs/base/ui/unstable_standalone_ui.js" + }, + "./base/types": { "types": "./types.d.ts", "default": "./types.js" }, @@ -36,6 +36,10 @@ "types": "./dist/types/src/base/index.d.ts", "default": "./dist/cjs/base/index.js" }, + "./interface/models": { + "types": "./dist/types/src/interface/models/models.d.ts", + "default": "./dist/cjs/interface/models/models.js" + }, "./interface/ui": { "types": "./dist/types/src/interface/ui/ui.d.ts", "default": "./dist/cjs/interface/ui/ui.js" @@ -112,9 +116,12 @@ "@types/lodash.capitalize": "^4.2.9", "@types/lodash.clamp": "^4.0.9", "@types/prettier": "^1.19.0", - "@types/react-dom": "^16.9.24", + "@types/prop-types": "^15.7.12", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", "@types/react-window": "^1.8.8", "@types/styled-system": "^5.1.4", + "@types/styled-system__core": "^5.1.6", "@typescript-eslint/eslint-plugin": "^7.13.0", "@typescript-eslint/parser": "^7.13.0", "babel-eslint": "^11.0.0-beta.0", @@ -141,9 +148,6 @@ "dependencies": { "@babel/runtime": "^7.7.6", "@styled-system/core": "^5.1.2", - "@types/prop-types": "^15.7.12", - "@types/react": "^16.14.60", - "@types/styled-system__core": "^5.1.6", "core-js": "^3.4.8", "emotion": "^10.0.23", "fast-deep-equal": "^3.1.1", @@ -153,8 +157,8 @@ "use-subscription": "^1.3.0" }, "peerDependencies": { - "react": "^16.14.0 || ^17.0.0", - "react-dom": "^16.9.24 || ^17.0.0" + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.9.24 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "jest": { "setupFiles": [ diff --git a/packages/sdk/scripts/check_typescript_when_installed_in_block.sh b/packages/sdk/scripts/check_typescript_when_installed_in_block.sh index d61311eff..a01411881 100755 --- a/packages/sdk/scripts/check_typescript_when_installed_in_block.sh +++ b/packages/sdk/scripts/check_typescript_when_installed_in_block.sh @@ -11,7 +11,8 @@ cd "$work_dir" cat - > tsconfig.json <<'EOF' { "compilerOptions": { - "module": "commonjs", + "module": "NodeNext", + "moduleResolution": "nodenext", "target": "es2018", "allowSyntheticDefaultImports": true }, @@ -29,9 +30,9 @@ cat - > package.json < source.ts <<'EOF' -import * as sdk from '@airtable/blocks'; -import * as ui from '@airtable/blocks/ui'; -import {Box} from '@airtable/blocks/unstable_standalone_ui'; +import * as sdk from '@airtable/blocks/base'; +import * as ui from '@airtable/blocks/base/ui'; +import {Box} from '@airtable/blocks/base/unstable_standalone_ui'; console.log(sdk); EOF diff --git a/packages/sdk/src/base/models/base.ts b/packages/sdk/src/base/models/base.ts index 32eedfe72..46d6ef50c 100644 --- a/packages/sdk/src/base/models/base.ts +++ b/packages/sdk/src/base/models/base.ts @@ -20,7 +20,7 @@ import Table from './table'; * * @example * ```js - * import {base} from '@airtable/blocks'; + * import {base} from '@airtable/blocks/base'; * * console.log('The name of your base is', base.name); * ``` diff --git a/packages/sdk/src/base/models/create_aggregators.ts b/packages/sdk/src/base/models/create_aggregators.ts index fa8d170df..01a4ad4e0 100644 --- a/packages/sdk/src/base/models/create_aggregators.ts +++ b/packages/sdk/src/base/models/create_aggregators.ts @@ -10,7 +10,7 @@ import Field from './field'; * * @example * ```js - * import {aggregators} from '@airtable/blocks/models'; + * import {aggregators} from '@airtable/blocks/base/models'; * * // To get a list of aggregators supported for a specific field: * const fieldAggregators = myField.availableAggregators; diff --git a/packages/sdk/src/base/models/cursor.ts b/packages/sdk/src/base/models/cursor.ts index b223b9679..7ac2a0817 100644 --- a/packages/sdk/src/base/models/cursor.ts +++ b/packages/sdk/src/base/models/cursor.ts @@ -44,7 +44,7 @@ interface CursorData { * {@link useLoadable} to access them. * * ```js - * import {useCursor, useWatchable} from '@airtable/blocks/ui'; + * import {useCursor, useWatchable} from '@airtable/blocks/base/ui'; * * function ActiveTableAndView() { * const cursor = useCursor(); @@ -60,7 +60,7 @@ interface CursorData { * ``` * * ```js - * import {useCursor, useLoadable, useWatchable} from '@airtable/blocks/ui'; + * import {useCursor, useLoadable, useWatchable} from '@airtable/blocks/base/ui'; * * function SelectedRecordAndFieldIds() { * const cursor = useCursor(); diff --git a/packages/sdk/src/base/models/field.ts b/packages/sdk/src/base/models/field.ts index 61f4e3b80..c481a4b30 100644 --- a/packages/sdk/src/base/models/field.ts +++ b/packages/sdk/src/base/models/field.ts @@ -13,7 +13,7 @@ import {Aggregator} from './create_aggregators'; * * @example * ```js - * import {base} from '@airtable/blocks'; + * import {base} from '@airtable/blocks/base'; * * const table = base.getTableByName('Table 1'); * const field = table.getFieldByName('Name'); @@ -50,7 +50,7 @@ class Field extends FieldCore { * @param aggregator The aggregator object or aggregator key. * @example * ```js - * import {aggregators} from '@airtable/blocks/models'; + * import {aggregators} from '@airtable/blocks/base/models'; * const aggregator = aggregators.totalAttachmentSize; * * // Using an aggregator object diff --git a/packages/sdk/src/base/models/record_coloring.ts b/packages/sdk/src/base/models/record_coloring.ts index c0a70bf06..370bf3df3 100644 --- a/packages/sdk/src/base/models/record_coloring.ts +++ b/packages/sdk/src/base/models/record_coloring.ts @@ -73,8 +73,8 @@ export const serialize = (mode: RecordColorMode) => { * @alias recordColoring.modes * @example * ```js - * import {recordColoring} from '@airtable/blocks/models'; - * import {useRecords} from '@airtable/blocks/ui'; + * import {recordColoring} from '@airtable/blocks/base/models'; + * import {useRecords} from '@airtable/blocks/base/ui'; * * // no record coloring: * const recordColorMode = recordColoring.modes.none(); diff --git a/packages/sdk/src/base/models/record_query_result.ts b/packages/sdk/src/base/models/record_query_result.ts index 0c640b0d7..5de449fef 100644 --- a/packages/sdk/src/base/models/record_query_result.ts +++ b/packages/sdk/src/base/models/record_query_result.ts @@ -139,7 +139,7 @@ export interface ViewMetadataForUpdate { * supported record color modes: none, by a view, and by a select field. * * ```js - * import {recordColoring} from '@airtable/blocks/models'; + * import {recordColoring} from '@airtable/blocks/base/models'; * // No record coloring: * const opts = { * recordColorMode: recordColoring.modes.none(), diff --git a/packages/sdk/src/base/models/session.ts b/packages/sdk/src/base/models/session.ts index 92193baa0..3cace6742 100644 --- a/packages/sdk/src/base/models/session.ts +++ b/packages/sdk/src/base/models/session.ts @@ -8,7 +8,7 @@ import {MutationTypes} from '../types/mutations'; * * @example * ```js - * import {useSession} from '@airtable/blocks/ui'; + * import {useSession} from '@airtable/blocks/base/ui'; * * function Username() { * const session = useSession(); @@ -35,7 +35,7 @@ class Session extends SessionCore { * * @example * ```js - * import {useSession} from '@airtable/blocks/ui'; + * import {useSession} from '@airtable/blocks/base/ui'; * * function UpdateButton({onClick}) { * const session = useSession(); @@ -76,7 +76,7 @@ class Session extends SessionCore { * * @example * ```js - * import {useSession} from '@airtable/blocks/ui'; + * import {useSession} from '@airtable/blocks/base/ui'; * * function CreateButton({onClick}) { * const session = useSession(); @@ -117,7 +117,7 @@ class Session extends SessionCore { * * @example * ```js - * import {useSession} from '@airtable/blocks/ui'; + * import {useSession} from '@airtable/blocks/base/ui'; * * function DeleteButton({onClick}) { * const session = useSession(); diff --git a/packages/sdk/src/base/models/table.ts b/packages/sdk/src/base/models/table.ts index 8cc9fc9b6..3ebd2adef 100644 --- a/packages/sdk/src/base/models/table.ts +++ b/packages/sdk/src/base/models/table.ts @@ -251,7 +251,7 @@ class Table extends TableCore { * @param opts Options for the query, such as sorts and fields. * @example * ```js - * import {useBase, useRecords} from '@airtable/blocks/ui'; + * import {useBase, useRecords} from '@airtable/blocks/base/ui'; * import React from 'react'; * * function TodoList() { @@ -316,7 +316,7 @@ class Table extends TableCore { * correct type, that view will be returned before checking the other views in the table. * @example * ```js - * import {ViewType} from '@airtable/blocks/models'; + * import {ViewType} from '@airtable/blocks/base/models'; * const firstCalendarView = myTable.getFirstViewOfType(ViewType.CALENDAR); * if (firstCalendarView !== null) { * console.log(firstCalendarView.name); diff --git a/packages/sdk/src/base/models/view.ts b/packages/sdk/src/base/models/view.ts index a87a89c8f..0715a4f03 100644 --- a/packages/sdk/src/base/models/view.ts +++ b/packages/sdk/src/base/models/view.ts @@ -146,7 +146,7 @@ class View extends AbstractModel { * default, records will be coloured according to the view. * @example * ```js - * import {useBase, useRecords} from '@airtable/blocks/UI'; + * import {useBase, useRecords} from '@airtable/blocks/base/ui'; * import React from 'react'; * * function TodoList() { diff --git a/packages/sdk/src/base/perform_record_action.ts b/packages/sdk/src/base/perform_record_action.ts index 7329d4b3e..b799b8e49 100644 --- a/packages/sdk/src/base/perform_record_action.ts +++ b/packages/sdk/src/base/perform_record_action.ts @@ -164,7 +164,7 @@ export class PerformRecordAction extends AbstractModelWithAsyncData< * @example * ```js * import React, {useEffect, useState} from 'react'; - * import {registerRecordActionDataCallback} from '@airtable/blocks/ui'; + * import {registerRecordActionDataCallback} from '@airtable/blocks/base/ui'; * * function LatestRecordAction() { * const [recordActionData, setRecordActionData] = useState(null); diff --git a/packages/sdk/src/base/settings_button.ts b/packages/sdk/src/base/settings_button.ts index 40c343712..93375d21f 100644 --- a/packages/sdk/src/base/settings_button.ts +++ b/packages/sdk/src/base/settings_button.ts @@ -25,8 +25,8 @@ type WatchableSettingsButtonKey = ObjectValues { diff --git a/packages/sdk/src/base/types/view.ts b/packages/sdk/src/base/types/view.ts index bf70d4a16..48b06547e 100644 --- a/packages/sdk/src/base/types/view.ts +++ b/packages/sdk/src/base/types/view.ts @@ -8,7 +8,7 @@ import {FieldId, RecordId, ViewId} from '../../shared/types/hyper_ids'; * * @example * ```js - * import {ViewType} from '@airtable/blocks/models'; + * import {ViewType} from '@airtable/blocks/base/models'; * const gridViews = myTable.views.filter(view => ( * view.type === ViewType.GRID * )); diff --git a/packages/sdk/src/base/ui/base_provider.tsx b/packages/sdk/src/base/ui/base_provider.tsx index e1b7db67d..43699e5ab 100644 --- a/packages/sdk/src/base/ui/base_provider.tsx +++ b/packages/sdk/src/base/ui/base_provider.tsx @@ -22,7 +22,7 @@ interface BaseProviderProps { * ```js * import React from 'react'; * import ReactDOM from 'react-dom'; - * import {BaseProvider} from '@airtable/blocks/ui'; + * import {BaseProvider} from '@airtable/blocks/base/ui'; * * function getHtmlStringForRecordCard(base, record) { * return ReactDOM.renderToStaticMarkup( diff --git a/packages/sdk/src/base/ui/expand_record.ts b/packages/sdk/src/base/ui/expand_record.ts index d4ca78444..0ee837ecd 100644 --- a/packages/sdk/src/base/ui/expand_record.ts +++ b/packages/sdk/src/base/ui/expand_record.ts @@ -17,7 +17,7 @@ export interface ExpandRecordOpts { * * @example * ```js - * import {expandRecord} from '@airtable/blocks/ui'; + * import {expandRecord} from '@airtable/blocks/base/ui'; * expandRecord(record1, { * records: [record1, record2, record3], * }); diff --git a/packages/sdk/src/base/ui/expand_record_list.ts b/packages/sdk/src/base/ui/expand_record_list.ts index 441a0547c..4cfc3c060 100644 --- a/packages/sdk/src/base/ui/expand_record_list.ts +++ b/packages/sdk/src/base/ui/expand_record_list.ts @@ -19,7 +19,7 @@ interface ExpandRecordListOpts { * * @example * ```js - * import {expandRecordList} from '@airtable/blocks/ui'; + * import {expandRecordList} from '@airtable/blocks/base/ui'; * expandRecordList([record1, record2, record3]); * * expandRecordList([record1, record2], { diff --git a/packages/sdk/src/base/ui/expand_record_picker_async.ts b/packages/sdk/src/base/ui/expand_record_picker_async.ts index d55d34131..910249c01 100644 --- a/packages/sdk/src/base/ui/expand_record_picker_async.ts +++ b/packages/sdk/src/base/ui/expand_record_picker_async.ts @@ -27,7 +27,7 @@ interface ExpandRecordPickerOpts { * @param opts An optional options object. * @example * ```js - * import {expandRecordPickerAsync} from '@airtable/blocks/ui'; + * import {expandRecordPickerAsync} from '@airtable/blocks/base/ui'; * * async function pickRecordsAsync() { * const recordA = await expandRecordPickerAsync([record1, record2, record3]); diff --git a/packages/sdk/src/base/ui/global_alert.tsx b/packages/sdk/src/base/ui/global_alert.tsx index 4bc859fb7..ced3d6bd6 100644 --- a/packages/sdk/src/base/ui/global_alert.tsx +++ b/packages/sdk/src/base/ui/global_alert.tsx @@ -37,7 +37,7 @@ const GlobalAlertInfo = () => { * @hidden * @example * ```js - * import {globalAlert} from '@airtable/blocks/ui'; + * import {globalAlert} from '@airtable/blocks/base/ui'; * globalAlert.showReloadPrompt(); * ``` */ diff --git a/packages/sdk/src/base/ui/initialize_block.tsx b/packages/sdk/src/base/ui/initialize_block.tsx index ce689562b..f924fec2a 100644 --- a/packages/sdk/src/base/ui/initialize_block.tsx +++ b/packages/sdk/src/base/ui/initialize_block.tsx @@ -31,7 +31,7 @@ type DashboardOrEntryPoints = DashboardEntryElementFunction | EntryPoints; * * @example * ```js - * import {initializeBlock} from '@airtable/blocks/ui'; + * import {initializeBlock} from '@airtable/blocks/base/ui'; * import React from 'react'; * * function App() { @@ -105,11 +105,19 @@ export function initializeBlock(getEntryElement: DashboardOrEntryPoints) { ); } - sdk.__setBatchedUpdatesFn(ReactDOM.unstable_batchedUpdates); + if (ReactDOM.unstable_batchedUpdates) { + sdk.__setBatchedUpdatesFn(ReactDOM.unstable_batchedUpdates); + } const container = document.createElement('div'); body.appendChild(container); - ReactDOM.render({entryElement}, container); + + try { + const {createRoot} = require('react-dom/client'); + createRoot(container).render({entryElement}); + } catch (e) { + ReactDOM.render({entryElement}, container); + } } let sdk: Sdk; diff --git a/packages/sdk/src/base/ui/progress_bar.tsx b/packages/sdk/src/base/ui/progress_bar.tsx index ddb7ec4cf..a5368d45f 100644 --- a/packages/sdk/src/base/ui/progress_bar.tsx +++ b/packages/sdk/src/base/ui/progress_bar.tsx @@ -119,7 +119,7 @@ const progressBarClassName = css({ * * @example * ```js - * import {ProgressBar} from '@airtable/blocks/ui'; + * import {ProgressBar} from '@airtable/blocks/base/ui'; * * function MyComponent() { * return ( diff --git a/packages/sdk/src/base/ui/types/tooltip_anchor_props.ts b/packages/sdk/src/base/ui/types/tooltip_anchor_props.ts index 5b83bd59a..d68c6fcf8 100644 --- a/packages/sdk/src/base/ui/types/tooltip_anchor_props.ts +++ b/packages/sdk/src/base/ui/types/tooltip_anchor_props.ts @@ -1,3 +1,4 @@ +import React from 'react'; import PropTypes from 'prop-types'; /** @hidden */ diff --git a/packages/sdk/src/base/ui/ui.ts b/packages/sdk/src/base/ui/ui.ts index a4e820e6d..f455282fd 100644 --- a/packages/sdk/src/base/ui/ui.ts +++ b/packages/sdk/src/base/ui/ui.ts @@ -29,7 +29,7 @@ export {default as SwitchSynced} from './switch_synced'; export {default as ViewportConstraint} from './viewport_constraint'; export {initializeBlock} from './initialize_block'; export {default as withHooks} from '../../shared/ui/with_hooks'; -export {default as useLoadable} from '../../shared/ui/use_loadable'; +export {default as useLoadable} from './use_loadable'; export {useRecordIds, useRecords, useRecordById, useRecordQueryResult} from './use_records'; export {default as useBase} from './use_base'; export {default as useCursor} from './use_cursor'; diff --git a/packages/sdk/src/base/ui/use_base.ts b/packages/sdk/src/base/ui/use_base.ts index 77f648712..e764061f6 100644 --- a/packages/sdk/src/base/ui/use_base.ts +++ b/packages/sdk/src/base/ui/use_base.ts @@ -16,7 +16,7 @@ import useBaseInternal from '../../shared/ui/use_base'; * * @example * ```js - * import {useBase} from '@airtable/blocks/ui'; + * import {useBase} from '@airtable/blocks/base/ui'; * * // renders a list of tables and automatically updates * function TableList() { diff --git a/packages/sdk/src/base/ui/use_cursor.ts b/packages/sdk/src/base/ui/use_cursor.ts index 3e53bd156..c18fb444b 100644 --- a/packages/sdk/src/base/ui/use_cursor.ts +++ b/packages/sdk/src/base/ui/use_cursor.ts @@ -17,7 +17,7 @@ import {BaseSdkMode} from '../../sdk_mode'; * * @example * ```js - * import {useBase, useCursor} from '@airtable/blocks/ui'; + * import {useBase, useCursor} from '@airtable/blocks/base/ui'; * * // renders a list of tables and automatically updates * function TableList() { diff --git a/packages/sdk/src/shared/ui/use_loadable.ts b/packages/sdk/src/base/ui/use_loadable.ts similarity index 95% rename from packages/sdk/src/shared/ui/use_loadable.ts rename to packages/sdk/src/base/ui/use_loadable.ts index 767693b88..33a51e9d5 100644 --- a/packages/sdk/src/shared/ui/use_loadable.ts +++ b/packages/sdk/src/base/ui/use_loadable.ts @@ -1,9 +1,9 @@ /** @module @airtable/blocks/ui: useLoadable */ /** */ import {useMemo, useEffect} from 'react'; import {useSubscription} from 'use-subscription'; -import {compact, has} from '../private_utils'; -import {spawnError} from '../error_utils'; -import useArrayIdentity from './use_array_identity'; +import {compact, has} from '../../shared/private_utils'; +import {spawnError} from '../../shared/error_utils'; +import useArrayIdentity from '../../shared/ui/use_array_identity'; /** * A model that can be loaded. @@ -59,7 +59,7 @@ interface UseLoadableOpts { * * @example * ```js - * import {useCursor, useLoadable, useWatchable} from '@airtable/blocks/ui'; + * import {useCursor, useLoadable, useWatchable} from '@airtable/blocks/base/ui'; * * function SelectedRecordIds() { * const cursor = useCursor(); @@ -76,7 +76,7 @@ interface UseLoadableOpts { * * @example * ```js - * import {useLoadable} from '@airtable/blocks/ui'; + * import {useLoadable} from '@airtable/blocks/base/ui'; * * function LoadTwoQueryResults({queryResultA, queryResultB}) { * // load the queryResults: @@ -89,7 +89,7 @@ interface UseLoadableOpts { * * @example * ```js - * import {useLoadable, useBase} from '@airtable/blocks/ui'; + * import {useLoadable, useBase} from '@airtable/blocks/base/ui'; * * function LoadAllRecords() { * const base = useBase(); diff --git a/packages/sdk/src/base/ui/use_record_action_data.ts b/packages/sdk/src/base/ui/use_record_action_data.ts index 77906709d..791a370ae 100644 --- a/packages/sdk/src/base/ui/use_record_action_data.ts +++ b/packages/sdk/src/base/ui/use_record_action_data.ts @@ -1,10 +1,10 @@ /** @module @airtable/blocks/ui: useRecordActionData */ /** */ import {RecordActionData} from '../types/record_action_data'; import {WatchablePerformRecordActionKeys} from '../perform_record_action'; -import useLoadable from '../../shared/ui/use_loadable'; import useWatchable from '../../shared/ui/use_watchable'; import {useSdk} from '../../shared/ui/sdk_context'; import {BaseSdkMode} from '../../sdk_mode'; +import useLoadable from './use_loadable'; /** * A hook to watch "open extension" / "perform record action" events (from button field). Returns @@ -34,7 +34,7 @@ import {BaseSdkMode} from '../../sdk_mode'; * @example * ```js * import React from 'react'; - * import {useRecordActionData} from '@airtable/blocks/ui'; + * import {useRecordActionData} from '@airtable/blocks/base/ui'; * * function LatestRecordAction() { * const recordActionData = useRecordActionData(); diff --git a/packages/sdk/src/base/ui/use_records.ts b/packages/sdk/src/base/ui/use_records.ts index e47dcabc6..b6818d697 100644 --- a/packages/sdk/src/base/ui/use_records.ts +++ b/packages/sdk/src/base/ui/use_records.ts @@ -12,8 +12,8 @@ import RecordQueryResult, { import Record from '../models/record'; import * as RecordColoring from '../models/record_coloring'; import View from '../models/view'; -import useLoadable from '../../shared/ui/use_loadable'; import useWatchable from '../../shared/ui/use_watchable'; +import useLoadable from './use_loadable'; /** */ type AnyQueryResult = TableOrViewQueryResult | LinkedRecordsQueryResult; @@ -79,7 +79,7 @@ export function useRecordIds(tableOrViewOrQueryResult: null): null; * @param opts? If passing a Table or View, optional {@link RecordIdsQueryResultOpts} to control the results. * @example * ```js - * import {useRecordIds, useBase} from '@airtable/blocks/ui'; + * import {useRecordIds, useBase} from '@airtable/blocks/base/ui'; * * function RecordCount() { * const base = useBase(); @@ -143,7 +143,7 @@ export function useRecords(tableOrViewOrQueryResult: null): null; * @param opts? If passing a Table or View, optional {@link RecordQueryResultOpts} to control the results. * @example * ```js - * import {useRecords, useBase} from '@airtable/blocks/ui'; + * import {useRecords, useBase} from '@airtable/blocks/base/ui'; * * function GetRecords() { * const base = useBase(); @@ -176,7 +176,7 @@ export function useRecords(tableOrViewOrQueryResult: null): null; * * @example * ```js - * import {useRecords, useBase} from '@airtable/blocks/ui'; + * import {useRecords, useBase} from '@airtable/blocks/base/ui'; * * function RecordList() { * const base = useBase(); @@ -240,7 +240,7 @@ export function useRecordById(queryResult: AnyQueryResult, recordId: RecordId): * @param opts? If passing a Table or View, optional {@link SingleRecordQueryResultOpts} to control the results. * @example * ```js - * import {useRecordById, useRecordIds, useBase} from '@airtable/blocks/ui'; + * import {useRecordById, useRecordIds, useBase} from '@airtable/blocks/base/ui'; * * // this component concerns a single record - it only updates when that specific record updates * function RecordListItem({table, recordId}) { diff --git a/packages/sdk/src/base/ui/use_session.ts b/packages/sdk/src/base/ui/use_session.ts index 4f1cc8ea7..bc6a334dd 100644 --- a/packages/sdk/src/base/ui/use_session.ts +++ b/packages/sdk/src/base/ui/use_session.ts @@ -13,7 +13,7 @@ import useSessionInternal from '../../shared/ui/use_session'; * * @example * ```js - * import {CollaboratorToken, useSession} from '@airtable/blocks/ui'; + * import {CollaboratorToken, useSession} from '@airtable/blocks/base/ui'; * * // Says hello to the current user and updates in realtime if the current user's * // name or profile pic changes. diff --git a/packages/sdk/src/base/ui/use_settings_button.ts b/packages/sdk/src/base/ui/use_settings_button.ts index 988079a8d..1a70807d9 100644 --- a/packages/sdk/src/base/ui/use_settings_button.ts +++ b/packages/sdk/src/base/ui/use_settings_button.ts @@ -14,7 +14,7 @@ import {BaseSdkMode} from '../../sdk_mode'; * * @example * ```js - * import {useSettingsButton} from '@airtable/blocks/ui'; + * import {useSettingsButton} from '@airtable/blocks/base/ui'; * import {useState} from 'react'; * * function ComponentWithSettings() { diff --git a/packages/sdk/src/base/ui/use_view_metadata.ts b/packages/sdk/src/base/ui/use_view_metadata.ts index ae76fad29..8a11f3aa9 100644 --- a/packages/sdk/src/base/ui/use_view_metadata.ts +++ b/packages/sdk/src/base/ui/use_view_metadata.ts @@ -1,8 +1,8 @@ /** @module @airtable/blocks/ui: useViewMetadata */ /** */ import ViewMetadataQueryResult from '../models/view_metadata_query_result'; import View from '../models/view'; -import useLoadable from '../../shared/ui/use_loadable'; import useWatchable from '../../shared/ui/use_watchable'; +import useLoadable from './use_loadable'; /** */ function useViewMetadata( @@ -22,7 +22,7 @@ function useViewMetadata( * @param viewOrViewMetadataQueryResult The {@link View} or {@link ViewMetadataQueryResult} to watch and use metadata from. * @example * ```js - * import {useBase, useViewMetadata} from '@airtable/blocks/ui'; + * import {useBase, useViewMetadata} from '@airtable/blocks/base/ui'; * * function ViewFields({view}) { * const viewMetadata = useViewMetadata(view); diff --git a/packages/sdk/src/base/ui/use_viewport.ts b/packages/sdk/src/base/ui/use_viewport.ts index ecc86b143..3122371e3 100644 --- a/packages/sdk/src/base/ui/use_viewport.ts +++ b/packages/sdk/src/base/ui/use_viewport.ts @@ -10,7 +10,7 @@ import {BaseSdkMode} from '../../sdk_mode'; * * @example * ```js - * import {useViewport} from '@airtable/blocks/ui'; + * import {useViewport} from '@airtable/blocks/base/ui'; * * function ViewportSize() { * const viewport = useViewport(); diff --git a/packages/sdk/src/base/ui/viewport_constraint.tsx b/packages/sdk/src/base/ui/viewport_constraint.tsx index 6786458b0..5a62585b3 100644 --- a/packages/sdk/src/base/ui/viewport_constraint.tsx +++ b/packages/sdk/src/base/ui/viewport_constraint.tsx @@ -43,13 +43,13 @@ const didSizeChange = ( * * @example * ```js - * import {ViewportConstraint} from '@airtable/blocks/ui'; + * import {ViewportConstraint} from '@airtable/blocks/base/ui'; * * ``` * * @example * ```js - * import {ViewportConstraint} from '@airtable/blocks/ui'; + * import {ViewportConstraint} from '@airtable/blocks/base/ui'; * *
    I need a max fullscreen size!
    *
    diff --git a/packages/sdk/src/base/viewport.ts b/packages/sdk/src/base/viewport.ts index 22fd01ea6..388e19397 100644 --- a/packages/sdk/src/base/viewport.ts +++ b/packages/sdk/src/base/viewport.ts @@ -53,7 +53,7 @@ const compareWithNulls = ( * * @example * ```js - * import {viewport} from '@airtable/blocks'; + * import {viewport} from '@airtable/blocks/base'; * ``` * @docsPath models/Viewport */ diff --git a/packages/sdk/src/interface/models/models.ts b/packages/sdk/src/interface/models/models.ts new file mode 100644 index 000000000..b3f65e466 --- /dev/null +++ b/packages/sdk/src/interface/models/models.ts @@ -0,0 +1,7 @@ +/** @ignore */ /** */ +export {FieldType, FieldConfig} from '../../shared/types/field'; +export {Base} from './base'; +export {Table} from './table'; +export {Field} from './field'; +export {Record} from './record'; +export {Session} from './session'; diff --git a/packages/sdk/src/interface/sdk.ts b/packages/sdk/src/interface/sdk.ts index 5c3a6309e..2f6d4e406 100644 --- a/packages/sdk/src/interface/sdk.ts +++ b/packages/sdk/src/interface/sdk.ts @@ -11,8 +11,6 @@ import { BlockRunContext, } from './types/airtable_interface'; - - /** @hidden */ export class InterfaceBlockSdk extends BlockSdkCore { constructor(airtableInterface: InterfaceSdkMode['AirtableInterfaceT']) { @@ -60,7 +58,6 @@ export class InterfaceBlockSdk extends BlockSdkCore { this.globalConfig.__setMultipleKvPaths(updates); } - /** * @internal */ diff --git a/packages/sdk/src/interface/ui/expand_record.ts b/packages/sdk/src/interface/ui/expand_record.ts index 2664e68db..b5e6453a6 100644 --- a/packages/sdk/src/interface/ui/expand_record.ts +++ b/packages/sdk/src/interface/ui/expand_record.ts @@ -9,7 +9,8 @@ import {Record} from '../models/record'; * @example * ```js * import {expandRecord} from '@airtable/blocks/interface/ui'; - * expandRecord(record); + * + * * ``` * @docsPath UI/utils/expandRecord */ diff --git a/packages/sdk/src/interface/ui/initialize_block.tsx b/packages/sdk/src/interface/ui/initialize_block.tsx index aa81d8ec2..62d03923a 100644 --- a/packages/sdk/src/interface/ui/initialize_block.tsx +++ b/packages/sdk/src/interface/ui/initialize_block.tsx @@ -35,7 +35,6 @@ interface EntryPoints { * initializeBlock({interface: () => }); * ``` * @docsPath UI/utils/initializeBlock - * @internal */ export function initializeBlock(entryPoints: EntryPoints) { const body = typeof document !== 'undefined' ? document.body : null; @@ -78,7 +77,13 @@ export function initializeBlock(entryPoints: EntryPoints) { const container = document.createElement('div'); body.appendChild(container); - ReactDOM.render({entryElement}, container); + + try { + const {createRoot} = require('react-dom/client'); + createRoot(container).render({entryElement}); + } catch (e) { + ReactDOM.render({entryElement}, container); + } } let sdk: InterfaceBlockSdk; diff --git a/packages/sdk/src/interface/ui/ui.ts b/packages/sdk/src/interface/ui/ui.ts index 6dc684b7b..a08fe55aa 100644 --- a/packages/sdk/src/interface/ui/ui.ts +++ b/packages/sdk/src/interface/ui/ui.ts @@ -8,9 +8,13 @@ export {useRecords} from './use_records'; export {useRunInfo} from './use_run_info'; export {useSession} from './use_session'; export {default as useGlobalConfig} from '../../shared/ui/use_global_config'; -export {default as useLoadable} from '../../shared/ui/use_loadable'; export {default as useSynced} from '../../shared/ui/use_synced'; export {default as useWatchable} from '../../shared/ui/use_watchable'; export {default as withHooks} from '../../shared/ui/with_hooks'; export {default as colors} from '../../shared/colors'; export {default as colorUtils} from '../../shared/color_utils'; +export { + loadCSSFromString, + loadCSSFromURLAsync, + loadScriptFromURLAsync, +} from '../../shared/ui/remote_utils'; diff --git a/packages/sdk/src/interface/ui/use_custom_properties.ts b/packages/sdk/src/interface/ui/use_custom_properties.ts index 49e9569ef..fc6448ccd 100644 --- a/packages/sdk/src/interface/ui/use_custom_properties.ts +++ b/packages/sdk/src/interface/ui/use_custom_properties.ts @@ -14,8 +14,25 @@ import {spawnUnknownSwitchCaseError} from '../../shared/error_utils'; import {Field} from '../models/field'; /** - * TODO document - * @hidden + * An object that represents a custom property that a block can set. + * + * ``` + * type BlockPageElementCustomProperty = {key: string; label: string} & ( + * | {type: 'boolean'; defaultValue: boolean} + * | {type: 'string'; defaultValue?: string} + * | { + * type: 'enum'; + * possibleValues: Array<{value: string; label: string}>; + * defaultValue?: string; + * } + * | { + * type: 'field'; + * table: Table; + * possibleValues?: Array; // If not provided, all visible fields in the table will be shown in the dropdown. + * defaultValue?: Field; + * } + * ); + * ``` */ type BlockPageElementCustomProperty = {key: string; label: string} & ( | {type: 'boolean'; defaultValue: boolean} @@ -35,9 +52,39 @@ type BlockPageElementCustomProperty = {key: string; label: string} & ( ); /** - * TODO document. Make sure to describe that getCustomProperties - * should be wrapped in useCallback. - * @hidden + * A hook for integrating configuration settings for your block with the Interface Designer properties + * panel. Under the hood, this uses {@link GlobalConfig} to store the custom property values. + * + * Returns an object with: + * - `customPropertyValueByKey`: an object mapping custom property keys to their current value. + * - `errorState`: an object with an `error` property if there was an error setting the custom properties + * + * @param getCustomProperties A function that returns an array of {@link BlockPageElementCustomProperty}. + * This function should have a stable identity, so it should either be defined at the top level of the + * file or wrapped in useCallback. It will receive an instance of {@link Base} as an argument. + * + * @example + * ```js + * import {useCustomProperties} from '@airtable/blocks/interface/ui'; + * + * function getCustomProperties(base: Base) { + * const table = base.tables[0]; + * const numberFields = table.fields.filter(field => field.type === FieldType.NUMBER); + * return [ + * {key: 'title', label: 'Title', type: 'string', defaultValue: 'Chart'}, + * {key: 'xAxis', label: 'X-axis', type: 'field', table, possibleValues: numberFields}, + * {key: 'yAxis', label: 'Y-axis', type: 'field', table, possibleValues: numberFields}, + * {key: 'color', label: 'Color', type: 'enum', possibleValues: ['red', 'blue', 'green'], defaultValue: 'red'}, + * {key: 'showLegend', label: 'Show Legend', type: 'boolean', defaultValue: true}, + * ]; + * } + * + * function MyApp() { + * const {customPropertyValueByKey, errorState} = useCustomProperties(getCustomProperties); + * } + * ``` + * @docsPath UI/hooks/useCustomProperties + * @hook */ export function useCustomProperties( getCustomProperties: (base: Base) => Array, diff --git a/packages/sdk/src/shared/color_utils.ts b/packages/sdk/src/shared/color_utils.ts index ed8419e95..a65c8fe06 100644 --- a/packages/sdk/src/shared/color_utils.ts +++ b/packages/sdk/src/shared/color_utils.ts @@ -24,7 +24,7 @@ interface ColorUtils { * @param colorString * @example * ```js - * import {colorUtils, colors} from '@airtable/blocks/ui'; + * import {colorUtils, colors} from '@airtable/blocks/[placeholder-path]/ui'; * * colorUtils.getHexForColor(colors.RED); * // => '#ef3061' @@ -43,7 +43,7 @@ interface ColorUtils { * @param colorString * @example * ```js - * import {colorUtils, colors} from '@airtable/blocks/ui'; + * import {colorUtils, colors} from '@airtable/blocks/[placeholder-path]/ui'; * * colorUtils.getRgbForColor(colors.PURPLE_DARK_1); * // => {r: 107, g: 28, b: 176} @@ -62,7 +62,7 @@ interface ColorUtils { * @param colorString * @example * ```js - * import {colorUtils, colors} from '@airtable/blocks/ui'; + * import {colorUtils, colors} from '@airtable/blocks/[placeholder-path]/ui'; * * colorUtils.shouldUseLightTextOnColor(colors.PINK_LIGHT_1); * // => false diff --git a/packages/sdk/src/shared/colors.ts b/packages/sdk/src/shared/colors.ts index 9d5de5a05..bf0fb73e1 100644 --- a/packages/sdk/src/shared/colors.ts +++ b/packages/sdk/src/shared/colors.ts @@ -10,9 +10,9 @@ import {ObjectValues} from './private_utils'; * * @example * ```js - * import {Box, colors} from '@airtable/blocks/ui'; + * import {colors, colorUtils} from '@airtable/blocks/[placeholder-path]/ui'; * - * + *
    Hello world
    * ``` * * @docsPath UI/utils/colors diff --git a/packages/sdk/src/shared/global_config.ts b/packages/sdk/src/shared/global_config.ts index 3fb370691..87d0f76b0 100644 --- a/packages/sdk/src/shared/global_config.ts +++ b/packages/sdk/src/shared/global_config.ts @@ -38,10 +38,6 @@ type WatchableGlobalConfigKey = string; * The maximum allowed size for a given GlobalConfig instance is 150kB. * The maximum number of keys for a given GlobalConfig instance is 1000. * - * @example - * ```js - * import {globalConfig} from '@airtable/blocks'; - * ``` * @docsPath models/GlobalConfig */ class GlobalConfig extends Watchable { @@ -111,10 +107,13 @@ class GlobalConfig extends Watchable { * @param key A string for the top-level key, or an array of strings describing the path to the value. * @example * ```js - * import {globalConfig} from '@airtable/blocks'; + * import {useGlobalConfig} from '@airtable/blocks/[placeholder-path]/ui'; * - * const topLevelValue = globalConfig.get('topLevelKey'); - * const nestedValue = globalConfig.get(['topLevelKey', 'nested', 'deeply']); + * function MyApp() { + * const globalConfig = useGlobalConfig(); + * const topLevelValue = globalConfig.get('topLevelKey'); + * const nestedValue = globalConfig.get(['topLevelKey', 'nested', 'deeply']); + * } * ``` */ get(key: GlobalConfigKey): unknown { @@ -210,23 +209,26 @@ class GlobalConfig extends Watchable { * @param value The value to set at the specified path. Use `undefined` to delete the value at the given path. * @example * ```js - * import {globalConfig} from '@airtable/blocks'; - * - * function updateFavoriteColorIfPossible(color) { - * if (globalConfig.hasPermissionToSetPaths('favoriteColor', color)) { - * globalConfig.setAsync('favoriteColor', color); + * import {useGlobalConfig} from '@airtable/blocks/[placeholder-path]/ui'; + * + * function MyApp() { + * const globalConfig = useGlobalConfig(); + * const updateFavoriteColorIfPossible = (color) => { + * if (globalConfig.hasPermissionToSet('favoriteColor', color)) { + * globalConfig.setAsync('favoriteColor', color); + * } + * // The update is now applied within your extension (eg will be + * // reflected in globalConfig) but are still being saved to + * // Airtable servers (e.g. may not be updated for other users yet) * } - * // The update is now applied within your extension (eg will be - * // reflected in globalConfig) but are still being saved to - * // Airtable servers (e.g. may not be updated for other users yet) - * } * - * async function updateFavoriteColorIfPossibleAsync(color) { - * if (globalConfig.hasPermissionToSet('favoriteColor', color)) { - * await globalConfig.setAsync('favoriteColor', color); + * const updateFavoriteColorIfPossibleAsync = async (color) => { + * if (globalConfig.hasPermissionToSet('favoriteColor', color)) { + * await globalConfig.setAsync('favoriteColor', color); + * } + * // globalConfig updates have been saved to Airtable servers. + * alert('favoriteColor has been updated'); * } - * // globalConfig updates have been saved to Airtable servers. - * alert('favoriteColor has been updated'); * } * ``` */ @@ -313,28 +315,31 @@ class GlobalConfig extends Watchable { * @param updates The paths and values to set. * @example * ```js - * import {globalConfig} from '@airtable/blocks'; - * - * const updates = [ - * {path: ['topLevelKey1', 'nestedKey1'], value: 'foo'}, - * {path: ['topLevelKey2', 'nestedKey2'], value: 'bar'}, - * ]; - * - * function applyUpdatesIfPossible() { - * if (globalConfig.hasPermissionToSetPaths(updates)) { - * globalConfig.setPathsAsync(updates); + * import {useGlobalConfig} from '@airtable/blocks/[placeholder-path]/ui'; + * + * function MyApp() { + * const globalConfig = useGlobalConfig(); + * const updates = [ + * {path: ['topLevelKey1', 'nestedKey1'], value: 'foo'}, + * {path: ['topLevelKey2', 'nestedKey2'], value: 'bar'}, + * ]; + * + * const applyUpdatesIfPossible = () => { + * if (globalConfig.hasPermissionToSetPaths(updates)) { + * globalConfig.setPathsAsync(updates); + * } + * // The updates are now applied within your extension (eg will be reflected in + * // globalConfig) but are still being saved to Airtable servers (e.g. they + * // may not be updated for other users yet) * } - * // The updates are now applied within your extension (eg will be reflected in - * // globalConfig) but are still being saved to Airtable servers (e.g. they - * // may not be updated for other users yet) - * } * - * async function applyUpdatesIfPossibleAsync() { - * if (globalConfig.hasPermissionToSetPaths(updates)) { - * await globalConfig.setPathsAsync(updates); + * const applyUpdatesIfPossibleAsync = async () => { + * if (globalConfig.hasPermissionToSetPaths(updates)) { + * await globalConfig.setPathsAsync(updates); + * } + * // globalConfig updates have been saved to Airtable servers. + * alert('globalConfig has been updated'); * } - * // globalConfig updates have been saved to Airtable servers. - * alert('globalConfig has been updated'); * } * ``` */ diff --git a/packages/sdk/src/shared/models/base_core.ts b/packages/sdk/src/shared/models/base_core.ts index eed7cd235..0b624e8e8 100644 --- a/packages/sdk/src/shared/models/base_core.ts +++ b/packages/sdk/src/shared/models/base_core.ts @@ -85,8 +85,12 @@ export abstract class BaseCore extends AbstractModel< * * @example * ```js - * import {base} from '@airtable/blocks'; - * console.log('The name of your base is', base.name); + * import {useBase} from '@airtable/blocks/[placeholder-path]/ui'; + * + * function MyApp() { + * const base = useBase(); + * console.log('The name of your base is', base.name); + * } * ``` */ get name(): string { @@ -98,8 +102,12 @@ export abstract class BaseCore extends AbstractModel< * * @example * ```js - * import {base} from '@airtable/blocks'; - * console.log('The workspace id of your base is', base.workspaceId); + * import {useBase} from '@airtable/blocks/[placeholder-path]/ui'; + * + * function MyApp() { + * const base = useBase(); + * console.log('The workspace id of your base is', base.workspaceId); + * } * ``` */ get workspaceId(): string { @@ -111,9 +119,16 @@ export abstract class BaseCore extends AbstractModel< * * @example * ```js - * import {base} from '@airtable/blocks'; - * import {Box} from '@airtable/blocks/ui'; - * const exampleBox = This box's background is the same color as the base background + * import {colorUtils, useBase} from '@airtable/blocks/[placeholder-path]/ui'; + * + * function MyApp() { + * const base = useBase(); + * return ( + *
    + * This div's background is the same color as the base background + *
    + * ); + * } * ``` */ get color(): string { @@ -125,8 +140,12 @@ export abstract class BaseCore extends AbstractModel< * * @example * ```js - * import {base} from '@airtable/blocks'; - * console.log(`You have ${base.tables.length} tables`); + * import {useBase} from '@airtable/blocks/[placeholder-path]/ui'; + * + * function MyApp() { + * const base = useBase(); + * console.log(`You have ${base.tables.length} tables`); + * } * ``` */ get tables(): Array { @@ -155,8 +174,12 @@ export abstract class BaseCore extends AbstractModel< * * @example * ```js - * import {base} from '@airtable/blocks'; - * console.log(base.activeCollaborators[0].email); + * import {useBase} from '@airtable/blocks/[placeholder-path]/ui'; + * + * function MyApp() { + * const base = useBase(); + * console.log(base.activeCollaborators[0].email); + * } * ``` */ get activeCollaborators(): Array { diff --git a/packages/sdk/src/shared/models/field_core.ts b/packages/sdk/src/shared/models/field_core.ts index fd55f67ad..a99583fc1 100644 --- a/packages/sdk/src/shared/models/field_core.ts +++ b/packages/sdk/src/shared/models/field_core.ts @@ -108,7 +108,7 @@ export abstract class FieldCore extends AbstractModel< * @see {@link FieldType} * @example * ```js - * import {FieldType} from '@airtable/blocks/models'; + * import {FieldType} from '@airtable/blocks/[placeholder-path]/models'; * * if (myField.type === FieldType.CURRENCY) { * console.log(myField.options.symbol); diff --git a/packages/sdk/src/shared/models/record_core.ts b/packages/sdk/src/shared/models/record_core.ts index 1dfea992f..e100a061c 100644 --- a/packages/sdk/src/shared/models/record_core.ts +++ b/packages/sdk/src/shared/models/record_core.ts @@ -64,7 +64,7 @@ export abstract class RecordCore< * @internal (since we may not be able to return parent model instances in the immutable models world) * @example * ```js - * import {useRecords} from '@airtable/blocks/ui'; + * import {useRecords} from '@airtable/blocks/base/ui'; * const records = useRecords(myTable); * console.log(records[0].parentTable.id === myTable.id); * // => true diff --git a/packages/sdk/src/shared/models/session_core.ts b/packages/sdk/src/shared/models/session_core.ts index a1b7a07be..81716658e 100644 --- a/packages/sdk/src/shared/models/session_core.ts +++ b/packages/sdk/src/shared/models/session_core.ts @@ -78,7 +78,7 @@ export abstract class SessionCore extends AbstractMode * * @example * ```js - * import {useSession} from '@airtable/blocks/ui'; + * import {useSession} from '@airtable/blocks/[placeholder-path]/ui'; * * function CurrentUser() { * const session = useSession(); diff --git a/packages/sdk/src/shared/models/table_core.ts b/packages/sdk/src/shared/models/table_core.ts index 5f57c44e2..d3633b977 100644 --- a/packages/sdk/src/shared/models/table_core.ts +++ b/packages/sdk/src/shared/models/table_core.ts @@ -63,7 +63,7 @@ export abstract class TableCore< * @internal (since we may not be able to return parent model instances in the immutable models world) * @example * ```js - * import {base} from '@airtable/blocks'; + * import {base} from '@airtable/blocks/base'; * const table = base.getTableByName('Table 1'); * console.log(table.parentBase.id === base.id); * // => true diff --git a/packages/sdk/src/shared/private_utils.ts b/packages/sdk/src/shared/private_utils.ts index 190633ff0..bfbc9ecc8 100644 --- a/packages/sdk/src/shared/private_utils.ts +++ b/packages/sdk/src/shared/private_utils.ts @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, {JSX} from 'react'; import getAirtableInterface from '../injected/airtable_interface'; import {spawnError} from './error_utils'; diff --git a/packages/sdk/src/shared/sdk_core.ts b/packages/sdk/src/shared/sdk_core.ts index fad0d5770..6fcdced0f 100644 --- a/packages/sdk/src/shared/sdk_core.ts +++ b/packages/sdk/src/shared/sdk_core.ts @@ -7,7 +7,7 @@ import {BlockInstallationId} from './types/hyper_ids'; * @hidden * @example * ```js - * import {runInfo} from '@airtable/blocks'; + * import {runInfo} from '@airtable/blocks/base'; * if (runInfo.isFirstRun) { * // The current user just installed this block. * // Take the opportunity to show any onboarding and set @@ -48,7 +48,7 @@ export abstract class BlockSdkCore { * * @example * ```js - * import {installationId} from '@airtable/blocks'; + * import {installationId} from '@airtable/blocks/base'; * console.log(installationId); * // => 'blifDutUr92OKwnUn' * ``` @@ -100,8 +100,8 @@ export abstract class BlockSdkCore { * @example * ```js * import React from 'react'; - * import {reload} from '@airtable/blocks'; - * import {Button, initializeBlock} from '@airtable/blocks/ui'; + * import {reload} from '@airtable/blocks/base'; + * import {Button, initializeBlock} from '@airtable/blocks/base/ui'; * function MyBlock() { * return ; * } diff --git a/packages/sdk/src/shared/types/field.ts b/packages/sdk/src/shared/types/field.ts index af4818123..93983ccbe 100644 --- a/packages/sdk/src/shared/types/field.ts +++ b/packages/sdk/src/shared/types/field.ts @@ -10,7 +10,7 @@ export type PrivateColumnType = string; * * @example * ```js - * import {FieldType} from '@airtable/blocks/models'; + * import {FieldType} from '@airtable/blocks/[placeholder-path]/models'; * const numberFields = myTable.fields.filter(field => ( * field.type === FieldType.NUMBER * )); diff --git a/packages/sdk/src/shared/ui/remote_utils.ts b/packages/sdk/src/shared/ui/remote_utils.ts index 09aaf119c..90638af15 100644 --- a/packages/sdk/src/shared/ui/remote_utils.ts +++ b/packages/sdk/src/shared/ui/remote_utils.ts @@ -7,7 +7,7 @@ import {invariant} from '../error_utils'; * @param css The CSS string. * @example * ```js - * import {loadCSSFromString} from '@airtable/blocks/ui'; + * import {loadCSSFromString} from '@airtable/blocks/[placeholder-path]/ui'; * loadCSSFromString('body { background: red; }'); * ``` * @docsPath UI/utils/loadCSSFromString @@ -28,7 +28,7 @@ export function loadCSSFromString(css: string): HTMLStyleElement { * @param url The URL of the stylesheet. * @example * ```js - * import {loadCSSFromURLAsync} from '@airtable/blocks/ui'; + * import {loadCSSFromURLAsync} from '@airtable/blocks/[placeholder-path]/ui'; * loadCSSFromURLAsync('https://example.com/style.css'); * ``` * @docsPath UI/utils/loadCSSFromURLAsync @@ -58,7 +58,7 @@ export function loadCSSFromURLAsync(url: string): Promise { * @param url The URL of the script. * @example * ```js - * import {loadScriptFromURLAsync} from '@airtable/blocks/ui'; + * import {loadScriptFromURLAsync} from '@airtable/blocks/[placeholder-path]/ui'; * loadScriptFromURLAsync('https://example.com/script.js'); * ``` * @docsPath UI/utils/loadScriptFromURLAsync diff --git a/packages/sdk/src/shared/ui/use_global_config.ts b/packages/sdk/src/shared/ui/use_global_config.ts index 00214de42..dbecd9010 100644 --- a/packages/sdk/src/shared/ui/use_global_config.ts +++ b/packages/sdk/src/shared/ui/use_global_config.ts @@ -9,9 +9,10 @@ import {useSdk} from './sdk_context'; * * @example * ```js - * import {Button, useGlobalConfig} from '@airtable/blocks/ui'; + * import {useGlobalConfig, useRunInfo} from '@airtable/blocks/[placeholder-path]/ui'; * * function SyncedCounter() { + * const runInfo = useRunInfo(); * const globalConfig = useGlobalConfig(); * const count = globalConfig.get('count'); * @@ -19,13 +20,17 @@ import {useSdk} from './sdk_context'; * const decrement = () => globalConfig.setAsync('count', count - 1); * const isEnabled = globalConfig.hasPermissionToSet('count'); * - * return ( - * - * + * {count} + * + *
    + * ); + * } else { + * return
    {count}
    ; + * } * } * ``` * @docsPath UI/hooks/useGlobalConfig diff --git a/packages/sdk/src/shared/ui/use_synced.ts b/packages/sdk/src/shared/ui/use_synced.ts index e47c90dba..60d0596e3 100644 --- a/packages/sdk/src/shared/ui/use_synced.ts +++ b/packages/sdk/src/shared/ui/use_synced.ts @@ -9,7 +9,7 @@ import {useSdk} from './sdk_context'; * @param globalConfigKey * @example * ```js - * import {useBase, useSynced} from '@airtable/blocks/ui'; + * import {useBase, useSynced} from '@airtable/blocks/[placeholder-path]/ui'; * * function CustomInputSynced() { * const [value, setValue, canSetValue] = useSynced('myGlobalConfigKey'); diff --git a/packages/sdk/src/shared/ui/use_watchable.ts b/packages/sdk/src/shared/ui/use_watchable.ts index 35e6a430d..06b16a46b 100644 --- a/packages/sdk/src/shared/ui/use_watchable.ts +++ b/packages/sdk/src/shared/ui/use_watchable.ts @@ -28,39 +28,20 @@ import useArrayIdentity from './use_array_identity'; * * @example * ```js - * import {useWatchable} from '@airtable/blocks/ui'; + * import {useWatchable} from '@airtable/blocks/[placeholder-path]/ui'; * * function TableName({table}) { * useWatchable(table, 'name'); * return The table name is {table.name}; * } * - * function ViewNameAndType({view}) { - * useWatchable(view, ['name', 'type']); - * return The view name is {view.name} and the type is {view.type}; - * } - * - * function RecordValuesAndColorInViewIfExists({record, field, view}) { - * useWatchable(record, ['cellValues', view ? `colorInView:${view.id}` : null]); + * function RecordValues({record, field}) { + * useWatchable(record, ['cellValues']); * return * The record has cell value {record.getCellValue(field)} in {field.name}. - * {view ? `The record has color ${record.getColorInView(view)} in ${view.name}.` : null} * * } * ``` - * - * @example - * ```js - * import {useWatchable} from '@airtable/blocks/ui'; - * - * function ActiveView({cursor}) { - * useWatchable(cursor, 'activeViewId', () => { - * alert('active view changed!!!') - * }); - * - * return Active view id: {cursor.activeViewId}; - * } - * ``` * @docsPath UI/hooks/useWatchable * @hook */ diff --git a/packages/sdk/src/shared/ui/with_hooks.tsx b/packages/sdk/src/shared/ui/with_hooks.tsx index da68017a3..c05ef2a61 100644 --- a/packages/sdk/src/shared/ui/with_hooks.tsx +++ b/packages/sdk/src/shared/ui/with_hooks.tsx @@ -17,7 +17,7 @@ import {spawnError} from '../error_utils'; * @example * ```js * import React from 'react'; - * import {useRecords, withHooks} from '@airtable/blocks/ui'; + * import {useRecords, withHooks} from '@airtable/blocks/base/ui'; * * // RecordList takes a list of records and renders it * class RecordList extends React.Component { @@ -53,8 +53,8 @@ import {spawnError} from '../error_utils'; * @example * ```js * import React from 'react'; - * import {Record, Table} from '@airtable/blocks/models'; - * import {withHooks, useRecords} from '@airtable/blocks/ui'; + * import {Record, Table} from '@airtable/blocks/[placeholder-path]/models'; + * import {withHooks, useRecords} from '@airtable/blocks/[placeholder-path]/ui'; * // with typescript, things are a little more complex: we need to provide some type annotations to * // indicate which props are injected: * diff --git a/packages/sdk/stories/box/box.stories.tsx b/packages/sdk/stories/box/box.stories.tsx index 59dc7ea02..08e2a64fd 100644 --- a/packages/sdk/stories/box/box.stories.tsx +++ b/packages/sdk/stories/box/box.stories.tsx @@ -56,7 +56,7 @@ function BoxExample() { : ''; const borderProp = border !== 'none' ? `border="${border}"` : ''; return ` - import {Box, Text} from '@airtable/blocks/ui'; + import {Box, Text} from '@airtable/blocks/base/ui'; const boxExample = ( { const fieldName = ReadableFieldTypes[fieldType]; return ` - import {CellRenderer, useBase, useRecords} from '@airtable/blocks/ui'; + import {CellRenderer, useBase, useRecords} from '@airtable/blocks/base/ui'; const CellRendererExample = (props) => { const base = useBase(); diff --git a/packages/sdk/stories/choice_token.stories.tsx b/packages/sdk/stories/choice_token.stories.tsx index 097ae13d6..881745572 100644 --- a/packages/sdk/stories/choice_token.stories.tsx +++ b/packages/sdk/stories/choice_token.stories.tsx @@ -14,7 +14,7 @@ function ChoiceTokenExample() { styleProps={Object.keys(choiceTokenStylePropTypes)} renderCodeFn={() => { return ` - import {ChoiceToken, useBase} from '@airtable/blocks/ui'; + import {ChoiceToken, useBase} from '@airtable/blocks/base/ui'; const ChoiceTokenExample = () => { const base = useBase(); diff --git a/packages/sdk/stories/collaborator_token.stories.tsx b/packages/sdk/stories/collaborator_token.stories.tsx index eeaf0c77a..cff6ab48d 100644 --- a/packages/sdk/stories/collaborator_token.stories.tsx +++ b/packages/sdk/stories/collaborator_token.stories.tsx @@ -16,7 +16,7 @@ function CollaboratorTokenExample() { styleProps={Object.keys(collaboratorTokenStylePropTypes)} renderCodeFn={() => { return ` - import {CollaboratorToken, useBase} from '@airtable/blocks/ui'; + import {CollaboratorToken, useBase} from '@airtable/blocks/base/ui'; const CollaboratorTokenExample = () => { const base = useBase(); diff --git a/packages/sdk/stories/color_palette.stories.tsx b/packages/sdk/stories/color_palette.stories.tsx index 0139b3433..61b0bb4b7 100644 --- a/packages/sdk/stories/color_palette.stories.tsx +++ b/packages/sdk/stories/color_palette.stories.tsx @@ -52,7 +52,7 @@ function ColorPaletteExample() { return ` import React, {useState} from 'react'; - import {ColorPalette, colors} from '@airtable/blocks/ui'; + import {ColorPalette, colors} from '@airtable/blocks/base/ui'; const allowedColors = [colors.BLUE, colors.BLUE_BRIGHT, colors.BLUE_DARK_1, colors.BLUE_LIGHT_1, colors. BLUE_LIGHT_2]; @@ -110,7 +110,7 @@ function ColorPaletteSyncedExample() { return ` import React from 'react'; - import {ColorPaletteSynced, colors} from '@airtable/blocks/ui'; + import {ColorPaletteSynced, colors} from '@airtable/blocks/base/ui'; const allowedColors = [colors.BLUE, colors.BLUE_BRIGHT, colors.BLUE_DARK_1, colors.BLUE_LIGHT_1, colors. BLUE_LIGHT_2]; diff --git a/packages/sdk/stories/confirmation_dialog.stories.tsx b/packages/sdk/stories/confirmation_dialog.stories.tsx index 2351df9d5..c7cc241b3 100644 --- a/packages/sdk/stories/confirmation_dialog.stories.tsx +++ b/packages/sdk/stories/confirmation_dialog.stories.tsx @@ -35,7 +35,7 @@ function ConfirmationDialogExample() { const props = createJsxPropsStringFromValuesMap(values); return ` import React, { useState } from 'react'; - import {Button, ConfirmationDialog} from '@airtable/blocks/ui'; + import {Button, ConfirmationDialog} from '@airtable/blocks/base/ui'; const DialogExample = () => { const [isDialogOpen, setIsDialogOpen] = useState(false); diff --git a/packages/sdk/stories/dialog.stories.tsx b/packages/sdk/stories/dialog.stories.tsx index 289efef2b..6447ff960 100644 --- a/packages/sdk/stories/dialog.stories.tsx +++ b/packages/sdk/stories/dialog.stories.tsx @@ -25,7 +25,7 @@ function DialogExample() { renderCodeFn={values => { return ` import React, { useState } from 'react'; - import {Button, Dialog, Heading, Text} from '@airtable/blocks/ui'; + import {Button, Dialog, Heading, Text} from '@airtable/blocks/base/ui'; const DialogExample = () => { const [isDialogOpen, setIsDialogOpen] = useState(false); diff --git a/packages/sdk/stories/field_icon.stories.tsx b/packages/sdk/stories/field_icon.stories.tsx index 66db1470a..3857a8208 100644 --- a/packages/sdk/stories/field_icon.stories.tsx +++ b/packages/sdk/stories/field_icon.stories.tsx @@ -32,7 +32,7 @@ function FieldIconExample() { renderCodeFn={({fieldType, size}) => { const fieldName = ReadableFieldTypes[fieldType]; return ` - import {FieldIcon, useBase, useRecords} from '@airtable/blocks/ui'; + import {FieldIcon, useBase, useRecords} from '@airtable/blocks/base/ui'; const FieldIconExample = (props) => { const base = useBase(); diff --git a/packages/sdk/stories/field_picker.stories.tsx b/packages/sdk/stories/field_picker.stories.tsx index c093665a0..1d7699efe 100644 --- a/packages/sdk/stories/field_picker.stories.tsx +++ b/packages/sdk/stories/field_picker.stories.tsx @@ -47,7 +47,7 @@ function FieldPickerExample() { return ` import React, {useState} from 'react'; - import {FieldPicker, useBase} from '@airtable/blocks/ui'; + import {FieldPicker, useBase} from '@airtable/blocks/base/ui'; const FieldPickerExample = () => { const [field, setField] = useState(null); @@ -98,7 +98,7 @@ function FieldPickerSyncedExample() { return ` import React from 'react'; - import {FieldPickerSynced, useBase} from '@airtable/blocks/ui'; + import {FieldPickerSynced, useBase} from '@airtable/blocks/base/ui'; const FieldPickerSyncedExample = () => { const base = useBase(); diff --git a/packages/sdk/stories/form_field.stories.tsx b/packages/sdk/stories/form_field.stories.tsx index 660e67067..ff85c31f4 100644 --- a/packages/sdk/stories/form_field.stories.tsx +++ b/packages/sdk/stories/form_field.stories.tsx @@ -34,7 +34,7 @@ function FormFieldExample() { _value ? 'This is a description for this field.' : null, }); return ` - import {FormField, Input} from '@airtable/blocks/ui'; + import {FormField, Input} from '@airtable/blocks/base/ui'; const FormFieldExample = () => { const [value, setValue] = useState(''); diff --git a/packages/sdk/stories/heading.stories.tsx b/packages/sdk/stories/heading.stories.tsx index 020912029..f3c8cdaa9 100644 --- a/packages/sdk/stories/heading.stories.tsx +++ b/packages/sdk/stories/heading.stories.tsx @@ -42,7 +42,7 @@ function HeadingExample() { sizeOutOfBoundsComment = ''; } return ` - import {Heading} from '@airtable/blocks/ui'; + import {Heading} from '@airtable/blocks/base/ui'; ${sizeOutOfBoundsComment} const headingExample = ( diff --git a/packages/sdk/stories/icon_example.tsx b/packages/sdk/stories/icon_example.tsx index 6f97e8e7c..3ffc3bdfd 100644 --- a/packages/sdk/stories/icon_example.tsx +++ b/packages/sdk/stories/icon_example.tsx @@ -160,7 +160,7 @@ export default function IconExample() { } return ` import React from 'react'; - import {Icon} from '@airtable/blocks/ui'; + import {Icon} from '@airtable/blocks/base/ui'; ${deprecatedWarning} ${exampleCode} diff --git a/packages/sdk/stories/input.stories.tsx b/packages/sdk/stories/input.stories.tsx index 0ed5cfe85..080e4276d 100644 --- a/packages/sdk/stories/input.stories.tsx +++ b/packages/sdk/stories/input.stories.tsx @@ -38,7 +38,7 @@ function InputExample() { return ` import React, {useState} from 'react'; - import {Input} from '@airtable/blocks/ui'; + import {Input} from '@airtable/blocks/base/ui'; const InputExample = () => { const [value, setValue] = useState(''); @@ -85,7 +85,7 @@ function InputSyncedExample() { return ` import React, {useState} from 'react'; - import {InputSynced} from '@airtable/blocks/ui'; + import {InputSynced} from '@airtable/blocks/base/ui'; const InputSyncedExample = () => { return ( diff --git a/packages/sdk/stories/label.stories.tsx b/packages/sdk/stories/label.stories.tsx index 72adde36d..065c2f803 100644 --- a/packages/sdk/stories/label.stories.tsx +++ b/packages/sdk/stories/label.stories.tsx @@ -33,7 +33,7 @@ function LabelExample() { renderCodeFn={values => { const props = createJsxPropsStringFromValuesMap(values); return ` - import {Label, Box, Input} from '@airtable/blocks/ui'; + import {Label, Box, Input} from '@airtable/blocks/base/ui'; // You might want to consider using \`FormField\` instead. const LabelExample = () => { diff --git a/packages/sdk/stories/link.stories.tsx b/packages/sdk/stories/link.stories.tsx index debe6c3e8..4b3e38c3b 100644 --- a/packages/sdk/stories/link.stories.tsx +++ b/packages/sdk/stories/link.stories.tsx @@ -69,7 +69,7 @@ function LinkExample() { ); return ` - import {Link} from '@airtable/blocks/ui'; + import {Link} from '@airtable/blocks/base/ui'; ${ariaLabelComment} const linkExample = ( diff --git a/packages/sdk/stories/loader.stories.tsx b/packages/sdk/stories/loader.stories.tsx index 2715b8b38..7a7501caf 100644 --- a/packages/sdk/stories/loader.stories.tsx +++ b/packages/sdk/stories/loader.stories.tsx @@ -24,7 +24,7 @@ function LoaderExample() { const props = createJsxPropsStringFromValuesMap(values); return ` - import {Loader} from '@airtable/blocks/ui'; + import {Loader} from '@airtable/blocks/base/ui'; const loaderExample = ( diff --git a/packages/sdk/stories/progress_bar.stories.tsx b/packages/sdk/stories/progress_bar.stories.tsx index 489449501..e39650ba4 100644 --- a/packages/sdk/stories/progress_bar.stories.tsx +++ b/packages/sdk/stories/progress_bar.stories.tsx @@ -25,7 +25,7 @@ function ProgressBarExample() { const props = createJsxPropsStringFromValuesMap(values); return ` - import {ProgressBar} from '@airtable/blocks/ui'; + import {ProgressBar} from '@airtable/blocks/base/ui'; const loaderExample = ( diff --git a/packages/sdk/stories/record_card.stories.tsx b/packages/sdk/stories/record_card.stories.tsx index 769f9272d..3af8988b2 100644 --- a/packages/sdk/stories/record_card.stories.tsx +++ b/packages/sdk/stories/record_card.stories.tsx @@ -15,7 +15,7 @@ function RecordCardExample() { styleProps={Object.keys(recordCardStylePropTypes)} renderCodeFn={() => { return ` - import {RecordCard, useBase, useRecords} from '@airtable/blocks/ui'; + import {RecordCard, useBase, useRecords} from '@airtable/blocks/base/ui'; const RecordCardExample = () => { const base = useBase(); diff --git a/packages/sdk/stories/record_card_list.stories.tsx b/packages/sdk/stories/record_card_list.stories.tsx index e1361f401..66139f492 100644 --- a/packages/sdk/stories/record_card_list.stories.tsx +++ b/packages/sdk/stories/record_card_list.stories.tsx @@ -52,7 +52,7 @@ function RecordCardListExample() { styleProps={Object.keys(recordCardListStylePropTypes)} renderCodeFn={() => { return ` - import {RecordCardList, useBase, useRecords} from '@airtable/blocks/ui'; + import {RecordCardList, useBase, useRecords} from '@airtable/blocks/base/ui'; const RecordCardListExample = () => { const base = useBase(); diff --git a/packages/sdk/stories/select.stories.tsx b/packages/sdk/stories/select.stories.tsx index 267376a1a..02249dec6 100644 --- a/packages/sdk/stories/select.stories.tsx +++ b/packages/sdk/stories/select.stories.tsx @@ -45,7 +45,7 @@ function SelectExample() { return ` import React, {useState} from 'react'; - import {Select} from '@airtable/blocks/ui'; + import {Select} from '@airtable/blocks/base/ui'; const options = ${JSON.stringify(options)}; @@ -85,7 +85,7 @@ function SelectSyncedExample() { const props = createJsxPropsStringFromValuesMap(values); return ` - import {SelectSynced} from '@airtable/blocks/ui'; + import {SelectSynced} from '@airtable/blocks/base/ui'; const options = ${JSON.stringify(options)}; diff --git a/packages/sdk/stories/select_buttons.stories.tsx b/packages/sdk/stories/select_buttons.stories.tsx index 268cbefa2..3b01b7380 100644 --- a/packages/sdk/stories/select_buttons.stories.tsx +++ b/packages/sdk/stories/select_buttons.stories.tsx @@ -43,7 +43,7 @@ function SelectButtonsExample() { return ` import React, {useState} from 'react'; - import {SelectButtons} from '@airtable/blocks/ui'; + import {SelectButtons} from '@airtable/blocks/base/ui'; const options = ${JSON.stringify(options)}; @@ -108,7 +108,7 @@ function SelectButtonsSyncedExample() { return ` import React from 'react'; - import {SelectButtonsSynced} from '@airtable/blocks/ui'; + import {SelectButtonsSynced} from '@airtable/blocks/base/ui'; const options = ${JSON.stringify(options)}; diff --git a/packages/sdk/stories/switch.stories.tsx b/packages/sdk/stories/switch.stories.tsx index 707e5a0cd..e4c656150 100644 --- a/packages/sdk/stories/switch.stories.tsx +++ b/packages/sdk/stories/switch.stories.tsx @@ -50,7 +50,7 @@ function SwitchExample() { return ` import React, {useState} from 'react'; - import {Switch} from '@airtable/blocks/ui'; + import {Switch} from '@airtable/blocks/base/ui'; const SwitchExample = () => { const [isEnabled, setIsEnabled] = useState(true); @@ -100,7 +100,7 @@ function SwitchSyncedExample() { return ` import React, {useState} from 'react'; - import {SwitchSynced} from '@airtable/blocks/ui'; + import {SwitchSynced} from '@airtable/blocks/base/ui'; const SwitchSyncedExample = () => { return ( diff --git a/packages/sdk/stories/table_picker.stories.tsx b/packages/sdk/stories/table_picker.stories.tsx index d6c80266a..e44eae8ca 100644 --- a/packages/sdk/stories/table_picker.stories.tsx +++ b/packages/sdk/stories/table_picker.stories.tsx @@ -51,7 +51,7 @@ function TablePickerExample() { return ` import React, {useState} from 'react'; - import {TablePicker} from '@airtable/blocks/ui'; + import {TablePicker} from '@airtable/blocks/base/ui'; const TablePickerExample = () => { const [table, setTable] = useState(null); @@ -99,7 +99,7 @@ function TablePickerSyncedExample() { return ` import React from 'react'; - import {TablePickerSynced} from '@airtable/blocks/ui'; + import {TablePickerSynced} from '@airtable/blocks/base/ui'; const TablePickerSyncedExample = () => ( diff --git a/packages/sdk/stories/text.stories.tsx b/packages/sdk/stories/text.stories.tsx index 4d1223302..836071053 100644 --- a/packages/sdk/stories/text.stories.tsx +++ b/packages/sdk/stories/text.stories.tsx @@ -44,7 +44,7 @@ function TextExample() { }); return ` - import {Text} from '@airtable/blocks/ui'; + import {Text} from '@airtable/blocks/base/ui'; const textExample = ( ${childrenForVariant[values.variant]} diff --git a/packages/sdk/stories/text_button.stories.tsx b/packages/sdk/stories/text_button.stories.tsx index 9bac60d82..9c143ce07 100644 --- a/packages/sdk/stories/text_button.stories.tsx +++ b/packages/sdk/stories/text_button.stories.tsx @@ -66,7 +66,7 @@ function TextButtonExample() { ); return ` - import {TextButton} from '@airtable/blocks/ui'; + import {TextButton} from '@airtable/blocks/base/ui'; ${ariaLabelComment} const buttonExample = ( diff --git a/packages/sdk/stories/tooltip.stories.tsx b/packages/sdk/stories/tooltip.stories.tsx index 97e3d88f2..3bc2d74e6 100644 --- a/packages/sdk/stories/tooltip.stories.tsx +++ b/packages/sdk/stories/tooltip.stories.tsx @@ -50,7 +50,7 @@ function TextExample() { const placementXProp = `placementX={Tooltip.placements.${placementX.toUpperCase()}}`; const placementYProp = `placementY={Tooltip.placements.${placementY.toUpperCase()}}`; return ` - import {Tooltip, Button} from '@airtable/blocks/ui'; + import {Tooltip, Button} from '@airtable/blocks/base/ui'; const tooltipExample = ( diff --git a/packages/sdk/stories/view_picker.stories.tsx b/packages/sdk/stories/view_picker.stories.tsx index 110522f72..afbdc90da 100644 --- a/packages/sdk/stories/view_picker.stories.tsx +++ b/packages/sdk/stories/view_picker.stories.tsx @@ -47,7 +47,7 @@ function ViewPickerExample() { return ` import React, {useState} from 'react'; - import {ViewPicker, useBase} from '@airtable/blocks/ui'; + import {ViewPicker, useBase} from '@airtable/blocks/base/ui'; const ViewPickerExample = () => { const [view, setView] = useState(null); @@ -98,7 +98,7 @@ function ViewPickerSyncedExample() { return ` import React from 'react'; - import {useBase, ViewPickerSynced} from '@airtable/blocks/ui'; + import {useBase, ViewPickerSynced} from '@airtable/blocks/base/ui'; const ViewPickerSyncedExample = () => { const base = useBase(); diff --git a/packages/sdk/test/ui/use_loadable.test.tsx b/packages/sdk/test/ui/use_loadable.test.tsx index 5d96a4fb8..5e720ba16 100644 --- a/packages/sdk/test/ui/use_loadable.test.tsx +++ b/packages/sdk/test/ui/use_loadable.test.tsx @@ -3,7 +3,7 @@ import ReactDOM from 'react-dom'; import {mount} from 'enzyme'; import {act} from 'react-dom/test-utils'; import AbstractModelWithAsyncData from '../../src/base/models/abstract_model_with_async_data'; -import useLoadable from '../../src/shared/ui/use_loadable'; +import useLoadable from '../../src/base/ui/use_loadable'; import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; import Sdk from '../../src/base/sdk'; diff --git a/packages/sdk/ui.d.ts b/packages/sdk/ui.d.ts deleted file mode 100644 index ee2aad69c..000000000 --- a/packages/sdk/ui.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './dist/types/src/base/ui/ui'; diff --git a/packages/sdk/ui.js b/packages/sdk/ui.js deleted file mode 100644 index b69ed851d..000000000 --- a/packages/sdk/ui.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./dist/cjs/base/ui/ui'); diff --git a/packages/sdk/unstable_private_utils.d.ts b/packages/sdk/unstable_private_utils.d.ts deleted file mode 100644 index 9df599283..000000000 --- a/packages/sdk/unstable_private_utils.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './dist/types/src/shared/unstable_private_utils'; diff --git a/packages/sdk/unstable_private_utils.js b/packages/sdk/unstable_private_utils.js deleted file mode 100644 index ac4f56a29..000000000 --- a/packages/sdk/unstable_private_utils.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./dist/cjs/shared/unstable_private_utils'); diff --git a/packages/sdk/unstable_standalone_ui.d.ts b/packages/sdk/unstable_standalone_ui.d.ts deleted file mode 100644 index 434e5432b..000000000 --- a/packages/sdk/unstable_standalone_ui.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './dist/types/src/base/ui/unstable_standalone_ui'; diff --git a/packages/sdk/unstable_standalone_ui.js b/packages/sdk/unstable_standalone_ui.js deleted file mode 100644 index 12ca04efb..000000000 --- a/packages/sdk/unstable_standalone_ui.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./dist/cjs/base/ui/unstable_standalone_ui'); diff --git a/packages/sdk/unstable_testing_utils.d.ts b/packages/sdk/unstable_testing_utils.d.ts deleted file mode 100644 index fe483d6b4..000000000 --- a/packages/sdk/unstable_testing_utils.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './dist/types/src/base/unstable_testing_utils'; diff --git a/packages/sdk/unstable_testing_utils.js b/packages/sdk/unstable_testing_utils.js deleted file mode 100644 index eeedbf9de..000000000 --- a/packages/sdk/unstable_testing_utils.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./dist/cjs/base/unstable_testing_utils'); diff --git a/yarn.lock b/yarn.lock index 7197e4319..45a45c38e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3620,7 +3620,7 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== -"@types/react-dom@^16.9.24": +"@types/react-dom@^16.9.24", "@types/react-dom@^19.1.2": version "16.9.24" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.24.tgz#4d193d7d011267fca842e8a10a2d738f92ec5c30" integrity sha512-Gcmq2JTDheyWn/1eteqyzzWKSqDjYU6KYsIvH7thb7CR5OYInAWOX+7WnKf6PaU/cbdOc4szJItcDEJO7UGmfA== @@ -3634,7 +3634,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^16", "@types/react@^16.14.0", "@types/react@^16.14.60", "@types/react@^16.9.24", "@types/react@^17.0.1": +"@types/react@*", "@types/react@^16", "@types/react@^16.14.0", "@types/react@^16.9.24", "@types/react@^19.1.2": version "16.14.63" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.63.tgz#0970c0bca7f23f630c85332cee00729d4ad53845" integrity sha512-s83gano0fRBVEw3ejdLpjgvU83F0LIeeuXqdxfPZF/Sc2bhr60tEqCK1zZ+aLirBwRSD6V5zCtOsEjcwKow3JQ== From 6fb9870212146ffc7a3abf7b3429a702178d29a8 Mon Sep 17 00:00:00 2001 From: Airtable Date: Mon, 9 Jun 2025 20:12:24 +0000 Subject: [PATCH 03/14] @airtable/blocks@0.0.0-experimental-f1c9010b3-20250609 --- package.json | 3 +- packages/sdk/CHANGELOG.md | 11 +- packages/sdk/package.json | 2 +- packages/sdk/src/base/ui/initialize_block.tsx | 2 + .../ui/select_and_select_buttons_helpers.ts | 2 + .../utils/ensure_numbers_are_within_scale.ts | 2 + packages/sdk/src/base/ui/ui.ts | 1 + packages/sdk/src/interface/models/table.ts | 42 +++ packages/sdk/src/interface/types/table.ts | 1 + .../sdk/src/interface/ui/initialize_block.tsx | 2 + packages/sdk/src/interface/ui/ui.ts | 1 + .../sdk/src/shared/ui/use_color_scheme.ts | 47 ++++ .../sdk/test/ui/use_color_scheme.test.tsx | 100 +++++++ yarn.lock | 261 +++++------------- 14 files changed, 275 insertions(+), 202 deletions(-) create mode 100644 packages/sdk/src/shared/ui/use_color_scheme.ts create mode 100644 packages/sdk/test/ui/use_color_scheme.test.tsx diff --git a/package.json b/package.json index 0b2aa42d1..3fddf056f 100644 --- a/package.json +++ b/package.json @@ -54,5 +54,6 @@ "hooks": { "pre-commit": "pretty-quick --staged" } - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index d36a57929..4d3527697 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -9,10 +9,19 @@ Not every commit needs to result in a change to this file (e.g. docs and chore c commit that affects the code in a way that consumers might care about should include edits to the 'Unreleased' section though. Breaking changes should be prefixed with `**BREAKING:**`. -## [Unreleased](https://github.com/airtable/blocks/compare/@airtable/blocks@1.18.2...HEAD) +## [Unreleased](https://github.com/airtable/blocks/compare/@airtable/blocks@1.19.0...HEAD) No changes. +## [1.19.0](https://github.com/airtable/blocks/compare/@airtable/blocks@1.18.2...@airtable/blocks@1.19.0) - 2025-06-06 + +- Add `useColorScheme` React hook, making a user's light/dark mode preference available in + JavaScript. Airtable will also set + [`color-scheme`](https://developer.mozilla.org/en-US/docs/Web/CSS/color-scheme) on extension + iframes, so + [`prefers-color-scheme`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) + media queries will match the user's Airtable preferences before browser-wide settings. + ## [1.18.2](https://github.com/airtable/blocks/compare/@airtable/blocks@1.18.1...@airtable/blocks@1.18.2) - 2024-09-25 - Upgrade Typescript version to 5.4.5 diff --git a/packages/sdk/package.json b/packages/sdk/package.json index c11116017..8a8bb574c 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@airtable/blocks", - "version": "1.18.2", + "version": "1.19.0", "description": "Airtable Blocks SDK", "repository": { "type": "git", diff --git a/packages/sdk/src/base/ui/initialize_block.tsx b/packages/sdk/src/base/ui/initialize_block.tsx index f924fec2a..5b93a72be 100644 --- a/packages/sdk/src/base/ui/initialize_block.tsx +++ b/packages/sdk/src/base/ui/initialize_block.tsx @@ -110,6 +110,8 @@ export function initializeBlock(getEntryElement: DashboardOrEntryPoints) { } const container = document.createElement('div'); + container.style.height = '100%'; + container.style.width = '100%'; body.appendChild(container); try { diff --git a/packages/sdk/src/base/ui/select_and_select_buttons_helpers.ts b/packages/sdk/src/base/ui/select_and_select_buttons_helpers.ts index dd29a0d37..01b19208e 100644 --- a/packages/sdk/src/base/ui/select_and_select_buttons_helpers.ts +++ b/packages/sdk/src/base/ui/select_and_select_buttons_helpers.ts @@ -8,6 +8,8 @@ import * as React from 'react'; export type SelectOptionValue = string | number | boolean | null | undefined; /** @internal */ + +// eslint-disable-next-line airtable/is-returns-boolean export function isSelectOptionValue(value: unknown): value is SelectOptionValue { return ( value === null || diff --git a/packages/sdk/src/base/ui/system/utils/ensure_numbers_are_within_scale.ts b/packages/sdk/src/base/ui/system/utils/ensure_numbers_are_within_scale.ts index c2fb5037d..f739d7fce 100644 --- a/packages/sdk/src/base/ui/system/utils/ensure_numbers_are_within_scale.ts +++ b/packages/sdk/src/base/ui/system/utils/ensure_numbers_are_within_scale.ts @@ -2,6 +2,8 @@ import {get, Scale} from '@styled-system/core'; import {spawnError} from '../../../../shared/error_utils'; /** @internal */ + +// eslint-disable-next-line airtable/is-returns-boolean function isNumber(n: unknown): n is number { return typeof n === 'number' && !isNaN(n); } diff --git a/packages/sdk/src/base/ui/ui.ts b/packages/sdk/src/base/ui/ui.ts index f455282fd..772563f2e 100644 --- a/packages/sdk/src/base/ui/ui.ts +++ b/packages/sdk/src/base/ui/ui.ts @@ -30,6 +30,7 @@ export {default as ViewportConstraint} from './viewport_constraint'; export {initializeBlock} from './initialize_block'; export {default as withHooks} from '../../shared/ui/with_hooks'; export {default as useLoadable} from './use_loadable'; +export {useColorScheme} from '../../shared/ui/use_color_scheme'; export {useRecordIds, useRecords, useRecordById, useRecordQueryResult} from './use_records'; export {default as useBase} from './use_base'; export {default as useCursor} from './use_cursor'; diff --git a/packages/sdk/src/interface/models/table.ts b/packages/sdk/src/interface/models/table.ts index 6e6df8adb..cdff35a24 100644 --- a/packages/sdk/src/interface/models/table.ts +++ b/packages/sdk/src/interface/models/table.ts @@ -1,6 +1,7 @@ import {TableCore} from '../../shared/models/table_core'; import {InterfaceSdkMode} from '../../sdk_mode'; import {FieldId} from '../../shared/types/hyper_ids'; +import {PermissionCheckResult} from '../../shared/types/mutations_core'; import {Field} from './field'; /** @@ -25,4 +26,45 @@ export class Table extends TableCore { _constructField(fieldId: FieldId): Field { return new Field(this.parentBase.__sdk, this, fieldId); } + + /** + * Checks whether records in this table can be expanded. + * + * Returns `{hasPermission: true}` if records can be expanded, + * `{hasPermission: false, reasonDisplayString: string}` otherwise. + * + * @example + * ```js + * const expandRecordsCheckResult = table.checkPermissionToExpandRecords(); + * if (!expandRecordsCheckResult.hasPermission) { + * alert(expandRecordsCheckResult.reasonDisplayString); + * } + * ``` + */ + checkPermissionToExpandRecords(): PermissionCheckResult { + const canExpand = this._baseData.tablesById[this.id].isRecordExpansionEnabled; + return canExpand + ? {hasPermission: true} + : { + hasPermission: false, + reasonDisplayString: 'Record expansion is not enabled for this table', + }; + } + + /** + * An alias for `checkPermissionsForExpandRecords().hasPermission`. + * + * Whether records in this table can be expanded. + * + * @example + * ```js + * const isRecordExpansionEnabled = table.hasPermissionToExpandRecords(); + * if (isRecordExpansionEnabled) { + * expandRecord(record); + * } + * ``` + */ + hasPermissionToExpandRecords(): boolean { + return this.checkPermissionToExpandRecords().hasPermission; + } } diff --git a/packages/sdk/src/interface/types/table.ts b/packages/sdk/src/interface/types/table.ts index 44863bf84..b3fc1eefe 100644 --- a/packages/sdk/src/interface/types/table.ts +++ b/packages/sdk/src/interface/types/table.ts @@ -7,4 +7,5 @@ import {RecordData} from './record'; export interface TableData extends TableDataCore { recordsById: ObjectMap; recordOrder: Array; + isRecordExpansionEnabled: boolean; } diff --git a/packages/sdk/src/interface/ui/initialize_block.tsx b/packages/sdk/src/interface/ui/initialize_block.tsx index 62d03923a..b89aed031 100644 --- a/packages/sdk/src/interface/ui/initialize_block.tsx +++ b/packages/sdk/src/interface/ui/initialize_block.tsx @@ -76,6 +76,8 @@ export function initializeBlock(entryPoints: EntryPoints) { } const container = document.createElement('div'); + container.style.height = '100%'; + container.style.width = '100%'; body.appendChild(container); try { diff --git a/packages/sdk/src/interface/ui/ui.ts b/packages/sdk/src/interface/ui/ui.ts index a08fe55aa..41838d9f9 100644 --- a/packages/sdk/src/interface/ui/ui.ts +++ b/packages/sdk/src/interface/ui/ui.ts @@ -3,6 +3,7 @@ import '..'; export {expandRecord} from './expand_record'; export {initializeBlock} from './initialize_block'; export {useBase} from './use_base'; +export {useColorScheme} from '../../shared/ui/use_color_scheme'; export {useCustomProperties} from './use_custom_properties'; export {useRecords} from './use_records'; export {useRunInfo} from './use_run_info'; diff --git a/packages/sdk/src/shared/ui/use_color_scheme.ts b/packages/sdk/src/shared/ui/use_color_scheme.ts new file mode 100644 index 000000000..6853ed03c --- /dev/null +++ b/packages/sdk/src/shared/ui/use_color_scheme.ts @@ -0,0 +1,47 @@ +/** @module @airtable/blocks/ui: useColorScheme */ /** */ +import {useState, useEffect} from 'react'; + +/** + * A hook for checking whether Airtable is in light mode or dark mode. + * + * @returns An object with a `colorScheme` property, which can be `'light'` or `'dark'`. + * + * @example + * ```js + * import {useColorScheme} from '@airtable/blocks/[placeholder-path]/ui'; + * + * function MyApp() { + * const {colorScheme} = useColorScheme(); + * return ( + *
    + * Tada! + *
    + * ); + * } + * ``` + * @docsPath UI/hooks/useColorScheme + * @hook + */ + +export function useColorScheme(): {colorScheme: 'light' | 'dark'} { + const [colorScheme, setColorScheme] = useState<'light' | 'dark'>( + window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light', + ); + + useEffect(() => { + const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const darkModeListener = (e: MediaQueryListEvent) => { + setColorScheme(e.matches ? 'dark' : 'light'); + }; + + darkModeQuery.addEventListener('change', darkModeListener); + return () => { + darkModeQuery.removeEventListener('change', darkModeListener); + }; + }, []); + + return {colorScheme}; +} diff --git a/packages/sdk/test/ui/use_color_scheme.test.tsx b/packages/sdk/test/ui/use_color_scheme.test.tsx new file mode 100644 index 000000000..1e6be4059 --- /dev/null +++ b/packages/sdk/test/ui/use_color_scheme.test.tsx @@ -0,0 +1,100 @@ +import React from 'react'; +import {mount} from 'enzyme'; +import {act} from 'react-dom/test-utils'; +import {useColorScheme} from '../../src/shared/ui/use_color_scheme'; + +describe('useColorScheme', () => { + it('returns light by default when the actual color scheme is unknown', () => { + window.matchMedia = jest.fn().mockImplementation(query => { + return { + matches: false, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + }; + }); + const Component = () => { + const {colorScheme} = useColorScheme(); + return
    {colorScheme}
    ; + }; + const wrapper = mount(); + expect(wrapper.text()).toBe('light'); + }); + + it('returns dark when the actual color scheme is dark', () => { + window.matchMedia = jest.fn().mockImplementation(query => { + if (query.includes('dark')) { + return { + matches: true, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + }; + } else { + return { + matches: false, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + }; + } + }); + const Component = () => { + const {colorScheme} = useColorScheme(); + return
    {colorScheme}
    ; + }; + const wrapper = mount(); + expect(wrapper.text()).toBe('dark'); + }); + + type MockMediaQueryListEventListener = (e: Partial) => void; + it('updates when the actual color scheme changes', () => { + let isCurrentlyDarkMode = true; + const darkChangeListeners: Array = []; + const lightChangeListeners: Array = []; + window.matchMedia = jest.fn().mockImplementation(query => { + if (query.includes('dark')) { + return { + matches: isCurrentlyDarkMode, + addEventListener: ( + eventName: 'change', + listenerFn: MockMediaQueryListEventListener, + ) => { + darkChangeListeners.push(listenerFn); + }, + removeEventListener: jest.fn(), + }; + } else { + return { + matches: !isCurrentlyDarkMode, + addEventListener: ( + eventName: 'change', + listenerFn: MockMediaQueryListEventListener, + ) => { + lightChangeListeners.push(listenerFn); + }, + removeEventListener: jest.fn(), + }; + } + }); + const Component = () => { + const {colorScheme} = useColorScheme(); + return
    {colorScheme}
    ; + }; + const wrapper = mount(); + expect(wrapper.text()).toBe('dark'); + + const darkChangeEvent: Partial = { + matches: false, + media: '(prefers-color-scheme: dark)', + }; + const lightChangeEvent: Partial = { + matches: true, + media: '(prefers-color-scheme: light)', + }; + act(() => { + isCurrentlyDarkMode = false; + + darkChangeListeners.forEach(listenerFn => listenerFn(darkChangeEvent)); + lightChangeListeners.forEach(listenerFn => listenerFn(lightChangeEvent)); + }); + expect(wrapper.text()).toBe('light'); + }); +}); diff --git a/yarn.lock b/yarn.lock index 45a45c38e..4b98dc949 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3273,7 +3273,7 @@ dependencies: "@types/node" "*" -"@types/cpx@^1.5.2": +"@types/cpx@^1.5.5": version "1.5.5" resolved "https://registry.yarnpkg.com/@types/cpx/-/cpx-1.5.5.tgz#21c173496e2e4e4d5e10ef6cd806a2df712a5afc" integrity sha512-PwM+cN40GZcjG9YgGFp/rQGKOpTqr6scUl1Q85NHL5jieh9I203kKiArjJcExwxy4+vTABmVUNRkNvGbPnRQZg== @@ -3638,6 +3638,9 @@ version "16.14.63" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.63.tgz#0970c0bca7f23f630c85332cee00729d4ad53845" integrity sha512-s83gano0fRBVEw3ejdLpjgvU83F0LIeeuXqdxfPZF/Sc2bhr60tEqCK1zZ+aLirBwRSD6V5zCtOsEjcwKow3JQ== + version "16.14.65" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.65.tgz#e7e4c1f7e90c3c615105c22e6e5cf43766663716" + integrity sha512-Guc3kE+W8LrQB9I3bF3blvNH15dXFIVIHIJTqrF8cp5XI/3IJcHGo4C3sJNPb8Zx49aofXKnAGIKyonE4f7XWg== dependencies: "@types/prop-types" "*" "@types/scheduler" "^0.16" @@ -4491,14 +4494,6 @@ ansicolors@~0.3.2: resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" integrity sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg== -anymatch@^1.3.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" - integrity sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA== - dependencies: - micromatch "^2.1.5" - normalize-path "^2.0.0" - anymatch@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" @@ -4643,23 +4638,11 @@ aria-query@^4.2.2: "@babel/runtime" "^7.10.2" "@babel/runtime-corejs3" "^7.10.2" -arr-diff@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" - integrity sha512-dtXTVMkh6VkEEA7OhXnN1Ecb8aAGFdZ1LFxtOCoqj4qkyOJMt7+qs6Ahdy6p/NQCPYsRSXXivhSB/J5E9jmYKA== - dependencies: - arr-flatten "^1.0.1" - arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" integrity sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA== -arr-flatten@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== - arr-union@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" @@ -4713,11 +4696,6 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -array-unique@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" - integrity sha512-G2n5bG5fSUCpnsXz4+8FUkYsGPkNfLn9YvS66U5qbTIXI2Ynnlo4Bi42bWv+omKUCqz+ejzfClwne0alJWJPhg== - array-unique@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" @@ -4941,7 +4919,7 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== -async-each@^1.0.0, async-each@^1.0.1: +async-each@^1.0.1: version "1.0.6" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.6.tgz#52f1d9403818c179b7561e11a5d1b77eb2160e77" integrity sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg== @@ -5418,14 +5396,6 @@ babel-preset-minify@^0.5.1: babel-plugin-transform-undefined-to-void "^6.9.4" lodash "^4.17.11" -babel-runtime@^6.9.2: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" - integrity sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g== - dependencies: - core-js "^2.4.0" - regenerator-runtime "^0.11.0" - babelify@^10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/babelify/-/babelify-10.0.0.tgz#fe73b1a22583f06680d8d072e25a1e0d1d1d7fb5" @@ -5681,7 +5651,7 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@3.0.3, braces@^1.8.2, braces@^2.3.1, braces@^2.3.2, braces@^3.0.3, braces@~3.0.2: +braces@3.0.3, braces@^2.3.1, braces@^2.3.2, braces@^3.0.3, braces@~3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== @@ -6334,22 +6304,6 @@ cheerio@^1.0.0-rc.3: parse5 "^7.0.0" parse5-htmlparser2-tree-adapter "^7.0.0" -chokidar@^1.6.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" - integrity sha512-mk8fAWcRUOxY7btlLtitj3A45jOwSAxH4tOFOoEGbVsl6cL6pPMWUy7dwZ/canfj3QEdP6FHSnf/l1c6/WkzVg== - dependencies: - anymatch "^1.3.0" - async-each "^1.0.0" - glob-parent "^2.0.0" - inherits "^2.0.1" - is-binary-path "^1.0.0" - is-glob "^2.0.0" - path-is-absolute "^1.0.0" - readdirp "^2.0.0" - optionalDependencies: - fsevents "^1.0.0" - chokidar@^2.1.1: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -6938,7 +6892,7 @@ core-js-pure@^3.30.2: resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.37.1.tgz#2b4b34281f54db06c9a9a5bd60105046900553bd" integrity sha512-J/r5JTHSmzTxbiYYrzXg9w1VpqrYt+gexenBE9pugeyhwPZTAEJddyiReJWsLO6uNQ8xJZFbod6XC7KKwatCiA== -core-js@^2.4.0, core-js@^2.6.5: +core-js@^2.6.5: version "2.6.12" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== @@ -7000,21 +6954,23 @@ cosmiconfig@^7.0.1: path-type "^4.0.0" yaml "^1.10.0" -cpx@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/cpx/-/cpx-1.5.0.tgz#185be018511d87270dedccc293171e37655ab88f" - integrity sha512-jHTjZhsbg9xWgsP2vuNW2jnnzBX+p4T+vNI9Lbjzs1n4KhOfa22bQppiFYLsWQKd8TzmL5aSP/Me3yfsCwXbDA== +cpx2@7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cpx2/-/cpx2-7.0.1.tgz#429fde3fc344459cca9c1f29cb3e6f660ca10278" + integrity sha512-ZgK/DRvPFM5ATZ5DQ5UzY6ajkBrI/p9Uc7VyLHc7b4OSFeBO4yOQz/GEmccc4Om6capGYlY4K1XX+BtYQiPPIA== dependencies: - babel-runtime "^6.9.2" - chokidar "^1.6.0" + debounce "^2.0.0" + debug "^4.1.1" duplexer "^0.1.1" - glob "^7.0.5" - glob2base "^0.0.12" - minimatch "^3.0.2" - mkdirp "^0.5.1" - resolve "^1.1.7" - safe-buffer "^5.0.1" - shell-quote "^1.6.1" + fs-extra "^11.1.0" + glob "^10.3.10" + glob2base "0.0.12" + ignore "^5.2.4" + minimatch "^9.0.0" + p-map "^6.0.0" + resolve "^1.12.0" + safe-buffer "^5.2.0" + shell-quote "^1.8.0" subarg "^1.0.0" crc-32@^1.2.0: @@ -7372,6 +7328,11 @@ debounce@^1.1.0: resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== +debounce@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/debounce/-/debounce-2.2.0.tgz#f895fa2fbdb579a0f0d3dcf5dde19657e50eaad5" + integrity sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw== + debug@2.6.9, debug@^2.2.0, debug@^2.3.3: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -9023,13 +8984,6 @@ exit@^0.1.2: resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== -expand-brackets@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" - integrity sha512-hxx03P2dJxss6ceIeri9cmYOT4SRs3Zk3afZwWpOsRqLqprhTR8u++SlC+sFGsQr7WGFPdMF7Gjc1njDLDK6UA== - dependencies: - is-posix-bracket "^0.1.0" - expand-brackets@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" @@ -9132,13 +9086,6 @@ external-editor@^3.0.3, external-editor@^3.1.0: iconv-lite "^0.4.24" tmp "^0.0.33" -extglob@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" - integrity sha512-1FOj1LOwn42TMrruOHGt18HemVnbwAmAak7krWk+wa93KXxGbK+2jpezm+ytJYDaBX0/SPLZFHKM7m+tKobWGg== - dependencies: - is-extglob "^1.0.0" - extglob@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" @@ -9322,11 +9269,6 @@ filelist@^1.0.4: dependencies: minimatch "^5.0.1" -filename-regex@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" - integrity sha512-BTCqyBaWBTsauvnHiE8i562+EdJj+oUpkqWp2R1iCoR8f6oo8STRu3of7WJJ0TqWtxN50a5YFpzYK4Jj9esYfQ== - fill-keys@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/fill-keys/-/fill-keys-1.0.2.tgz#9a8fa36f4e8ad634e3bf6b4f3c8882551452eb20" @@ -9534,18 +9476,11 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" -for-in@^1.0.1, for-in@^1.0.2: +for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" integrity sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ== -for-own@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" - integrity sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw== - dependencies: - for-in "^1.0.1" - foreground-child@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-2.0.0.tgz#71b32800c9f15aa8f2f83f4a6bd9bff35d861a53" @@ -9692,6 +9627,15 @@ fs-extra@^10.0.0, fs-extra@^10.0.1: jsonfile "^6.0.1" universalify "^2.0.0" +fs-extra@^11.1.0: + version "11.3.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.3.0.tgz#0daced136bbaf65a555a326719af931adc7a314d" + integrity sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^11.2.0: version "11.2.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" @@ -9760,7 +9704,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@^1.0.0, fsevents@^1.2.7: +fsevents@^1.2.7: version "1.2.13" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38" integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw== @@ -10041,21 +9985,6 @@ github-slugger@^1.2.1: resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.5.0.tgz#17891bbc73232051474d68bd867a34625c955f7d" integrity sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw== -glob-base@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" - integrity sha512-ab1S1g1EbO7YzauaJLkgLp7DZVAqj9M/dvKlTt8DkXA2tiOIcSMrlVI2J1RZyB5iJVccEscjGn+kpOG9788MHA== - dependencies: - glob-parent "^2.0.0" - is-glob "^2.0.0" - -glob-parent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" - integrity sha512-JDYOvfxio/t42HKdxkAYaCiBN7oYiuxykOxKxdaUW5Qn0zaYN3gRQWolrwdnf0shM9/EP0ebuuTmyoXNr1cC5w== - dependencies: - is-glob "^2.0.0" - glob-parent@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" @@ -10083,14 +10012,14 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob2base@0.0.12, glob2base@^0.0.12: +glob2base@0.0.12: version "0.0.12" resolved "https://registry.yarnpkg.com/glob2base/-/glob2base-0.0.12.tgz#9d419b3e28f12e83a362164a277055922c9c0d56" integrity sha512-ZyqlgowMbfj2NPjxaZZ/EtsXlOch28FRXgMd64vqZWk1bT9+wvSRLYD1om9M7QfQru51zJPAT17qXm4/zd+9QA== dependencies: find-index "^0.1.1" -glob@^10.4.5: +glob@^10.3.10, glob@^10.4.5: version "10.4.5" resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== @@ -10102,7 +10031,7 @@ glob@^10.4.5: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" -glob@^7.0.0, glob@^7.0.5, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0, glob@^7.2.3: +glob@^7.0.0, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0, glob@^7.2.3: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -11353,18 +11282,6 @@ is-docker@^3.0.0: resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200" integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ== -is-dotfile@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" - integrity sha512-9YclgOGtN/f8zx0Pr4FQYMdibBiTaH3sn52vjYip4ZSf6C4/6RfTEZ+MR4GvKhCxdPh21Bg42/WL55f6KSnKpg== - -is-equal-shallow@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" - integrity sha512-0EygVC5qPvIyb+gSz7zdD5/AAoS6Qrx1e//6N4yv4oNm30kqvdmG66oZFWVlQHUWe5OjP08FuTw2IdT0EOTcYA== - dependencies: - is-primitive "^2.0.0" - is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" @@ -11377,11 +11294,6 @@ is-extendable@^1.0.1: dependencies: is-plain-object "^2.0.4" -is-extglob@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" - integrity sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww== - is-extglob@^2.1.0, is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -11440,13 +11352,6 @@ is-generator-function@^1.0.7: has-tostringtag "^1.0.2" safe-regex-test "^1.1.0" -is-glob@^2.0.0, is-glob@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" - integrity sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg== - dependencies: - is-extglob "^1.0.0" - is-glob@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" @@ -11598,21 +11503,11 @@ is-plain-object@^5.0.0: resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== -is-posix-bracket@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" - integrity sha512-Yu68oeXJ7LeWNmZ3Zov/xg/oDBnBK2RNxwYY1ilNJX+tKKZqgPK+qOn/Gs9jEu66KDY9Netf5XLKNGzas/vPfQ== - is-potential-custom-element-name@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== -is-primitive@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" - integrity sha512-N3w1tFaRfk3UrPfqeRyD+GYDASU3W5VinKhlORy8EWVf/sIdDL9GAcew85XmktCfH+ngG7SRXEVDoO18WMdB/Q== - is-regex@^1.0.5, is-regex@^1.1.0, is-regex@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" @@ -13687,25 +13582,6 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== -micromatch@^2.1.5: - version "2.3.11" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" - integrity sha512-LnU2XFEk9xxSJ6rfgAry/ty5qwUTyHYOBU0g4R6tIw5ljwgGIBmiKhRWLw5NpMOnrgUNcDJ4WMp8rl3sYVHLNA== - dependencies: - arr-diff "^2.0.0" - array-unique "^0.2.1" - braces "^1.8.2" - expand-brackets "^0.1.4" - extglob "^0.3.1" - filename-regex "^2.0.0" - is-extglob "^1.0.0" - is-glob "^2.0.1" - kind-of "^3.0.2" - normalize-path "^2.0.1" - object.omit "^2.0.0" - parse-glob "^3.0.4" - regex-cache "^0.4.2" - micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -13811,7 +13687,7 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== -minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.0, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -13825,6 +13701,13 @@ minimatch@^5.0.1, minimatch@^5.1.0, minimatch@^5.1.6: dependencies: brace-expansion "^2.0.1" +minimatch@^9.0.0: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + minimatch@^9.0.4: version "9.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" @@ -14245,7 +14128,7 @@ normalize-package-data@^3.0.0: semver "^7.3.4" validate-npm-package-license "^3.0.1" -normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1: +normalize-path@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" integrity sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w== @@ -14505,14 +14388,6 @@ object.hasown@^1.1.4: es-abstract "^1.23.2" es-object-atoms "^1.0.0" -object.omit@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" - integrity sha512-UiAM5mhmIuKLsOvrL+B0U2d1hXHF3bFYWIuH1LMpuV2EJEHG1Ntz06PgLEHjm6VFd87NpH8rastvPoyv6UW2fA== - dependencies: - for-own "^0.1.4" - is-extendable "^0.1.1" - object.pick@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" @@ -14846,6 +14721,11 @@ p-map@^4.0.0: dependencies: aggregate-error "^3.0.0" +p-map@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-6.0.0.tgz#4d9c40d3171632f86c47601b709f4b4acd70fed4" + integrity sha512-T8BatKGY+k5rU+Q/GTYgrEf2r4xRMevAN5mtXc2aPc4rS1j3s+vWTaO2Wag94neXuCAUAs8cxBL9EeB5EA6diw== + p-reduce@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa" @@ -14988,16 +14868,6 @@ parse-entities@^1.0.2, parse-entities@^1.1.0: is-decimal "^1.0.0" is-hexadecimal "^1.0.0" -parse-glob@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" - integrity sha512-FC5TeK0AwXzq3tUBFtH74naWkPQCEWs4K+xMxWZBlKDWu0bVHXGZa+KKqxKidd7xwhdZ19ZNuF2uO1M/r196HA== - dependencies: - glob-base "^0.3.0" - is-dotfile "^1.0.0" - is-extglob "^1.0.0" - is-glob "^2.0.0" - parse-imports@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/parse-imports/-/parse-imports-2.1.0.tgz#22a152f4503da34e8fb89c902383895f59e93d2d" @@ -15953,7 +15823,7 @@ readdir-glob@^1.0.0, readdir-glob@^1.1.2: dependencies: minimatch "^5.1.0" -readdirp@^2.0.0, readdirp@^2.2.1: +readdirp@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== @@ -16051,11 +15921,6 @@ regenerate@^1.4.2: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== -regenerator-runtime@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" - integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== - regenerator-runtime@^0.13.4: version "0.13.11" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" @@ -16073,13 +15938,6 @@ regenerator-transform@^0.15.2: dependencies: "@babel/runtime" "^7.8.4" -regex-cache@^0.4.2: - version "0.4.4" - resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd" - integrity sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ== - dependencies: - is-equal-shallow "^0.1.3" - regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" @@ -16411,7 +16269,7 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg== -resolve@^1.1.3, resolve@^1.1.4, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.20.0, resolve@^1.22.4, resolve@^1.4.0, resolve@^1.8.1: +resolve@^1.1.3, resolve@^1.1.4, resolve@^1.1.6, resolve@^1.10.0, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.20.0, resolve@^1.22.4, resolve@^1.4.0, resolve@^1.8.1: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -16941,6 +16799,11 @@ shell-quote@^1.4.2, shell-quote@^1.6.1, shell-quote@^1.8.1: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== +shell-quote@^1.8.0: + version "1.8.2" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.2.tgz#d2d83e057959d53ec261311e9e9b8f51dcb2934a" + integrity sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA== + shelljs@0.8.5, shelljs@^0.8.1, shelljs@^0.8.3, shelljs@^0.8.4: version "0.8.5" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" From 1a544d689f54aec159113d6975e3e92acdf10ec4 Mon Sep 17 00:00:00 2001 From: Airtable Date: Mon, 16 Jun 2025 23:39:12 +0000 Subject: [PATCH 04/14] @airtable/blocks@0.0.0-experimental-15dc8a3ea-20250616 --- packages/sdk/package.json | 16 +- .../sdk/scripts/interface-alpha-release.js | 212 ++++ packages/sdk/src/base/models/base.ts | 2 +- packages/sdk/src/base/models/field.ts | 2 +- .../models/linked_records_query_result.ts | 2 +- packages/sdk/src/base/models/models.ts | 2 +- packages/sdk/src/base/models/mutations.ts | 233 +--- packages/sdk/src/base/models/record.ts | 3 +- .../src/base/models/record_query_result.ts | 2 +- packages/sdk/src/base/models/table.ts | 1006 +---------------- .../sdk/src/base/types/airtable_interface.ts | 2 +- packages/sdk/src/base/types/base.ts | 7 +- packages/sdk/src/base/types/field.ts | 7 + packages/sdk/src/base/types/mutations.ts | 98 +- packages/sdk/src/base/types/table.ts | 11 +- packages/sdk/src/base/ui/cell_renderer.tsx | 2 +- packages/sdk/src/base/ui/field_picker.tsx | 2 +- packages/sdk/src/base/ui/record_card.tsx | 2 +- .../sdk/src/base/unstable_testing_utils.ts | 3 +- packages/sdk/src/interface/models/models.ts | 2 +- .../sdk/src/interface/models/mutations.ts | 55 +- packages/sdk/src/interface/models/record.ts | 35 + .../src/interface/types/airtable_interface.ts | 15 +- packages/sdk/src/interface/types/base.ts | 7 +- packages/sdk/src/interface/types/field.ts | 11 + packages/sdk/src/interface/types/table.ts | 17 +- packages/sdk/src/sdk_mode.ts | 18 +- packages/sdk/src/shared/models/field_core.ts | 6 +- .../sdk/src/shared/models/mutations_core.ts | 244 +++- packages/sdk/src/shared/models/record_core.ts | 4 +- packages/sdk/src/shared/models/table_core.ts | 1004 +++++++++++++++- .../shared/types/airtable_interface_core.ts | 18 +- packages/sdk/src/shared/types/base_core.ts | 6 +- .../shared/types/{field.ts => field_core.ts} | 4 +- .../sdk/src/shared/types/mutations_core.ts | 102 +- packages/sdk/src/shared/types/table_core.ts | 9 +- .../abstract_mock_airtable_interface.ts | 2 +- .../sdk/stories/cell_renderer.stories.tsx | 2 +- packages/sdk/stories/field_icon.stories.tsx | 2 +- .../stories/helpers/fake_cell_renderer.tsx | 2 +- .../sdk/stories/helpers/fake_record_card.tsx | 2 +- packages/sdk/stories/helpers/field_type.ts | 2 +- .../sdk/stories/record_card_list.stories.tsx | 2 +- .../airtable_interface_mocks/fixture_data.ts | 3 +- .../linked_records.ts | 2 +- .../project_tracker.tsx | 2 +- packages/sdk/test/models/base.test.ts | 2 +- packages/sdk/test/models/field.test.ts | 2 +- packages/sdk/test/models/mutations.test.ts | 2 +- packages/sdk/test/models/session.test.ts | 4 +- packages/sdk/test/models/table.test.ts | 2 +- .../models/table_or_view_query_result.test.ts | 2 +- 52 files changed, 1786 insertions(+), 1420 deletions(-) create mode 100644 packages/sdk/scripts/interface-alpha-release.js create mode 100644 packages/sdk/src/base/types/field.ts create mode 100644 packages/sdk/src/interface/types/field.ts rename packages/sdk/src/shared/types/{field.ts => field_core.ts} (99%) diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 8a8bb574c..4f671fd6a 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -66,7 +66,7 @@ "ci": "echo '--- sdk' && yarn run build && yarn run test:coverage && ./scripts/check_typescript_when_installed_in_block.sh", "pretest": "yarn run lint && yarn run types", "version": "changelog-publish --github-repo-url='https://github.com/airtable/blocks' --git-tag-prefix='@airtable/blocks@' && yarn run build:docs && git add CHANGELOG.md ../blocks-docs/docs.json", - "release": "npm_config_registry=https://registry.npmjs.org/ release-it", + "release": "node ./scripts/interface-alpha-release.js", "types": "tsc", "lint": "ESLINT_USE_FLAT_CONFIG=false eslint --report-unused-disable-directives --ext .js,.ts,.tsx src test", "lint:quiet": "yarn run lint --quiet", @@ -176,19 +176,5 @@ "statements": 100 } } - }, - "release-it": { - "git": { - "tagName": "@airtable/blocks@${version}", - "commitMessage": "Release @airtable/blocks@${version}" - }, - "hooks": { - "before:init": "../../bin/check-repo-for-release && yarn build && yarn test", - "after:bump": "yarn build && rm -rf dist/types/{stories,test}", - "after:release": "../../tools/git-mirror/bin/git-mirror sync @airtable/blocks@${version}" - }, - "npm": { - "access": "public" - } } } diff --git a/packages/sdk/scripts/interface-alpha-release.js b/packages/sdk/scripts/interface-alpha-release.js new file mode 100644 index 000000000..615deff9c --- /dev/null +++ b/packages/sdk/scripts/interface-alpha-release.js @@ -0,0 +1,212 @@ +const {execSync} = require('child_process'); +const readline = require('readline'); +const path = require('path'); +const fs = require('fs'); + +function execCommand(command) { + try { + return execSync(command, {encoding: 'utf8'}).trim(); + } catch (error) { + console.error(`Error executing command: ${command}`); + console.error(error.message); + process.exit(1); + } +} + +function checkMainBranch() { + const currentBranch = execCommand('git rev-parse --abbrev-ref HEAD'); + if (currentBranch !== 'main') { + console.error('❌ Error: Must be on main branch to release'); + process.exit(1); + } +} + +function checkUncommittedChanges() { + const status = execCommand('git status --porcelain'); + if (status) { + console.error('❌ Error: There are uncommitted changes in the working tree'); + process.exit(1); + } +} + +function checkRemoteSync() { + execCommand('git fetch origin'); + const localHead = execCommand('git rev-parse HEAD'); + const remoteHead = execCommand('git rev-parse origin/main'); + + if (localHead !== remoteHead) { + console.error('❌ Error: Local main branch is not in sync with origin/main'); + process.exit(1); + } +} + +function createVersionString() { + const gitSha = execCommand('git rev-parse HEAD').substring(0, 9); + const date = new Date() + .toISOString() + .slice(0, 10) + .replace(/-/g, ''); + return `0.0.0-experimental-${gitSha}-${date}`; +} + +/** + * Prompts the user with a y/n question, and resolves if the user answers 'y'. + */ +function promptUser(rl, questionString) { + return new Promise(resolve => { + rl.question(questionString, answer => { + resolve(answer.toLowerCase() === 'y'); + }); + }); +} + +function updatePackageJsonVersion(versionString) { + const packageJsonPath = path.join(__dirname, '..', 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + const originalVersion = packageJson.version; + + packageJson.version = versionString; + fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 4) + '\n'); + + return originalVersion; +} + +function restorePackageJsonVersion(originalVersion) { + const packageJsonPath = path.join(__dirname, '..', 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + + packageJson.version = originalVersion; + fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 4) + '\n'); +} + +/** + * Does a "test run" of modifying package.json and restoring it. + */ +function verifyPackageJsonModification(versionString) { + restorePackageJsonVersion(updatePackageJsonVersion(versionString)); + const statusAfterRestore = execCommand('git status --porcelain'); + if (statusAfterRestore) { + console.error('❌ Error: Package.json modification resulted in unexpected changes'); + console.error('Changes detected:'); + console.error(statusAfterRestore); + process.exit(1); + } +} + +/** + * Verifies that the current working directory is packages/sdk + */ +function verifyWorkingDirectory() { + const expectedDir = path.join(__dirname, '..'); + const currentDir = process.cwd(); + + if (currentDir !== expectedDir) { + console.error('❌ Error: Script must be run from the packages/sdk directory'); + console.error(`Current directory: ${currentDir}`); + console.error(`Expected directory: ${expectedDir}`); + console.error('\nPlease run this script with `yarn workspace @airtable/blocks release`'); + process.exit(1); + } +} + +/** + * Checks if a git tag with this tagName already exists. + */ +function checkGitTag(tagName) { + const existingTag = execCommand(`git tag --list ${tagName}`).trim(); + if (existingTag) { + console.error(`❌ Error: Git tag ${tagName} already exists`); + process.exit(1); + } +} + +function getNpmOtp(rl) { + return new Promise(resolve => { + rl.question('Enter npm one-time password: ', answer => { + resolve(answer.trim()); + }); + }); +} + +async function main() { + let originalVersion = null; + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + try { + console.log('Checking prerequisites...'); + verifyWorkingDirectory(); + checkMainBranch(); + checkUncommittedChanges(); + checkRemoteSync(); + + console.log('Building and testing...'); + execCommand('yarn build'); + execCommand('yarn test'); + execCommand('rm -rf dist/types/{stories,test}'); + + const versionString = createVersionString(); + const confirmed = await promptUser( + rl, + `Version string will be: ${versionString}\nDoes this look good? (y/n): `, + ); + if (!confirmed) { + console.log('Release cancelled by user'); + process.exit(0); + } + + const gitTagName = `@airtable/blocks@${versionString}`; + checkGitTag(gitTagName); + + verifyPackageJsonModification(versionString); + + originalVersion = updatePackageJsonVersion(versionString); + + const npmRegistry = 'https://registry.npmjs.org/'; + const npmTagName = 'interface-alpha-next'; + console.log('Performing dry run of npm publish...'); + const dryRunOutput = execCommand( + `npm publish --dry-run --tag ${npmTagName} --registry ${npmRegistry}`, + ); + console.log('\nDry run output:'); + console.log(dryRunOutput); + + const publishConfirmed = await promptUser(rl, 'Does the dry run output look good? (y/n): '); + if (!publishConfirmed) { + console.log('Publish cancelled by user'); + restorePackageJsonVersion(originalVersion); + process.exit(0); + } + + const otp = await getNpmOtp(rl); + + console.log('Publishing to NPM...'); + execCommand(`npm publish --tag ${npmTagName} --registry ${npmRegistry} --otp ${otp}`); + console.log(`✅ Published to NPM with ${npmTagName} tag`); + + restorePackageJsonVersion(originalVersion); + + console.log('Creating and pushing git tag...'); + execCommand(`git tag ${gitTagName}`); + execCommand(`git push origin tag ${gitTagName}`); + console.log(`✅ Created and pushed git tag @airtable/blocks@${versionString}`); + + console.log('** IMPORTANT **'); + console.log( + `Verify that this version is good. Once you are satisfied that it should be released to customers, manually run \`npm dist-tag add @airtable/blocks@${versionString} interface-alpha\``, + ); + } catch (error) { + console.error('Error during release process:', error); + if (originalVersion) { + restorePackageJsonVersion(originalVersion); + } + process.exit(1); + } finally { + rl.close(); + } +} + +main(); diff --git a/packages/sdk/src/base/models/base.ts b/packages/sdk/src/base/models/base.ts index 46d6ef50c..6e848a48b 100644 --- a/packages/sdk/src/base/models/base.ts +++ b/packages/sdk/src/base/models/base.ts @@ -1,7 +1,7 @@ /** @module @airtable/blocks/models: Base */ /** */ import {BaseCore, ChangedPathsForType, WatchableBaseKeys} from '../../shared/models/base_core'; import {MutationTypes} from '../types/mutations'; -import {FieldType} from '../../shared/types/field'; +import {FieldType} from '../../shared/types/field_core'; import {PermissionCheckResult} from '../../shared/types/mutations_core'; import {BaseSdkMode} from '../../sdk_mode'; import {TableId} from '../../shared/types/hyper_ids'; diff --git a/packages/sdk/src/base/models/field.ts b/packages/sdk/src/base/models/field.ts index c481a4b30..91721be6c 100644 --- a/packages/sdk/src/base/models/field.ts +++ b/packages/sdk/src/base/models/field.ts @@ -1,6 +1,6 @@ /** @module @airtable/blocks/models: Field */ /** */ import {FieldCore} from '../../shared/models/field_core'; -import {FieldOptions} from '../../shared/types/field'; +import {FieldOptions} from '../../shared/types/field_core'; import {UpdateFieldOptionsOpts, MutationTypes} from '../types/mutations'; import {BaseSdkMode} from '../../sdk_mode'; import {PermissionCheckResult} from '../../shared/types/mutations_core'; diff --git a/packages/sdk/src/base/models/linked_records_query_result.ts b/packages/sdk/src/base/models/linked_records_query_result.ts index f17005181..5394556d8 100644 --- a/packages/sdk/src/base/models/linked_records_query_result.ts +++ b/packages/sdk/src/base/models/linked_records_query_result.ts @@ -1,5 +1,5 @@ /** @module @airtable/blocks/models: RecordQueryResult */ /** */ -import {FieldType} from '../../shared/types/field'; +import {FieldType} from '../../shared/types/field_core'; import Sdk from '../sdk'; import {FlowAnyFunction, FlowAnyObject, ObjectMap} from '../../shared/private_utils'; import {invariant} from '../../shared/error_utils'; diff --git a/packages/sdk/src/base/models/models.ts b/packages/sdk/src/base/models/models.ts index f8dea8bea..f6eb9a1c7 100644 --- a/packages/sdk/src/base/models/models.ts +++ b/packages/sdk/src/base/models/models.ts @@ -1,7 +1,7 @@ /** @ignore */ /** */ import * as recordColoring from './record_coloring'; import createAggregators from './create_aggregators'; -export {FieldType, FieldConfig} from '../../shared/types/field'; +export {FieldType, FieldConfig} from '../../shared/types/field_core'; export {ViewType} from '../types/view'; export {default as Base} from './base'; export {default as Table} from './table'; diff --git a/packages/sdk/src/base/models/mutations.ts b/packages/sdk/src/base/models/mutations.ts index d276a8647..6e7a725a4 100644 --- a/packages/sdk/src/base/models/mutations.ts +++ b/packages/sdk/src/base/models/mutations.ts @@ -1,7 +1,6 @@ import {BlockRunContextType} from '../types/airtable_interface'; import {ModelChange} from '../../shared/types/base_core'; import {Mutation, MutationTypes} from '../types/mutations'; -import {entries, ObjectMap} from '../../shared/private_utils'; import {spawnError, spawnUnknownSwitchCaseError} from '../../shared/error_utils'; import {FieldId} from '../../shared/types/hyper_ids'; import { @@ -10,26 +9,21 @@ import { MAX_TABLE_NAME_LENGTH, MAX_NUM_FIELDS_PER_TABLE, } from '../../shared/types/mutation_constants'; -import {MutationsCore, MUTATIONS_MAX_BATCH_SIZE} from '../../shared/models/mutations_core'; +import {MutationsCore} from '../../shared/models/mutations_core'; import {BaseSdkMode} from '../../sdk_mode'; import Table from './table'; -import Field from './field'; +import RecordStore from './record_store'; /** @hidden */ class Mutations extends MutationsCore { /** @internal */ - _doesMutationExceedBatchSizeLimit(mutation: Mutation): boolean { - switch (mutation.type) { - case MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES: - case MutationTypes.CREATE_MULTIPLE_RECORDS: - return mutation.records.length > MUTATIONS_MAX_BATCH_SIZE; - case MutationTypes.DELETE_MULTIPLE_RECORDS: - return mutation.recordIds.length > MUTATIONS_MAX_BATCH_SIZE; - case MutationTypes.SET_MULTIPLE_GLOBAL_CONFIG_PATHS: - return mutation.updates.length > MUTATIONS_MAX_BATCH_SIZE; - default: - return false; - } + _isRecordStoreReadyForMutations(recordStore: RecordStore): boolean { + return recordStore.isRecordMetadataLoaded; + } + + /** @internal */ + _isFieldAvailableForMutation(recordStore: RecordStore, fieldId: FieldId): boolean { + return recordStore.areCellValuesLoadedForFieldId(fieldId); } /** @internal */ @@ -39,131 +33,11 @@ class Mutations extends MutationsCore { const billingPlanGrouping = this._base.__billingPlanGrouping; switch (mutation.type) { - case MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES: { - const {tableId, records} = mutation; - const table = this._base.getTableByIdIfExists(tableId); - if (!table) { - throw spawnError("Can't set cell values: No table with id %s exists", tableId); - } - - const recordStore = this._base.__getRecordStore(tableId); - - const checkedFieldIds = new Set(); - - for (const record of records) { - let existingRecord = null; - if (recordStore.isRecordMetadataLoaded) { - existingRecord = recordStore.getRecordByIdIfExists(record.id); - if (!existingRecord) { - throw spawnError( - "Can't set cell values: No record with id %s exists", - record.id, - ); - } - } - - for (const fieldId of Object.keys(record.cellValuesByFieldId)) { - const field = table.getFieldByIdIfExists(fieldId); - if (!field) { - throw spawnError( - "Can't set cell values: No field with id %s exists in table '%s'", - fieldId, - table.name, - ); - } - - if (!checkedFieldIds.has(fieldId)) { - this._assertFieldIsValidForMutation(field); - checkedFieldIds.add(fieldId); - } - - if (existingRecord && recordStore.areCellValuesLoadedForFieldId(fieldId)) { - const validationResult = this._airtableInterface.fieldTypeProvider.validateCellValueForUpdate( - appInterface, - record.cellValuesByFieldId[fieldId], - existingRecord._getRawCellValue(field), - field._data, - ); - if (!validationResult.isValid) { - throw spawnError( - "Can't set cell values: invalid cell value for field '%s'.\n%s", - field.name, - validationResult.reason, - ); - } - } - } - } - return; - } - + case MutationTypes.SET_MULTIPLE_GLOBAL_CONFIG_PATHS: + case MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES: + case MutationTypes.CREATE_MULTIPLE_RECORDS: case MutationTypes.DELETE_MULTIPLE_RECORDS: { - const {tableId, recordIds} = mutation; - const table = this._base.getTableByIdIfExists(tableId); - if (!table) { - throw spawnError("Can't delete records: No table with id %s exists", tableId); - } - - const recordStore = this._base.__getRecordStore(tableId); - if (recordStore.isRecordMetadataLoaded) { - for (const recordId of recordIds) { - const record = recordStore.getRecordByIdIfExists(recordId); - if (!record) { - throw spawnError( - "Can't delete records: No record with id %s exists in table '%s'", - recordId, - table.name, - ); - } - } - } - return; - } - - case MutationTypes.CREATE_MULTIPLE_RECORDS: { - const {tableId, records} = mutation; - const checkedFieldIds = new Set(); - - const table = this._base.getTableByIdIfExists(tableId); - if (!table) { - throw spawnError("Can't create records: No table with id %s exists", tableId); - } - - for (const record of records) { - for (const fieldId of Object.keys(record.cellValuesByFieldId)) { - const field = table.getFieldByIdIfExists(fieldId); - if (!field) { - throw spawnError( - "Can't create records: No field with id %s exists in table '%s'", - fieldId, - table.name, - ); - } - - if (!checkedFieldIds.has(fieldId)) { - this._assertFieldIsValidForMutation(field); - checkedFieldIds.add(fieldId); - } - - const validationResult = this._airtableInterface.fieldTypeProvider.validateCellValueForUpdate( - appInterface, - record.cellValuesByFieldId[fieldId], - null, - field._data, - ); - if (!validationResult.isValid) { - throw spawnError( - "Can't create records: invalid cell value for field '%s'.\n%s", - field.name, - validationResult.reason, - ); - } - } - } - return; - } - - case MutationTypes.SET_MULTIPLE_GLOBAL_CONFIG_PATHS: { + super._assertMutationIsValid(mutation); return; } @@ -417,16 +291,6 @@ class Mutations extends MutationsCore { } } - /** @internal */ - _assertFieldIsValidForMutation(field: Field) { - if (field.isComputed) { - throw spawnError( - "Can't set cell values: Field '%s' is computed and cannot be set", - field.name, - ); - } - } - /** @internal */ _assertFieldNameIsValidForMutation(name: string, table: Table) { if (!name) { @@ -453,96 +317,53 @@ class Mutations extends MutationsCore { /** @internal */ _getOptimisticModelChangesForMutation(mutation: Mutation): Array { switch (mutation.type) { - case MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES: { + case MutationTypes.SET_MULTIPLE_GLOBAL_CONFIG_PATHS: + case MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES: + return super._getOptimisticModelChangesForMutation(mutation); + case MutationTypes.CREATE_MULTIPLE_RECORDS: { const {tableId, records} = mutation; const recordStore = this._base.__getRecordStore(tableId); - return records.flatMap(record => - Object.keys(record.cellValuesByFieldId) - .filter(fieldId => recordStore.areCellValuesLoadedForFieldId(fieldId)) - .map(fieldId => ({ - path: [ - 'tablesById', - tableId, - 'recordsById', - record.id, - 'cellValuesByFieldId', - fieldId, - ], - value: record.cellValuesByFieldId[fieldId], - })), - ); - } - - case MutationTypes.DELETE_MULTIPLE_RECORDS: { - const {tableId, recordIds} = mutation; - const recordStore = this._base.__getRecordStore(tableId); - if (!recordStore.isRecordMetadataLoaded) { return []; } + const superChanges = super._getOptimisticModelChangesForMutation(mutation); return [ - ...recordIds.map(recordId => ({ - path: ['tablesById', tableId, 'recordsById', recordId], - value: undefined, - })), + ...superChanges, ...this._base.getTableById(tableId).views.flatMap(view => { const viewDataStore = recordStore.getViewDataStore(view.id); if (!viewDataStore.isDataLoaded) { return []; } - return viewDataStore.__generateChangesForParentTableDeleteMultipleRecords( - recordIds, + return viewDataStore.__generateChangesForParentTableAddMultipleRecords( + records.map(record => record.id), ); }), ]; } - - case MutationTypes.CREATE_MULTIPLE_RECORDS: { - const {tableId, records} = mutation; + case MutationTypes.DELETE_MULTIPLE_RECORDS: { + const {tableId, recordIds} = mutation; const recordStore = this._base.__getRecordStore(tableId); - if (!recordStore.isRecordMetadataLoaded) { return []; } + const superChanges = super._getOptimisticModelChangesForMutation(mutation); return [ - ...records.map(record => { - const filteredCellValuesByFieldId: ObjectMap = {}; - for (const [fieldId, cellValue] of entries(record.cellValuesByFieldId)) { - if (recordStore.areCellValuesLoadedForFieldId(fieldId)) { - filteredCellValuesByFieldId[fieldId] = cellValue; - } - } - return { - path: ['tablesById', tableId, 'recordsById', record.id], - value: { - id: record.id, - cellValuesByFieldId: filteredCellValuesByFieldId, - commentCount: 0, - createdTime: new Date().toJSON(), - }, - }; - }), + ...superChanges, ...this._base.getTableById(tableId).views.flatMap(view => { const viewDataStore = recordStore.getViewDataStore(view.id); if (!viewDataStore.isDataLoaded) { return []; } - return viewDataStore.__generateChangesForParentTableAddMultipleRecords( - records.map(record => record.id), + return viewDataStore.__generateChangesForParentTableDeleteMultipleRecords( + recordIds, ); }), ]; } - case MutationTypes.SET_MULTIPLE_GLOBAL_CONFIG_PATHS: { - throw spawnError( - 'attempting to generate model updates for SET_MULTIPLE_GLOBAL_CONFIG_PATH', - ); - } - case MutationTypes.CREATE_SINGLE_FIELD: case MutationTypes.UPDATE_SINGLE_FIELD_CONFIG: case MutationTypes.UPDATE_SINGLE_FIELD_DESCRIPTION: diff --git a/packages/sdk/src/base/models/record.ts b/packages/sdk/src/base/models/record.ts index ed3604180..9a2458261 100644 --- a/packages/sdk/src/base/models/record.ts +++ b/packages/sdk/src/base/models/record.ts @@ -7,6 +7,7 @@ import {isEnumValue, ObjectValues, FlowAnyObject, isObjectEmpty} from '../../sha import BlockSdk from '../sdk'; import {invariant} from '../../shared/error_utils'; import colorUtils from '../../shared/color_utils'; +import {FieldType} from '../../shared/types/field_core'; import LinkedRecordsQueryResult from './linked_records_query_result'; import ObjectPool from './object_pool'; import RecordStore from './record_store'; @@ -95,7 +96,7 @@ class Record extends RecordCore { ); return super.getCellValueAsString(field.id); } - _getRawCellValue(field: Field): unknown { + _getRawCellValue(field: {id: FieldId; type: FieldType}): unknown { invariant( this._parentRecordStore.areCellValuesLoadedForFieldId(field.id), 'Cell values for field %s are not loaded', diff --git a/packages/sdk/src/base/models/record_query_result.ts b/packages/sdk/src/base/models/record_query_result.ts index 5de449fef..1d6a94d5b 100644 --- a/packages/sdk/src/base/models/record_query_result.ts +++ b/packages/sdk/src/base/models/record_query_result.ts @@ -2,7 +2,7 @@ import Colors, {Color} from '../../shared/colors'; import Sdk from '../sdk'; import {FieldId, RecordId} from '../../shared/types/hyper_ids'; -import {FieldType} from '../../shared/types/field'; +import {FieldType} from '../../shared/types/field_core'; import { isEnumValue, assertEnumValue, diff --git a/packages/sdk/src/base/models/table.ts b/packages/sdk/src/base/models/table.ts index 3ebd2adef..3c88667d3 100644 --- a/packages/sdk/src/base/models/table.ts +++ b/packages/sdk/src/base/models/table.ts @@ -2,18 +2,11 @@ import {TableCore, WatchableTableKeysCore} from '../../shared/models/table_core'; import {ViewType} from '../types/view'; import {spawnError} from '../../shared/error_utils'; -import { - entries, - cast, - ObjectMap, - keys, - isEnumValue, - ObjectValues, -} from '../../shared/private_utils'; +import {entries, cast, isEnumValue, ObjectValues} from '../../shared/private_utils'; import BlockSdk from '../sdk'; -import {FieldId, ViewId, RecordId} from '../../shared/types/hyper_ids'; +import {FieldId, ViewId} from '../../shared/types/hyper_ids'; import {TableData} from '../types/table'; -import {FieldType, FieldOptions} from '../../shared/types/field'; +import {FieldType, FieldOptions} from '../../shared/types/field_core'; import {MutationTypes} from '../types/mutations'; import {BaseSdkMode} from '../../sdk_mode'; import {PermissionCheckResult} from '../../shared/types/mutations_core'; @@ -25,7 +18,6 @@ import TableOrViewQueryResult from './table_or_view_query_result'; import View from './view'; import ObjectPool from './object_pool'; import Base from './base'; -import Record from './record'; export const WatchableTableKeys = Object.freeze({ ...WatchableTableKeysCore, @@ -533,998 +525,6 @@ class Table extends TableCore { return this.getFieldById(fieldId); } - /** - * Updates cell values for a record. - * - * Throws an error if the user does not have permission to update the given cell values in - * the record, or if invalid input is provided (eg. invalid cell values). - * - * Refer to {@link FieldType} for cell value write formats. - * - * This action is asynchronous: `await` the returned promise if you wish to wait for the updated - * cell values to be persisted to Airtable servers. - * Updates are applied optimistically locally, so your changes will be reflected in your extension - * before the promise resolves. - * - * @param recordOrRecordId the record to update - * @param fields cell values to update in that record, specified as object mapping `FieldId` or field name to value for that field. - * @example - * ```js - * function updateRecord(record, recordFields) { - * if (table.hasPermissionToUpdateRecord(record, recordFields)) { - * table.updateRecordAsync(record, recordFields); - * } - * // The updated values will now show in your extension (eg in - * // `table.selectRecords()` result) but are still being saved to Airtable - * // servers (e.g. other users may not be able to see them yet). - * } - * - * async function updateRecordAsync(record, recordFields) { - * if (table.hasPermissionToUpdateRecord(record, recordFields)) { - * await table.updateRecordAsync(record, recordFields); - * } - * // New record has been saved to Airtable servers. - * alert(`record with ID ${record.id} has been updated`); - * } - * - * // Fields can be specified by name or ID - * updateRecord(record1, { - * 'Post Title': 'How to make: orange-mango pound cake', - * 'Publication Date': '2020-01-01', - * }); - * updateRecord(record2, { - * [postTitleField.id]: 'Cake decorating tips & tricks', - * [publicationDateField.id]: '2020-02-02', - * }); - * - * // Cell values should generally have format matching the output of - * // record.getCellValue() for the field being updated - * updateRecord(record1, { - * 'Category (single select)': {name: 'Recipe'}, - * 'Tags (multiple select)': [{name: 'Desserts'}, {id: 'someChoiceId'}], - * 'Images (attachment)': [{url: 'http://mywebsite.com/cake.png'}], - * 'Related posts (linked records)': [{id: 'someRecordId'}], - * }); - * ``` - */ - async updateRecordAsync( - recordOrRecordId: Record | RecordId, - fields: ObjectMap, - ): Promise { - const recordId = - typeof recordOrRecordId === 'string' ? recordOrRecordId : recordOrRecordId.id; - - await this.updateRecordsAsync([ - { - id: recordId, - fields, - }, - ]); - } - /** - * Checks whether the current user has permission to perform the given record update. - * - * Accepts partial input, in the same format as {@link updateRecordAsync}. - * The more information provided, the more accurate the permissions check will be. - * - * Returns `{hasPermission: true}` if the current user can update the specified record, - * `{hasPermission: false, reasonDisplayString: string}` otherwise. `reasonDisplayString` may be - * used to display an error message to the user. - * - * @param recordOrRecordId the record to update - * @param fields cell values to update in that record, specified as object mapping `FieldId` or field name to value for that field. - * @example - * ```js - * // Check if user can update specific fields for a specific record. - * const updateRecordCheckResult = - * table.checkPermissionsForUpdateRecord(record, { - * 'Post Title': 'How to make: orange-mango pound cake', - * 'Publication Date': '2020-01-01', - * }); - * if (!updateRecordCheckResult.hasPermission) { - * alert(updateRecordCheckResult.reasonDisplayString); - * } - * - * // Like updateRecordAsync, you can use either field names or field IDs. - * const updateRecordCheckResultWithFieldIds = - * table.checkPermissionsForUpdateRecord(record, { - * [postTitleField.id]: 'Cake decorating tips & tricks', - * [publicationDateField.id]: '2020-02-02', - * }); - * - * // Check if user could update a given record, when you don't know the - * // specific fields that will be updated yet (e.g. to check whether you should - * // allow a user to select a certain record to update). - * const updateUnknownFieldsCheckResult = - * table.checkPermissionsForUpdateRecord(record); - * - * // Check if user could update specific fields, when you don't know the - * // specific record that will be updated yet. (for example, if the field is - * // selected by the user and you want to check if your extension can write to it). - * const updateUnknownRecordCheckResult = - * table.checkPermissionsForUpdateRecord(undefined, { - * 'My field name': 'updated value', - * // You can use undefined if you know you're going to update a field, - * // but don't know the new cell value yet. - * 'Another field name': undefined, - * }); - * - * // Check if user could perform updates within the table, without knowing the - * // specific record or fields that will be updated yet (e.g., to render your - * // extension in "read only" mode). - * const updateUnknownRecordAndFieldsCheckResult = - * table.checkPermissionsForUpdateRecord(); - * ``` - */ - checkPermissionsForUpdateRecord( - recordOrRecordId?: Record | RecordId, - fields?: ObjectMap, - ): PermissionCheckResult { - const recordId = - typeof recordOrRecordId === 'object' && recordOrRecordId !== null - ? recordOrRecordId.id - : recordOrRecordId; - - return this.checkPermissionsForUpdateRecords([ - { - id: recordId, - fields, - }, - ]); - } - /** - * An alias for `checkPermissionsForUpdateRecord(recordOrRecordId, fields).hasPermission`. - * - * Checks whether the current user has permission to perform the given record update. - * - * Accepts partial input, in the same format as {@link updateRecordAsync}. - * The more information provided, the more accurate the permissions check will be. - * - * @param recordOrRecordId the record to update - * @param fields cell values to update in that record, specified as object mapping `FieldId` or field name to value for that field. - * @example - * ```js - * // Check if user can update specific fields for a specific record. - * const canUpdateRecord = table.hasPermissionToUpdateRecord(record, { - * 'Post Title': 'How to make: orange-mango pound cake', - * 'Publication Date': '2020-01-01', - * }); - * if (!canUpdateRecord) { - * alert('not allowed!'); - * } - * - * // Like updateRecordAsync, you can use either field names or field IDs. - * const canUpdateRecordWithFieldIds = - * table.hasPermissionToUpdateRecord(record, { - * [postTitleField.id]: 'Cake decorating tips & tricks', - * [publicationDateField.id]: '2020-02-02', - * }); - * - * // Check if user could update a given record, when you don't know the - * // specific fields that will be updated yet (e.g. to check whether you should - * // allow a user to select a certain record to update). - * const canUpdateUnknownFields = table.hasPermissionToUpdateRecord(record); - * - * // Check if user could update specific fields, when you don't know the - * // specific record that will be updated yet (e.g. if the field is selected - * // by the user and you want to check if your extension can write to it). - * const canUpdateUnknownRecord = - * table.hasPermissionToUpdateRecord(undefined, { - * 'My field name': 'updated value', - * // You can use undefined if you know you're going to update a field, - * // but don't know the new cell value yet. - * 'Another field name': undefined, - * }); - * - * // Check if user could perform updates within the table, without knowing the - * // specific record or fields that will be updated yet. (for example, to - * // render your extension in "read only" mode) - * const canUpdateUnknownRecordAndFields = table.hasPermissionToUpdateRecord(); - * ``` - */ - hasPermissionToUpdateRecord( - recordOrRecordId?: Record | RecordId, - fields?: ObjectMap, - ): boolean { - return this.checkPermissionsForUpdateRecord(recordOrRecordId, fields).hasPermission; - } - /** - * Updates cell values for records. - * - * Throws an error if the user does not have permission to update the given cell values in - * the records, or if invalid input is provided (eg. invalid cell values). - * - * Refer to {@link FieldType} for cell value write formats. - * - * You may only update up to 50 records in one call to `updateRecordsAsync`. - * See [Write back to Airtable](/guides/write-back-to-airtable) for more information - * about write limits. - * - * This action is asynchronous: `await` the returned promise if you wish to wait for the - * updates to be persisted to Airtable servers. - * Updates are applied optimistically locally, so your changes will be reflected in your extension - * before the promise resolves. - * - * @param records Array of objects containing recordId and fields/cellValues to update for that record (specified as an object mapping `FieldId` or field name to cell value) - * @example - * ```js - * const recordsToUpdate = [ - * // Fields can be specified by name or ID - * { - * id: record1.id, - * fields: { - * 'Post Title': 'How to make: orange-mango pound cake', - * 'Publication Date': '2020-01-01', - * }, - * }, - * { - * id: record2.id, - * fields: { - * // Sets the cell values to be empty. - * 'Post Title': '', - * 'Publication Date': '', - * }, - * }, - * { - * id: record3.id, - * fields: { - * [postTitleField.id]: 'Cake decorating tips & tricks', - * [publicationDateField.id]: '2020-02-02', - * }, - * }, - * // Cell values should generally have format matching the output of - * // record.getCellValue() for the field being updated - * { - * id: record4.id, - * fields: { - * 'Category (single select)': {name: 'Recipe'}, - * 'Tags (multiple select)': [{name: 'Desserts'}, {id: 'choiceId'}], - * 'Images (attachment)': [{url: 'http://mywebsite.com/cake.png'}], - * 'Related posts (linked records)': [{id: 'someRecordId'}], - * }, - * }, - * ]; - * - * function updateRecords() { - * if (table.hasPermissionToUpdateRecords(recordsToUpdate)) { - * table.updateRecordsAsync(recordsToUpdate); - * } - * // The records are now updated within your extension (eg will be reflected in - * // `table.selectRecords()`) but are still being saved to Airtable servers - * // (e.g. they may not be updated for other users yet). - * } - * - * async function updateRecordsAsync() { - * if (table.hasPermissionToUpdateRecords(recordsToUpdate)) { - * await table.updateRecordsAsync(recordsToUpdate); - * } - * // Record updates have been saved to Airtable servers. - * alert('records have been updated'); - * } - * ``` - */ - async updateRecordsAsync( - records: ReadonlyArray<{ - readonly id: RecordId; - readonly fields: ObjectMap; - }>, - ): Promise { - const recordsWithCellValuesByFieldId = records.map(record => ({ - id: record.id, - cellValuesByFieldId: this._cellValuesByFieldIdOrNameToCellValuesByFieldId( - record.fields, - ), - })); - - await this._sdk.__mutations.applyMutationAsync({ - type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, - tableId: this.id, - records: recordsWithCellValuesByFieldId, - opts: {parseDateCellValueInColumnTimeZone: true}, - }); - } - /** - * Checks whether the current user has permission to perform the given record updates. - * - * Accepts partial input, in the same format as {@link updateRecordsAsync}. - * The more information provided, the more accurate the permissions check will be. - * - * Returns `{hasPermission: true}` if the current user can update the specified records, - * `{hasPermission: false, reasonDisplayString: string}` otherwise. `reasonDisplayString` may be - * used to display an error message to the user. - * - * @param records Array of objects containing recordId and fields/cellValues to update for that record (specified as an object mapping `FieldId` or field name to cell value) - * @example - * ```js - * const recordsToUpdate = [ - * { - * // Validating a complete record update - * id: record1.id, - * fields: { - * 'Post Title': 'How to make: orange-mango pound cake', - * 'Publication Date': '2020-01-01', - * }, - * }, - * { - * // Like updateRecordsAsync, fields can be specified by name or ID - * id: record2.id, - * fields: { - * [postTitleField.id]: 'Cake decorating tips & tricks', - * [publicationDateField.id]: '2020-02-02', - * }, - * }, - * { - * // Validating an update to a specific record, not knowing what - * // fields will be updated - * id: record3.id, - * }, - * { - * // Validating an update to specific cell values, not knowing what - * // record will be updated - * fields: { - * 'My field name': 'updated value for unknown record', - * // You can use undefined if you know you're going to update a - * // field, but don't know the new cell value yet. - * 'Another field name': undefined, - * }, - * }, - * ]; - * - * const updateRecordsCheckResult = - * table.checkPermissionsForUpdateRecords(recordsToUpdate); - * if (!updateRecordsCheckResult.hasPermission) { - * alert(updateRecordsCheckResult.reasonDisplayString); - * } - * - * // Check if user could potentially update records. - * // Equivalent to table.checkPermissionsForUpdateRecord() - * const updateUnknownRecordAndFieldsCheckResult = - * table.checkPermissionsForUpdateRecords(); - * ``` - */ - checkPermissionsForUpdateRecords( - records?: ReadonlyArray<{ - readonly id?: RecordId | void; - readonly fields?: ObjectMap | void; - }>, - ): PermissionCheckResult { - return this._sdk.__mutations.checkPermissionsForMutation({ - type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, - tableId: this.id, - records: records - ? records.map(record => ({ - id: record.id || undefined, - cellValuesByFieldId: record.fields - ? this._cellValuesByFieldIdOrNameToCellValuesByFieldId(record.fields) - : undefined, - })) - : undefined, - }); - } - /** - * An alias for `checkPermissionsForUpdateRecords(records).hasPermission`. - * - * Checks whether the current user has permission to perform the given record updates. - * - * Accepts partial input, in the same format as {@link updateRecordsAsync}. - * The more information provided, the more accurate the permissions check will be. - * - * @param records Array of objects containing recordId and fields/cellValues to update for that record (specified as an object mapping `FieldId` or field name to cell value) - * @example - * ```js - * const recordsToUpdate = [ - * { - * // Validating a complete record update - * id: record1.id, - * fields: { - * 'Post Title': 'How to make: orange-mango pound cake', - * 'Publication Date': '2020-01-01', - * }, - * }, - * { - * // Like updateRecordsAsync, fields can be specified by name or ID - * id: record2.id, - * fields: { - * [postTitleField.id]: 'Cake decorating tips & tricks', - * [publicationDateField.id]: '2020-02-02', - * }, - * }, - * { - * // Validating an update to a specific record, not knowing what - * // fields will be updated - * id: record3.id, - * }, - * { - * // Validating an update to specific cell values, not knowing what - * // record will be updated - * fields: { - * 'My field name': 'updated value for unknown record', - * // You can use undefined if you know you're going to update a - * // field, but don't know the new cell value yet. - * 'Another field name': undefined, - * }, - * }, - * ]; - * - * const canUpdateRecords = table.hasPermissionToUpdateRecords(recordsToUpdate); - * if (!canUpdateRecords) { - * alert('not allowed'); - * } - * - * // Check if user could potentially update records. - * // Equivalent to table.hasPermissionToUpdateRecord() - * const canUpdateUnknownRecordsAndFields = - * table.hasPermissionToUpdateRecords(); - * ``` - */ - hasPermissionToUpdateRecords( - records?: ReadonlyArray<{ - readonly id?: RecordId | void; - readonly fields?: ObjectMap | void; - }>, - ): boolean { - return this.checkPermissionsForUpdateRecords(records).hasPermission; - } - /** - * Delete the given record. - * - * Throws an error if the user does not have permission to delete the given record. - * - * This action is asynchronous: `await` the returned promise if you wish to wait for the - * delete to be persisted to Airtable servers. - * Updates are applied optimistically locally, so your changes will be reflected in your extension - * before the promise resolves. - * - * @param recordOrRecordId the record to be deleted - * @example - * ```js - * function deleteRecord(record) { - * if (table.hasPermissionToDeleteRecord(record)) { - * table.deleteRecordAsync(record); - * } - * // The record is now deleted within your extension (eg will not be returned - * // in `table.selectRecords`) but it is still being saved to Airtable - * // servers (e.g. it may not look deleted to other users yet). - * } - * - * async function deleteRecordAsync(record) { - * if (table.hasPermissionToDeleteRecord(record)) { - * await table.deleteRecordAsync(record); - * } - * // Record deletion has been saved to Airtable servers. - * alert('record has been deleted'); - * } - * ``` - */ - async deleteRecordAsync(recordOrRecordId: Record | RecordId): Promise { - await this.deleteRecordsAsync([recordOrRecordId]); - } - /** - * Checks whether the current user has permission to delete the specified record. - * - * Accepts optional input, in the same format as {@link deleteRecordAsync}. - * The more information provided, the more accurate the permissions check will be. - * - * Returns `{hasPermission: true}` if the current user can delete the specified record, - * `{hasPermission: false, reasonDisplayString: string}` otherwise. `reasonDisplayString` may be - * used to display an error message to the user. - * - * @param recordOrRecordId the record to be deleted - * @example - * ```js - * // Check if user can delete a specific record - * const deleteRecordCheckResult = - * table.checkPermissionsForDeleteRecord(record); - * if (!deleteRecordCheckResult.hasPermission) { - * alert(deleteRecordCheckResult.reasonDisplayString); - * } - * - * // Check if user could potentially delete a record. - * // Use when you don't know the specific record you want to delete yet (for - * // example, to show/hide UI controls that let you select a record to delete). - * const deleteUnknownRecordCheckResult = - * table.checkPermissionsForDeleteRecord(); - * ``` - */ - checkPermissionsForDeleteRecord(recordOrRecordId?: Record | RecordId): PermissionCheckResult { - return this.checkPermissionsForDeleteRecords( - recordOrRecordId ? [recordOrRecordId] : undefined, - ); - } - /** - * An alias for `checkPermissionsForDeleteRecord(recordOrRecordId).hasPermission`. - * - * Checks whether the current user has permission to delete the specified record. - * - * Accepts optional input, in the same format as {@link deleteRecordAsync}. - * The more information provided, the more accurate the permissions check will be. - * - * @param recordOrRecordId the record to be deleted - * @example - * ```js - * // Check if user can delete a specific record - * const canDeleteRecord = table.hasPermissionToDeleteRecord(record); - * if (!canDeleteRecord) { - * alert('not allowed'); - * } - * - * // Check if user could potentially delete a record. - * // Use when you don't know the specific record you want to delete yet (for - * // example, to show/hide UI controls that let you select a record to delete). - * const canDeleteUnknownRecord = table.hasPermissionToDeleteRecord(); - * ``` - */ - hasPermissionToDeleteRecord(recordOrRecordId?: Record | RecordId): boolean { - return this.checkPermissionsForDeleteRecord(recordOrRecordId).hasPermission; - } - /** - * Delete the given records. - * - * Throws an error if the user does not have permission to delete the given records. - * - * You may only delete up to 50 records in one call to `deleteRecordsAsync`. - * See [Write back to Airtable](/guides/write-back-to-airtable#size-limits-rate-limits) for - * more information about write limits. - * - * This action is asynchronous: `await` the returned promise if you wish to wait for the - * delete to be persisted to Airtable servers. - * Updates are applied optimistically locally, so your changes will be reflected in your extension - * before the promise resolves. - * - * @param recordsOrRecordIds Array of Records and RecordIds - * @example - * ```js - * - * function deleteRecords(records) { - * if (table.hasPermissionToDeleteRecords(records)) { - * table.deleteRecordsAsync(records); - * } - * // The records are now deleted within your extension (eg will not be - * // returned in `table.selectRecords()`) but are still being saved to - * // Airtable servers (e.g. they may not look deleted to other users yet). - * } - * - * async function deleteRecordsAsync(records) { - * if (table.hasPermissionToDeleteRecords(records)) { - * await table.deleteRecordsAsync(records); - * } - * // Record deletions have been saved to Airtable servers. - * alert('records have been deleted'); - * } - * ``` - */ - async deleteRecordsAsync(recordsOrRecordIds: ReadonlyArray): Promise { - const recordIds = recordsOrRecordIds.map(recordOrRecordId => - typeof recordOrRecordId === 'string' ? recordOrRecordId : recordOrRecordId.id, - ); - - await this._sdk.__mutations.applyMutationAsync({ - type: MutationTypes.DELETE_MULTIPLE_RECORDS, - tableId: this.id, - recordIds, - }); - } - /** - * Checks whether the current user has permission to delete the specified records. - * - * Accepts optional input, in the same format as {@link deleteRecordsAsync}. - * The more information provided, the more accurate the permissions check will be. - * - * Returns `{hasPermission: true}` if the current user can delete the specified records, - * `{hasPermission: false, reasonDisplayString: string}` otherwise. `reasonDisplayString` may be - * used to display an error message to the user. - * - * @param recordsOrRecordIds the records to be deleted - * @example - * ```js - * // Check if user can delete specific records - * const deleteRecordsCheckResult = - * table.checkPermissionsForDeleteRecords([record1, record2]); - * if (!deleteRecordsCheckResult.hasPermission) { - * alert(deleteRecordsCheckResult.reasonDisplayString); - * } - * - * // Check if user could potentially delete records. - * // Use when you don't know the specific records you want to delete yet (for - * // example, to show/hide UI controls that let you select records to delete). - * // Equivalent to table.hasPermissionToDeleteRecord() - * const deleteUnknownRecordsCheckResult = - * table.checkPermissionsForDeleteRecords(); - * ``` - */ - checkPermissionsForDeleteRecords( - recordsOrRecordIds?: ReadonlyArray, - ): PermissionCheckResult { - return this._sdk.__mutations.checkPermissionsForMutation({ - type: MutationTypes.DELETE_MULTIPLE_RECORDS, - tableId: this.id, - recordIds: recordsOrRecordIds - ? recordsOrRecordIds.map(recordOrRecordId => - typeof recordOrRecordId === 'string' ? recordOrRecordId : recordOrRecordId.id, - ) - : undefined, - }); - } - /** - * An alias for `checkPermissionsForDeleteRecords(recordsOrRecordIds).hasPermission`. - * - * Checks whether the current user has permission to delete the specified records. - * - * Accepts optional input, in the same format as {@link deleteRecordsAsync}. - * The more information provided, the more accurate the permissions check will be. - * - * @param recordsOrRecordIds the records to be deleted - * @example - * ```js - * // Check if user can delete specific records - * const canDeleteRecords = - * table.hasPermissionToDeleteRecords([record1, record2]); - * if (!canDeleteRecords) { - * alert('not allowed!'); - * } - * - * // Check if user could potentially delete records. - * // Use when you don't know the specific records you want to delete yet (for - * // example, to show/hide UI controls that let you select records to delete). - * // Equivalent to table.hasPermissionToDeleteRecord() - * const canDeleteUnknownRecords = table.hasPermissionToDeleteRecords(); - * ``` - */ - hasPermissionToDeleteRecords(recordsOrRecordIds?: ReadonlyArray): boolean { - return this.checkPermissionsForDeleteRecords(recordsOrRecordIds).hasPermission; - } - - /** - * Creates a new record with the specified cell values. - * - * Throws an error if the user does not have permission to create the given records, or - * if invalid input is provided (eg. invalid cell values). - * - * Refer to {@link FieldType} for cell value write formats. - * - * This action is asynchronous: `await` the returned promise if you wish to wait for the new - * record to be persisted to Airtable servers. - * Updates are applied optimistically locally, so your changes will be reflected in your extension - * before the promise resolves. - * - * The returned promise will resolve to the RecordId of the new record once it is persisted. - * - * @param fields object mapping `FieldId` or field name to value for that field. - * @example - * ```js - * function createNewRecord(recordFields) { - * if (table.hasPermissionToCreateRecord(recordFields)) { - * table.createRecordAsync(recordFields); - * } - * // You can now access the new record in your extension (eg - * // `table.selectRecords()`) but it is still being saved to Airtable - * // servers (e.g. other users may not be able to see it yet). - * } - * - * async function createNewRecordAsync(recordFields) { - * if (table.hasPermissionToCreateRecord(recordFields)) { - * const newRecordId = await table.createRecordAsync(recordFields); - * } - * // New record has been saved to Airtable servers. - * alert(`new record with ID ${newRecordId} has been created`); - * } - * - * // Fields can be specified by name or ID - * createNewRecord({ - * 'Project Name': 'Advertising campaign', - * 'Budget': 100, - * }); - * createNewRecord({ - * [projectNameField.id]: 'Cat video', - * [budgetField.id]: 200, - * }); - * - * // Cell values should generally have format matching the output of - * // record.getCellValue() for the field being updated - * createNewRecord({ - * 'Project Name': 'Cat video 2' - * 'Category (single select)': {name: 'Video'}, - * 'Tags (multiple select)': [{name: 'Cats'}, {id: 'someChoiceId'}], - * 'Assets (attachment)': [{url: 'http://mywebsite.com/cats.mp4'}], - * 'Related projects (linked records)': [{id: 'someRecordId'}], - * }); - * ``` - */ - async createRecordAsync(fields: ObjectMap = {}): Promise { - const recordIds = await this.createRecordsAsync([{fields}]); - return recordIds[0]; - } - - /** - * Checks whether the current user has permission to create the specified record. - * - * Accepts partial input, in the same format as {@link createRecordAsync}. - * The more information provided, the more accurate the permissions check will be. - * - * Returns `{hasPermission: true}` if the current user can create the specified record, - * `{hasPermission: false, reasonDisplayString: string}` otherwise. `reasonDisplayString` may be - * used to display an error message to the user. - * - * @param fields object mapping `FieldId` or field name to value for that field. - * @example - * ```js - * // Check if user can create a specific record, when you already know what - * // fields/cell values will be set for the record. - * const createRecordCheckResult = table.checkPermissionsForCreateRecord({ - * 'Project Name': 'Advertising campaign', - * 'Budget': 100, - * }); - * if (!createRecordCheckResult.hasPermission) { - * alert(createRecordCheckResult.reasonDisplayString); - * } - * - * // Like createRecordAsync, you can use either field names or field IDs. - * const checkResultWithFieldIds = table.checkPermissionsForCreateRecord({ - * [projectNameField.id]: 'Cat video', - * [budgetField.id]: 200, - * }); - * - * // Check if user could potentially create a record. - * // Use when you don't know the specific fields/cell values yet (for example, - * // to show or hide UI controls that let you start creating a record.) - * const createUnknownRecordCheckResult = - * table.checkPermissionsForCreateRecord(); - * ``` - */ - checkPermissionsForCreateRecord( - fields?: ObjectMap, - ): PermissionCheckResult { - return this.checkPermissionsForCreateRecords([ - { - fields: fields || undefined, - }, - ]); - } - /** - * An alias for `checkPermissionsForCreateRecord(fields).hasPermission`. - * - * Checks whether the current user has permission to create the specified record. - * - * Accepts partial input, in the same format as {@link createRecordAsync}. - * The more information provided, the more accurate the permissions check will be. - * - * @param fields object mapping `FieldId` or field name to value for that field. - * @example - * ```js - * // Check if user can create a specific record, when you already know what - * // fields/cell values will be set for the record. - * const canCreateRecord = table.hasPermissionToCreateRecord({ - * 'Project Name': 'Advertising campaign', - * 'Budget': 100, - * }); - * if (!canCreateRecord) { - * alert('not allowed!'); - * } - * - * // Like createRecordAsync, you can use either field names or field IDs. - * const canCreateRecordWithFieldIds = table.hasPermissionToCreateRecord({ - * [projectNameField.id]: 'Cat video', - * [budgetField.id]: 200, - * }); - * - * // Check if user could potentially create a record. - * // Use when you don't know the specific fields/cell values yet (for example, - * // to show or hide UI controls that let you start creating a record.) - * const canCreateUnknownRecord = table.hasPermissionToCreateRecord(); - * ``` - */ - hasPermissionToCreateRecord(fields?: ObjectMap): boolean { - return this.checkPermissionsForCreateRecord(fields).hasPermission; - } - /** - * Creates new records with the specified cell values. - * - * Throws an error if the user does not have permission to create the given records, or - * if invalid input is provided (eg. invalid cell values). - * - * Refer to {@link FieldType} for cell value write formats. - * - * You may only create up to 50 records in one call to `createRecordsAsync`. - * See [Write back to Airtable](/guides/write-back-to-airtable#size-limits-rate-limits) for - * more information about write limits. - * - * This action is asynchronous: `await` the returned promise if you wish to wait for the new - * record to be persisted to Airtable servers. - * Updates are applied optimistically locally, so your changes will be reflected in your extension - * before the promise resolves. - * - * The returned promise will resolve to an array of RecordIds of the new records once the new - * records are persisted. - * - * @param records Array of objects with a `fields` key mapping `FieldId` or field name to value for that field. - * @example - * ```js - * const recordDefs = [ - * // Fields can be specified by name or ID - * { - * fields: { - * 'Project Name': 'Advertising campaign', - * 'Budget': 100, - * }, - * }, - * { - * fields: { - * [projectNameField.id]: 'Cat video', - * [budgetField.id]: 200, - * }, - * }, - * // Specifying no fields will create a new record with no cell values set - * { - * fields: {}, - * }, - * // Cell values should generally have format matching the output of - * // record.getCellValue() for the field being updated - * { - * fields: { - * 'Project Name': 'Cat video 2' - * 'Category (single select)': {name: 'Video'}, - * 'Tags (multiple select)': [{name: 'Cats'}, {id: 'choiceId'}], - * 'Assets (attachment)': [{url: 'http://mywebsite.com/cats.mp4'}], - * 'Related projects (linked records)': [{id: 'someRecordId'}], - * }, - * }, - * ]; - * - * function createNewRecords() { - * if (table.hasPermissionToCreateRecords(recordDefs)) { - * table.createRecordsAsync(recordDefs); - * } - * // You can now access the new records in your extension (e.g. - * // `table.selectRecords()`) but they are still being saved to Airtable - * // servers (e.g. other users may not be able to see them yet.) - * } - * - * async function createNewRecordsAsync() { - * if (table.hasPermissionToCreateRecords(recordDefs)) { - * const newRecordIds = await table.createRecordsAsync(recordDefs); - * } - * // New records have been saved to Airtable servers. - * alert(`new records with IDs ${newRecordIds} have been created`); - * } - * ``` - */ - async createRecordsAsync( - records: ReadonlyArray<{fields: ObjectMap}>, - ): Promise> { - const recordsToCreate = records.map(recordDef => { - const recordDefKeys = keys(recordDef); - let fields: ObjectMap; - if (recordDefKeys.length === 1 && recordDefKeys[0] === 'fields') { - fields = recordDef.fields; - } else { - throw spawnError( - 'Invalid record format. Please define field mappings using a `fields` key for each record definition object', - ); - } - return { - id: this._sdk.__airtableInterface.idGenerator.generateRecordId(), - cellValuesByFieldId: this._cellValuesByFieldIdOrNameToCellValuesByFieldId(fields), - }; - }); - - await this._sdk.__mutations.applyMutationAsync({ - type: MutationTypes.CREATE_MULTIPLE_RECORDS, - tableId: this.id, - records: recordsToCreate, - opts: {parseDateCellValueInColumnTimeZone: true}, - }); - - return recordsToCreate.map(record => record.id); - } - /** - * Checks whether the current user has permission to create the specified records. - * - * Accepts partial input, in the same format as {@link createRecordsAsync}. - * The more information provided, the more accurate the permissions check will be. - * - * Returns `{hasPermission: true}` if the current user can create the specified records, - * `{hasPermission: false, reasonDisplayString: string}` otherwise. `reasonDisplayString` may be - * used to display an error message to the user. - * - * @param records Array of objects mapping `FieldId` or field name to value for that field. - * @example - * ```js - * // Check if user can create specific records, when you already know what - * // fields/cell values will be set for the records. - * const createRecordsCheckResult = table.checkPermissionsForCreateRecords([ - * // Like createRecordsAsync, fields can be specified by name or ID - * { - * fields: { - * 'Project Name': 'Advertising campaign', - * 'Budget': 100, - * }, - * }, - * { - * fields: { - * [projectNameField.id]: 'Cat video', - * [budgetField.id]: 200, - * }, - * }, - * {}, - * ]); - * if (!createRecordsCheckResult.hasPermission) { - * alert(createRecordsCheckResult.reasonDisplayString); - * } - * - * // Check if user could potentially create records. - * // Use when you don't know the specific fields/cell values yet (for example, - * // to show or hide UI controls that let you start creating records.) - * // Equivalent to table.checkPermissionsForCreateRecord() - * const createUnknownRecordCheckResult = - * table.checkPermissionsForCreateRecords(); - * ``` - */ - checkPermissionsForCreateRecords( - records?: ReadonlyArray<{ - readonly fields?: ObjectMap | void; - }>, - ): PermissionCheckResult { - return this._sdk.__mutations.checkPermissionsForMutation({ - type: MutationTypes.CREATE_MULTIPLE_RECORDS, - tableId: this.id, - records: records - ? records.map(record => ({ - id: undefined, - cellValuesByFieldId: record.fields - ? this._cellValuesByFieldIdOrNameToCellValuesByFieldId(record.fields) - : undefined, - })) - : undefined, - }); - } - /** - * An alias for `checkPermissionsForCreateRecords(records).hasPermission`. - * - * Checks whether the current user has permission to create the specified records. - * - * Accepts partial input, in the same format as {@link createRecordsAsync}. - * The more information provided, the more accurate the permissions check will be. - * - * @param records Array of objects mapping `FieldId` or field name to value for that field. - * @example - * ```js - * // Check if user can create specific records, when you already know what fields/cell values - * // will be set for the records. - * const canCreateRecords = table.hasPermissionToCreateRecords([ - * // Like createRecordsAsync, fields can be specified by name or ID - * { - * fields: { - * 'Project Name': 'Advertising campaign', - * 'Budget': 100, - * } - * }, - * { - * fields: { - * [projectNameField.id]: 'Cat video', - * [budgetField.id]: 200, - * } - * }, - * {}, - * ]); - * if (!canCreateRecords) { - * alert('not allowed'); - * } - * - * // Check if user could potentially create records. - * // Use when you don't know the specific fields/cell values yet (for example, - * // to show or hide UI controls that let you start creating records). - * // Equivalent to table.hasPermissionToCreateRecord() - * const canCreateUnknownRecords = table.hasPermissionToCreateRecords(); - * ``` - */ - hasPermissionToCreateRecords( - records?: ReadonlyArray<{ - readonly fields?: ObjectMap | void; - }>, - ): boolean { - return this.checkPermissionsForCreateRecords(records).hasPermission; - } - /** @internal */ __triggerOnChangeForDirtyPaths(dirtyPaths: ChangedPathsForType): boolean { let didTableSchemaChange = false; diff --git a/packages/sdk/src/base/types/airtable_interface.ts b/packages/sdk/src/base/types/airtable_interface.ts index a1e67d8c4..33c56cebc 100644 --- a/packages/sdk/src/base/types/airtable_interface.ts +++ b/packages/sdk/src/base/types/airtable_interface.ts @@ -7,8 +7,8 @@ import { FieldTypeConfig, SdkInitDataCore, } from '../../shared/types/airtable_interface_core'; -import {FieldData} from '../../shared/types/field'; import {BaseSdkMode} from '../../sdk_mode'; +import {FieldData} from './field'; import {RecordData} from './record'; import {ViewportSizeConstraint} from './viewport'; import {AggregatorKey} from './aggregators'; diff --git a/packages/sdk/src/base/types/base.ts b/packages/sdk/src/base/types/base.ts index b0aba6abe..bac60c963 100644 --- a/packages/sdk/src/base/types/base.ts +++ b/packages/sdk/src/base/types/base.ts @@ -1,6 +1,6 @@ -import {BaseDataCore} from '../../shared/types/base_core'; +import {BaseDataCore, BasePermissionDataCore} from '../../shared/types/base_core'; import {TableId} from '../../shared/types/hyper_ids'; -import {TableData} from './table'; +import {TableData, TablePermissionData} from './table'; import {CursorData} from './cursor'; /** @hidden */ @@ -9,3 +9,6 @@ export interface BaseData extends BaseDataCore { activeTableId: TableId | null; cursorData: CursorData | null; } + +/** @hidden */ +export interface BasePermissionData extends BasePermissionDataCore {} diff --git a/packages/sdk/src/base/types/field.ts b/packages/sdk/src/base/types/field.ts new file mode 100644 index 000000000..e5bff8319 --- /dev/null +++ b/packages/sdk/src/base/types/field.ts @@ -0,0 +1,7 @@ +import {FieldDataCore, FieldPermissionDataCore} from '../../shared/types/field_core'; + +/** @hidden */ +export interface FieldData extends FieldDataCore {} + +/** @hidden */ +export interface FieldPermissionData extends FieldPermissionDataCore {} diff --git a/packages/sdk/src/base/types/mutations.ts b/packages/sdk/src/base/types/mutations.ts index bc5ba3d32..e80f44990 100644 --- a/packages/sdk/src/base/types/mutations.ts +++ b/packages/sdk/src/base/types/mutations.ts @@ -1,6 +1,6 @@ /** @module @airtable/blocks: mutations */ /** */ -import {ObjectValues, ObjectMap} from '../../shared/private_utils'; -import {TableId, FieldId, ViewId, RecordId} from '../../shared/types/hyper_ids'; +import {ObjectValues} from '../../shared/private_utils'; +import {TableId, FieldId, ViewId} from '../../shared/types/hyper_ids'; import { MutationTypesCore, MutationCore, @@ -12,9 +12,6 @@ import {NormalizedViewMetadata} from './airtable_interface'; /** @hidden */ export const MutationTypes = Object.freeze({ ...MutationTypesCore, - SET_MULTIPLE_RECORDS_CELL_VALUES: 'setMultipleRecordsCellValues' as const, - DELETE_MULTIPLE_RECORDS: 'deleteMultipleRecords' as const, - CREATE_MULTIPLE_RECORDS: 'createMultipleRecords' as const, CREATE_SINGLE_FIELD: 'createSingleField' as const, UPDATE_SINGLE_FIELD_CONFIG: 'updateSingleFieldConfig' as const, UPDATE_SINGLE_FIELD_DESCRIPTION: 'updateSingleFieldDescription' as const, @@ -26,91 +23,6 @@ export const MutationTypes = Object.freeze({ /** @hidden */ export type MutationType = ObjectValues; -/** - * The Mutation emitted when the App modifies one or more {@link Record|Records}. - * - * @docsPath testing/mutations/SetMultipleRecordsCellValuesMutation - */ -export interface SetMultipleRecordsCellValuesMutation { - /** This Mutation's [discriminant property](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) */ - readonly type: typeof MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES; - /** The identifier for the @link Table in which Records are being modified */ - readonly tableId: TableId; - /** The Records being modified */ - readonly records: ReadonlyArray<{ - readonly id: RecordId; - readonly cellValuesByFieldId: ObjectMap; - }>; - /** @hidden */ - readonly opts?: {parseDateCellValueInColumnTimeZone?: boolean}; -} - -/** @hidden */ -export interface PartialSetMultipleRecordsCellValuesMutation { - readonly type: typeof MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES; - readonly tableId: TableId | undefined; - readonly records: - | ReadonlyArray<{ - readonly id: RecordId | undefined; - readonly cellValuesByFieldId: ObjectMap | undefined; - }> - | undefined; - readonly opts?: {parseDateCellValueInColumnTimeZone?: boolean}; -} - -/** - * The Mutation emitted when the App deletes one or more {@link Record|Records}. - * - * @docsPath testing/mutations/DeleteMultipleRecordsMutation - */ -export interface DeleteMultipleRecordsMutation { - /** This Mutation's [discriminant property](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) */ - readonly type: typeof MutationTypes.DELETE_MULTIPLE_RECORDS; - /** The identifier for the Table in which Records are being deleted */ - readonly tableId: TableId; - /** The identifiers for records being deleted */ - readonly recordIds: ReadonlyArray; -} - -/** @hidden */ -export interface PartialDeleteMultipleRecordsMutation { - readonly type: typeof MutationTypes.DELETE_MULTIPLE_RECORDS; - readonly tableId: TableId | undefined; - readonly recordIds: ReadonlyArray | undefined; -} - -/** - * The Mutation emitted when the App creates one or more {@link Record|Records}. - * - * @docsPath testing/mutations/CreateMultipleRecordsMutation - */ -export interface CreateMultipleRecordsMutation { - /** This Mutation's [discriminant property](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) */ - readonly type: typeof MutationTypes.CREATE_MULTIPLE_RECORDS; - /** The identifier for the Table in which Records are being created */ - readonly tableId: TableId; - /** The records being created */ - readonly records: ReadonlyArray<{ - readonly id: RecordId; - readonly cellValuesByFieldId: ObjectMap; - }>; - /** @hidden */ - readonly opts?: {parseDateCellValueInColumnTimeZone?: boolean}; -} - -/** @hidden */ -export interface PartialCreateMultipleRecordsMutation { - readonly type: typeof MutationTypes.CREATE_MULTIPLE_RECORDS; - readonly tableId: TableId | undefined; - readonly records: - | ReadonlyArray<{ - readonly id: RecordId | undefined; - readonly cellValuesByFieldId: ObjectMap | undefined; - }> - | undefined; - readonly opts?: {parseDateCellValueInColumnTimeZone?: boolean}; -} - /** * The Mutation emitted when the App creates a {@link Field}. * @@ -291,9 +203,6 @@ export interface PartialUpdateViewMetadataMutation { /** @hidden */ export type Mutation = | MutationCore - | SetMultipleRecordsCellValuesMutation - | DeleteMultipleRecordsMutation - | CreateMultipleRecordsMutation | CreateSingleFieldMutation | UpdateSingleFieldConfigMutation | UpdateSingleFieldDescriptionMutation @@ -304,9 +213,6 @@ export type Mutation = /** @hidden */ export type PartialMutation = | PartialMutationCore - | PartialSetMultipleRecordsCellValuesMutation - | PartialDeleteMultipleRecordsMutation - | PartialCreateMultipleRecordsMutation | PartialCreateSingleFieldMutation | PartialUpdateSingleFieldConfigMutation | PartialUpdateSingleFieldDescriptionMutation diff --git a/packages/sdk/src/base/types/table.ts b/packages/sdk/src/base/types/table.ts index a2e2e6603..484353142 100644 --- a/packages/sdk/src/base/types/table.ts +++ b/packages/sdk/src/base/types/table.ts @@ -1,13 +1,20 @@ -import {TableDataCore} from '../../shared/types/table_core'; -import {RecordId, ViewId} from '../../shared/types/hyper_ids'; +import {TableDataCore, TablePermissionDataCore} from '../../shared/types/table_core'; +import {FieldId, RecordId, ViewId} from '../../shared/types/hyper_ids'; import {ObjectMap} from '../../shared/private_utils'; import {ViewData} from './view'; import {RecordData} from './record'; +import {FieldData, FieldPermissionData} from './field'; /** @hidden */ export interface TableData extends TableDataCore { + fieldsById: ObjectMap; activeViewId: ViewId | null; viewOrder: Array; viewsById: ObjectMap; recordsById?: ObjectMap; } + +/** @hidden */ +export interface TablePermissionData extends TablePermissionDataCore { + readonly fieldsById: ObjectMap; +} diff --git a/packages/sdk/src/base/ui/cell_renderer.tsx b/packages/sdk/src/base/ui/cell_renderer.tsx index d8eb77993..3da9a458a 100644 --- a/packages/sdk/src/base/ui/cell_renderer.tsx +++ b/packages/sdk/src/base/ui/cell_renderer.tsx @@ -7,7 +7,7 @@ import {spawnError} from '../../shared/error_utils'; import Sdk from '../sdk'; import Record from '../models/record'; import Field from '../models/field'; -import {FieldType} from '../../shared/types/field'; +import {FieldType} from '../../shared/types/field_core'; import {RecordId} from '../../shared/types/hyper_ids'; import {ObjectMap} from '../../shared/private_utils'; import withHooks from '../../shared/ui/with_hooks'; diff --git a/packages/sdk/src/base/ui/field_picker.tsx b/packages/sdk/src/base/ui/field_picker.tsx index f0ff5023e..810102c15 100644 --- a/packages/sdk/src/base/ui/field_picker.tsx +++ b/packages/sdk/src/base/ui/field_picker.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import {values, ObjectMap, has} from '../../shared/private_utils'; import Field from '../models/field'; import Table from '../models/table'; -import {FieldType} from '../../shared/types/field'; +import {FieldType} from '../../shared/types/field_core'; import useWatchable from '../../shared/ui/use_watchable'; import {useSdk} from '../../shared/ui/sdk_context'; import {BaseSdkMode} from '../../sdk_mode'; diff --git a/packages/sdk/src/base/ui/record_card.tsx b/packages/sdk/src/base/ui/record_card.tsx index cad04a739..3a16846de 100644 --- a/packages/sdk/src/base/ui/record_card.tsx +++ b/packages/sdk/src/base/ui/record_card.tsx @@ -12,7 +12,7 @@ import { } from '../../shared/private_utils'; import {invariant, spawnError} from '../../shared/error_utils'; import {AttachmentData} from '../../shared/types/attachment'; -import {FieldType} from '../../shared/types/field'; +import {FieldType} from '../../shared/types/field_core'; import {RecordDef} from '../../shared/types/record'; import Field from '../models/field'; import Record from '../models/record'; diff --git a/packages/sdk/src/base/unstable_testing_utils.ts b/packages/sdk/src/base/unstable_testing_utils.ts index 57d5db074..45e43a8fb 100644 --- a/packages/sdk/src/base/unstable_testing_utils.ts +++ b/packages/sdk/src/base/unstable_testing_utils.ts @@ -16,7 +16,8 @@ export {RecordData} from './types/record'; export {CursorData} from './types/cursor'; -export {FieldData, FieldType} from '../shared/types/field'; +export {FieldData} from './types/field'; +export {FieldType} from '../shared/types/field_core'; export {ViewType} from './types/view'; diff --git a/packages/sdk/src/interface/models/models.ts b/packages/sdk/src/interface/models/models.ts index b3f65e466..3820ce1f8 100644 --- a/packages/sdk/src/interface/models/models.ts +++ b/packages/sdk/src/interface/models/models.ts @@ -1,5 +1,5 @@ /** @ignore */ /** */ -export {FieldType, FieldConfig} from '../../shared/types/field'; +export {FieldType, FieldConfig} from '../../shared/types/field_core'; export {Base} from './base'; export {Table} from './table'; export {Field} from './field'; diff --git a/packages/sdk/src/interface/models/mutations.ts b/packages/sdk/src/interface/models/mutations.ts index e03810aab..8cc6b1178 100644 --- a/packages/sdk/src/interface/models/mutations.ts +++ b/packages/sdk/src/interface/models/mutations.ts @@ -1,40 +1,43 @@ import {InterfaceSdkMode} from '../../sdk_mode'; -import {MUTATIONS_MAX_BATCH_SIZE, MutationsCore} from '../../shared/models/mutations_core'; +import {MutationsCore} from '../../shared/models/mutations_core'; import {ModelChange} from '../../shared/types/base_core'; -import {spawnError, spawnUnknownSwitchCaseError} from '../../shared/error_utils'; -import {MutationTypes} from '../types/mutations'; +import {Mutation, MutationTypes} from '../types/mutations'; /** @hidden */ export class Mutations extends MutationsCore { /** @internal */ - _doesMutationExceedBatchSizeLimit(mutation: InterfaceSdkMode['MutationT']): boolean { - switch (mutation.type) { - case MutationTypes.SET_MULTIPLE_GLOBAL_CONFIG_PATHS: - return mutation.updates.length > MUTATIONS_MAX_BATCH_SIZE; - default: - throw spawnUnknownSwitchCaseError('mutation type', mutation.type, 'type'); - } + _isRecordStoreReadyForMutations(): boolean { + return true; } + /** @internal */ - _assertMutationIsValid(mutation: InterfaceSdkMode['MutationT']): void { - switch (mutation.type) { - case MutationTypes.SET_MULTIPLE_GLOBAL_CONFIG_PATHS: - return; - default: - throw spawnUnknownSwitchCaseError('mutation type', mutation.type, 'type'); - } + _isFieldAvailableForMutation(): boolean { + return true; } + /** @internal */ - _getOptimisticModelChangesForMutation( - mutation: InterfaceSdkMode['MutationT'], - ): Array { + _getOptimisticModelChangesForMutation(mutation: Mutation): Array { switch (mutation.type) { - case MutationTypes.SET_MULTIPLE_GLOBAL_CONFIG_PATHS: - throw spawnError( - 'attempting to generate model updates for SET_MULTIPLE_GLOBAL_CONFIG_PATH', - ); - default: - throw spawnUnknownSwitchCaseError('mutation type', mutation.type, 'type'); + case MutationTypes.CREATE_MULTIPLE_RECORDS: { + return super._getOptimisticModelChangesForMutation(mutation); + } + case MutationTypes.DELETE_MULTIPLE_RECORDS: { + const {tableId, recordIds: deletedRecordIds} = mutation; + const recordStore = this._base.__getRecordStore(tableId); + const deletedRecordIdsSet = new Set(deletedRecordIds); + return [ + { + path: ['tablesById', tableId, 'recordOrder'], + value: recordStore.recordIds.filter( + recordId => !deletedRecordIdsSet.has(recordId), + ), + }, + ...super._getOptimisticModelChangesForMutation(mutation), + ]; + } + default: { + return super._getOptimisticModelChangesForMutation(mutation); + } } } } diff --git a/packages/sdk/src/interface/models/record.ts b/packages/sdk/src/interface/models/record.ts index 7c8d6c00b..e6c54b53c 100644 --- a/packages/sdk/src/interface/models/record.ts +++ b/packages/sdk/src/interface/models/record.ts @@ -1,6 +1,10 @@ import {InterfaceSdkMode} from '../../sdk_mode'; +import {spawnError} from '../../shared/error_utils'; import {RecordCore, WatchableRecordKeysCore} from '../../shared/models/record_core'; import {ObjectValues} from '../../shared/private_utils'; +import {FieldType} from '../../shared/types/field_core'; +import {RecordId} from '../../shared/types/hyper_ids'; +import {Field} from './field'; const WatchableRecordKeys = Object.freeze({ ...WatchableRecordKeysCore, @@ -22,4 +26,35 @@ type WatchableRecordKey = ObjectValues | string; export class Record extends RecordCore { /** @internal */ static _className = 'Record'; + + /** + * Fetch foreign records for a field. Subsequent calls to this method will + * override previous calls that are still pending. The previous call(s) + * will immediately resolve with an empty `records` array. + * + * @param fieldId - The ID of the field to fetch foreign records for. + * @param filterString - The filter string to use to filter the records. + * @returns A promise that resolves to the foreign records. + */ + fetchForeignRecordsAsync( + field: Field, + filterString: string, + ): Promise<{ + records: ReadonlyArray<{recordId: RecordId; displayName: string}>; + }> { + const parentTable = this.parentTable; + if (field.parentTable !== parentTable) { + throw spawnError('Field %s is not in the same table as the record', field.name); + } + if (field.type !== FieldType.MULTIPLE_RECORD_LINKS) { + throw spawnError('Field %s is not a multiple record links field', field.name); + } + const airtableInterface = this.parentTable.parentBase.__sdk.__airtableInterface; + return airtableInterface.fetchForeignRecordsAsync( + parentTable.id, + this.id, + field.id, + filterString, + ); + } } diff --git a/packages/sdk/src/interface/types/airtable_interface.ts b/packages/sdk/src/interface/types/airtable_interface.ts index 6bb77b1f0..da3b13138 100644 --- a/packages/sdk/src/interface/types/airtable_interface.ts +++ b/packages/sdk/src/interface/types/airtable_interface.ts @@ -1,7 +1,7 @@ import {AirtableInterfaceCore, SdkInitDataCore} from '../../shared/types/airtable_interface_core'; import {InterfaceSdkMode} from '../../sdk_mode'; import {BaseDataCore} from '../../shared/types/base_core'; -import {TableId, PageId, FieldId} from '../../shared/types/hyper_ids'; +import {TableId, PageId, FieldId, RecordId} from '../../shared/types/hyper_ids'; import {TableData} from './table'; /** @hidden */ @@ -25,6 +25,11 @@ export interface SdkInitData extends SdkInitDataCore { baseData: BaseDataCore; } +/** @hidden */ +export interface IdGenerator { + generateRecordId(): string; +} + /** @hidden */ export enum BlockInstallationPageElementCustomPropertyTypeForAirtableInterface { BOOLEAN = 'boolean', @@ -68,9 +73,15 @@ export type BlockInstallationPageElementCustomPropertyForAirtableInterface = { * @hidden */ export interface AirtableInterface extends AirtableInterfaceCore { - sdkInitData: SdkInitData; + idGenerator: IdGenerator; expandRecord(tableId: string, recordId: string): void; + fetchForeignRecordsAsync( + tableId: string, + recordId: string, + fieldId: string, + filterString: string, + ): Promise<{records: ReadonlyArray<{recordId: RecordId; displayName: string}>}>; setCustomPropertiesAsync( properties: Array, ): Promise; diff --git a/packages/sdk/src/interface/types/base.ts b/packages/sdk/src/interface/types/base.ts index 4cbeb456f..548057ae0 100644 --- a/packages/sdk/src/interface/types/base.ts +++ b/packages/sdk/src/interface/types/base.ts @@ -1,5 +1,8 @@ -import {BaseDataCore} from '../../shared/types/base_core'; -import {TableData} from './table'; +import {BaseDataCore, BasePermissionDataCore} from '../../shared/types/base_core'; +import {TableData, TablePermissionData} from './table'; /** @hidden */ export interface BaseData extends BaseDataCore {} + +/** @hidden */ +export interface BasePermissionData extends BasePermissionDataCore {} diff --git a/packages/sdk/src/interface/types/field.ts b/packages/sdk/src/interface/types/field.ts new file mode 100644 index 000000000..e7b42f8cf --- /dev/null +++ b/packages/sdk/src/interface/types/field.ts @@ -0,0 +1,11 @@ +import {FieldDataCore, FieldPermissionDataCore} from '../../shared/types/field_core'; + +/** @hidden */ +export interface FieldData extends FieldDataCore { + isEditable: boolean; +} + +/** @hidden */ +export interface FieldPermissionData extends FieldPermissionDataCore { + readonly isEditable: boolean; +} diff --git a/packages/sdk/src/interface/types/table.ts b/packages/sdk/src/interface/types/table.ts index b3fc1eefe..1e477f236 100644 --- a/packages/sdk/src/interface/types/table.ts +++ b/packages/sdk/src/interface/types/table.ts @@ -1,11 +1,24 @@ -import {TableDataCore} from '../../shared/types/table_core'; -import {RecordId} from '../../shared/types/hyper_ids'; +import {TableDataCore, TablePermissionDataCore} from '../../shared/types/table_core'; +import {FieldId, RecordId} from '../../shared/types/hyper_ids'; import {ObjectMap} from '../../shared/private_utils'; import {RecordData} from './record'; +import {FieldData, FieldPermissionData} from './field'; /** @hidden */ export interface TableData extends TableDataCore { + fieldsById: ObjectMap; recordsById: ObjectMap; recordOrder: Array; isRecordExpansionEnabled: boolean; + canCreateRecordsInline: boolean; + canEditRecordsInline: boolean; + canDestroyRecordsInline: boolean; +} + +/** @hidden */ +export interface TablePermissionData extends TablePermissionDataCore { + readonly fieldsById: ObjectMap; + readonly canCreateRecordsInline: boolean; + readonly canEditRecordsInline: boolean; + readonly canDestroyRecordsInline: boolean; } diff --git a/packages/sdk/src/sdk_mode.ts b/packages/sdk/src/sdk_mode.ts index 403942cb9..52216724b 100644 --- a/packages/sdk/src/sdk_mode.ts +++ b/packages/sdk/src/sdk_mode.ts @@ -1,9 +1,17 @@ import BaseBlockSdk from './base/sdk'; -import {BaseData as BaseDataForBaseSdkMode} from './base/types/base'; +import { + BaseData as BaseDataForBaseSdkMode, + BasePermissionData as BasePermissionDataForBaseSdkMode, +} from './base/types/base'; import {TableData as TableDataForBaseSdkMode} from './base/types/table'; +import {FieldData as FieldDataForBaseSdkMode} from './base/types/field'; import {RecordData as RecordDataForBaseSdkMode} from './base/types/record'; -import {BaseData as BaseDataForInterfaceSdkMode} from './interface/types/base'; +import { + BaseData as BaseDataForInterfaceSdkMode, + BasePermissionData as BasePermissionDataForInterfaceSdkMode, +} from './interface/types/base'; import {TableData as TableDataForInterfaceSdkMode} from './interface/types/table'; +import {FieldData as FieldDataForInterfaceSdkMode} from './interface/types/field'; import {RecordData as RecordDataForInterfaceSdkMode} from './interface/types/record'; import BaseForBaseSdkMode from './base/models/base'; import {Base as BaseForInterfaceSdkMode} from './interface/models/base'; @@ -53,8 +61,11 @@ export interface BaseSdkMode { BaseDataT: BaseDataForBaseSdkMode; TableDataT: TableDataForBaseSdkMode; + FieldDataT: FieldDataForBaseSdkMode; RecordDataT: RecordDataForBaseSdkMode; + BasePermissionDataT: BasePermissionDataForBaseSdkMode; + BaseT: BaseForBaseSdkMode; TableT: TableForBaseSdkMode; FieldT: FieldForBaseSdkMode; @@ -77,8 +88,11 @@ export interface InterfaceSdkMode { BaseDataT: BaseDataForInterfaceSdkMode; TableDataT: TableDataForInterfaceSdkMode; + FieldDataT: FieldDataForInterfaceSdkMode; RecordDataT: RecordDataForInterfaceSdkMode; + BasePermissionDataT: BasePermissionDataForInterfaceSdkMode; + BaseT: BaseForInterfaceSdkMode; TableT: TableForInterfaceSdkMode; FieldT: FieldForInterfaceSdkMode; diff --git a/packages/sdk/src/shared/models/field_core.ts b/packages/sdk/src/shared/models/field_core.ts index a99583fc1..e27c15773 100644 --- a/packages/sdk/src/shared/models/field_core.ts +++ b/packages/sdk/src/shared/models/field_core.ts @@ -1,4 +1,4 @@ -import {FieldData, FieldType, FieldOptions, FieldConfig} from '../types/field'; +import {FieldType, FieldOptions, FieldConfig} from '../types/field_core'; import {isEnumValue, cloneDeep, ObjectValues, FlowAnyObject} from '../private_utils'; import {SdkMode} from '../../sdk_mode'; import {FieldTypeConfig} from '../types/airtable_interface_core'; @@ -26,7 +26,7 @@ export type WatchableFieldKey = ObjectValues; /** @hidden */ export abstract class FieldCore extends AbstractModel< SdkModeT, - FieldData, + SdkModeT['FieldDataT'], WatchableFieldKey > { /** @internal */ @@ -52,7 +52,7 @@ export abstract class FieldCore extends AbstractModel< /** * @internal */ - get _dataOrNullIfDeleted(): FieldData | null { + get _dataOrNullIfDeleted(): SdkModeT['FieldDataT'] | null { const tableData = this._baseData.tablesById[this.parentTable.id]; return tableData?.fieldsById[this._id] ?? null; } diff --git a/packages/sdk/src/shared/models/mutations_core.ts b/packages/sdk/src/shared/models/mutations_core.ts index cff149129..d003ca3da 100644 --- a/packages/sdk/src/shared/models/mutations_core.ts +++ b/packages/sdk/src/shared/models/mutations_core.ts @@ -8,6 +8,8 @@ import { } from '../types/mutations_core'; import {spawnError} from '../error_utils'; import {AirtableInterfaceCore} from '../types/airtable_interface_core'; +import {entries, ObjectMap} from '../private_utils'; +import {FieldId} from '../types/hyper_ids'; export const MUTATIONS_MAX_BATCH_SIZE = 50; @@ -127,11 +129,245 @@ export abstract class MutationsCore { } /** @internal */ - abstract _doesMutationExceedBatchSizeLimit(mutation: SdkModeT['MutationT']): boolean; + protected _doesMutationExceedBatchSizeLimit(mutation: SdkModeT['MutationT']): boolean { + switch (mutation.type) { + case MutationTypesCore.SET_MULTIPLE_RECORDS_CELL_VALUES: + case MutationTypesCore.CREATE_MULTIPLE_RECORDS: + return mutation.records.length > MUTATIONS_MAX_BATCH_SIZE; + case MutationTypesCore.DELETE_MULTIPLE_RECORDS: + return mutation.recordIds.length > MUTATIONS_MAX_BATCH_SIZE; + case MutationTypesCore.SET_MULTIPLE_GLOBAL_CONFIG_PATHS: + return mutation.updates.length > MUTATIONS_MAX_BATCH_SIZE; + default: + return false; + } + } + + /** @internal */ + _assertFieldIsValidForMutation(field: SdkModeT['FieldT']): void { + if (field.isComputed) { + throw spawnError( + "Can't set cell values: Field '%s' is computed and cannot be set", + field.name, + ); + } + } + /** @internal */ - abstract _assertMutationIsValid(mutation: SdkModeT['MutationT']): void; + protected _assertMutationIsValid(mutation: SdkModeT['MutationT']): void { + + const appInterface = this._sdk.__appInterface; + + switch (mutation.type) { + case MutationTypesCore.SET_MULTIPLE_RECORDS_CELL_VALUES: { + const {tableId, records} = mutation; + const table = this._base.getTableByIdIfExists(tableId); + if (!table) { + throw spawnError("Can't set cell values: No table with id %s exists", tableId); + } + + const recordStore = this._base.__getRecordStore(tableId); + + const checkedFieldIds = new Set(); + + for (const record of records) { + let existingRecord = null; + if (this._isRecordStoreReadyForMutations(recordStore)) { + existingRecord = recordStore.getRecordByIdIfExists(record.id); + if (!existingRecord) { + throw spawnError( + "Can't set cell values: No record with id %s exists", + record.id, + ); + } + } + + for (const fieldId of Object.keys(record.cellValuesByFieldId)) { + const field = table.getFieldByIdIfExists(fieldId); + if (!field) { + throw spawnError( + "Can't set cell values: No field with id %s exists in table '%s'", + fieldId, + table.name, + ); + } + + if (!checkedFieldIds.has(fieldId)) { + this._assertFieldIsValidForMutation(field); + checkedFieldIds.add(fieldId); + } + + if ( + existingRecord && + this._isFieldAvailableForMutation(recordStore, field.id) + ) { + const validationResult = this._airtableInterface.fieldTypeProvider.validateCellValueForUpdate( + appInterface, + record.cellValuesByFieldId[fieldId], + existingRecord._getRawCellValue(field), + field._data, + ); + if (!validationResult.isValid) { + throw spawnError( + "Can't set cell values: invalid cell value for field '%s'.\n%s", + field.name, + validationResult.reason, + ); + } + } + } + } + return; + } + + case MutationTypesCore.DELETE_MULTIPLE_RECORDS: { + const {tableId, recordIds} = mutation; + const table = this._base.getTableByIdIfExists(tableId); + if (!table) { + throw spawnError("Can't delete records: No table with id %s exists", tableId); + } + + const recordStore = this._base.__getRecordStore(tableId); + if (this._isRecordStoreReadyForMutations(recordStore)) { + for (const recordId of recordIds) { + const record = recordStore.getRecordByIdIfExists(recordId); + if (!record) { + throw spawnError( + "Can't delete records: No record with id %s exists in table '%s'", + recordId, + table.name, + ); + } + } + } + return; + } + + case MutationTypesCore.CREATE_MULTIPLE_RECORDS: { + const {tableId, records} = mutation; + const checkedFieldIds = new Set(); + + const table = this._base.getTableByIdIfExists(tableId); + if (!table) { + throw spawnError("Can't create records: No table with id %s exists", tableId); + } + + for (const record of records) { + for (const fieldId of Object.keys(record.cellValuesByFieldId)) { + const field = table.getFieldByIdIfExists(fieldId); + if (!field) { + throw spawnError( + "Can't create records: No field with id %s exists in table '%s'", + fieldId, + table.name, + ); + } + + if (!checkedFieldIds.has(fieldId)) { + this._assertFieldIsValidForMutation(field); + checkedFieldIds.add(fieldId); + } + + const validationResult = this._airtableInterface.fieldTypeProvider.validateCellValueForUpdate( + appInterface, + record.cellValuesByFieldId[fieldId], + null, + field._data, + ); + if (!validationResult.isValid) { + throw spawnError( + "Can't create records: invalid cell value for field '%s'.\n%s", + field.name, + validationResult.reason, + ); + } + } + } + return; + } + + case MutationTypesCore.SET_MULTIPLE_GLOBAL_CONFIG_PATHS: { + return; + } + default: + throw spawnError('unhandled mutation type: %s', mutation.type); + } + } + /** @internal */ - abstract _getOptimisticModelChangesForMutation( + protected _getOptimisticModelChangesForMutation( mutation: SdkModeT['MutationT'], - ): Array; + ): Array { + switch (mutation.type) { + case MutationTypesCore.SET_MULTIPLE_RECORDS_CELL_VALUES: { + const {tableId, records} = mutation; + const recordStore = this._base.__getRecordStore(tableId); + + return records.flatMap(record => + Object.keys(record.cellValuesByFieldId) + .filter(fieldId => this._isFieldAvailableForMutation(recordStore, fieldId)) + .map(fieldId => ({ + path: [ + 'tablesById', + tableId, + 'recordsById', + record.id, + 'cellValuesByFieldId', + fieldId, + ], + value: record.cellValuesByFieldId[fieldId], + })), + ); + } + + case MutationTypesCore.DELETE_MULTIPLE_RECORDS: { + const {tableId, recordIds} = mutation; + return recordIds.map(recordId => ({ + path: ['tablesById', tableId, 'recordsById', recordId], + value: undefined, + })); + } + + case MutationTypesCore.CREATE_MULTIPLE_RECORDS: { + const {tableId, records} = mutation; + const recordStore = this._base.__getRecordStore(tableId); + + return records.map(record => { + const filteredCellValuesByFieldId: ObjectMap = {}; + for (const [fieldId, cellValue] of entries(record.cellValuesByFieldId)) { + if (this._isFieldAvailableForMutation(recordStore, fieldId)) { + filteredCellValuesByFieldId[fieldId] = cellValue; + } + } + + return { + path: ['tablesById', tableId, 'recordsById', record.id], + value: { + id: record.id, + cellValuesByFieldId: filteredCellValuesByFieldId, + commentCount: 0, + createdTime: new Date().toJSON(), + }, + }; + }); + } + + case MutationTypesCore.SET_MULTIPLE_GLOBAL_CONFIG_PATHS: { + throw spawnError( + 'attempting to generate model updates for SET_MULTIPLE_GLOBAL_CONFIG_PATH', + ); + } + + default: + throw spawnError('unhandled mutation type: %s', mutation.type); + } + } + + /** @internal */ + abstract _isRecordStoreReadyForMutations(recordStore: SdkModeT['RecordStoreT']): boolean; + /** @internal */ + abstract _isFieldAvailableForMutation( + recordStore: SdkModeT['RecordStoreT'], + fieldId: FieldId, + ): boolean; } diff --git a/packages/sdk/src/shared/models/record_core.ts b/packages/sdk/src/shared/models/record_core.ts index e100a061c..8b486b369 100644 --- a/packages/sdk/src/shared/models/record_core.ts +++ b/packages/sdk/src/shared/models/record_core.ts @@ -2,7 +2,7 @@ import {SdkMode} from '../../sdk_mode'; import {cloneDeep, FlowAnyObject, isEnumValue, isObjectEmpty, ObjectValues} from '../private_utils'; import {invariant} from '../error_utils'; import {FieldId, RecordId} from '../types/hyper_ids'; -import {FieldType} from '../types/field'; +import {FieldType} from '../types/field_core'; import AbstractModel from './abstract_model'; import {FieldCore} from './field_core'; @@ -91,7 +91,7 @@ export abstract class RecordCore< * That format is incompatible with fieldTypeProvider methods, which expect the public API * format - use _getRawCellValue instead. */ - _getRawCellValue(field: SdkModeT['FieldT']): unknown { + _getRawCellValue(field: {id: FieldId; type: FieldType}): unknown { const {cellValuesByFieldId} = this._data; if (!cellValuesByFieldId) { return null; diff --git a/packages/sdk/src/shared/models/table_core.ts b/packages/sdk/src/shared/models/table_core.ts index d3633b977..bf6a0139b 100644 --- a/packages/sdk/src/shared/models/table_core.ts +++ b/packages/sdk/src/shared/models/table_core.ts @@ -1,7 +1,8 @@ -import {FieldId} from '../types/hyper_ids'; -import {isEnumValue, entries, has, ObjectValues, ObjectMap} from '../private_utils'; +import {FieldId, RecordId} from '../types/hyper_ids'; +import {isEnumValue, entries, has, ObjectValues, ObjectMap, keys} from '../private_utils'; import {spawnError} from '../error_utils'; import {SdkMode} from '../../sdk_mode'; +import {MutationTypesCore, PermissionCheckResult} from '../types/mutations_core'; import AbstractModel from './abstract_model'; import {ChangedPathsForType} from './base_core'; import {FieldCore} from './field_core'; @@ -301,6 +302,1005 @@ export abstract class TableCore< } return field; } + + /** + * Updates cell values for a record. + * + * Throws an error if the user does not have permission to update the given cell values in + * the record, or if invalid input is provided (eg. invalid cell values). + * + * Refer to {@link FieldType} for cell value write formats. + * + * This action is asynchronous: `await` the returned promise if you wish to wait for the updated + * cell values to be persisted to Airtable servers. + * Updates are applied optimistically locally, so your changes will be reflected in your extension + * before the promise resolves. + * + * @param recordOrRecordId the record to update + * @param fields cell values to update in that record, specified as object mapping `FieldId` or field name to value for that field. + * @example + * ```js + * function updateRecord(record, recordFields) { + * if (table.hasPermissionToUpdateRecord(record, recordFields)) { + * table.updateRecordAsync(record, recordFields); + * } + * // The updated values will now show in your extension (eg in + * // `table.selectRecords()` result) but are still being saved to Airtable + * // servers (e.g. other users may not be able to see them yet). + * } + * + * async function updateRecordAsync(record, recordFields) { + * if (table.hasPermissionToUpdateRecord(record, recordFields)) { + * await table.updateRecordAsync(record, recordFields); + * } + * // New record has been saved to Airtable servers. + * alert(`record with ID ${record.id} has been updated`); + * } + * + * // Fields can be specified by name or ID + * updateRecord(record1, { + * 'Post Title': 'How to make: orange-mango pound cake', + * 'Publication Date': '2020-01-01', + * }); + * updateRecord(record2, { + * [postTitleField.id]: 'Cake decorating tips & tricks', + * [publicationDateField.id]: '2020-02-02', + * }); + * + * // Cell values should generally have format matching the output of + * // record.getCellValue() for the field being updated + * updateRecord(record1, { + * 'Category (single select)': {name: 'Recipe'}, + * 'Tags (multiple select)': [{name: 'Desserts'}, {id: 'someChoiceId'}], + * 'Images (attachment)': [{url: 'http://mywebsite.com/cake.png'}], + * 'Related posts (linked records)': [{id: 'someRecordId'}], + * }); + * ``` + */ + async updateRecordAsync( + recordOrRecordId: SdkModeT['RecordT'] | RecordId, + fields: ObjectMap, + ): Promise { + const recordId = + typeof recordOrRecordId === 'string' ? recordOrRecordId : recordOrRecordId.id; + + await this.updateRecordsAsync([ + { + id: recordId, + fields, + }, + ]); + } + /** + * Checks whether the current user has permission to perform the given record update. + * + * Accepts partial input, in the same format as {@link updateRecordAsync}. + * The more information provided, the more accurate the permissions check will be. + * + * Returns `{hasPermission: true}` if the current user can update the specified record, + * `{hasPermission: false, reasonDisplayString: string}` otherwise. `reasonDisplayString` may be + * used to display an error message to the user. + * + * @param recordOrRecordId the record to update + * @param fields cell values to update in that record, specified as object mapping `FieldId` or field name to value for that field. + * @example + * ```js + * // Check if user can update specific fields for a specific record. + * const updateRecordCheckResult = + * table.checkPermissionsForUpdateRecord(record, { + * 'Post Title': 'How to make: orange-mango pound cake', + * 'Publication Date': '2020-01-01', + * }); + * if (!updateRecordCheckResult.hasPermission) { + * alert(updateRecordCheckResult.reasonDisplayString); + * } + * + * // Like updateRecordAsync, you can use either field names or field IDs. + * const updateRecordCheckResultWithFieldIds = + * table.checkPermissionsForUpdateRecord(record, { + * [postTitleField.id]: 'Cake decorating tips & tricks', + * [publicationDateField.id]: '2020-02-02', + * }); + * + * // Check if user could update a given record, when you don't know the + * // specific fields that will be updated yet (e.g. to check whether you should + * // allow a user to select a certain record to update). + * const updateUnknownFieldsCheckResult = + * table.checkPermissionsForUpdateRecord(record); + * + * // Check if user could update specific fields, when you don't know the + * // specific record that will be updated yet. (for example, if the field is + * // selected by the user and you want to check if your extension can write to it). + * const updateUnknownRecordCheckResult = + * table.checkPermissionsForUpdateRecord(undefined, { + * 'My field name': 'updated value', + * // You can use undefined if you know you're going to update a field, + * // but don't know the new cell value yet. + * 'Another field name': undefined, + * }); + * + * // Check if user could perform updates within the table, without knowing the + * // specific record or fields that will be updated yet (e.g., to render your + * // extension in "read only" mode). + * const updateUnknownRecordAndFieldsCheckResult = + * table.checkPermissionsForUpdateRecord(); + * ``` + */ + checkPermissionsForUpdateRecord( + recordOrRecordId?: SdkModeT['RecordT'] | RecordId, + fields?: ObjectMap, + ): PermissionCheckResult { + const recordId = + typeof recordOrRecordId === 'object' && recordOrRecordId !== null + ? recordOrRecordId.id + : recordOrRecordId; + + return this.checkPermissionsForUpdateRecords([ + { + id: recordId, + fields, + }, + ]); + } + /** + * An alias for `checkPermissionsForUpdateRecord(recordOrRecordId, fields).hasPermission`. + * + * Checks whether the current user has permission to perform the given record update. + * + * Accepts partial input, in the same format as {@link updateRecordAsync}. + * The more information provided, the more accurate the permissions check will be. + * + * @param recordOrRecordId the record to update + * @param fields cell values to update in that record, specified as object mapping `FieldId` or field name to value for that field. + * @example + * ```js + * // Check if user can update specific fields for a specific record. + * const canUpdateRecord = table.hasPermissionToUpdateRecord(record, { + * 'Post Title': 'How to make: orange-mango pound cake', + * 'Publication Date': '2020-01-01', + * }); + * if (!canUpdateRecord) { + * alert('not allowed!'); + * } + * + * // Like updateRecordAsync, you can use either field names or field IDs. + * const canUpdateRecordWithFieldIds = + * table.hasPermissionToUpdateRecord(record, { + * [postTitleField.id]: 'Cake decorating tips & tricks', + * [publicationDateField.id]: '2020-02-02', + * }); + * + * // Check if user could update a given record, when you don't know the + * // specific fields that will be updated yet (e.g. to check whether you should + * // allow a user to select a certain record to update). + * const canUpdateUnknownFields = table.hasPermissionToUpdateRecord(record); + * + * // Check if user could update specific fields, when you don't know the + * // specific record that will be updated yet (e.g. if the field is selected + * // by the user and you want to check if your extension can write to it). + * const canUpdateUnknownRecord = + * table.hasPermissionToUpdateRecord(undefined, { + * 'My field name': 'updated value', + * // You can use undefined if you know you're going to update a field, + * // but don't know the new cell value yet. + * 'Another field name': undefined, + * }); + * + * // Check if user could perform updates within the table, without knowing the + * // specific record or fields that will be updated yet. (for example, to + * // render your extension in "read only" mode) + * const canUpdateUnknownRecordAndFields = table.hasPermissionToUpdateRecord(); + * ``` + */ + hasPermissionToUpdateRecord( + recordOrRecordId?: SdkModeT['RecordT'] | RecordId, + fields?: ObjectMap, + ): boolean { + return this.checkPermissionsForUpdateRecord(recordOrRecordId, fields).hasPermission; + } + /** + * Updates cell values for records. + * + * Throws an error if the user does not have permission to update the given cell values in + * the records, or if invalid input is provided (eg. invalid cell values). + * + * Refer to {@link FieldType} for cell value write formats. + * + * You may only update up to 50 records in one call to `updateRecordsAsync`. + * See [Write back to Airtable](/guides/write-back-to-airtable) for more information + * about write limits. + * + * This action is asynchronous: `await` the returned promise if you wish to wait for the + * updates to be persisted to Airtable servers. + * Updates are applied optimistically locally, so your changes will be reflected in your extension + * before the promise resolves. + * + * @param records Array of objects containing recordId and fields/cellValues to update for that record (specified as an object mapping `FieldId` or field name to cell value) + * @example + * ```js + * const recordsToUpdate = [ + * // Fields can be specified by name or ID + * { + * id: record1.id, + * fields: { + * 'Post Title': 'How to make: orange-mango pound cake', + * 'Publication Date': '2020-01-01', + * }, + * }, + * { + * id: record2.id, + * fields: { + * // Sets the cell values to be empty. + * 'Post Title': '', + * 'Publication Date': '', + * }, + * }, + * { + * id: record3.id, + * fields: { + * [postTitleField.id]: 'Cake decorating tips & tricks', + * [publicationDateField.id]: '2020-02-02', + * }, + * }, + * // Cell values should generally have format matching the output of + * // record.getCellValue() for the field being updated + * { + * id: record4.id, + * fields: { + * 'Category (single select)': {name: 'Recipe'}, + * 'Tags (multiple select)': [{name: 'Desserts'}, {id: 'choiceId'}], + * 'Images (attachment)': [{url: 'http://mywebsite.com/cake.png'}], + * 'Related posts (linked records)': [{id: 'someRecordId'}], + * }, + * }, + * ]; + * + * function updateRecords() { + * if (table.hasPermissionToUpdateRecords(recordsToUpdate)) { + * table.updateRecordsAsync(recordsToUpdate); + * } + * // The records are now updated within your extension (eg will be reflected in + * // `table.selectRecords()`) but are still being saved to Airtable servers + * // (e.g. they may not be updated for other users yet). + * } + * + * async function updateRecordsAsync() { + * if (table.hasPermissionToUpdateRecords(recordsToUpdate)) { + * await table.updateRecordsAsync(recordsToUpdate); + * } + * // Record updates have been saved to Airtable servers. + * alert('records have been updated'); + * } + * ``` + */ + async updateRecordsAsync( + records: ReadonlyArray<{ + readonly id: RecordId; + readonly fields: ObjectMap; + }>, + ): Promise { + const recordsWithCellValuesByFieldId = records.map(record => ({ + id: record.id, + cellValuesByFieldId: this._cellValuesByFieldIdOrNameToCellValuesByFieldId( + record.fields, + ), + })); + + await this._sdk.__mutations.applyMutationAsync({ + type: MutationTypesCore.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: this.id, + records: recordsWithCellValuesByFieldId, + opts: {parseDateCellValueInColumnTimeZone: true}, + }); + } + /** + * Checks whether the current user has permission to perform the given record updates. + * + * Accepts partial input, in the same format as {@link updateRecordsAsync}. + * The more information provided, the more accurate the permissions check will be. + * + * Returns `{hasPermission: true}` if the current user can update the specified records, + * `{hasPermission: false, reasonDisplayString: string}` otherwise. `reasonDisplayString` may be + * used to display an error message to the user. + * + * @param records Array of objects containing recordId and fields/cellValues to update for that record (specified as an object mapping `FieldId` or field name to cell value) + * @example + * ```js + * const recordsToUpdate = [ + * { + * // Validating a complete record update + * id: record1.id, + * fields: { + * 'Post Title': 'How to make: orange-mango pound cake', + * 'Publication Date': '2020-01-01', + * }, + * }, + * { + * // Like updateRecordsAsync, fields can be specified by name or ID + * id: record2.id, + * fields: { + * [postTitleField.id]: 'Cake decorating tips & tricks', + * [publicationDateField.id]: '2020-02-02', + * }, + * }, + * { + * // Validating an update to a specific record, not knowing what + * // fields will be updated + * id: record3.id, + * }, + * { + * // Validating an update to specific cell values, not knowing what + * // record will be updated + * fields: { + * 'My field name': 'updated value for unknown record', + * // You can use undefined if you know you're going to update a + * // field, but don't know the new cell value yet. + * 'Another field name': undefined, + * }, + * }, + * ]; + * + * const updateRecordsCheckResult = + * table.checkPermissionsForUpdateRecords(recordsToUpdate); + * if (!updateRecordsCheckResult.hasPermission) { + * alert(updateRecordsCheckResult.reasonDisplayString); + * } + * + * // Check if user could potentially update records. + * // Equivalent to table.checkPermissionsForUpdateRecord() + * const updateUnknownRecordAndFieldsCheckResult = + * table.checkPermissionsForUpdateRecords(); + * ``` + */ + checkPermissionsForUpdateRecords( + records?: ReadonlyArray<{ + readonly id?: RecordId | void; + readonly fields?: ObjectMap | void; + }>, + ): PermissionCheckResult { + return this._sdk.__mutations.checkPermissionsForMutation({ + type: MutationTypesCore.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: this.id, + records: records + ? records.map(record => ({ + id: record.id || undefined, + cellValuesByFieldId: record.fields + ? this._cellValuesByFieldIdOrNameToCellValuesByFieldId(record.fields) + : undefined, + })) + : undefined, + }); + } + /** + * An alias for `checkPermissionsForUpdateRecords(records).hasPermission`. + * + * Checks whether the current user has permission to perform the given record updates. + * + * Accepts partial input, in the same format as {@link updateRecordsAsync}. + * The more information provided, the more accurate the permissions check will be. + * + * @param records Array of objects containing recordId and fields/cellValues to update for that record (specified as an object mapping `FieldId` or field name to cell value) + * @example + * ```js + * const recordsToUpdate = [ + * { + * // Validating a complete record update + * id: record1.id, + * fields: { + * 'Post Title': 'How to make: orange-mango pound cake', + * 'Publication Date': '2020-01-01', + * }, + * }, + * { + * // Like updateRecordsAsync, fields can be specified by name or ID + * id: record2.id, + * fields: { + * [postTitleField.id]: 'Cake decorating tips & tricks', + * [publicationDateField.id]: '2020-02-02', + * }, + * }, + * { + * // Validating an update to a specific record, not knowing what + * // fields will be updated + * id: record3.id, + * }, + * { + * // Validating an update to specific cell values, not knowing what + * // record will be updated + * fields: { + * 'My field name': 'updated value for unknown record', + * // You can use undefined if you know you're going to update a + * // field, but don't know the new cell value yet. + * 'Another field name': undefined, + * }, + * }, + * ]; + * + * const canUpdateRecords = table.hasPermissionToUpdateRecords(recordsToUpdate); + * if (!canUpdateRecords) { + * alert('not allowed'); + * } + * + * // Check if user could potentially update records. + * // Equivalent to table.hasPermissionToUpdateRecord() + * const canUpdateUnknownRecordsAndFields = + * table.hasPermissionToUpdateRecords(); + * ``` + */ + hasPermissionToUpdateRecords( + records?: ReadonlyArray<{ + readonly id?: RecordId | void; + readonly fields?: ObjectMap | void; + }>, + ): boolean { + return this.checkPermissionsForUpdateRecords(records).hasPermission; + } + /** + * Delete the given record. + * + * Throws an error if the user does not have permission to delete the given record. + * + * This action is asynchronous: `await` the returned promise if you wish to wait for the + * delete to be persisted to Airtable servers. + * Updates are applied optimistically locally, so your changes will be reflected in your extension + * before the promise resolves. + * + * @param recordOrRecordId the record to be deleted + * @example + * ```js + * function deleteRecord(record) { + * if (table.hasPermissionToDeleteRecord(record)) { + * table.deleteRecordAsync(record); + * } + * // The record is now deleted within your extension (eg will not be returned + * // in `table.selectRecords`) but it is still being saved to Airtable + * // servers (e.g. it may not look deleted to other users yet). + * } + * + * async function deleteRecordAsync(record) { + * if (table.hasPermissionToDeleteRecord(record)) { + * await table.deleteRecordAsync(record); + * } + * // Record deletion has been saved to Airtable servers. + * alert('record has been deleted'); + * } + * ``` + */ + async deleteRecordAsync(recordOrRecordId: SdkModeT['RecordT'] | RecordId): Promise { + await this.deleteRecordsAsync([recordOrRecordId]); + } + /** + * Checks whether the current user has permission to delete the specified record. + * + * Accepts optional input, in the same format as {@link deleteRecordAsync}. + * The more information provided, the more accurate the permissions check will be. + * + * Returns `{hasPermission: true}` if the current user can delete the specified record, + * `{hasPermission: false, reasonDisplayString: string}` otherwise. `reasonDisplayString` may be + * used to display an error message to the user. + * + * @param recordOrRecordId the record to be deleted + * @example + * ```js + * // Check if user can delete a specific record + * const deleteRecordCheckResult = + * table.checkPermissionsForDeleteRecord(record); + * if (!deleteRecordCheckResult.hasPermission) { + * alert(deleteRecordCheckResult.reasonDisplayString); + * } + * + * // Check if user could potentially delete a record. + * // Use when you don't know the specific record you want to delete yet (for + * // example, to show/hide UI controls that let you select a record to delete). + * const deleteUnknownRecordCheckResult = + * table.checkPermissionsForDeleteRecord(); + * ``` + */ + checkPermissionsForDeleteRecord( + recordOrRecordId?: SdkModeT['RecordT'] | RecordId, + ): PermissionCheckResult { + return this.checkPermissionsForDeleteRecords( + recordOrRecordId ? [recordOrRecordId] : undefined, + ); + } + /** + * An alias for `checkPermissionsForDeleteRecord(recordOrRecordId).hasPermission`. + * + * Checks whether the current user has permission to delete the specified record. + * + * Accepts optional input, in the same format as {@link deleteRecordAsync}. + * The more information provided, the more accurate the permissions check will be. + * + * @param recordOrRecordId the record to be deleted + * @example + * ```js + * // Check if user can delete a specific record + * const canDeleteRecord = table.hasPermissionToDeleteRecord(record); + * if (!canDeleteRecord) { + * alert('not allowed'); + * } + * + * // Check if user could potentially delete a record. + * // Use when you don't know the specific record you want to delete yet (for + * // example, to show/hide UI controls that let you select a record to delete). + * const canDeleteUnknownRecord = table.hasPermissionToDeleteRecord(); + * ``` + */ + hasPermissionToDeleteRecord(recordOrRecordId?: SdkModeT['RecordT'] | RecordId): boolean { + return this.checkPermissionsForDeleteRecord(recordOrRecordId).hasPermission; + } + /** + * Delete the given records. + * + * Throws an error if the user does not have permission to delete the given records. + * + * You may only delete up to 50 records in one call to `deleteRecordsAsync`. + * See [Write back to Airtable](/guides/write-back-to-airtable#size-limits-rate-limits) for + * more information about write limits. + * + * This action is asynchronous: `await` the returned promise if you wish to wait for the + * delete to be persisted to Airtable servers. + * Updates are applied optimistically locally, so your changes will be reflected in your extension + * before the promise resolves. + * + * @param recordsOrRecordIds Array of Records and RecordIds + * @example + * ```js + * + * function deleteRecords(records) { + * if (table.hasPermissionToDeleteRecords(records)) { + * table.deleteRecordsAsync(records); + * } + * // The records are now deleted within your extension (eg will not be + * // returned in `table.selectRecords()`) but are still being saved to + * // Airtable servers (e.g. they may not look deleted to other users yet). + * } + * + * async function deleteRecordsAsync(records) { + * if (table.hasPermissionToDeleteRecords(records)) { + * await table.deleteRecordsAsync(records); + * } + * // Record deletions have been saved to Airtable servers. + * alert('records have been deleted'); + * } + * ``` + */ + async deleteRecordsAsync( + recordsOrRecordIds: ReadonlyArray, + ): Promise { + const recordIds = recordsOrRecordIds.map(recordOrRecordId => + typeof recordOrRecordId === 'string' ? recordOrRecordId : recordOrRecordId.id, + ); + + await this._sdk.__mutations.applyMutationAsync({ + type: MutationTypesCore.DELETE_MULTIPLE_RECORDS, + tableId: this.id, + recordIds, + }); + } + /** + * Checks whether the current user has permission to delete the specified records. + * + * Accepts optional input, in the same format as {@link deleteRecordsAsync}. + * The more information provided, the more accurate the permissions check will be. + * + * Returns `{hasPermission: true}` if the current user can delete the specified records, + * `{hasPermission: false, reasonDisplayString: string}` otherwise. `reasonDisplayString` may be + * used to display an error message to the user. + * + * @param recordsOrRecordIds the records to be deleted + * @example + * ```js + * // Check if user can delete specific records + * const deleteRecordsCheckResult = + * table.checkPermissionsForDeleteRecords([record1, record2]); + * if (!deleteRecordsCheckResult.hasPermission) { + * alert(deleteRecordsCheckResult.reasonDisplayString); + * } + * + * // Check if user could potentially delete records. + * // Use when you don't know the specific records you want to delete yet (for + * // example, to show/hide UI controls that let you select records to delete). + * // Equivalent to table.hasPermissionToDeleteRecord() + * const deleteUnknownRecordsCheckResult = + * table.checkPermissionsForDeleteRecords(); + * ``` + */ + checkPermissionsForDeleteRecords( + recordsOrRecordIds?: ReadonlyArray, + ): PermissionCheckResult { + return this._sdk.__mutations.checkPermissionsForMutation({ + type: MutationTypesCore.DELETE_MULTIPLE_RECORDS, + tableId: this.id, + recordIds: recordsOrRecordIds + ? recordsOrRecordIds.map(recordOrRecordId => + typeof recordOrRecordId === 'string' ? recordOrRecordId : recordOrRecordId.id, + ) + : undefined, + }); + } + /** + * An alias for `checkPermissionsForDeleteRecords(recordsOrRecordIds).hasPermission`. + * + * Checks whether the current user has permission to delete the specified records. + * + * Accepts optional input, in the same format as {@link deleteRecordsAsync}. + * The more information provided, the more accurate the permissions check will be. + * + * @param recordsOrRecordIds the records to be deleted + * @example + * ```js + * // Check if user can delete specific records + * const canDeleteRecords = + * table.hasPermissionToDeleteRecords([record1, record2]); + * if (!canDeleteRecords) { + * alert('not allowed!'); + * } + * + * // Check if user could potentially delete records. + * // Use when you don't know the specific records you want to delete yet (for + * // example, to show/hide UI controls that let you select records to delete). + * // Equivalent to table.hasPermissionToDeleteRecord() + * const canDeleteUnknownRecords = table.hasPermissionToDeleteRecords(); + * ``` + */ + hasPermissionToDeleteRecords( + recordsOrRecordIds?: ReadonlyArray, + ): boolean { + return this.checkPermissionsForDeleteRecords(recordsOrRecordIds).hasPermission; + } + + /** + * Creates a new record with the specified cell values. + * + * Throws an error if the user does not have permission to create the given records, or + * if invalid input is provided (eg. invalid cell values). + * + * Refer to {@link FieldType} for cell value write formats. + * + * This action is asynchronous: `await` the returned promise if you wish to wait for the new + * record to be persisted to Airtable servers. + * Updates are applied optimistically locally, so your changes will be reflected in your extension + * before the promise resolves. + * + * The returned promise will resolve to the RecordId of the new record once it is persisted. + * + * @param fields object mapping `FieldId` or field name to value for that field. + * @example + * ```js + * function createNewRecord(recordFields) { + * if (table.hasPermissionToCreateRecord(recordFields)) { + * table.createRecordAsync(recordFields); + * } + * // You can now access the new record in your extension (eg + * // `table.selectRecords()`) but it is still being saved to Airtable + * // servers (e.g. other users may not be able to see it yet). + * } + * + * async function createNewRecordAsync(recordFields) { + * if (table.hasPermissionToCreateRecord(recordFields)) { + * const newRecordId = await table.createRecordAsync(recordFields); + * } + * // New record has been saved to Airtable servers. + * alert(`new record with ID ${newRecordId} has been created`); + * } + * + * // Fields can be specified by name or ID + * createNewRecord({ + * 'Project Name': 'Advertising campaign', + * 'Budget': 100, + * }); + * createNewRecord({ + * [projectNameField.id]: 'Cat video', + * [budgetField.id]: 200, + * }); + * + * // Cell values should generally have format matching the output of + * // record.getCellValue() for the field being updated + * createNewRecord({ + * 'Project Name': 'Cat video 2' + * 'Category (single select)': {name: 'Video'}, + * 'Tags (multiple select)': [{name: 'Cats'}, {id: 'someChoiceId'}], + * 'Assets (attachment)': [{url: 'http://mywebsite.com/cats.mp4'}], + * 'Related projects (linked records)': [{id: 'someRecordId'}], + * }); + * ``` + */ + async createRecordAsync(fields: ObjectMap = {}): Promise { + const recordIds = await this.createRecordsAsync([{fields}]); + return recordIds[0]; + } + + /** + * Checks whether the current user has permission to create the specified record. + * + * Accepts partial input, in the same format as {@link createRecordAsync}. + * The more information provided, the more accurate the permissions check will be. + * + * Returns `{hasPermission: true}` if the current user can create the specified record, + * `{hasPermission: false, reasonDisplayString: string}` otherwise. `reasonDisplayString` may be + * used to display an error message to the user. + * + * @param fields object mapping `FieldId` or field name to value for that field. + * @example + * ```js + * // Check if user can create a specific record, when you already know what + * // fields/cell values will be set for the record. + * const createRecordCheckResult = table.checkPermissionsForCreateRecord({ + * 'Project Name': 'Advertising campaign', + * 'Budget': 100, + * }); + * if (!createRecordCheckResult.hasPermission) { + * alert(createRecordCheckResult.reasonDisplayString); + * } + * + * // Like createRecordAsync, you can use either field names or field IDs. + * const checkResultWithFieldIds = table.checkPermissionsForCreateRecord({ + * [projectNameField.id]: 'Cat video', + * [budgetField.id]: 200, + * }); + * + * // Check if user could potentially create a record. + * // Use when you don't know the specific fields/cell values yet (for example, + * // to show or hide UI controls that let you start creating a record.) + * const createUnknownRecordCheckResult = + * table.checkPermissionsForCreateRecord(); + * ``` + */ + checkPermissionsForCreateRecord( + fields?: ObjectMap, + ): PermissionCheckResult { + return this.checkPermissionsForCreateRecords([ + { + fields: fields || undefined, + }, + ]); + } + /** + * An alias for `checkPermissionsForCreateRecord(fields).hasPermission`. + * + * Checks whether the current user has permission to create the specified record. + * + * Accepts partial input, in the same format as {@link createRecordAsync}. + * The more information provided, the more accurate the permissions check will be. + * + * @param fields object mapping `FieldId` or field name to value for that field. + * @example + * ```js + * // Check if user can create a specific record, when you already know what + * // fields/cell values will be set for the record. + * const canCreateRecord = table.hasPermissionToCreateRecord({ + * 'Project Name': 'Advertising campaign', + * 'Budget': 100, + * }); + * if (!canCreateRecord) { + * alert('not allowed!'); + * } + * + * // Like createRecordAsync, you can use either field names or field IDs. + * const canCreateRecordWithFieldIds = table.hasPermissionToCreateRecord({ + * [projectNameField.id]: 'Cat video', + * [budgetField.id]: 200, + * }); + * + * // Check if user could potentially create a record. + * // Use when you don't know the specific fields/cell values yet (for example, + * // to show or hide UI controls that let you start creating a record.) + * const canCreateUnknownRecord = table.hasPermissionToCreateRecord(); + * ``` + */ + hasPermissionToCreateRecord(fields?: ObjectMap): boolean { + return this.checkPermissionsForCreateRecord(fields).hasPermission; + } + /** + * Creates new records with the specified cell values. + * + * Throws an error if the user does not have permission to create the given records, or + * if invalid input is provided (eg. invalid cell values). + * + * Refer to {@link FieldType} for cell value write formats. + * + * You may only create up to 50 records in one call to `createRecordsAsync`. + * See [Write back to Airtable](/guides/write-back-to-airtable#size-limits-rate-limits) for + * more information about write limits. + * + * This action is asynchronous: `await` the returned promise if you wish to wait for the new + * record to be persisted to Airtable servers. + * Updates are applied optimistically locally, so your changes will be reflected in your extension + * before the promise resolves. + * + * The returned promise will resolve to an array of RecordIds of the new records once the new + * records are persisted. + * + * @param records Array of objects with a `fields` key mapping `FieldId` or field name to value for that field. + * @example + * ```js + * const recordDefs = [ + * // Fields can be specified by name or ID + * { + * fields: { + * 'Project Name': 'Advertising campaign', + * 'Budget': 100, + * }, + * }, + * { + * fields: { + * [projectNameField.id]: 'Cat video', + * [budgetField.id]: 200, + * }, + * }, + * // Specifying no fields will create a new record with no cell values set + * { + * fields: {}, + * }, + * // Cell values should generally have format matching the output of + * // record.getCellValue() for the field being updated + * { + * fields: { + * 'Project Name': 'Cat video 2' + * 'Category (single select)': {name: 'Video'}, + * 'Tags (multiple select)': [{name: 'Cats'}, {id: 'choiceId'}], + * 'Assets (attachment)': [{url: 'http://mywebsite.com/cats.mp4'}], + * 'Related projects (linked records)': [{id: 'someRecordId'}], + * }, + * }, + * ]; + * + * function createNewRecords() { + * if (table.hasPermissionToCreateRecords(recordDefs)) { + * table.createRecordsAsync(recordDefs); + * } + * // You can now access the new records in your extension (e.g. + * // `table.selectRecords()`) but they are still being saved to Airtable + * // servers (e.g. other users may not be able to see them yet.) + * } + * + * async function createNewRecordsAsync() { + * if (table.hasPermissionToCreateRecords(recordDefs)) { + * const newRecordIds = await table.createRecordsAsync(recordDefs); + * } + * // New records have been saved to Airtable servers. + * alert(`new records with IDs ${newRecordIds} have been created`); + * } + * ``` + */ + async createRecordsAsync( + records: ReadonlyArray<{fields: ObjectMap}>, + ): Promise> { + const recordsToCreate = records.map(recordDef => { + const recordDefKeys = keys(recordDef); + let fields: ObjectMap; + if (recordDefKeys.length === 1 && recordDefKeys[0] === 'fields') { + fields = recordDef.fields; + } else { + throw spawnError( + 'Invalid record format. Please define field mappings using a `fields` key for each record definition object', + ); + } + return { + id: this._sdk.__airtableInterface.idGenerator.generateRecordId(), + cellValuesByFieldId: this._cellValuesByFieldIdOrNameToCellValuesByFieldId(fields), + }; + }); + + await this._sdk.__mutations.applyMutationAsync({ + type: MutationTypesCore.CREATE_MULTIPLE_RECORDS, + tableId: this.id, + records: recordsToCreate, + opts: {parseDateCellValueInColumnTimeZone: true}, + }); + + return recordsToCreate.map(record => record.id); + } + /** + * Checks whether the current user has permission to create the specified records. + * + * Accepts partial input, in the same format as {@link createRecordsAsync}. + * The more information provided, the more accurate the permissions check will be. + * + * Returns `{hasPermission: true}` if the current user can create the specified records, + * `{hasPermission: false, reasonDisplayString: string}` otherwise. `reasonDisplayString` may be + * used to display an error message to the user. + * + * @param records Array of objects mapping `FieldId` or field name to value for that field. + * @example + * ```js + * // Check if user can create specific records, when you already know what + * // fields/cell values will be set for the records. + * const createRecordsCheckResult = table.checkPermissionsForCreateRecords([ + * // Like createRecordsAsync, fields can be specified by name or ID + * { + * fields: { + * 'Project Name': 'Advertising campaign', + * 'Budget': 100, + * }, + * }, + * { + * fields: { + * [projectNameField.id]: 'Cat video', + * [budgetField.id]: 200, + * }, + * }, + * {}, + * ]); + * if (!createRecordsCheckResult.hasPermission) { + * alert(createRecordsCheckResult.reasonDisplayString); + * } + * + * // Check if user could potentially create records. + * // Use when you don't know the specific fields/cell values yet (for example, + * // to show or hide UI controls that let you start creating records.) + * // Equivalent to table.checkPermissionsForCreateRecord() + * const createUnknownRecordCheckResult = + * table.checkPermissionsForCreateRecords(); + * ``` + */ + checkPermissionsForCreateRecords( + records?: ReadonlyArray<{ + readonly fields?: ObjectMap | void; + }>, + ): PermissionCheckResult { + return this._sdk.__mutations.checkPermissionsForMutation({ + type: MutationTypesCore.CREATE_MULTIPLE_RECORDS, + tableId: this.id, + records: records + ? records.map(record => ({ + id: undefined, + cellValuesByFieldId: record.fields + ? this._cellValuesByFieldIdOrNameToCellValuesByFieldId(record.fields) + : undefined, + })) + : undefined, + }); + } + /** + * An alias for `checkPermissionsForCreateRecords(records).hasPermission`. + * + * Checks whether the current user has permission to create the specified records. + * + * Accepts partial input, in the same format as {@link createRecordsAsync}. + * The more information provided, the more accurate the permissions check will be. + * + * @param records Array of objects mapping `FieldId` or field name to value for that field. + * @example + * ```js + * // Check if user can create specific records, when you already know what fields/cell values + * // will be set for the records. + * const canCreateRecords = table.hasPermissionToCreateRecords([ + * // Like createRecordsAsync, fields can be specified by name or ID + * { + * fields: { + * 'Project Name': 'Advertising campaign', + * 'Budget': 100, + * } + * }, + * { + * fields: { + * [projectNameField.id]: 'Cat video', + * [budgetField.id]: 200, + * } + * }, + * {}, + * ]); + * if (!canCreateRecords) { + * alert('not allowed'); + * } + * + * // Check if user could potentially create records. + * // Use when you don't know the specific fields/cell values yet (for example, + * // to show or hide UI controls that let you start creating records). + * // Equivalent to table.hasPermissionToCreateRecord() + * const canCreateUnknownRecords = table.hasPermissionToCreateRecords(); + * ``` + */ + hasPermissionToCreateRecords( + records?: ReadonlyArray<{ + readonly fields?: ObjectMap | void; + }>, + ): boolean { + return this.checkPermissionsForCreateRecords(records).hasPermission; + } + /** * @internal */ diff --git a/packages/sdk/src/shared/types/airtable_interface_core.ts b/packages/sdk/src/shared/types/airtable_interface_core.ts index c9037104f..2a7364cd9 100644 --- a/packages/sdk/src/shared/types/airtable_interface_core.ts +++ b/packages/sdk/src/shared/types/airtable_interface_core.ts @@ -2,14 +2,14 @@ import {ObjectMap} from '../private_utils'; import {SdkMode} from '../../sdk_mode'; import {Stat} from './stat'; import {FieldId, BlockInstallationId} from './hyper_ids'; -import {FieldType, FieldData} from './field'; +import {FieldType, FieldDataCore} from './field_core'; import { GlobalConfigUpdate, GlobalConfigData, GlobalConfigPath, GlobalConfigPathValidationResult, } from './global_config'; -import {BaseDataCore, ModelChange, BasePermissionData} from './base_core'; +import {BaseDataCore, ModelChange} from './base_core'; import {TableDataCore} from './table_core'; import {PermissionCheckResult} from './mutations_core'; @@ -33,33 +33,33 @@ export interface FieldTypeConfig { } /** @hidden */ export interface FieldTypeProviderCore { - isComputed(fieldData: FieldData): boolean; + isComputed(fieldData: FieldDataCore): boolean; validateCellValueForUpdate( appInterface: AppInterface, newCellValue: unknown, currentCellValue: unknown, - fieldData: FieldData, + fieldData: FieldDataCore, ): CellValueValidationResult; getConfig( appInterface: AppInterface, - fieldData: FieldData, + fieldData: FieldDataCore, fieldNamesById: ObjectMap, ): FieldTypeConfig; convertStringToCellValue( appInterface: AppInterface, string: string, - fieldData: FieldData, + fieldData: FieldDataCore, opts?: {parseDateCellValueInColumnTimeZone?: boolean}, ): unknown; convertCellValueToString( appInterface: AppInterface, cellValue: unknown, - fieldData: FieldData, + fieldData: FieldDataCore, ): string; getCellRendererData( appInterface: AppInterface, cellValue: unknown, - fieldData: FieldData, + fieldData: FieldDataCore, shouldWrap: boolean, ): {cellValueHtml: string; attributes: {[key: string]: unknown}}; } @@ -100,7 +100,7 @@ export interface AirtableInterfaceCore { applyMutationAsync(mutation: SdkModeT['MutationT'], opts?: {holdForMs?: number}): Promise; checkPermissionsForMutation( mutation: SdkModeT['PartialMutationT'], - basePermissionData: BasePermissionData, + basePermissionData: SdkModeT['BasePermissionDataT'], ): PermissionCheckResult; /** diff --git a/packages/sdk/src/shared/types/base_core.ts b/packages/sdk/src/shared/types/base_core.ts index 1c6e8c406..89053bd03 100644 --- a/packages/sdk/src/shared/types/base_core.ts +++ b/packages/sdk/src/shared/types/base_core.ts @@ -2,7 +2,7 @@ import {ObjectMap} from '../private_utils'; import {AppInterface} from './airtable_interface_core'; import {PermissionLevel} from './permission_levels'; -import {TableDataCore, TablePermissionData} from './table_core'; +import {TableDataCore, TablePermissionDataCore} from './table_core'; import {CollaboratorData} from './collaborator'; import {TableId, UserId, BaseId} from './hyper_ids'; @@ -36,7 +36,7 @@ export interface BaseDataCore { } /** @hidden */ -export interface BasePermissionData { +export interface BasePermissionDataCore { readonly permissionLevel: PermissionLevel; - readonly tablesById: ObjectMap; + readonly tablesById: ObjectMap; } diff --git a/packages/sdk/src/shared/types/field.ts b/packages/sdk/src/shared/types/field_core.ts similarity index 99% rename from packages/sdk/src/shared/types/field.ts rename to packages/sdk/src/shared/types/field_core.ts index 93983ccbe..a3a8f98a4 100644 --- a/packages/sdk/src/shared/types/field.ts +++ b/packages/sdk/src/shared/types/field_core.ts @@ -1183,7 +1183,7 @@ export enum FieldType { /** @hidden */ export type FieldLock = unknown; /** @hidden */ -export interface FieldData { +export interface FieldDataCore { id: FieldId; name: string; type: PrivateColumnType; @@ -1194,7 +1194,7 @@ export interface FieldData { } /** @hidden */ -export interface FieldPermissionData { +export interface FieldPermissionDataCore { readonly id: FieldId; readonly name: string; readonly type: PrivateColumnType; diff --git a/packages/sdk/src/shared/types/mutations_core.ts b/packages/sdk/src/shared/types/mutations_core.ts index 4ec5d05b0..4a16c7344 100644 --- a/packages/sdk/src/shared/types/mutations_core.ts +++ b/packages/sdk/src/shared/types/mutations_core.ts @@ -1,9 +1,14 @@ /** @module @airtable/blocks: mutations */ /** */ +import {ObjectMap} from '../private_utils'; +import {FieldId, RecordId, TableId} from './hyper_ids'; import {GlobalConfigUpdate, GlobalConfigValue} from './global_config'; /** @hidden */ export const MutationTypesCore = Object.freeze({ SET_MULTIPLE_GLOBAL_CONFIG_PATHS: 'setMultipleGlobalConfigPaths' as const, + SET_MULTIPLE_RECORDS_CELL_VALUES: 'setMultipleRecordsCellValues' as const, + DELETE_MULTIPLE_RECORDS: 'deleteMultipleRecords' as const, + CREATE_MULTIPLE_RECORDS: 'createMultipleRecords' as const, }); @@ -31,11 +36,104 @@ export interface PartialSetMultipleGlobalConfigPathsMutation { | undefined; } +/** + * The Mutation emitted when the App modifies one or more {@link Record|Records}. + * + * @docsPath testing/mutations/SetMultipleRecordsCellValuesMutation + */ +export interface SetMultipleRecordsCellValuesMutation { + /** This Mutation's [discriminant property](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) */ + readonly type: typeof MutationTypesCore.SET_MULTIPLE_RECORDS_CELL_VALUES; + /** The identifier for the @link Table in which Records are being modified */ + readonly tableId: TableId; + /** The Records being modified */ + readonly records: ReadonlyArray<{ + readonly id: RecordId; + readonly cellValuesByFieldId: ObjectMap; + }>; + /** @hidden */ + readonly opts?: {parseDateCellValueInColumnTimeZone?: boolean}; +} + +/** @hidden */ +export interface PartialSetMultipleRecordsCellValuesMutation { + readonly type: typeof MutationTypesCore.SET_MULTIPLE_RECORDS_CELL_VALUES; + readonly tableId: TableId | undefined; + readonly records: + | ReadonlyArray<{ + readonly id: RecordId | undefined; + readonly cellValuesByFieldId: ObjectMap | undefined; + }> + | undefined; + readonly opts?: {parseDateCellValueInColumnTimeZone?: boolean}; +} + +/** + * The Mutation emitted when the App deletes one or more {@link Record|Records}. + * + * @docsPath testing/mutations/DeleteMultipleRecordsMutation + */ +export interface DeleteMultipleRecordsMutation { + /** This Mutation's [discriminant property](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) */ + readonly type: typeof MutationTypesCore.DELETE_MULTIPLE_RECORDS; + /** The identifier for the Table in which Records are being deleted */ + readonly tableId: TableId; + /** The identifiers for records being deleted */ + readonly recordIds: ReadonlyArray; +} + +/** @hidden */ +export interface PartialDeleteMultipleRecordsMutation { + readonly type: typeof MutationTypesCore.DELETE_MULTIPLE_RECORDS; + readonly tableId: TableId | undefined; + readonly recordIds: ReadonlyArray | undefined; +} + +/** + * The Mutation emitted when the App creates one or more {@link Record|Records}. + * + * @docsPath testing/mutations/CreateMultipleRecordsMutation + */ +export interface CreateMultipleRecordsMutation { + /** This Mutation's [discriminant property](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) */ + readonly type: typeof MutationTypesCore.CREATE_MULTIPLE_RECORDS; + /** The identifier for the Table in which Records are being created */ + readonly tableId: TableId; + /** The records being created */ + readonly records: ReadonlyArray<{ + readonly id: RecordId; + readonly cellValuesByFieldId: ObjectMap; + }>; + /** @hidden */ + readonly opts?: {parseDateCellValueInColumnTimeZone?: boolean}; +} + +/** @hidden */ +export interface PartialCreateMultipleRecordsMutation { + readonly type: typeof MutationTypesCore.CREATE_MULTIPLE_RECORDS; + readonly tableId: TableId | undefined; + readonly records: + | ReadonlyArray<{ + readonly id: RecordId | undefined; + readonly cellValuesByFieldId: ObjectMap | undefined; + }> + | undefined; + readonly opts?: {parseDateCellValueInColumnTimeZone?: boolean}; +} + /** @hidden */ -export type MutationCore = SetMultipleGlobalConfigPathsMutation; +export type MutationCore = + | SetMultipleGlobalConfigPathsMutation + | SetMultipleRecordsCellValuesMutation + | DeleteMultipleRecordsMutation + | CreateMultipleRecordsMutation; /** @hidden */ -export type PartialMutationCore = PartialSetMultipleGlobalConfigPathsMutation; +export type PartialMutationCore = + | PartialSetMultipleGlobalConfigPathsMutation + | PartialSetMultipleRecordsCellValuesMutation + | PartialDeleteMultipleRecordsMutation + | PartialCreateMultipleRecordsMutation; /** */ export interface SuccessfulPermissionCheckResult { diff --git a/packages/sdk/src/shared/types/table_core.ts b/packages/sdk/src/shared/types/table_core.ts index 2b43457bb..057036962 100644 --- a/packages/sdk/src/shared/types/table_core.ts +++ b/packages/sdk/src/shared/types/table_core.ts @@ -1,7 +1,5 @@ /** @module @airtable/blocks/models: Table */ /** */ -import {ObjectMap} from '../private_utils'; -import {FieldData, FieldPermissionData} from './field'; -import {TableId, FieldId} from './hyper_ids'; +import {TableId} from './hyper_ids'; /** @hidden */ export type TableLock = unknown; @@ -13,16 +11,15 @@ export interface TableDataCore { id: TableId; name: string; primaryFieldId: string; - fieldsById: ObjectMap; description: string | null; lock: TableLock | null; externalSyncById: ExternalSyncById | null; } /** @hidden */ -export interface TablePermissionData { +export interface TablePermissionDataCore { readonly id: TableId; readonly name: string; - readonly fieldsById: {readonly [key: string]: FieldPermissionData}; readonly lock: TableLock | null; + readonly externalSyncById: ExternalSyncById | null; } diff --git a/packages/sdk/src/testing/abstract_mock_airtable_interface.ts b/packages/sdk/src/testing/abstract_mock_airtable_interface.ts index 0ce9809c6..ad9e7f0a0 100644 --- a/packages/sdk/src/testing/abstract_mock_airtable_interface.ts +++ b/packages/sdk/src/testing/abstract_mock_airtable_interface.ts @@ -18,8 +18,8 @@ import { import {TableId, FieldId, ViewId, RecordId} from '../shared/types/hyper_ids'; import {cloneDeep, ObjectMap} from '../shared/private_utils'; import {spawnError} from '../shared/error_utils'; -import {FieldData} from '../shared/types/field'; import {ModelChange} from '../shared/types/base_core'; +import {FieldData} from '../base/types/field'; import {RecordData} from '../base/types/record'; import {ViewportSizeConstraint} from '../base/types/viewport'; import {PermissionCheckResult} from '../shared/types/mutations_core'; diff --git a/packages/sdk/stories/cell_renderer.stories.tsx b/packages/sdk/stories/cell_renderer.stories.tsx index 3170f7df4..3f5345b62 100644 --- a/packages/sdk/stories/cell_renderer.stories.tsx +++ b/packages/sdk/stories/cell_renderer.stories.tsx @@ -4,7 +4,7 @@ import Example from './helpers/example'; import {cellRendererStylePropTypes} from '../src/base/ui/cell_renderer'; import {values} from '../src/shared/private_utils'; import FakeCellRenderer from './helpers/fake_cell_renderer'; -import {FieldType} from '../src/shared/types/field'; +import {FieldType} from '../src/shared/types/field_core'; import {ReadableFieldTypes} from './helpers/field_type'; export default { diff --git a/packages/sdk/stories/field_icon.stories.tsx b/packages/sdk/stories/field_icon.stories.tsx index 3857a8208..0e76922d2 100644 --- a/packages/sdk/stories/field_icon.stories.tsx +++ b/packages/sdk/stories/field_icon.stories.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {values as objectValues} from '../src/shared/private_utils'; import Icon, {iconStylePropTypes} from '../src/base/ui/icon'; -import {FieldType} from '../src/shared/types/field'; +import {FieldType} from '../src/shared/types/field_core'; import {ReadableFieldTypes, IconNamesByFieldType} from './helpers/field_type'; import Example from './helpers/example'; diff --git a/packages/sdk/stories/helpers/fake_cell_renderer.tsx b/packages/sdk/stories/helpers/fake_cell_renderer.tsx index 93c5f387f..8c48da4d2 100644 --- a/packages/sdk/stories/helpers/fake_cell_renderer.tsx +++ b/packages/sdk/stories/helpers/fake_cell_renderer.tsx @@ -9,7 +9,7 @@ import {CONTROL_WIDTH} from './code_utils'; import choiceOptions from './choice_options'; import syncSourceOptions from './sync_source_options'; import collaboratorOptions from './collaborator_options'; -import {FieldType} from '../../src/shared/types/field'; +import {FieldType} from '../../src/shared/types/field_core'; import attachments from './attachments'; import FakeForeignRecord from './fake_foreign_record'; diff --git a/packages/sdk/stories/helpers/fake_record_card.tsx b/packages/sdk/stories/helpers/fake_record_card.tsx index 1d628a714..3a012a005 100644 --- a/packages/sdk/stories/helpers/fake_record_card.tsx +++ b/packages/sdk/stories/helpers/fake_record_card.tsx @@ -6,7 +6,7 @@ import Text from '../../src/base/ui/text'; import Heading from '../../src/base/ui/heading'; import Dialog from '../../src/base/ui/dialog'; import Button from '../../src/base/ui/button'; -import {FieldType} from '../../src/shared/types/field'; +import {FieldType} from '../../src/shared/types/field_core'; import FakeCellRenderer from './fake_cell_renderer'; import {recordCardAttachment} from './attachments'; import {CONTROL_WIDTH} from './code_utils'; diff --git a/packages/sdk/stories/helpers/field_type.ts b/packages/sdk/stories/helpers/field_type.ts index f81613101..8cf53bed5 100644 --- a/packages/sdk/stories/helpers/field_type.ts +++ b/packages/sdk/stories/helpers/field_type.ts @@ -1,6 +1,6 @@ import {ObjectMap} from '../../src/shared/private_utils'; import {IconName} from '../../src/base/ui/icon_config'; -import {FieldType} from '../../src/shared/types/field'; +import {FieldType} from '../../src/shared/types/field_core'; export const ReadableFieldTypes: ObjectMap = { [FieldType.SINGLE_LINE_TEXT]: 'Single line text', diff --git a/packages/sdk/stories/record_card_list.stories.tsx b/packages/sdk/stories/record_card_list.stories.tsx index 66139f492..45a36a9bb 100644 --- a/packages/sdk/stories/record_card_list.stories.tsx +++ b/packages/sdk/stories/record_card_list.stories.tsx @@ -3,7 +3,7 @@ import React from 'react'; import Example from './helpers/example'; import {recordCardListStylePropTypes} from '../src/base/ui/record_card_list'; import Box from '../src/base/ui/box'; -import {FieldType} from '../src/shared/types/field'; +import {FieldType} from '../src/shared/types/field_core'; import FakeRecordCard from './helpers/fake_record_card'; import {RecordCardList} from '../src/base/ui/ui'; diff --git a/packages/sdk/test/airtable_interface_mocks/fixture_data.ts b/packages/sdk/test/airtable_interface_mocks/fixture_data.ts index 9e8e0af88..ee3c3b3d5 100644 --- a/packages/sdk/test/airtable_interface_mocks/fixture_data.ts +++ b/packages/sdk/test/airtable_interface_mocks/fixture_data.ts @@ -1,6 +1,7 @@ import {BaseId, TableId, FieldId, ViewId, RecordId} from '../../src/shared/types/hyper_ids'; import {TableData} from '../../src/base/types/table'; -import {FieldType, FieldData} from '../../src/shared/types/field'; +import {FieldType} from '../../src/shared/types/field_core'; +import {FieldData} from '../../src/base/types/field'; import {ViewData, ViewType} from '../../src/base/types/view'; import {CollaboratorData} from '../../src/shared/types/collaborator'; import {Color} from '../../src/shared/colors'; diff --git a/packages/sdk/test/airtable_interface_mocks/linked_records.ts b/packages/sdk/test/airtable_interface_mocks/linked_records.ts index cea3a94fe..173773702 100644 --- a/packages/sdk/test/airtable_interface_mocks/linked_records.ts +++ b/packages/sdk/test/airtable_interface_mocks/linked_records.ts @@ -1,5 +1,5 @@ import {ViewType} from '../../src/base/types/view'; -import {FieldType} from '../../src/shared/types/field'; +import {FieldType} from '../../src/shared/types/field_core'; import {FixtureData} from './fixture_data'; const linkedRecords: FixtureData = { diff --git a/packages/sdk/test/airtable_interface_mocks/project_tracker.tsx b/packages/sdk/test/airtable_interface_mocks/project_tracker.tsx index 6058b15f4..fad370d76 100644 --- a/packages/sdk/test/airtable_interface_mocks/project_tracker.tsx +++ b/packages/sdk/test/airtable_interface_mocks/project_tracker.tsx @@ -1,5 +1,5 @@ import {ViewType} from '../../src/base/types/view'; -import {FieldType} from '../../src/shared/types/field'; +import {FieldType} from '../../src/shared/types/field_core'; import {FixtureData} from './fixture_data'; const projectTracker: FixtureData = { diff --git a/packages/sdk/test/models/base.test.ts b/packages/sdk/test/models/base.test.ts index 94259c128..f465e135c 100644 --- a/packages/sdk/test/models/base.test.ts +++ b/packages/sdk/test/models/base.test.ts @@ -1,5 +1,5 @@ import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; -import {FieldType} from '../../src/shared/types/field'; +import {FieldType} from '../../src/shared/types/field_core'; import {MutationTypes} from '../../src/base/types/mutations'; import Base from '../../src/base/models/base'; import Sdk from '../../src/base/sdk'; diff --git a/packages/sdk/test/models/field.test.ts b/packages/sdk/test/models/field.test.ts index 2652e0c20..7654ce473 100644 --- a/packages/sdk/test/models/field.test.ts +++ b/packages/sdk/test/models/field.test.ts @@ -1,6 +1,6 @@ import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; import Field from '../../src/base/models/field'; -import {FieldType} from '../../src/shared/types/field'; +import {FieldType} from '../../src/shared/types/field_core'; import {__reset, __sdk as sdk} from '../../src/base'; import {MutationTypes} from '../../src/base/types/mutations'; diff --git a/packages/sdk/test/models/mutations.test.ts b/packages/sdk/test/models/mutations.test.ts index 88e49d3fc..22874e8af 100644 --- a/packages/sdk/test/models/mutations.test.ts +++ b/packages/sdk/test/models/mutations.test.ts @@ -4,7 +4,7 @@ import Mutations from '../../src/base/models/mutations'; import Sdk from '../../src/base/sdk'; import Session from '../../src/base/models/session'; import {ModelChange} from '../../src/shared/types/base_core'; -import {FieldType} from '../../src/shared/types/field'; +import {FieldType} from '../../src/shared/types/field_core'; import {MutationTypes} from '../../src/base/types/mutations'; import {BlockRunContextType} from '../../src/base/types/airtable_interface'; import {FieldTypeConfig} from '../../src/shared/types/airtable_interface_core'; diff --git a/packages/sdk/test/models/session.test.ts b/packages/sdk/test/models/session.test.ts index 0e1f66692..82f26e3aa 100644 --- a/packages/sdk/test/models/session.test.ts +++ b/packages/sdk/test/models/session.test.ts @@ -1,12 +1,12 @@ import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; import {UserId} from '../../src/shared/types/hyper_ids'; import {PermissionLevel, PermissionLevels} from '../../src/shared/types/permission_levels'; +import {MutationTypes} from '../../src/base/types/mutations'; import { - MutationTypes, DeleteMultipleRecordsMutation, CreateMultipleRecordsMutation, SetMultipleRecordsCellValuesMutation, -} from '../../src/base/types/mutations'; +} from '../../src/shared/types/mutations_core'; import {__reset, __sdk as sdk} from '../../src/base'; let mockAirtableInterface: jest.Mocked; diff --git a/packages/sdk/test/models/table.test.ts b/packages/sdk/test/models/table.test.ts index eaeb99db1..28e19fa9c 100644 --- a/packages/sdk/test/models/table.test.ts +++ b/packages/sdk/test/models/table.test.ts @@ -6,7 +6,7 @@ import Field from '../../src/base/models/field'; import View from '../../src/base/models/view'; import {TableId, FieldId, ViewId, RecordId} from '../../src/shared/types/hyper_ids'; import {ViewType} from '../../src/base/types/view'; -import {FieldType} from '../../src/shared/types/field'; +import {FieldType} from '../../src/shared/types/field_core'; import {MutationTypes} from '../../src/base/types/mutations'; import Sdk from '../../src/base/sdk'; diff --git a/packages/sdk/test/models/table_or_view_query_result.test.ts b/packages/sdk/test/models/table_or_view_query_result.test.ts index f5c4a9031..a812b8ee2 100644 --- a/packages/sdk/test/models/table_or_view_query_result.test.ts +++ b/packages/sdk/test/models/table_or_view_query_result.test.ts @@ -3,7 +3,7 @@ import Base from '../../src/base/models/base'; import {waitForWatchKeyAsync} from '../test_helpers'; import {__reset, __sdk as sdk} from '../../src/base'; import {modes as recordColorModes} from '../../src/base/models/record_coloring'; -import {FieldType} from '../../src/shared/types/field'; +import {FieldType} from '../../src/shared/types/field_core'; import {RecordData} from '../../src/base/types/record'; import Table from '../../src/base/models/table'; import Field from '../../src/base/models/field'; From 56beed6fe6c23d4e3768021c0e0b9840eb5789d2 Mon Sep 17 00:00:00 2001 From: Airtable Date: Thu, 26 Jun 2025 21:18:25 +0000 Subject: [PATCH 05/14] @airtable/blocks@0.0.0-experimental-c4b2228f4-20250630 --- packages/sdk/src/interface/models/record.ts | 2 +- packages/sdk/src/interface/types/airtable_interface.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/interface/models/record.ts b/packages/sdk/src/interface/models/record.ts index e6c54b53c..95ae8633b 100644 --- a/packages/sdk/src/interface/models/record.ts +++ b/packages/sdk/src/interface/models/record.ts @@ -40,7 +40,7 @@ export class Record extends RecordCore { field: Field, filterString: string, ): Promise<{ - records: ReadonlyArray<{recordId: RecordId; displayName: string}>; + records: ReadonlyArray<{id: RecordId; name: string}>; }> { const parentTable = this.parentTable; if (field.parentTable !== parentTable) { diff --git a/packages/sdk/src/interface/types/airtable_interface.ts b/packages/sdk/src/interface/types/airtable_interface.ts index da3b13138..a0e0820bb 100644 --- a/packages/sdk/src/interface/types/airtable_interface.ts +++ b/packages/sdk/src/interface/types/airtable_interface.ts @@ -81,7 +81,7 @@ export interface AirtableInterface extends AirtableInterfaceCore}>; + ): Promise<{records: ReadonlyArray<{id: RecordId; name: string}>}>; setCustomPropertiesAsync( properties: Array, ): Promise; From cdf2cac100f0a38e7d78146b0c68c1ec5cc77ec4 Mon Sep 17 00:00:00 2001 From: Airtable Date: Thu, 4 Sep 2025 18:36:05 +0000 Subject: [PATCH 06/14] @airtable/blocks@0.0.0-experimental-3649c7162-20250904 --- package.json | 8 +- packages/sdk/{.eslintrc.js => .eslintrc.cjs} | 3 + .../sdk/{.prettierrc.js => .prettierrc.cjs} | 0 packages/sdk/babel.config.cjs | 16 + packages/sdk/babel.config.js | 30 - packages/sdk/global_constants.cjs | 8 + packages/sdk/global_constants.js | 4 - packages/sdk/models.js | 2 +- packages/sdk/package.json | 45 +- ...heck_typescript_when_installed_in_block.sh | 8 +- ...release.js => interface-alpha-release.cjs} | 10 + packages/sdk/src/base/assert_run_context.ts | 14 + packages/sdk/src/base/index.ts | 4 +- packages/sdk/src/base/models/base.ts | 26 + .../sdk/src/base/models/create_aggregators.ts | 30 +- packages/sdk/src/base/models/field.ts | 9 +- packages/sdk/src/base/models/models.ts | 16 +- packages/sdk/src/base/models/mutations.ts | 9 + packages/sdk/src/base/sdk.ts | 6 - packages/sdk/src/base/ui/box.tsx | 42 +- packages/sdk/src/base/ui/button.tsx | 44 +- packages/sdk/src/base/ui/cell_renderer.tsx | 34 +- packages/sdk/src/base/ui/choice_token.tsx | 24 +- .../sdk/src/base/ui/collaborator_token.tsx | 26 +- packages/sdk/src/base/ui/color_palette.tsx | 35 +- .../sdk/src/base/ui/color_palette_synced.tsx | 13 +- .../sdk/src/base/ui/confirmation_dialog.tsx | 20 +- packages/sdk/src/base/ui/control_sizes.ts | 2 - packages/sdk/src/base/ui/dialog.tsx | 54 +- .../sdk/src/base/ui/dialog_close_button.tsx | 102 +- packages/sdk/src/base/ui/field_icon.tsx | 8 +- packages/sdk/src/base/ui/field_picker.tsx | 19 +- .../sdk/src/base/ui/field_picker_synced.tsx | 8 +- packages/sdk/src/base/ui/form_field.tsx | 27 - packages/sdk/src/base/ui/heading.tsx | 27 +- packages/sdk/src/base/ui/icon.tsx | 30 +- packages/sdk/src/base/ui/icon_config.ts | 2 - packages/sdk/src/base/ui/initialize_block.tsx | 13 +- packages/sdk/src/base/ui/input.tsx | 54 +- packages/sdk/src/base/ui/input_synced.tsx | 8 +- packages/sdk/src/base/ui/label.tsx | 38 +- packages/sdk/src/base/ui/link.tsx | 48 +- packages/sdk/src/base/ui/loader.tsx | 36 +- packages/sdk/src/base/ui/modal.tsx | 25 +- .../sdk/src/base/ui/model_picker_select.tsx | 20 +- packages/sdk/src/base/ui/popover.tsx | 90 +- packages/sdk/src/base/ui/progress_bar.tsx | 73 +- packages/sdk/src/base/ui/record_card.tsx | 37 - packages/sdk/src/base/ui/record_card_list.tsx | 51 - packages/sdk/src/base/ui/select.tsx | 54 +- .../ui/select_and_select_buttons_helpers.ts | 7 - packages/sdk/src/base/ui/select_buttons.tsx | 51 +- .../sdk/src/base/ui/select_buttons_synced.tsx | 11 +- packages/sdk/src/base/ui/select_synced.tsx | 8 +- packages/sdk/src/base/ui/switch.tsx | 45 +- packages/sdk/src/base/ui/switch_synced.tsx | 8 +- packages/sdk/src/base/ui/synced.ts | 6 - .../sdk/src/base/ui/system/all_styles_set.ts | 21 - .../ui/system/appearance/appearance_set.ts | 3 - .../ui/system/appearance/background_color.ts | 2 - .../src/base/ui/system/appearance/border.ts | 2 - .../ui/system/appearance/border_radius.ts | 2 - .../base/ui/system/appearance/box_shadow.ts | 2 - .../src/base/ui/system/appearance/opacity.ts | 2 - .../ui/system/dimensions/dimensions_set.ts | 3 - .../src/base/ui/system/dimensions/height.ts | 2 - .../base/ui/system/dimensions/max_height.ts | 2 - .../base/ui/system/dimensions/max_width.ts | 2 - .../base/ui/system/dimensions/min_height.ts | 2 - .../base/ui/system/dimensions/min_width.ts | 2 - .../src/base/ui/system/dimensions/width.ts | 2 - packages/sdk/src/base/ui/system/display.ts | 2 - .../ui/system/flex_container/align_content.ts | 2 - .../ui/system/flex_container/align_items.ts | 2 - .../flex_container/flex_container_set.ts | 3 - .../system/flex_container/flex_direction.ts | 2 - .../ui/system/flex_container/flex_wrap.ts | 2 - .../system/flex_container/justify_content.ts | 2 - .../ui/system/flex_container/justify_items.ts | 2 - .../base/ui/system/flex_item/align_self.ts | 2 - .../sdk/src/base/ui/system/flex_item/flex.ts | 2 - .../base/ui/system/flex_item/flex_basis.ts | 2 - .../src/base/ui/system/flex_item/flex_grow.ts | 2 - .../base/ui/system/flex_item/flex_item_set.ts | 3 - .../base/ui/system/flex_item/flex_shrink.ts | 2 - .../base/ui/system/flex_item/justify_self.ts | 2 - .../sdk/src/base/ui/system/flex_item/order.ts | 2 - packages/sdk/src/base/ui/system/index.ts | 168 +- packages/sdk/src/base/ui/system/overflow.ts | 2 - .../sdk/src/base/ui/system/position/bottom.ts | 2 - .../sdk/src/base/ui/system/position/left.ts | 2 - .../src/base/ui/system/position/position.ts | 2 - .../base/ui/system/position/position_set.ts | 3 - .../sdk/src/base/ui/system/position/right.ts | 2 - .../sdk/src/base/ui/system/position/top.ts | 2 - .../src/base/ui/system/position/z_index.ts | 2 - .../sdk/src/base/ui/system/spacing/margin.ts | 2 - .../sdk/src/base/ui/system/spacing/padding.ts | 2 - .../src/base/ui/system/spacing/spacing_set.ts | 3 - .../base/ui/system/typography/font_family.ts | 2 - .../base/ui/system/typography/font_size.ts | 2 - .../base/ui/system/typography/font_style.ts | 2 - .../base/ui/system/typography/font_weight.ts | 2 - .../ui/system/typography/letter_spacing.ts | 2 - .../base/ui/system/typography/line_height.ts | 2 - .../base/ui/system/typography/text_align.ts | 2 - .../base/ui/system/typography/text_color.ts | 2 - .../ui/system/typography/text_decoration.ts | 2 - .../ui/system/typography/text_transform.ts | 2 - .../ui/system/typography/typography_set.ts | 3 - .../utils/create_responsive_prop_type.ts | 16 - .../system/utils/create_style_prop_types.ts | 17 - .../ui/system/utils/enum_prop_type_utils.ts | 29 - packages/sdk/src/base/ui/table_picker.tsx | 15 +- .../sdk/src/base/ui/table_picker_synced.tsx | 8 +- packages/sdk/src/base/ui/text.tsx | 50 +- packages/sdk/src/base/ui/text_button.tsx | 45 +- packages/sdk/src/base/ui/tooltip.tsx | 30 +- packages/sdk/src/base/ui/types/aria_props.ts | 13 - .../src/base/ui/types/data_attributes_prop.ts | 5 - .../src/base/ui/types/tooltip_anchor_props.ts | 8 - packages/sdk/src/base/ui/ui.ts | 2 + packages/sdk/src/base/ui/view_picker.tsx | 19 +- .../sdk/src/base/ui/view_picker_synced.tsx | 8 +- .../sdk/src/base/ui/viewport_constraint.tsx | 14 - .../sdk/src/base/ui/with_styled_system.tsx | 26 +- .../sdk/src/base/unstable_testing_utils.ts | 2 +- .../sdk/src/interface/assert_run_context.ts | 11 + packages/sdk/src/interface/index.ts | 12 +- packages/sdk/src/interface/models/models.ts | 2 + .../sdk/src/interface/models/mutations.ts | 8 + packages/sdk/src/interface/models/table.ts | 29 +- packages/sdk/src/interface/types/field.ts | 6 + .../sdk/src/interface/ui/initialize_block.tsx | 9 +- packages/sdk/src/interface/ui/ui.ts | 2 + .../src/interface/ui/use_custom_properties.ts | 2 +- packages/sdk/src/shared/models/field_core.ts | 4 +- .../sdk/src/shared/models/mutations_core.ts | 7 +- packages/sdk/src/shared/models/table_core.ts | 90 +- packages/sdk/src/shared/private_utils.ts | 20 - packages/sdk/src/shared/types/field_core.ts | 20 +- .../sdk/src/shared/types/mutations_core.ts | 20 +- .../global_config_synced_component_helpers.ts | 5 - packages/sdk/src/shared/ui/loader.tsx | 21 +- packages/sdk/src/shared/ui/use_watchable.ts | 6 +- packages/sdk/src/shared/ui/with_hooks.tsx | 12 +- .../abstract_mock_airtable_interface.ts | 32 +- .../abstract_mock_airtable_interface.ts | 161 ++ packages/sdk/stories/box/box.stories.tsx | 2 - packages/sdk/stories/button.stories.tsx | 3 +- .../sdk/stories/cell_renderer.stories.tsx | 2 - packages/sdk/stories/choice_token.stories.tsx | 3 +- .../stories/collaborator_token.stories.tsx | 5 +- .../sdk/stories/color_palette.stories.tsx | 4 +- .../stories/confirmation_dialog.stories.tsx | 2 - packages/sdk/stories/dialog.stories.tsx | 3 +- packages/sdk/stories/field_icon.stories.tsx | 3 +- packages/sdk/stories/field_picker.stories.tsx | 4 +- packages/sdk/stories/form_field.stories.tsx | 3 +- packages/sdk/stories/heading.stories.tsx | 2 - packages/sdk/stories/helpers/example.tsx | 7 - .../sdk/stories/helpers/style_prop_list.tsx | 73 - packages/sdk/stories/icon_example.tsx | 3 +- packages/sdk/stories/input.stories.tsx | 4 +- packages/sdk/stories/label.stories.tsx | 2 - packages/sdk/stories/link.stories.tsx | 3 +- packages/sdk/stories/loader.stories.tsx | 3 +- packages/sdk/stories/progress_bar.stories.tsx | 3 +- packages/sdk/stories/record_card.stories.tsx | 2 - .../sdk/stories/record_card_list.stories.tsx | 2 - packages/sdk/stories/select.stories.tsx | 4 +- .../sdk/stories/select_buttons.stories.tsx | 4 +- packages/sdk/stories/switch.stories.tsx | 4 +- packages/sdk/stories/table_picker.stories.tsx | 4 +- packages/sdk/stories/text.stories.tsx | 2 - packages/sdk/stories/text_button.stories.tsx | 3 +- packages/sdk/stories/view_picker.stories.tsx | 4 +- .../airtable_interface_mocks/README.md | 0 .../airtable_interface_mocks/fixture_data.ts | 18 +- .../linked_records.ts | 4 +- .../mock_airtable_interface.ts | 18 +- .../project_tracker.ts} | 4 +- packages/sdk/test/{ => base}/index.test.ts | 27 +- .../sdk/test/{ => base}/models/base.test.ts | 34 +- .../sdk/test/{ => base}/models/cursor.test.ts | 31 +- .../sdk/test/{ => base}/models/field.test.ts | 35 +- .../linked_records_query_result.test.ts | 37 +- .../test/{ => base}/models/mutations.test.ts | 124 +- .../{ => base}/models/object_pool.test.ts | 2 +- .../sdk/test/{ => base}/models/record.test.ts | 40 +- .../test/{ => base}/models/session.test.ts | 37 +- .../sdk/test/{ => base}/models/table.test.ts | 141 +- .../{ => base}/models/table_mutations.test.ts | 32 +- .../models/table_or_view_query_result.test.ts | 47 +- .../sdk/test/{ => base}/models/view.test.ts | 45 +- .../models/view_metadata_query_result.test.ts | 39 +- packages/sdk/test/{ => base}/sdk.test.ts | 39 +- .../sdk/test/base/ui/base_provider.test.tsx | 36 + .../sdk/test/base/ui/block_wrapper.test.tsx | 57 + packages/sdk/test/base/ui/box.test.tsx | 9 + packages/sdk/test/base/ui/button.test.tsx | 9 + .../sdk/test/base/ui/cell_renderer.test.tsx | 34 + .../sdk/test/base/ui/choice_token.test.tsx | 9 + .../{ => base}/ui/collaborator_token.test.tsx | 6 +- .../sdk/test/base/ui/color_palette.test.tsx | 9 + .../base/ui/color_palette_synced.test.tsx | 33 + .../test/base/ui/confirmation_dialog.test.tsx | 11 + .../sdk/test/{ => base}/ui/dialog.test.tsx | 6 +- .../test/{ => base}/ui/expand_record.test.tsx | 46 +- .../{ => base}/ui/expand_record_list.test.tsx | 46 +- .../ui/expand_record_picker_async.test.tsx | 50 +- .../test/{ => base}/ui/field_icon.test.tsx | 35 +- .../sdk/test/base/ui/field_picker.test.tsx | 33 + .../test/base/ui/field_picker_synced.test.tsx | 33 + .../test/{ => base}/ui/form_field.test.tsx | 82 +- ...et_style_props_for_responsive_prop.test.ts | 2 +- .../sdk/test/base/ui/global_alert.test.tsx | 46 + packages/sdk/test/base/ui/heading.test.tsx | 9 + packages/sdk/test/base/ui/icon.test.tsx | 9 + .../test/base/ui/initialize_block.test.tsx | 34 + packages/sdk/test/base/ui/input.test.tsx | 23 + .../sdk/test/base/ui/input_synced.test.tsx | 33 + packages/sdk/test/base/ui/label.test.tsx | 9 + packages/sdk/test/base/ui/link.test.tsx | 9 + packages/sdk/test/base/ui/loader.test.tsx | 9 + .../sdk/test/{ => base}/ui/modal.test.tsx | 6 +- packages/sdk/test/base/ui/popover.test.tsx | 19 + .../sdk/test/base/ui/progress_bar.test.tsx | 35 + .../test/{ => base}/ui/record_card.test.tsx | 37 +- .../{ => base}/ui/record_card_list.test.tsx | 35 +- .../test/{ => base}/ui/remote_utils.test.ts | 2 +- packages/sdk/test/base/ui/select.test.tsx | 27 + .../sdk/test/base/ui/select_buttons.test.tsx | 9 + .../base/ui/select_buttons_synced.test.tsx | 33 + .../sdk/test/base/ui/select_synced.test.tsx | 33 + packages/sdk/test/base/ui/switch.test.tsx | 9 + .../sdk/test/base/ui/switch_synced.test.tsx | 37 + .../sdk/test/{ => base}/ui/synced.test.tsx | 43 +- .../sdk/test/base/ui/table_picker.test.tsx | 33 + .../test/base/ui/table_picker_synced.test.tsx | 33 + packages/sdk/test/base/ui/text.test.tsx | 9 + .../sdk/test/base/ui/text_button.test.tsx | 9 + .../sdk/test/{ => base}/ui/tooltip.test.tsx | 6 +- packages/sdk/test/{ => base}/ui/ui.test.tsx | 8 +- .../ui/unstable_standalone_ui.test.tsx | 2 +- .../{ => base}/ui/use_array_identity.test.tsx | 17 +- .../{ => base}/ui/use_color_scheme.test.tsx | 19 +- .../test/{ => base}/ui/use_loadable.test.tsx | 108 +- .../ui/use_record_action_data.test.tsx | 35 +- .../test/{ => base}/ui/use_records.test.tsx | 73 +- .../{ => base}/ui/use_view_metadata.test.tsx | 31 +- .../test/{ => base}/ui/use_watchable.test.tsx | 63 +- .../sdk/test/base/ui/view_picker.test.tsx | 33 + .../test/base/ui/view_picker_synced.test.tsx | 33 + .../ui/viewport_constraint.test.tsx | 39 +- .../airtable_interface_mocks/fixture_data.ts | 173 ++ .../linked_records.ts | 92 + .../mock_airtable_interface.ts | 84 + .../project_tracker.ts | 327 +++ .../sdk/test/interface/models/base.test.ts | 378 ++++ .../sdk/test/interface/models/field.test.ts | 396 ++++ .../test/interface/models/mutations.test.ts | 1090 ++++++++++ .../sdk/test/interface/models/record.test.ts | 922 +++++++++ .../sdk/test/interface/models/table.test.ts | 1805 +++++++++++++++++ .../test/interface/ui/block_wrapper.test.tsx | 31 + .../test/interface/ui/expand_record.test.tsx | 99 + packages/sdk/test/interface/ui/ui.test.ts | 34 + .../test/interface/ui/use_records.test.tsx | 77 + packages/sdk/test/setup_enzyme.ts | 4 - packages/sdk/test/setup_rtl.ts | 1 + .../sdk/test/{ => shared}/error_utils.test.ts | 2 +- .../test/{ => shared}/private_utils.test.ts | 4 +- .../unstable_private_utils.test.ts | 2 +- packages/sdk/test/test_helpers.ts | 23 +- packages/sdk/test/ui/base_provider.test.tsx | 34 - packages/sdk/test/ui/block_wrapper.test.tsx | 51 - packages/sdk/test/ui/box.test.tsx | 9 - packages/sdk/test/ui/button.test.tsx | 9 - packages/sdk/test/ui/cell_renderer.test.tsx | 32 - packages/sdk/test/ui/choice_token.test.tsx | 9 - packages/sdk/test/ui/color_palette.test.tsx | 9 - .../sdk/test/ui/color_palette_synced.test.tsx | 31 - .../sdk/test/ui/confirmation_dialog.test.tsx | 11 - packages/sdk/test/ui/field_picker.test.tsx | 31 - .../sdk/test/ui/field_picker_synced.test.tsx | 31 - packages/sdk/test/ui/global_alert.test.tsx | 40 - packages/sdk/test/ui/heading.test.tsx | 9 - packages/sdk/test/ui/icon.test.tsx | 9 - .../sdk/test/ui/initialize_block.test.tsx | 27 - packages/sdk/test/ui/input.test.tsx | 21 - packages/sdk/test/ui/input_synced.test.tsx | 31 - packages/sdk/test/ui/label.test.tsx | 9 - packages/sdk/test/ui/link.test.tsx | 9 - packages/sdk/test/ui/loader.test.tsx | 9 - packages/sdk/test/ui/popover.test.tsx | 13 - packages/sdk/test/ui/progress_bar.test.tsx | 37 - packages/sdk/test/ui/select.test.tsx | 25 - packages/sdk/test/ui/select_buttons.test.tsx | 9 - .../test/ui/select_buttons_synced.test.tsx | 31 - packages/sdk/test/ui/select_synced.test.tsx | 31 - packages/sdk/test/ui/switch.test.tsx | 9 - packages/sdk/test/ui/switch_synced.test.tsx | 35 - packages/sdk/test/ui/table_picker.test.tsx | 31 - .../sdk/test/ui/table_picker_synced.test.tsx | 31 - packages/sdk/test/ui/text.test.tsx | 9 - packages/sdk/test/ui/text_button.test.tsx | 9 - packages/sdk/test/ui/view_picker.test.tsx | 31 - .../sdk/test/ui/view_picker_synced.test.tsx | 31 - packages/sdk/tsconfig.json | 4 +- packages/sdk/types.js | 2 +- yarn.lock | 901 ++++---- 311 files changed, 8517 insertions(+), 3739 deletions(-) rename packages/sdk/{.eslintrc.js => .eslintrc.cjs} (95%) rename packages/sdk/{.prettierrc.js => .prettierrc.cjs} (100%) create mode 100644 packages/sdk/babel.config.cjs delete mode 100644 packages/sdk/babel.config.js create mode 100644 packages/sdk/global_constants.cjs delete mode 100644 packages/sdk/global_constants.js rename packages/sdk/scripts/{interface-alpha-release.js => interface-alpha-release.cjs} (91%) create mode 100644 packages/sdk/src/base/assert_run_context.ts delete mode 100644 packages/sdk/src/base/ui/system/utils/create_responsive_prop_type.ts delete mode 100644 packages/sdk/src/base/ui/system/utils/create_style_prop_types.ts delete mode 100644 packages/sdk/src/base/ui/system/utils/enum_prop_type_utils.ts create mode 100644 packages/sdk/src/interface/assert_run_context.ts rename packages/sdk/src/testing/{ => base}/abstract_mock_airtable_interface.ts (88%) create mode 100644 packages/sdk/src/testing/interface/abstract_mock_airtable_interface.ts delete mode 100644 packages/sdk/stories/helpers/style_prop_list.tsx rename packages/sdk/test/{ => base}/airtable_interface_mocks/README.md (100%) rename packages/sdk/test/{ => base}/airtable_interface_mocks/fixture_data.ts (92%) rename packages/sdk/test/{ => base}/airtable_interface_mocks/linked_records.ts (97%) rename packages/sdk/test/{ => base}/airtable_interface_mocks/mock_airtable_interface.ts (89%) rename packages/sdk/test/{airtable_interface_mocks/project_tracker.tsx => base/airtable_interface_mocks/project_tracker.ts} (99%) rename packages/sdk/test/{ => base}/index.test.ts (76%) rename packages/sdk/test/{ => base}/models/base.test.ts (96%) rename packages/sdk/test/{ => base}/models/cursor.test.ts (94%) rename packages/sdk/test/{ => base}/models/field.test.ts (96%) rename packages/sdk/test/{ => base}/models/linked_records_query_result.test.ts (98%) rename packages/sdk/test/{ => base}/models/mutations.test.ts (95%) rename packages/sdk/test/{ => base}/models/object_pool.test.ts (98%) rename packages/sdk/test/{ => base}/models/record.test.ts (97%) rename packages/sdk/test/{ => base}/models/session.test.ts (94%) rename packages/sdk/test/{ => base}/models/table.test.ts (93%) rename packages/sdk/test/{ => base}/models/table_mutations.test.ts (83%) rename packages/sdk/test/{ => base}/models/table_or_view_query_result.test.ts (98%) rename packages/sdk/test/{ => base}/models/view.test.ts (96%) rename packages/sdk/test/{ => base}/models/view_metadata_query_result.test.ts (93%) rename packages/sdk/test/{ => base}/sdk.test.ts (96%) create mode 100644 packages/sdk/test/base/ui/base_provider.test.tsx create mode 100644 packages/sdk/test/base/ui/block_wrapper.test.tsx create mode 100644 packages/sdk/test/base/ui/box.test.tsx create mode 100644 packages/sdk/test/base/ui/button.test.tsx create mode 100644 packages/sdk/test/base/ui/cell_renderer.test.tsx create mode 100644 packages/sdk/test/base/ui/choice_token.test.tsx rename packages/sdk/test/{ => base}/ui/collaborator_token.test.tsx (67%) create mode 100644 packages/sdk/test/base/ui/color_palette.test.tsx create mode 100644 packages/sdk/test/base/ui/color_palette_synced.test.tsx create mode 100644 packages/sdk/test/base/ui/confirmation_dialog.test.tsx rename packages/sdk/test/{ => base}/ui/dialog.test.tsx (62%) rename packages/sdk/test/{ => base}/ui/expand_record.test.tsx (69%) rename packages/sdk/test/{ => base}/ui/expand_record_list.test.tsx (72%) rename packages/sdk/test/{ => base}/ui/expand_record_picker_async.test.tsx (76%) rename packages/sdk/test/{ => base}/ui/field_icon.test.tsx (50%) create mode 100644 packages/sdk/test/base/ui/field_picker.test.tsx create mode 100644 packages/sdk/test/base/ui/field_picker_synced.test.tsx rename packages/sdk/test/{ => base}/ui/form_field.test.tsx (52%) rename packages/sdk/test/{ => base/ui}/get_style_props_for_responsive_prop.test.ts (91%) create mode 100644 packages/sdk/test/base/ui/global_alert.test.tsx create mode 100644 packages/sdk/test/base/ui/heading.test.tsx create mode 100644 packages/sdk/test/base/ui/icon.test.tsx create mode 100644 packages/sdk/test/base/ui/initialize_block.test.tsx create mode 100644 packages/sdk/test/base/ui/input.test.tsx create mode 100644 packages/sdk/test/base/ui/input_synced.test.tsx create mode 100644 packages/sdk/test/base/ui/label.test.tsx create mode 100644 packages/sdk/test/base/ui/link.test.tsx create mode 100644 packages/sdk/test/base/ui/loader.test.tsx rename packages/sdk/test/{ => base}/ui/modal.test.tsx (60%) create mode 100644 packages/sdk/test/base/ui/popover.test.tsx create mode 100644 packages/sdk/test/base/ui/progress_bar.test.tsx rename packages/sdk/test/{ => base}/ui/record_card.test.tsx (76%) rename packages/sdk/test/{ => base}/ui/record_card_list.test.tsx (58%) rename packages/sdk/test/{ => base}/ui/remote_utils.test.ts (89%) create mode 100644 packages/sdk/test/base/ui/select.test.tsx create mode 100644 packages/sdk/test/base/ui/select_buttons.test.tsx create mode 100644 packages/sdk/test/base/ui/select_buttons_synced.test.tsx create mode 100644 packages/sdk/test/base/ui/select_synced.test.tsx create mode 100644 packages/sdk/test/base/ui/switch.test.tsx create mode 100644 packages/sdk/test/base/ui/switch_synced.test.tsx rename packages/sdk/test/{ => base}/ui/synced.test.tsx (63%) create mode 100644 packages/sdk/test/base/ui/table_picker.test.tsx create mode 100644 packages/sdk/test/base/ui/table_picker_synced.test.tsx create mode 100644 packages/sdk/test/base/ui/text.test.tsx create mode 100644 packages/sdk/test/base/ui/text_button.test.tsx rename packages/sdk/test/{ => base}/ui/tooltip.test.tsx (64%) rename packages/sdk/test/{ => base}/ui/ui.test.tsx (85%) rename packages/sdk/test/{ => base}/ui/unstable_standalone_ui.test.tsx (64%) rename packages/sdk/test/{ => base}/ui/use_array_identity.test.tsx (74%) rename packages/sdk/test/{ => base}/ui/use_color_scheme.test.tsx (86%) rename packages/sdk/test/{ => base}/ui/use_loadable.test.tsx (74%) rename packages/sdk/test/{ => base}/ui/use_record_action_data.test.tsx (54%) rename packages/sdk/test/{ => base}/ui/use_records.test.tsx (68%) rename packages/sdk/test/{ => base}/ui/use_view_metadata.test.tsx (70%) rename packages/sdk/test/{ => base}/ui/use_watchable.test.tsx (81%) create mode 100644 packages/sdk/test/base/ui/view_picker.test.tsx create mode 100644 packages/sdk/test/base/ui/view_picker_synced.test.tsx rename packages/sdk/test/{ => base}/ui/viewport_constraint.test.tsx (58%) create mode 100644 packages/sdk/test/interface/airtable_interface_mocks/fixture_data.ts create mode 100644 packages/sdk/test/interface/airtable_interface_mocks/linked_records.ts create mode 100644 packages/sdk/test/interface/airtable_interface_mocks/mock_airtable_interface.ts create mode 100644 packages/sdk/test/interface/airtable_interface_mocks/project_tracker.ts create mode 100644 packages/sdk/test/interface/models/base.test.ts create mode 100644 packages/sdk/test/interface/models/field.test.ts create mode 100644 packages/sdk/test/interface/models/mutations.test.ts create mode 100644 packages/sdk/test/interface/models/record.test.ts create mode 100644 packages/sdk/test/interface/models/table.test.ts create mode 100644 packages/sdk/test/interface/ui/block_wrapper.test.tsx create mode 100644 packages/sdk/test/interface/ui/expand_record.test.tsx create mode 100644 packages/sdk/test/interface/ui/ui.test.ts create mode 100644 packages/sdk/test/interface/ui/use_records.test.tsx delete mode 100644 packages/sdk/test/setup_enzyme.ts create mode 100644 packages/sdk/test/setup_rtl.ts rename packages/sdk/test/{ => shared}/error_utils.test.ts (98%) rename packages/sdk/test/{ => shared}/private_utils.test.ts (99%) rename packages/sdk/test/{ => shared}/unstable_private_utils.test.ts (66%) delete mode 100644 packages/sdk/test/ui/base_provider.test.tsx delete mode 100644 packages/sdk/test/ui/block_wrapper.test.tsx delete mode 100644 packages/sdk/test/ui/box.test.tsx delete mode 100644 packages/sdk/test/ui/button.test.tsx delete mode 100644 packages/sdk/test/ui/cell_renderer.test.tsx delete mode 100644 packages/sdk/test/ui/choice_token.test.tsx delete mode 100644 packages/sdk/test/ui/color_palette.test.tsx delete mode 100644 packages/sdk/test/ui/color_palette_synced.test.tsx delete mode 100644 packages/sdk/test/ui/confirmation_dialog.test.tsx delete mode 100644 packages/sdk/test/ui/field_picker.test.tsx delete mode 100644 packages/sdk/test/ui/field_picker_synced.test.tsx delete mode 100644 packages/sdk/test/ui/global_alert.test.tsx delete mode 100644 packages/sdk/test/ui/heading.test.tsx delete mode 100644 packages/sdk/test/ui/icon.test.tsx delete mode 100644 packages/sdk/test/ui/initialize_block.test.tsx delete mode 100644 packages/sdk/test/ui/input.test.tsx delete mode 100644 packages/sdk/test/ui/input_synced.test.tsx delete mode 100644 packages/sdk/test/ui/label.test.tsx delete mode 100644 packages/sdk/test/ui/link.test.tsx delete mode 100644 packages/sdk/test/ui/loader.test.tsx delete mode 100644 packages/sdk/test/ui/popover.test.tsx delete mode 100644 packages/sdk/test/ui/progress_bar.test.tsx delete mode 100644 packages/sdk/test/ui/select.test.tsx delete mode 100644 packages/sdk/test/ui/select_buttons.test.tsx delete mode 100644 packages/sdk/test/ui/select_buttons_synced.test.tsx delete mode 100644 packages/sdk/test/ui/select_synced.test.tsx delete mode 100644 packages/sdk/test/ui/switch.test.tsx delete mode 100644 packages/sdk/test/ui/switch_synced.test.tsx delete mode 100644 packages/sdk/test/ui/table_picker.test.tsx delete mode 100644 packages/sdk/test/ui/table_picker_synced.test.tsx delete mode 100644 packages/sdk/test/ui/text.test.tsx delete mode 100644 packages/sdk/test/ui/text_button.test.tsx delete mode 100644 packages/sdk/test/ui/view_picker.test.tsx delete mode 100644 packages/sdk/test/ui/view_picker_synced.test.tsx diff --git a/package.json b/package.json index 3fddf056f..a4f4b6c69 100644 --- a/package.json +++ b/package.json @@ -20,13 +20,13 @@ "husky": "^3.1.0", "prettier": "^1.19.1", "pretty-quick": "^2.0.1", - "react": "^16.14.0", - "react-dom": "^16.9.24", + "react": "^19.1.1", + "react-dom": "^19.1.1", "release-it": "17.3.0" }, "resolutions": { - "@types/react": "^16.14.0", - "@types/react-dom": "^16.9.24", + "@types/react": "^19.1.10", + "@types/react-dom": "^19.1.7", "@typescript-eslint/eslint-plugin": "^7.13.0", "@typescript-eslint/parser": "^7.13.0", "**/mocha/mkdirp": "^0.5.6", diff --git a/packages/sdk/.eslintrc.js b/packages/sdk/.eslintrc.cjs similarity index 95% rename from packages/sdk/.eslintrc.js rename to packages/sdk/.eslintrc.cjs index 4710beb20..20c9a510b 100644 --- a/packages/sdk/.eslintrc.js +++ b/packages/sdk/.eslintrc.cjs @@ -84,6 +84,7 @@ module.exports = { 'no-path-concat': 'error', 'no-proto': 'error', 'no-prototype-builtins': 'error', + // Turning off as recommended by https://typescript-eslint.io/rules/no-redeclare/#ignoredeclarationmerge 'no-redeclare': 'off', 'no-regex-spaces': 'error', 'no-script-url': 'error', @@ -110,6 +111,7 @@ module.exports = { 'no-with': 'error', 'one-var-declaration-per-line': 'error', 'prefer-spread': 'error', + // let prettier handle this instead: quotes: 'off', radix: 'error', 'require-yield': 'off', @@ -120,6 +122,7 @@ module.exports = { 'airtable/no-missing-async-suffix': 'warn', 'airtable/no-missing-await': 'warn', 'airtable/no-process-domain': 'warn', + // TODO(alex): re-enable this. It has a bug when used with typescript-eslint 'airtable/noopener-noreferrer': 'off', '@airtable/blocks/no-throw-new': 'error', '@airtable/blocks/no-node-modules-invariant': 'error', diff --git a/packages/sdk/.prettierrc.js b/packages/sdk/.prettierrc.cjs similarity index 100% rename from packages/sdk/.prettierrc.js rename to packages/sdk/.prettierrc.cjs diff --git a/packages/sdk/babel.config.cjs b/packages/sdk/babel.config.cjs new file mode 100644 index 000000000..7533e1882 --- /dev/null +++ b/packages/sdk/babel.config.cjs @@ -0,0 +1,16 @@ +// Transpile for node 18 and the set of browsers currently supported by Airtable +const targets = { + node: '18', + firefox: '94', + chrome: '91', + safari: '14.1', + edge: '107', +}; + +module.exports = { + presets: ['@babel/typescript', ['@babel/env', {targets}], '@babel/react'], + plugins: [['transform-define', require('./global_constants.cjs')]], + parserOpts: { + allowAwaitOutsideFunction: true, + }, +}; diff --git a/packages/sdk/babel.config.js b/packages/sdk/babel.config.js deleted file mode 100644 index 5dee3babc..000000000 --- a/packages/sdk/babel.config.js +++ /dev/null @@ -1,30 +0,0 @@ -const targets = { - node: '8.10', - browsers: ['firefox >= 45', 'chrome >= 49', 'safari >= 10', 'edge >= 25'], -}; - -module.exports = { - presets: [ - '@babel/typescript', - [ - '@babel/env', - { - useBuiltIns: 'usage', - corejs: 3, - targets, - include: ['transform-classes'], - }, - ], - '@babel/react', - ], - plugins: [ - '@babel/proposal-class-properties', - '@babel/proposal-nullish-coalescing-operator', - '@babel/proposal-optional-chaining', - '@babel/transform-runtime', - ['transform-define', require('./global_constants')], - ], - parserOpts: { - allowAwaitOutsideFunction: true, - }, -}; diff --git a/packages/sdk/global_constants.cjs b/packages/sdk/global_constants.cjs new file mode 100644 index 000000000..c438b7df2 --- /dev/null +++ b/packages/sdk/global_constants.cjs @@ -0,0 +1,8 @@ +// Wherever the constants below are referenced, they'll be replaced by the values listed here at +// compile time. It's important that they're all under `global`, as otherwise the resulting flow +// errors will cause problems both here (which we can easily mitigate) and for consumers (which we +// can't) +module.exports = { + 'global.PACKAGE_VERSION': require('./package.json').version, + 'global.PACKAGE_NAME': require('./package.json').name, +}; diff --git a/packages/sdk/global_constants.js b/packages/sdk/global_constants.js deleted file mode 100644 index af92b70b3..000000000 --- a/packages/sdk/global_constants.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - 'global.PACKAGE_VERSION': require('./package.json').version, - 'global.PACKAGE_NAME': require('./package.json').name, -}; diff --git a/packages/sdk/models.js b/packages/sdk/models.js index 832abff19..67b35d7de 100644 --- a/packages/sdk/models.js +++ b/packages/sdk/models.js @@ -1 +1 @@ -module.exports = require('./dist/cjs/base/models/models'); +export * from './dist/esm/base/models/models'; diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 4f671fd6a..c274ddebf 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -2,6 +2,7 @@ "name": "@airtable/blocks", "version": "1.19.0", "description": "Airtable Blocks SDK", + "type": "module", "repository": { "type": "git", "url": "https://github.com/Airtable/blocks.git" @@ -10,15 +11,15 @@ "exports": { "./unstable_private_utils": { "types": "./dist/types/src/shared/unstable_private_utils.d.ts", - "default": "./dist/cjs/shared/unstable_private_utils.js" + "default": "./dist/esm/shared/unstable_private_utils.js" }, "./unstable_testing_utils": { "types": "./dist/types/src/base/unstable_testing_utils.d.ts", - "default": "./dist/cjs/base/unstable_testing_utils.js" + "default": "./dist/esm/base/unstable_testing_utils.js" }, "./base/unstable_standalone_ui": { "types": "./dist/types/src/base/ui/unstable_standalone_ui.d.ts", - "default": "./dist/cjs/base/ui/unstable_standalone_ui.js" + "default": "./dist/esm/base/ui/unstable_standalone_ui.js" }, "./base/types": { "types": "./types.d.ts", @@ -26,23 +27,23 @@ }, "./base/models": { "types": "./dist/types/src/base/models/models.d.ts", - "default": "./dist/cjs/base/models/models.js" + "default": "./dist/esm/base/models/models.js" }, "./base/ui": { "types": "./dist/types/src/base/ui/ui.d.ts", - "default": "./dist/cjs/base/ui/ui.js" + "default": "./dist/esm/base/ui/ui.js" }, "./base": { "types": "./dist/types/src/base/index.d.ts", - "default": "./dist/cjs/base/index.js" + "default": "./dist/esm/base/index.js" }, "./interface/models": { "types": "./dist/types/src/interface/models/models.d.ts", - "default": "./dist/cjs/interface/models/models.js" + "default": "./dist/esm/interface/models/models.js" }, "./interface/ui": { "types": "./dist/types/src/interface/ui/ui.d.ts", - "default": "./dist/cjs/interface/ui/ui.js" + "default": "./dist/esm/interface/ui/ui.js" } }, "files": [ @@ -66,7 +67,7 @@ "ci": "echo '--- sdk' && yarn run build && yarn run test:coverage && ./scripts/check_typescript_when_installed_in_block.sh", "pretest": "yarn run lint && yarn run types", "version": "changelog-publish --github-repo-url='https://github.com/airtable/blocks' --git-tag-prefix='@airtable/blocks@' && yarn run build:docs && git add CHANGELOG.md ../blocks-docs/docs.json", - "release": "node ./scripts/interface-alpha-release.js", + "release": "node ./scripts/interface-alpha-release.cjs", "types": "tsc", "lint": "ESLINT_USE_FLAT_CONFIG=false eslint --report-unused-disable-directives --ext .js,.ts,.tsx src test", "lint:quiet": "yarn run lint --quiet", @@ -75,7 +76,7 @@ "test": "yarn run build && yarn run jest", "test:coverage": "yarn run test --coverage", "build:clean": "rm -rf dist", - "build:babel": "babel src --out-dir dist/cjs --extensions=.js,.ts,.tsx --ignore='**/*.d.ts'", + "build:babel": "babel src --out-dir dist/esm --extensions=.js,.ts,.tsx --ignore='**/*.d.ts'", "watch:babel": "yarn run build:babel --watch --source-maps inline", "build:types": "tsc --outDir dist/types --declaration --declarationMap --noEmit false --allowJs false --checkJs false --emitDeclarationOnly --stripInternal", "watch:types": "yarn run build:types --watch", @@ -107,8 +108,9 @@ "@storybook/react": "^8.4.7", "@storybook/react-webpack5": "^8.4.7", "@storybook/storybook-deployer": "^2.8.1", - "@types/enzyme": "^3.10.4", - "@types/enzyme-adapter-react-16": "^1.0.5", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.7.0", + "@testing-library/react": "^16.3.0", "@types/fs-extra": "^8.0.1", "@types/glob": "^7.1.1", "@types/hoist-non-react-statics": "^3.3.5", @@ -117,8 +119,8 @@ "@types/lodash.clamp": "^4.0.9", "@types/prettier": "^1.19.0", "@types/prop-types": "^15.7.12", - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.2", + "@types/react": "^19.1.10", + "@types/react-dom": "^19.1.7", "@types/react-window": "^1.8.8", "@types/styled-system": "^5.1.4", "@types/styled-system__core": "^5.1.6", @@ -128,8 +130,6 @@ "babel-loader": "^8.0.6", "babel-plugin-transform-define": "^1.3.1", "concurrently": "^5.0.0", - "enzyme": "^3.10.0", - "enzyme-adapter-react-16": "^1.15.1", "eslint": "^9.5.0", "eslint-plugin-airtable": "github:hyperbase/eslint-plugin-airtable#01bbfe0", "eslint-plugin-import": "^2.29.1", @@ -138,6 +138,7 @@ "eslint-plugin-react-hooks": "^5.2.0", "glob": "^7.1.6", "jest": "^24.9.0", + "jest-environment-jsdom-sixteen": "^2.0.0", "lodash.capitalize": "^4.2.1", "lodash.clamp": "^4.0.3", "prettier": "^1.19.1", @@ -152,17 +153,17 @@ "emotion": "^10.0.23", "fast-deep-equal": "^3.1.1", "hoist-non-react-statics": "^3.3.2", - "prop-types": "15.8.1", - "react-window": "1.8.10", + "react-window": "1.8.11", "use-subscription": "^1.3.0" }, "peerDependencies": { - "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.9.24 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "react": "^19.0.0", + "react-dom": "^19.0.0" }, "jest": { - "setupFiles": [ - "/test/setup_enzyme.ts" + "testEnvironment": "jest-environment-jsdom-sixteen", + "setupFilesAfterEnv": [ + "/test/setup_rtl.ts" ], "collectCoverageFrom": [ "src/models/**/*", diff --git a/packages/sdk/scripts/check_typescript_when_installed_in_block.sh b/packages/sdk/scripts/check_typescript_when_installed_in_block.sh index a01411881..8880b3198 100755 --- a/packages/sdk/scripts/check_typescript_when_installed_in_block.sh +++ b/packages/sdk/scripts/check_typescript_when_installed_in_block.sh @@ -11,9 +11,9 @@ cd "$work_dir" cat - > tsconfig.json <<'EOF' { "compilerOptions": { - "module": "NodeNext", - "moduleResolution": "nodenext", - "target": "es2018", + "module": "esnext", + "moduleResolution": "bundler", + "target": "es2019", "allowSyntheticDefaultImports": true }, "include": ["source.ts"] @@ -24,7 +24,7 @@ cat - > package.json <().sdkInitData.runContext.type; + if ( + runContextType !== BlockRunContextType.DASHBOARD_APP && + runContextType !== BlockRunContextType.VIEW + ) { + throw spawnError('Unexpected import when running block in base'); + } +} diff --git a/packages/sdk/src/base/index.ts b/packages/sdk/src/base/index.ts index 31e477da3..f3f3629b0 100644 --- a/packages/sdk/src/base/index.ts +++ b/packages/sdk/src/base/index.ts @@ -1,9 +1,10 @@ +import './assert_run_context'; + import warn, {__injectSdkIntoWarning} from '../shared/warning'; import getAirtableInterface from '../injected/airtable_interface'; import {BaseSdkMode} from '../sdk_mode'; import {__injectSdkIntoPerformRecordAction} from './perform_record_action'; import Sdk from './sdk'; -import {__injectSdkIntoCreateAggregators} from './models/create_aggregators'; import {__injectSdkIntoInitializeBlock} from './ui/initialize_block'; /** @internal */ @@ -70,7 +71,6 @@ export function __reset() { unstable_fetchAsync, } = __sdk); - __injectSdkIntoCreateAggregators(__sdk); __injectSdkIntoPerformRecordAction(__sdk); __injectSdkIntoInitializeBlock(__sdk); __injectSdkIntoWarning(__sdk); diff --git a/packages/sdk/src/base/models/base.ts b/packages/sdk/src/base/models/base.ts index 6e848a48b..8d1087d5d 100644 --- a/packages/sdk/src/base/models/base.ts +++ b/packages/sdk/src/base/models/base.ts @@ -10,6 +10,7 @@ import {BaseData} from '../types/base'; import BaseBlockSdk from '../sdk'; import RecordStore from './record_store'; import Table from './table'; +import createAggregators, {Aggregators} from './create_aggregators'; /** * Model class representing a base. @@ -30,6 +31,8 @@ class Base extends BaseCore { /** @internal */ static _className = 'Base'; + _aggregators: Aggregators | null = null; + /** @internal */ _constructTable(tableId: TableId): Table { const recordStore = this.__getRecordStore(tableId); @@ -46,6 +49,29 @@ class Base extends BaseCore { return this._data.tableOrder; } + /** + * Aggregators can be used to compute aggregates for cell values. + * + * @example + * ```js + * import {base} from '@airtable/blocks/base'; + * + * // To get a list of aggregators supported for a specific field: + * const fieldAggregators = myField.availableAggregators; + * + * // To compute the total attachment size of an attachment field: + * const aggregator = base.aggregators.totalAttachmentSize; + * const value = aggregator.aggregate(myRecords, myAttachmentField); + * const valueAsString = aggregate.aggregateToString(myRecords, myAttachmentField); + * ``` + */ + get aggregators(): Aggregators { + if (!this._aggregators) { + this._aggregators = createAggregators(this._sdk); + } + return this._aggregators; + } + /** * Checks whether the current user has permission to create a table. * diff --git a/packages/sdk/src/base/models/create_aggregators.ts b/packages/sdk/src/base/models/create_aggregators.ts index 01a4ad4e0..39892e0a4 100644 --- a/packages/sdk/src/base/models/create_aggregators.ts +++ b/packages/sdk/src/base/models/create_aggregators.ts @@ -10,13 +10,13 @@ import Field from './field'; * * @example * ```js - * import {aggregators} from '@airtable/blocks/base/models'; + * import {base} from '@airtable/blocks/base'; * * // To get a list of aggregators supported for a specific field: * const fieldAggregators = myField.availableAggregators; * * // To compute the total attachment size of an attachment field: - * const aggregator = aggregators.totalAttachmentSize; + * const aggregator = base.aggregators.totalAttachmentSize; * const value = aggregator.aggregate(myRecords, myAttachmentField); * const valueAsString = aggregate.aggregateToString(myRecords, myAttachmentField); * ``` @@ -48,7 +48,12 @@ export interface Aggregators { [key: string]: Aggregator; } -const aggregate = (aggregatorKey: AggregatorKey, records: Array, field: Field) => { +const aggregate = ( + sdk: Sdk, + aggregatorKey: AggregatorKey, + records: Array, + field: Field, +) => { if (!field.isAggregatorAvailable(aggregatorKey)) { throw spawnError( 'The %s aggregator is not available for %s fields', @@ -67,7 +72,12 @@ const aggregate = (aggregatorKey: AggregatorKey, records: Array, field: ); }; -const aggregateToString = (aggregatorKey: AggregatorKey, records: Array, field: Field) => { +const aggregateToString = ( + sdk: Sdk, + aggregatorKey: AggregatorKey, + records: Array, + field: Field, +) => { if (!field.isAggregatorAvailable(aggregatorKey)) { throw spawnError( 'The %s aggregator is not available for %s fields', @@ -97,7 +107,7 @@ const aggregateToString = (aggregatorKey: AggregatorKey, records: Array, * * @hidden */ -export default function createAggregators() { +export default function createAggregators(sdk: Sdk) { const {__airtableInterface: airtableInterface} = sdk; const aggregators: Aggregators = {}; const aggregatorKeys = airtableInterface.aggregators.getAllAvailableAggregatorKeys(); @@ -108,8 +118,8 @@ export default function createAggregators() { key, displayName: config.displayName, shortDisplayName: config.shortDisplayName, - aggregate: aggregate.bind(null, key), - aggregateToString: aggregateToString.bind(null, key), + aggregate: aggregate.bind(null, sdk, key), + aggregateToString: aggregateToString.bind(null, sdk, key), }); } @@ -117,9 +127,3 @@ export default function createAggregators() { return aggregators; } - -let sdk: Sdk; - -export function __injectSdkIntoCreateAggregators(_sdk: Sdk) { - sdk = _sdk; -} diff --git a/packages/sdk/src/base/models/field.ts b/packages/sdk/src/base/models/field.ts index 91721be6c..b21217755 100644 --- a/packages/sdk/src/base/models/field.ts +++ b/packages/sdk/src/base/models/field.ts @@ -39,8 +39,8 @@ class Field extends FieldCore { airtableInterface.aggregators.getAvailableAggregatorKeysForField(this._data), ); - const {aggregators} = require('./models'); - return values(aggregators).filter(aggregator => { + const base = this.parentTable.parentBase; + return values(base.aggregators).filter(aggregator => { return availableAggregatorKeysSet.has(aggregator.key); }); } @@ -50,8 +50,9 @@ class Field extends FieldCore { * @param aggregator The aggregator object or aggregator key. * @example * ```js - * import {aggregators} from '@airtable/blocks/base/models'; - * const aggregator = aggregators.totalAttachmentSize; + * import {base} from '@airtable/blocks/base'; + * + * const aggregator = base.aggregators.totalAttachmentSize; * * // Using an aggregator object * console.log(myAttachmentField.isAggregatorAvailable(aggregator)); diff --git a/packages/sdk/src/base/models/models.ts b/packages/sdk/src/base/models/models.ts index f6eb9a1c7..1fd21949f 100644 --- a/packages/sdk/src/base/models/models.ts +++ b/packages/sdk/src/base/models/models.ts @@ -1,6 +1,7 @@ /** @ignore */ /** */ +import '../assert_run_context'; + import * as recordColoring from './record_coloring'; -import createAggregators from './create_aggregators'; export {FieldType, FieldConfig} from '../../shared/types/field_core'; export {ViewType} from '../types/view'; export {default as Base} from './base'; @@ -16,16 +17,3 @@ export {default as ViewMetadataQueryResult} from './view_metadata_query_result'; export {default as Session} from './session'; export {default as Cursor} from './cursor'; export {recordColoring}; - -// eslint-disable-next-line no-var -export declare var aggregators: ReturnType; -let initializedAggregators: null | typeof aggregators = null; -Object.defineProperty(exports, 'aggregators', { - enumerable: true, - get: () => { - if (!initializedAggregators) { - initializedAggregators = createAggregators(); - } - return initializedAggregators; - }, -}); diff --git a/packages/sdk/src/base/models/mutations.ts b/packages/sdk/src/base/models/mutations.ts index 6e7a725a4..78f640afb 100644 --- a/packages/sdk/src/base/models/mutations.ts +++ b/packages/sdk/src/base/models/mutations.ts @@ -11,6 +11,7 @@ import { } from '../../shared/types/mutation_constants'; import {MutationsCore} from '../../shared/models/mutations_core'; import {BaseSdkMode} from '../../sdk_mode'; +import {RecordData} from '../types/record'; import Table from './table'; import RecordStore from './record_store'; @@ -26,6 +27,14 @@ class Mutations extends MutationsCore { return recordStore.areCellValuesLoadedForFieldId(fieldId); } + /** @internal */ + _getDefaultRecordProperties(): Partial { + return { + commentCount: 0, + createdTime: new Date().toJSON(), + }; + } + /** @internal */ _assertMutationIsValid(mutation: Mutation): void { diff --git a/packages/sdk/src/base/sdk.ts b/packages/sdk/src/base/sdk.ts index da1111c45..52d138a56 100644 --- a/packages/sdk/src/base/sdk.ts +++ b/packages/sdk/src/base/sdk.ts @@ -1,8 +1,6 @@ /** @hidden */ /** */ -import * as React from 'react'; -import PropTypes from 'prop-types'; import {ModelChange} from '../shared/types/base_core'; import {GlobalConfigUpdate} from '../shared/types/global_config'; import {BlockSdkCore} from '../shared/sdk_core'; @@ -18,10 +16,6 @@ import {PerformRecordAction} from './perform_record_action'; import {AirtableInterface, BlockRunContext} from './types/airtable_interface'; import {RequestJson, ResponseJson} from './types/backend_fetch_types'; -if (!(React as any).PropTypes) { - (React as any).PropTypes = PropTypes; -} - /** @hidden */ type UpdateBatcher = (applyUpdates: () => void) => void; diff --git a/packages/sdk/src/base/ui/box.tsx b/packages/sdk/src/base/ui/box.tsx index 1d009e812..c616aea4f 100644 --- a/packages/sdk/src/base/ui/box.tsx +++ b/packages/sdk/src/base/ui/box.tsx @@ -1,12 +1,11 @@ /** @module @airtable/blocks/ui: Box */ /** */ import * as React from 'react'; -import PropTypes from 'prop-types'; import {cx} from 'emotion'; import useStyledSystem from './use_styled_system'; -import {allStylesPropTypes, AllStylesProps} from './system/index'; -import {ariaPropTypes, AriaProps} from './types/aria_props'; -import {tooltipAnchorPropTypes, TooltipAnchorProps} from './types/tooltip_anchor_props'; -import {dataAttributesPropType, DataAttributesProp} from './types/data_attributes_prop'; +import {AllStylesProps} from './system/index'; +import {AriaProps} from './types/aria_props'; +import {TooltipAnchorProps} from './types/tooltip_anchor_props'; +import {DataAttributesProp} from './types/data_attributes_prop'; /** * Props for the Box component. Also accepts: @@ -116,39 +115,6 @@ const Box = (props: BoxProps, ref: React.Ref) => { const ForwardedRefBox = React.forwardRef(Box); -(ForwardedRefBox as any).propTypes = { - as: PropTypes.oneOf([ - 'div', - 'span', - 'section', - 'main', - 'nav', - 'header', - 'footer', - 'aside', - 'article', - 'address', - 'hgroup', - 'blockquote', - 'figure', - 'figcaption', - 'ol', - 'ul', - 'li', - 'pre', - ]), - id: PropTypes.string, - children: PropTypes.node, - className: PropTypes.string, - style: PropTypes.object, - tabIndex: PropTypes.number, - role: PropTypes.string, - dataAttributes: dataAttributesPropType, - ...ariaPropTypes, - ...tooltipAnchorPropTypes, - ...allStylesPropTypes, -}; - ForwardedRefBox.displayName = 'Box'; export default ForwardedRefBox; diff --git a/packages/sdk/src/base/ui/button.tsx b/packages/sdk/src/base/ui/button.tsx index 388cfb112..24e2da0ea 100644 --- a/packages/sdk/src/base/ui/button.tsx +++ b/packages/sdk/src/base/ui/button.tsx @@ -1,42 +1,33 @@ /** @module @airtable/blocks/ui: Button */ /** */ -import PropTypes from 'prop-types'; import {cx} from 'emotion'; import * as React from 'react'; import {compose} from '@styled-system/core'; import {createEnum, EnumType} from '../../shared/private_utils'; import useStyledSystem from './use_styled_system'; import {OptionalResponsiveProp} from './system/utils/types'; -import createResponsivePropType from './system/utils/create_responsive_prop_type'; import { maxWidth, - maxWidthPropTypes, MaxWidthProps, minWidth, - minWidthPropTypes, MinWidthProps, width, - widthPropTypes, WidthProps, flexItemSet, - flexItemSetPropTypes, FlexItemSetProps, positionSet, - positionSetPropTypes, PositionSetProps, margin, - marginPropTypes, MarginProps, display, } from './system'; import useTheme from './theme/use_theme'; -import {ControlSize, ControlSizeProp, controlSizePropType, useButtonSize} from './control_sizes'; -import {ariaPropTypes, AriaProps} from './types/aria_props'; -import {tooltipAnchorPropTypes, TooltipAnchorProps} from './types/tooltip_anchor_props'; -import {IconName, iconNamePropType} from './icon_config'; +import {ControlSize, ControlSizeProp, useButtonSize} from './control_sizes'; +import {AriaProps} from './types/aria_props'; +import {TooltipAnchorProps} from './types/tooltip_anchor_props'; +import {IconName} from './icon_config'; import Icon from './icon'; import cssHelpers from './css_helpers'; import Box from './box'; -import {createPropTypeFromEnum} from './system/utils/enum_prop_type_utils'; /** * Style props for the {@link Button} component. Also accepts: @@ -62,16 +53,6 @@ interface ButtonStyleProps const styleParser = compose(display, maxWidth, minWidth, width, flexItemSet, positionSet, margin); -export const buttonStylePropTypes = { - display: createResponsivePropType(PropTypes.oneOf(['inline-flex', 'flex', 'none'])), - ...maxWidthPropTypes, - ...minWidthPropTypes, - ...widthPropTypes, - ...flexItemSetPropTypes, - ...positionSetPropTypes, - ...marginPropTypes, -}; - /** * Variants for the {@link Button} component: * @@ -93,7 +74,6 @@ export const buttonStylePropTypes = { */ type ButtonVariant = EnumType; const ButtonVariant = createEnum('default', 'primary', 'secondary', 'danger'); -const buttonVariantPropType = createPropTypeFromEnum(ButtonVariant); /** @internal */ function useButtonVariant(variant: ButtonVariant = ButtonVariant.default): string { @@ -224,22 +204,6 @@ const Button = (props: ButtonProps, ref: React.Ref) => { const ForwardedRefButton = React.forwardRef(Button); -ForwardedRefButton.propTypes = { - size: controlSizePropType, - variant: buttonVariantPropType, - icon: PropTypes.oneOfType([iconNamePropType, PropTypes.element]), - id: PropTypes.string, - className: PropTypes.string, - style: PropTypes.object, - type: PropTypes.oneOf(['button', 'submit', 'reset'] as const), - disabled: PropTypes.bool, - tabIndex: PropTypes.number, - children: PropTypes.node, - ...buttonStylePropTypes, - ...tooltipAnchorPropTypes, - ...ariaPropTypes, -}; - ForwardedRefButton.displayName = 'Button'; export default ForwardedRefButton; diff --git a/packages/sdk/src/base/ui/cell_renderer.tsx b/packages/sdk/src/base/ui/cell_renderer.tsx index 3da9a458a..9c013e592 100644 --- a/packages/sdk/src/base/ui/cell_renderer.tsx +++ b/packages/sdk/src/base/ui/cell_renderer.tsx @@ -1,5 +1,4 @@ /** @module @airtable/blocks/ui: CellRenderer */ /** */ -import PropTypes from 'prop-types'; import {cx} from 'emotion'; import * as React from 'react'; import {compose} from '@styled-system/core'; @@ -16,30 +15,23 @@ import {useSdk} from '../../shared/ui/sdk_context'; import {BaseSdkMode} from '../../sdk_mode'; import { display, - displayPropTypes, maxWidth, - maxWidthPropTypes, MaxWidthProps, minWidth, - minWidthPropTypes, MinWidthProps, width, - widthPropTypes, WidthProps, flexItemSet, - flexItemSetPropTypes, FlexItemSetProps, positionSet, - positionSetPropTypes, PositionSetProps, margin, - marginPropTypes, MarginProps, } from './system'; import useStyledSystem from './use_styled_system'; import {splitStyleProps} from './with_styled_system'; import {OptionalResponsiveProp} from './system/utils/types'; -import {tooltipAnchorPropTypes, TooltipAnchorProps} from './types/tooltip_anchor_props'; +import {TooltipAnchorProps} from './types/tooltip_anchor_props'; /** * Style props for the {@link CellRenderer} component. Also accepts: @@ -65,16 +57,6 @@ interface CellRendererStyleProps const styleParser = compose(display, flexItemSet, margin, maxWidth, minWidth, positionSet, width); -export const cellRendererStylePropTypes = { - ...displayPropTypes, - ...flexItemSetPropTypes, - ...marginPropTypes, - ...maxWidthPropTypes, - ...minWidthPropTypes, - ...positionSetPropTypes, - ...widthPropTypes, -}; - /** * Props for the {@link CellRenderer} component. Also accepts: * * {@link CellRendererStyleProps} @@ -114,20 +96,6 @@ interface CellRendererProps extends CellRendererStyleProps, TooltipAnchorProps { - /** @hidden */ - static propTypes = { - record: PropTypes.instanceOf(Record), - cellValue: PropTypes.any, - field: PropTypes.instanceOf(Field).isRequired, - shouldWrap: PropTypes.bool, - className: PropTypes.string, - style: PropTypes.object, - cellClassName: PropTypes.string, - cellStyle: PropTypes.object, - renderInvalidCellValue: PropTypes.func, - ...tooltipAnchorPropTypes, - ...cellRendererStylePropTypes, - }; /** @hidden */ static defaultProps = { shouldWrap: true, diff --git a/packages/sdk/src/base/ui/choice_token.tsx b/packages/sdk/src/base/ui/choice_token.tsx index 6957fb72b..9189fd79c 100644 --- a/packages/sdk/src/base/ui/choice_token.tsx +++ b/packages/sdk/src/base/ui/choice_token.tsx @@ -1,5 +1,4 @@ /** @module @airtable/blocks/ui: ChoiceToken */ /** */ -import PropTypes from 'prop-types'; import {cx} from 'emotion'; import * as React from 'react'; import {compose} from '@styled-system/core'; @@ -11,16 +10,13 @@ import useStyledSystem from './use_styled_system'; import useTextColorForBackgroundColor from './use_text_color_for_background_color'; import { flexItemSet, - flexItemSetPropTypes, FlexItemSetProps, positionSet, - positionSetPropTypes, PositionSetProps, margin, - marginPropTypes, MarginProps, } from './system'; -import {tooltipAnchorPropTypes, TooltipAnchorProps} from './types/tooltip_anchor_props'; +import {TooltipAnchorProps} from './types/tooltip_anchor_props'; /** * Style props for the {@link ChoiceToken} component. Accepts: @@ -34,12 +30,6 @@ interface ChoiceTokenStyleProps extends FlexItemSetProps, PositionSetProps, Marg const styleParser = compose(flexItemSet, positionSet, margin); -export const choiceTokenStylePropTypes = { - ...flexItemSetPropTypes, - ...positionSetPropTypes, - ...marginPropTypes, -}; - const DEFAULT_CHOICE_COLOR = 'gray'; /** An option from a select field. You should not create these objects from scratch, but should instead grab them from base data. */ @@ -119,16 +109,4 @@ const ChoiceToken = (props: ChoiceTokenProps) => { ); }; -ChoiceToken.propTypes = { - choice: PropTypes.shape({ - id: PropTypes.string, - name: PropTypes.string.isRequired, - color: PropTypes.string, - }).isRequired, - style: PropTypes.object, - className: PropTypes.string, - ...tooltipAnchorPropTypes, - ...choiceTokenStylePropTypes, -}; - export default ChoiceToken; diff --git a/packages/sdk/src/base/ui/collaborator_token.tsx b/packages/sdk/src/base/ui/collaborator_token.tsx index e72b4dec7..00ca70044 100644 --- a/packages/sdk/src/base/ui/collaborator_token.tsx +++ b/packages/sdk/src/base/ui/collaborator_token.tsx @@ -1,5 +1,4 @@ /** @module @airtable/blocks/ui: CollaboratorToken */ /** */ -import PropTypes from 'prop-types'; import {cx} from 'emotion'; import * as React from 'react'; import {compose} from '@styled-system/core'; @@ -11,16 +10,13 @@ import {baymax} from './baymax_utils'; import useStyledSystem from './use_styled_system'; import { flexItemSet, - flexItemSetPropTypes, FlexItemSetProps, positionSet, - positionSetPropTypes, PositionSetProps, margin, - marginPropTypes, MarginProps, } from './system'; -import {tooltipAnchorPropTypes, TooltipAnchorProps} from './types/tooltip_anchor_props'; +import {TooltipAnchorProps} from './types/tooltip_anchor_props'; const UNKNOWN_PROFILE_PIC_URL = 'https://static.airtable.com/images/userIcons/user_icon_unknown.png'; @@ -37,12 +33,6 @@ interface CollaboratorTokenStyleProps extends FlexItemSetProps, PositionSetProps const styleParser = compose(flexItemSet, positionSet, margin); -export const collaboratorTokenStylePropTypes = { - ...flexItemSetPropTypes, - ...positionSetPropTypes, - ...marginPropTypes, -}; - /** * Props for the {@link CollaboratorToken} component. Also accepts: * * {@link CollaboratorTokenStyleProps} @@ -147,20 +137,6 @@ const CollaboratorToken = (props: CollaboratorTokenProps) => { return ; }; -CollaboratorToken.propTypes = { - collaborator: PropTypes.shape({ - id: PropTypes.string, - email: PropTypes.string, - name: PropTypes.string, - profilePicUrl: PropTypes.string, - status: PropTypes.string, - }).isRequired, - className: PropTypes.string, - style: PropTypes.object, - ...tooltipAnchorPropTypes, - ...collaboratorTokenStylePropTypes, -}; - CollaboratorToken.Static = StaticCollaboratorToken; export default CollaboratorToken; diff --git a/packages/sdk/src/base/ui/color_palette.tsx b/packages/sdk/src/base/ui/color_palette.tsx index 28bd5037d..fd6fd5d8b 100644 --- a/packages/sdk/src/base/ui/color_palette.tsx +++ b/packages/sdk/src/base/ui/color_palette.tsx @@ -1,5 +1,4 @@ /** @module @airtable/blocks/ui: ColorPalette */ /** */ -import PropTypes from 'prop-types'; import {cx} from 'emotion'; import * as React from 'react'; import {compose} from '@styled-system/core'; @@ -12,25 +11,19 @@ import createDetectElementResize from './create_detect_element_resize'; import withStyledSystem from './with_styled_system'; import { maxWidth, - maxWidthPropTypes, MaxWidthProps, minWidth, - minWidthPropTypes, MinWidthProps, width, - widthPropTypes, WidthProps, flexItemSet, - flexItemSetPropTypes, FlexItemSetProps, positionSet, - positionSetPropTypes, PositionSetProps, margin, - marginPropTypes, MarginProps, } from './system'; -import {tooltipAnchorPropTypes, TooltipAnchorProps} from './types/tooltip_anchor_props'; +import {TooltipAnchorProps} from './types/tooltip_anchor_props'; const MIN_COLOR_SQUARE_SIZE = 16; const DEFAULT_COLOR_SQUARE_SIZE = 24; @@ -58,15 +51,6 @@ export interface ColorPaletteStyleProps const styleParser = compose(maxWidth, minWidth, width, flexItemSet, positionSet, margin); -export const colorPaletteStylePropTypes = { - ...maxWidthPropTypes, - ...minWidthPropTypes, - ...widthPropTypes, - ...flexItemSetPropTypes, - ...positionSetPropTypes, - ...marginPropTypes, -}; - /** * Props shared between the {@link ColorPalette} and {@link ColorPaletteSynced} components. */ @@ -85,16 +69,6 @@ export interface SharedColorPaletteProps extends ColorPaletteStyleProps, Tooltip disabled?: boolean; } -export const sharedColorPalettePropTypes = { - allowedColors: PropTypes.arrayOf(PropTypes.string).isRequired, - onChange: PropTypes.func, - squareMargin: PropTypes.number, - className: PropTypes.string, - style: PropTypes.object, - disabled: PropTypes.bool, - ...tooltipAnchorPropTypes, -}; - /** * Props for the {@link ColorPalette} component. Also accepts: * * {@link ColorPaletteStyleProps} @@ -121,11 +95,6 @@ interface ColorPaletteState { * @docsPath UI/components/ColorPalette */ export class ColorPalette extends React.Component { - /** @hidden */ - static propTypes = { - color: PropTypes.string, - ...sharedColorPalettePropTypes, - }; /** @hidden */ static defaultProps = { squareMargin: 4, @@ -288,4 +257,4 @@ export default withStyledSystem< ColorPaletteStyleProps, ColorPalette, {} ->(ColorPalette, styleParser, colorPaletteStylePropTypes); +>(ColorPalette, styleParser, {}); diff --git a/packages/sdk/src/base/ui/color_palette_synced.tsx b/packages/sdk/src/base/ui/color_palette_synced.tsx index 93fd34e9f..75fd991d9 100644 --- a/packages/sdk/src/base/ui/color_palette_synced.tsx +++ b/packages/sdk/src/base/ui/color_palette_synced.tsx @@ -2,13 +2,8 @@ import * as React from 'react'; import {spawnError} from '../../shared/error_utils'; import {GlobalConfigKey} from '../../shared/types/global_config'; -import globalConfigSyncedComponentHelpers from '../../shared/ui/global_config_synced_component_helpers'; import Synced from './synced'; -import ColorPalette, { - colorPaletteStylePropTypes, - sharedColorPalettePropTypes, - SharedColorPaletteProps, -} from './color_palette'; +import ColorPalette, {SharedColorPaletteProps} from './color_palette'; /** * Props for the {@link ColorPaletteSynced} component. Also accepts: @@ -32,12 +27,6 @@ interface ColorPaletteSyncedProps extends SharedColorPaletteProps { * @groupPath UI/components/ColorPalette */ class ColorPaletteSynced extends React.Component { - /** @hidden */ - static propTypes = { - globalConfigKey: globalConfigSyncedComponentHelpers.globalConfigKeyPropType, - ...colorPaletteStylePropTypes, - ...sharedColorPalettePropTypes, - }; /** @hidden */ render() { const {globalConfigKey, disabled, onChange, ...restOfProps} = this.props; diff --git a/packages/sdk/src/base/ui/confirmation_dialog.tsx b/packages/sdk/src/base/ui/confirmation_dialog.tsx index 8da4a39b8..814c04cc3 100644 --- a/packages/sdk/src/base/ui/confirmation_dialog.tsx +++ b/packages/sdk/src/base/ui/confirmation_dialog.tsx @@ -1,7 +1,6 @@ /** @module @airtable/blocks/ui: Dialog */ /** */ -import PropTypes from 'prop-types'; import * as React from 'react'; -import Dialog, {DialogStyleProps, dialogStylePropTypes} from './dialog'; +import Dialog, {DialogStyleProps} from './dialog'; import Heading from './heading'; import Text from './text'; import Button from './button'; @@ -55,23 +54,6 @@ interface ConfirmationDialogProps extends DialogStyleProps { * @docsPath UI/components/ConfirmationDialog */ class ConfirmationDialog extends React.Component { - /** @hidden */ - static propTypes = { - title: PropTypes.string.isRequired, - body: PropTypes.node, - cancelButtonText: PropTypes.string, - confirmButtonText: PropTypes.string, - isConfirmActionDangerous: PropTypes.bool, - className: PropTypes.string, - style: PropTypes.object, - backgroundClassName: PropTypes.string, - backgroundStyle: PropTypes.object, - onCancel: PropTypes.func.isRequired, - onConfirm: PropTypes.func.isRequired, - isCancelButtonDisabled: PropTypes.bool, - isConfirmButtonDisabled: PropTypes.bool, - ...dialogStylePropTypes, - }; /** @hidden */ static defaultProps = { cancelButtonText: 'Cancel', diff --git a/packages/sdk/src/base/ui/control_sizes.ts b/packages/sdk/src/base/ui/control_sizes.ts index 234cb89a0..43f8377b8 100644 --- a/packages/sdk/src/base/ui/control_sizes.ts +++ b/packages/sdk/src/base/ui/control_sizes.ts @@ -6,7 +6,6 @@ import {ResponsiveProp} from './system/utils/types'; import getStylePropsForResponsiveProp from './system/utils/get_style_props_for_responsive_prop'; import useStyledSystem from './use_styled_system'; import {allStylesParser} from './system'; -import {createResponsivePropTypeFromEnum} from './system/utils/enum_prop_type_utils'; /** * Sizes for the {@link Button}, {@link Input}, {@link Select}, {@link SelectButtons}, and {@link Switch} components. @@ -18,7 +17,6 @@ export const ControlSize = createEnum('small', 'default', 'large'); * Size prop for the {@link Button}, {@link Input}, {@link Select}, {@link SelectButtons}, and {@link Switch} components. */ export type ControlSizeProp = ResponsiveProp; -export const controlSizePropType = createResponsivePropTypeFromEnum(ControlSize); /** @internal */ export function useButtonSize(controlSizeProp: ControlSizeProp): string { diff --git a/packages/sdk/src/base/ui/dialog.tsx b/packages/sdk/src/base/ui/dialog.tsx index 86f16ceb1..b74b2ee11 100644 --- a/packages/sdk/src/base/ui/dialog.tsx +++ b/packages/sdk/src/base/ui/dialog.tsx @@ -1,19 +1,10 @@ /** @module @airtable/blocks/ui: Dialog */ /** */ -import PropTypes from 'prop-types'; import {cx} from 'emotion'; import * as React from 'react'; import {baymax} from './baymax_utils'; import Modal from './modal'; import DialogCloseButton from './dialog_close_button'; -import { - dimensionsSetPropTypes, - DimensionsSetProps, - displayPropTypes, - flexContainerSetPropTypes, - FlexContainerSetProps, - spacingSetPropTypes, - SpacingSetProps, -} from './system'; +import {DimensionsSetProps, FlexContainerSetProps, SpacingSetProps} from './system'; import {OptionalResponsiveProp} from './system/utils/types'; /** @@ -33,12 +24,14 @@ export interface DialogStyleProps display?: OptionalResponsiveProp<'block' | 'flex'>; } -export const dialogStylePropTypes = { - ...dimensionsSetPropTypes, - ...displayPropTypes, - ...flexContainerSetPropTypes, - ...spacingSetPropTypes, -}; +/** @internal */ +const DialogContext = React.createContext<{onDialogClose: (() => void) | null}>({ + onDialogClose: null, +}); +/** @internal */ +export function useDialogContext() { + return React.useContext(DialogContext); +} /** * Props for the {@link Dialog} component. Also accepts: @@ -74,32 +67,13 @@ interface DialogProps extends DialogStyleProps { class Dialog extends React.Component { /** @hidden */ static CloseButton = DialogCloseButton; - /** @hidden */ - static propTypes = { - onClose: PropTypes.func.isRequired, - className: PropTypes.string, - style: PropTypes.object, - backgroundClassName: PropTypes.string, - backgroundStyle: PropTypes.object, - children: PropTypes.node.isRequired, - ...dialogStylePropTypes, - }; - /** @hidden */ - static childContextTypes = { - onDialogClose: PropTypes.func, - }; + /** @hidden */ constructor(props: DialogProps) { super(props); this._onKeyDown = this._onKeyDown.bind(this); } /** @hidden */ - getChildContext() { - return { - onDialogClose: this.props.onClose, - }; - } - /** @hidden */ componentDidMount() { window.addEventListener('keydown', this._onKeyDown, false); } @@ -134,7 +108,13 @@ class Dialog extends React.Component { backgroundStyle={backgroundStyle} {...restOfProps} > - {children} + + {children} + ); } diff --git a/packages/sdk/src/base/ui/dialog_close_button.tsx b/packages/sdk/src/base/ui/dialog_close_button.tsx index 7855a3669..347d6d97c 100644 --- a/packages/sdk/src/base/ui/dialog_close_button.tsx +++ b/packages/sdk/src/base/ui/dialog_close_button.tsx @@ -1,5 +1,4 @@ /** @module @airtable/blocks/ui: Dialog */ /** */ -import PropTypes from 'prop-types'; import {cx} from 'emotion'; import * as React from 'react'; import {compose} from '@styled-system/core'; @@ -7,29 +6,23 @@ import {baymax} from './baymax_utils'; import withStyledSystem from './with_styled_system'; import { borderRadius, - borderRadiusPropTypes, BorderRadiusProps, dimensionsSet, - dimensionsSetPropTypes, DimensionsSetProps, display, - displayPropTypes, DisplayProps, flexContainerSet, - flexContainerSetPropTypes, FlexContainerSetProps, flexItemSet, - flexItemSetPropTypes, FlexItemSetProps, positionSet, - positionSetPropTypes, PositionSetProps, spacingSet, - spacingSetPropTypes, SpacingSetProps, } from './system'; -import {tooltipAnchorPropTypes, TooltipAnchorProps} from './types/tooltip_anchor_props'; +import {TooltipAnchorProps} from './types/tooltip_anchor_props'; import Icon from './icon'; +import {useDialogContext} from './dialog'; /** * Props for the {@link DialogCloseButton} component. Also accepts: @@ -79,83 +72,54 @@ const styleParser = compose( spacingSet, ); -const dialogCloseButtonStylePropTypes = { - ...borderRadiusPropTypes, - ...dimensionsSetPropTypes, - ...displayPropTypes, - ...flexContainerSetPropTypes, - ...flexItemSetPropTypes, - ...positionSetPropTypes, - ...spacingSetPropTypes, -}; - /** * A button that closes {@link Dialog}. Accessed via `Dialog.CloseButton`. */ -export class DialogCloseButton extends React.Component { - /** @hidden */ - static propTypes = { - className: PropTypes.string, - style: PropTypes.object, - tabIndex: PropTypes.number, - children: PropTypes.node, - ...tooltipAnchorPropTypes, - }; - /** @hidden */ - static contextTypes = { - onDialogClose: PropTypes.func, - }; - /** @hidden */ - constructor(props: DialogCloseButtonProps) { - super(props); - this._onClick = this._onClick.bind(this); - this._onKeyDown = this._onKeyDown.bind(this); - } - /** @internal */ - _onClick(e: React.MouseEvent) { - if (this.props.onClick) { - this.props.onClick(e); +export function DialogCloseButton(props: DialogCloseButtonProps) { + const {onDialogClose} = useDialogContext(); + const {onMouseEnter, onMouseLeave, className, style, tabIndex, onClick, children} = props; + + const _onClick = (e: React.MouseEvent) => { + if (onClick) { + onClick(e); } - this.context.onDialogClose(); - } - /** @internal */ - _onKeyDown(e: React.KeyboardEvent) { + onDialogClose?.(); + }; + + const _onKeyDown = (e: React.KeyboardEvent) => { if (e.ctrlKey || e.altKey || e.metaKey) { return; } if (['Enter', ' '].includes(e.key)) { e.preventDefault(); - this.context.onDialogClose(); + onDialogClose?.(); } - } - /** @hidden */ - render() { - const {onMouseEnter, onMouseLeave, className, style, tabIndex, children} = this.props; - return ( -
    - {children ? children : } -
    - ); - } + }; + + return ( +
    + {children ? children : } +
    + ); } export default withStyledSystem< DialogCloseButtonProps, DialogCloseButtonStyleProps, - DialogCloseButton, + React.ComponentType, {} ->(DialogCloseButton, styleParser, dialogCloseButtonStylePropTypes, { +>(DialogCloseButton, styleParser, { position: 'absolute', top: 0, right: 0, diff --git a/packages/sdk/src/base/ui/field_icon.tsx b/packages/sdk/src/base/ui/field_icon.tsx index 18acc4727..457de2e68 100644 --- a/packages/sdk/src/base/ui/field_icon.tsx +++ b/packages/sdk/src/base/ui/field_icon.tsx @@ -1,10 +1,9 @@ /** @module @airtable/blocks/ui: FieldIcon */ /** */ -import PropTypes from 'prop-types'; import * as React from 'react'; import Field from '../models/field'; import {useSdk} from '../../shared/ui/sdk_context'; import {BaseSdkMode} from '../../sdk_mode'; -import Icon, {sharedIconPropTypes, SharedIconProps} from './icon'; +import Icon, {SharedIconProps} from './icon'; import {IconName} from './icon_config'; /** @@ -40,9 +39,4 @@ const FieldIcon = (props: FieldIconProps) => { return ; }; -FieldIcon.propTypes = { - field: PropTypes.instanceOf(Field).isRequired, - ...sharedIconPropTypes, -}; - export default FieldIcon; diff --git a/packages/sdk/src/base/ui/field_picker.tsx b/packages/sdk/src/base/ui/field_picker.tsx index 810102c15..cc809a593 100644 --- a/packages/sdk/src/base/ui/field_picker.tsx +++ b/packages/sdk/src/base/ui/field_picker.tsx @@ -1,14 +1,13 @@ /** @module @airtable/blocks/ui: FieldPicker */ /** */ -import PropTypes from 'prop-types'; import * as React from 'react'; -import {values, ObjectMap, has} from '../../shared/private_utils'; +import {ObjectMap, has} from '../../shared/private_utils'; import Field from '../models/field'; import Table from '../models/table'; import {FieldType} from '../../shared/types/field_core'; import useWatchable from '../../shared/ui/use_watchable'; import {useSdk} from '../../shared/ui/sdk_context'; import {BaseSdkMode} from '../../sdk_mode'; -import {SharedSelectBaseProps, sharedSelectBasePropTypes} from './select'; +import {SharedSelectBaseProps} from './select'; import ModelPickerSelect from './model_picker_select'; /** @@ -27,15 +26,6 @@ export interface SharedFieldPickerProps extends SharedSelectBaseProps { onChange?: (fieldModel: Field | null) => void; } -export const sharedFieldPickerPropTypes = { - table: PropTypes.instanceOf(Table), - allowedTypes: PropTypes.arrayOf(PropTypes.oneOf(values(FieldType)).isRequired), - shouldAllowPickingNone: PropTypes.bool, - placeholder: PropTypes.string, - onChange: PropTypes.func, - ...sharedSelectBasePropTypes, -}; - /** * Props for the {@link FieldPicker} component. Also accepts: * * {@link SelectStyleProps} @@ -125,9 +115,4 @@ const ForwardedRefFieldPicker = React.forwardRef) => { const ForwardedRefFormField = React.forwardRef(FormField); -ForwardedRefFormField.propTypes = { - id: PropTypes.string, - className: PropTypes.string, - style: PropTypes.object, - label: PropTypes.node, - htmlFor: PropTypes.string, - description: PropTypes.node, - children: PropTypes.node, - ...formFieldStylePropTypes, -}; - ForwardedRefFormField.displayName = 'FormField'; export default ForwardedRefFormField; diff --git a/packages/sdk/src/base/ui/heading.tsx b/packages/sdk/src/base/ui/heading.tsx index a88be3e4d..2819f4a7e 100644 --- a/packages/sdk/src/base/ui/heading.tsx +++ b/packages/sdk/src/base/ui/heading.tsx @@ -1,20 +1,15 @@ /** @module @airtable/blocks/ui: Heading */ /** */ import * as React from 'react'; -import PropTypes from 'prop-types'; import {cx} from 'emotion'; import {invariant} from '../../shared/error_utils'; import {has, createEnum, ObjectMap, keys, EnumType} from '../../shared/private_utils'; import useStyledSystem from './use_styled_system'; -import {allStylesPropTypes, AllStylesProps} from './system/index'; +import {AllStylesProps} from './system/index'; import {ResponsiveProp, ResponsiveKey} from './system/utils/types'; import getStylePropsForResponsiveProp from './system/utils/get_style_props_for_responsive_prop'; import useTheme from './theme/use_theme'; -import {ariaPropTypes, AriaProps} from './types/aria_props'; -import {dataAttributesPropType, DataAttributesProp} from './types/data_attributes_prop'; -import { - createPropTypeFromEnum, - createResponsivePropTypeFromEnum, -} from './system/utils/enum_prop_type_utils'; +import {AriaProps} from './types/aria_props'; +import {DataAttributesProp} from './types/data_attributes_prop'; /** * Sizes for the {@link Heading} component. @@ -26,7 +21,6 @@ const HeadingSize = createEnum('xsmall', 'small', 'default', 'large', 'xlarge', * Size prop for the {@link Heading} component. */ type HeadingSizeProp = ResponsiveProp; -const headingSizePropType = createResponsivePropTypeFromEnum(HeadingSize); /** * Variant prop for the {@link Heading} component. @@ -35,7 +29,6 @@ const headingSizePropType = createResponsivePropTypeFromEnum(HeadingSize); */ type HeadingVariant = EnumType; const HeadingVariant = createEnum('default', 'caps'); -const headingVariantPropType = createPropTypeFromEnum(HeadingVariant); /** @internal */ function warnIfHeadingSizeOutOfRangeForVariant( @@ -172,20 +165,6 @@ const Heading = (props: HeadingProps, ref: React.Ref) => { const ForwardedRefHeading = React.forwardRef(Heading); -ForwardedRefHeading.propTypes = { - as: PropTypes.oneOf(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as const), - size: headingSizePropType, - variant: headingVariantPropType, - children: PropTypes.node, - id: PropTypes.string, - role: PropTypes.string, - dataAttributes: dataAttributesPropType, - className: PropTypes.string, - style: PropTypes.object, - ...allStylesPropTypes, - ...ariaPropTypes, -}; - ForwardedRefHeading.displayName = 'Heading'; export default ForwardedRefHeading; diff --git a/packages/sdk/src/base/ui/icon.tsx b/packages/sdk/src/base/ui/icon.tsx index 353fa393e..18788f6ca 100644 --- a/packages/sdk/src/base/ui/icon.tsx +++ b/packages/sdk/src/base/ui/icon.tsx @@ -1,28 +1,23 @@ /** @module @airtable/blocks/ui: Icon */ /** */ import React from 'react'; -import PropTypes from 'prop-types'; import {compose} from '@styled-system/core'; import {cx} from 'emotion'; import warning from '../../shared/warning'; import useStyledSystem from './use_styled_system'; import { flexItemSet, - flexItemSetPropTypes, FlexItemSetProps, positionSet, - positionSetPropTypes, PositionSetProps, margin, - marginPropTypes, MarginProps, width, WidthProps, height, HeightProps, } from './system'; -import {tooltipAnchorPropTypes, TooltipAnchorProps} from './types/tooltip_anchor_props'; +import {TooltipAnchorProps} from './types/tooltip_anchor_props'; import { - iconNamePropType, IconName, legacyIconNameToPhosphorIconName, phosphorIconConfig, @@ -41,12 +36,6 @@ export interface IconStyleProps extends FlexItemSetProps, PositionSetProps, Marg const styleParser = compose(flexItemSet, positionSet, margin, width, height); -export const iconStylePropTypes = { - ...flexItemSetPropTypes, - ...positionSetPropTypes, - ...marginPropTypes, -}; - /** * Props shared between the {@link Icon} and {@link FieldIcon} components. * @@ -68,17 +57,6 @@ export interface SharedIconProps extends IconStyleProps, TooltipAnchorProps) => { const ForwardedRefIcon = React.forwardRef(Icon); -ForwardedRefIcon.propTypes = { - name: iconNamePropType.isRequired, - suppressWarning: PropTypes.bool, - ...sharedIconPropTypes, -}; - ForwardedRefIcon.displayName = 'Icon'; export default ForwardedRefIcon; diff --git a/packages/sdk/src/base/ui/icon_config.ts b/packages/sdk/src/base/ui/icon_config.ts index 5407c6577..d2b780776 100644 --- a/packages/sdk/src/base/ui/icon_config.ts +++ b/packages/sdk/src/base/ui/icon_config.ts @@ -1,5 +1,4 @@ import {createEnum, EnumType} from '../../shared/private_utils'; -import {createPropTypeFromEnum} from './system/utils/enum_prop_type_utils'; export const iconNamesArray = [ 'aiAssistant', @@ -181,7 +180,6 @@ export const deprecatedIconNameToReplacementName = new Map([['blocks', 'apps']]) * [[ Story id="icon--example" title="Icon example" height="576px"]] */ export type IconName = EnumType; -export const iconNamePropType = createPropTypeFromEnum(iconNames); export const phosphorIconConfig = { AddressBook: diff --git a/packages/sdk/src/base/ui/initialize_block.tsx b/packages/sdk/src/base/ui/initialize_block.tsx index 5b93a72be..35651c6a2 100644 --- a/packages/sdk/src/base/ui/initialize_block.tsx +++ b/packages/sdk/src/base/ui/initialize_block.tsx @@ -1,6 +1,6 @@ /** @module @airtable/blocks/ui: initializeBlock */ /** */ import * as React from 'react'; -import ReactDOM from 'react-dom'; +import {createRoot} from 'react-dom/client'; import {spawnError} from '../../shared/error_utils'; import Sdk from '../sdk'; import getAirtableInterface from '../../injected/airtable_interface'; @@ -105,21 +105,12 @@ export function initializeBlock(getEntryElement: DashboardOrEntryPoints) { ); } - if (ReactDOM.unstable_batchedUpdates) { - sdk.__setBatchedUpdatesFn(ReactDOM.unstable_batchedUpdates); - } - const container = document.createElement('div'); container.style.height = '100%'; container.style.width = '100%'; body.appendChild(container); - try { - const {createRoot} = require('react-dom/client'); - createRoot(container).render({entryElement}); - } catch (e) { - ReactDOM.render({entryElement}, container); - } + createRoot(container).render({entryElement}); } let sdk: Sdk; diff --git a/packages/sdk/src/base/ui/input.tsx b/packages/sdk/src/base/ui/input.tsx index e3ee9d70c..438c7235f 100644 --- a/packages/sdk/src/base/ui/input.tsx +++ b/packages/sdk/src/base/ui/input.tsx @@ -1,5 +1,4 @@ /** @module @airtable/blocks/ui: Input */ /** */ -import PropTypes from 'prop-types'; import {cx} from 'emotion'; import * as React from 'react'; import {compose} from '@styled-system/core'; @@ -9,27 +8,20 @@ import useStyledSystem from './use_styled_system'; import useFormField from './use_form_field'; import { maxWidth, - maxWidthPropTypes, MaxWidthProps, minWidth, - minWidthPropTypes, MinWidthProps, width, - widthPropTypes, WidthProps, flexItemSet, - flexItemSetPropTypes, FlexItemSetProps, positionSet, - positionSetPropTypes, PositionSetProps, margin, - marginPropTypes, MarginProps, } from './system'; -import {tooltipAnchorPropTypes, TooltipAnchorProps} from './types/tooltip_anchor_props'; -import {ControlSizeProp, controlSizePropType, ControlSize, useInputSize} from './control_sizes'; -import {createPropTypeFromEnum} from './system/utils/enum_prop_type_utils'; +import {TooltipAnchorProps} from './types/tooltip_anchor_props'; +import {ControlSizeProp, ControlSize, useInputSize} from './control_sizes'; /** @internal */ type InputVariant = EnumType; @@ -79,15 +71,6 @@ export interface InputStyleProps const styleParser = compose(maxWidth, minWidth, width, flexItemSet, positionSet, margin); -export const inputStylePropTypes = { - ...maxWidthPropTypes, - ...minWidthPropTypes, - ...widthPropTypes, - ...flexItemSetPropTypes, - ...positionSetPropTypes, - ...marginPropTypes, -}; - /** * Props shared between the {@link Input} and {@link InputSynced} components. * @@ -165,34 +148,6 @@ export const SupportedInputType = createEnum( */ type SupportedInputType = EnumType; -export const sharedInputPropTypes = { - size: controlSizePropType, - type: createPropTypeFromEnum(SupportedInputType), - placeholder: PropTypes.string, - disabled: PropTypes.bool, - required: PropTypes.bool, - spellCheck: PropTypes.bool, - tabIndex: PropTypes.oneOfType([PropTypes.number]), - autoFocus: PropTypes.bool, - max: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - maxLength: PropTypes.number, - min: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - minLength: PropTypes.number, - step: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - pattern: PropTypes.string, - readOnly: PropTypes.bool, - autoComplete: PropTypes.string, - onChange: PropTypes.func, - onBlur: PropTypes.func, - onFocus: PropTypes.func, - style: PropTypes.object, - className: PropTypes.string, - 'aria-labelledby': PropTypes.string, - 'aria-describedby': PropTypes.string, - ...inputStylePropTypes, - ...tooltipAnchorPropTypes, -}; - /** * Props for the {@link Input} component. Also accepts: * * {@link InputStyleProps} @@ -299,11 +254,6 @@ const Input = (props: InputProps, ref: React.Ref) => { const ForwardedRefInput = React.forwardRef(Input); -ForwardedRefInput.propTypes = { - value: PropTypes.string.isRequired, - ...sharedInputPropTypes, -}; - ForwardedRefInput.displayName = 'Input'; export default ForwardedRefInput; diff --git a/packages/sdk/src/base/ui/input_synced.tsx b/packages/sdk/src/base/ui/input_synced.tsx index 6284c3d70..dcfab29fb 100644 --- a/packages/sdk/src/base/ui/input_synced.tsx +++ b/packages/sdk/src/base/ui/input_synced.tsx @@ -3,8 +3,7 @@ import * as React from 'react'; import {spawnError} from '../../shared/error_utils'; import {GlobalConfigKey} from '../../shared/types/global_config'; import useSynced from '../../shared/ui/use_synced'; -import globalConfigSyncedComponentHelpers from '../../shared/ui/global_config_synced_component_helpers'; -import Input, {sharedInputPropTypes, SharedInputProps, SupportedInputType} from './input'; +import Input, {SharedInputProps, SupportedInputType} from './input'; /** * Props for the {@link InputSynced} component. Also accepts: @@ -66,11 +65,6 @@ const InputSynced = (props: InputSyncedProps, ref: React.Ref) const ForwardedRefInputSynced = React.forwardRef(InputSynced); -ForwardedRefInputSynced.propTypes = { - globalConfigKey: globalConfigSyncedComponentHelpers.globalConfigKeyPropType, - ...sharedInputPropTypes, -}; - ForwardedRefInputSynced.displayName = 'InputSynced'; export default ForwardedRefInputSynced; diff --git a/packages/sdk/src/base/ui/label.tsx b/packages/sdk/src/base/ui/label.tsx index f34a862a7..13725b932 100644 --- a/packages/sdk/src/base/ui/label.tsx +++ b/packages/sdk/src/base/ui/label.tsx @@ -1,12 +1,11 @@ /** @module @airtable/blocks/ui: Label */ /** */ import * as React from 'react'; -import PropTypes from 'prop-types'; import {cx} from 'emotion'; import useStyledSystem from './use_styled_system'; -import {allStylesPropTypes, AllStylesProps} from './system/index'; -import {ariaPropTypes, AriaProps} from './types/aria_props'; -import {TextSize, textSizePropType, TextSizeProp, useTextStyle} from './text'; -import {dataAttributesPropType, DataAttributesProp} from './types/data_attributes_prop'; +import {allStylesParser, AllStylesProps} from './system/index'; +import {AriaProps} from './types/aria_props'; +import {TextSize, TextSizeProp, useTextStyle} from './text'; +import {DataAttributesProp} from './types/data_attributes_prop'; /** * Props for the {@link Label} component. Also accepts: @@ -65,13 +64,16 @@ const Label = (props: LabelProps, ref: React.Ref) => { } = props; const classNameForTextStyle = useTextStyle(size); - const classNameForStyleProps = useStyledSystem({ - display: 'inline-block', - textColor: 'light', - fontWeight: 'strong', - marginBottom: '6px', - ...styleProps, - }); + const classNameForStyleProps = useStyledSystem( + { + display: 'inline-block', + textColor: 'light', + fontWeight: 'strong', + marginBottom: '6px', + ...styleProps, + }, + allStylesParser, + ); return (