diff --git a/.github/workflows/generator-generic-ossf-slsa3-publish.yml b/.github/workflows/generator-generic-ossf-slsa3-publish.yml new file mode 100644 index 000000000..35c829b13 --- /dev/null +++ b/.github/workflows/generator-generic-ossf-slsa3-publish.yml @@ -0,0 +1,66 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +# This workflow lets you generate SLSA provenance file for your project. +# The generation satisfies level 3 for the provenance requirements - see https://slsa.dev/spec/v0.1/requirements +# The project is an initiative of the OpenSSF (openssf.org) and is developed at +# https://github.com/slsa-framework/slsa-github-generator. +# The provenance file can be verified using https://github.com/slsa-framework/slsa-verifier. +# For more information about SLSA and how it improves the supply-chain, visit slsa.dev. + +name: SLSA generic generator +on: + workflow_dispatch: + release: + types: [created] + +jobs: + build: + runs-on: ubuntu-latest + outputs: + digests: ${{ steps.hash.outputs.digests }} + + steps: + - uses: actions/checkout@v4 + + # ======================================================== + # + # Step 1: Build your artifacts. + # + # ======================================================== + - name: Build artifacts + run: | + # These are some amazing artifacts. + echo "artifact1" > artifact1 + echo "artifact2" > artifact2 + + # ======================================================== + # + # Step 2: Add a step to generate the provenance subjects + # as shown below. Update the sha256 sum arguments + # to include all binaries that you generate + # provenance for. + # + # ======================================================== + - name: Generate subject for provenance + id: hash + run: | + set -euo pipefail + + # List the artifacts the provenance will refer to. + files=$(ls artifact*) + # Generate the subjects (base64 encoded). + echo "hashes=$(sha256sum $files | base64 -w0)" >> "${GITHUB_OUTPUT}" + + provenance: + needs: [build] + permissions: + actions: read # To read the workflow path. + id-token: write # To sign the provenance. + contents: write # To add assets to a release. + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.4.0 + with: + base64-subjects: "${{ needs.build.outputs.digests }}" + upload-assets: true # Optional: Upload to a new release diff --git a/.prettierignore b/.prettierignore index 322f73c6d..9aba3944c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,6 +6,11 @@ /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/esbuild-bundler/test/fixtures/ /packages/webpack-bundler/test/fixtures/ +flow-typed +.nyc_output +.test-tmp diff --git a/package.json b/package.json index e75c7556d..f156236d3 100644 --- a/package.json +++ b/package.json @@ -18,15 +18,15 @@ }, "devDependencies": { "husky": "^3.1.0", - "prettier": "^1.19.1", - "pretty-quick": "^2.0.1", - "react": "^16.14.0", - "react-dom": "^16.9.24", + "prettier": "^3.6.2", + "pretty-quick": "^4.2.2", + "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", @@ -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": { @@ -53,5 +54,6 @@ "hooks": { "pre-commit": "pretty-quick --staged" } - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/packages/sdk/.eslintignore b/packages/sdk/.eslintignore deleted file mode 100644 index ad2d094e0..000000000 --- a/packages/sdk/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -/node_modules -/dist -/examples -/*.js \ No newline at end of file diff --git a/packages/sdk/.eslintrc.js b/packages/sdk/.eslintrc.js deleted file mode 100644 index 4710beb20..000000000 --- a/packages/sdk/.eslintrc.js +++ /dev/null @@ -1,159 +0,0 @@ -const path = require('path'); - -module.exports = { - env: { - browser: true, - es6: true, - node: true, - }, - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/eslint-recommended', - 'plugin:react/recommended', - ], - parser: '@typescript-eslint/parser', - parserOptions: { - project: path.join(__dirname, './tsconfig.json'), - sourceType: 'module', - }, - plugins: [ - 'airtable', - '@airtable/blocks', - 'react', - 'react-hooks', - 'import', - 'jsdoc', - '@typescript-eslint', - ], - settings: { - react: { - version: '16.14', - }, - jsdoc: { - ignorePrivate: true, - tagNamePreference: { - private: 'internal', - }, - }, - }, - rules: { - 'array-callback-return': 'error', - 'block-scoped-var': 'error', - 'consistent-return': 'error', - 'consistent-this': ['error', 'that'], - curly: 'error', - 'default-case': 'error', - eqeqeq: 'error', - 'guard-for-in': 'error', - 'handle-callback-err': 'error', - 'new-parens': 'error', - 'no-array-constructor': 'error', - 'no-async-promise-executor': 'error', - 'no-buffer-constructor': 'error', - 'no-caller': 'error', - 'no-case-declarations': 'error', - 'no-catch-shadow': 'error', - 'no-console': 'error', - 'no-constant-condition': 'error', - 'no-debugger': 'error', - 'no-div-regex': 'error', - 'no-duplicate-imports': 'error', - 'no-empty': 'error', - 'no-eq-null': 'error', - 'no-eval': 'error', - 'no-extend-native': 'error', - 'no-extra-bind': 'error', - 'no-extra-boolean-cast': 'error', - 'no-extra-label': 'error', - 'no-extra-semi': 'error', - 'no-global-assign': 'error', - 'no-implicit-globals': 'error', - 'no-implied-eval': 'error', - 'no-iterator': 'error', - 'no-label-var': 'error', - 'no-labels': 'error', - 'no-lone-blocks': 'error', - 'no-loop-func': 'error', - 'no-mixed-requires': 'error', - 'no-multi-str': 'error', - 'no-native-reassign': 'error', - 'no-new-object': 'error', - 'no-new-require': 'error', - 'no-new-wrappers': 'error', - 'no-octal-escape': 'error', - 'no-path-concat': 'error', - 'no-proto': 'error', - 'no-prototype-builtins': 'error', - 'no-redeclare': 'off', - 'no-regex-spaces': 'error', - 'no-script-url': 'error', - 'no-self-assign': 'error', - 'no-self-compare': 'error', - 'no-sequences': 'error', - 'no-shadow-restricted-names': 'error', - 'no-shadow': 'off', - 'no-sparse-arrays': 'error', - 'no-template-curly-in-string': 'error', - 'no-throw-literal': 'error', - 'no-undef-init': 'error', - 'no-undef': 'off', - 'no-unneeded-ternary': 'error', - 'no-unreachable': 'error', - 'no-unsafe-negation': 'error', - 'no-unused-expressions': 'error', - 'no-unused-vars': 'off', - 'no-useless-call': 'error', - 'no-useless-computed-key': 'error', - 'no-useless-concat': 'error', - 'no-useless-constructor': 'error', - 'no-void': 'error', - 'no-with': 'error', - 'one-var-declaration-per-line': 'error', - 'prefer-spread': 'error', - quotes: 'off', - radix: 'error', - 'require-yield': 'off', - yoda: 'error', - - 'airtable/background-image-url-double-quotes': 'warn', - 'airtable/is-returns-boolean': 'warn', - 'airtable/no-missing-async-suffix': 'warn', - 'airtable/no-missing-await': 'warn', - 'airtable/no-process-domain': 'warn', - 'airtable/noopener-noreferrer': 'off', - '@airtable/blocks/no-throw-new': 'error', - '@airtable/blocks/no-node-modules-invariant': 'error', - '@airtable/blocks/no-error-interpolation': ['error', {spawnError: 0, invariant: 1}], - - 'jsdoc/check-tag-names': [ - 'error', - { - definedTags: [ - 'hidden', - 'reactComponent', - 'noInheritDoc', - 'docsPath', - 'groupPath', - 'component', - 'hook', - ], - }, - ], - 'jsdoc/no-types': 'error', - - 'import/first': 'error', - 'import/order': 'error', - - 'react-hooks/exhaustive-deps': 'warn', - 'react-hooks/rules-of-hooks': 'error', - 'react/display-name': 'off', - 'react/jsx-boolean-value': ['error', 'always'], - 'react/jsx-fragments': ['error', 'element'], - 'react/no-unescaped-entities': 'off', - 'react/prop-types': ['error'], - 'react/react-in-jsx-scope': 'error', - - '@typescript-eslint/no-unused-vars': ['error', {vars: 'all', args: 'none'}], - '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], - }, -}; 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/.storybook/addons.js b/packages/sdk/.storybook/addons.js deleted file mode 100644 index 6aed412d0..000000000 --- a/packages/sdk/.storybook/addons.js +++ /dev/null @@ -1,2 +0,0 @@ -import '@storybook/addon-actions/register'; -import '@storybook/addon-links/register'; 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/preview-head.html b/packages/sdk/.storybook/preview-head.html deleted file mode 100644 index 7c7ecc623..000000000 --- a/packages/sdk/.storybook/preview-head.html +++ /dev/null @@ -1,58 +0,0 @@ - diff --git a/packages/sdk/.storybook/webpack.config.js b/packages/sdk/.storybook/webpack.config.js deleted file mode 100644 index 6b31972ee..000000000 --- a/packages/sdk/.storybook/webpack.config.js +++ /dev/null @@ -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/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/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/babel.config.cjs b/packages/sdk/babel.config.cjs new file mode 100644 index 000000000..4e7422fac --- /dev/null +++ b/packages/sdk/babel.config.cjs @@ -0,0 +1,23 @@ +// 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 = (api) => { + const isTest = api.env('test'); + return { + presets: [ + '@babel/typescript', + ['@babel/env', {modules: isTest ? 'commonjs' : false, 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/eslint.config.cjs b/packages/sdk/eslint.config.cjs new file mode 100644 index 000000000..bbaacbeef --- /dev/null +++ b/packages/sdk/eslint.config.cjs @@ -0,0 +1,221 @@ +const {defineConfig} = require('eslint/config'); + +const globals = require('globals'); +const tsParser = require('@typescript-eslint/parser'); +const airtable = require('eslint-plugin-airtable'); +const airtableBlocks = require('@airtable/eslint-plugin-blocks'); +const react = require('eslint-plugin-react'); +const reactHooks = require('eslint-plugin-react-hooks'); +const _import = require('eslint-plugin-import'); +const jsdoc = require('eslint-plugin-jsdoc'); +const typescriptEslint = require('@typescript-eslint/eslint-plugin'); + +const {fixupPluginRules} = require('@eslint/compat'); + +const js = require('@eslint/js'); + +const {FlatCompat} = require('@eslint/eslintrc'); + +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); +const path = require('path'); + +module.exports = defineConfig([ + { + ignores: ['node_modules', 'dist', 'examples', '/*.js'], + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + }, + + parser: tsParser, + sourceType: 'module', + + parserOptions: { + project: path.join(__dirname, './tsconfig.json'), + }, + }, + + extends: compat.extends( + 'eslint:recommended', + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:react/recommended', + ), + + plugins: { + airtable, + '@airtable/blocks': airtableBlocks, + react, + 'react-hooks': fixupPluginRules(reactHooks), + import: fixupPluginRules(_import), + jsdoc, + '@typescript-eslint': typescriptEslint, + }, + + settings: { + react: { + version: '16.14', + }, + + jsdoc: { + ignorePrivate: true, + + tagNamePreference: { + private: 'internal', + }, + }, + }, + + rules: { + 'array-callback-return': 'error', + 'block-scoped-var': 'error', + 'consistent-return': 'error', + 'consistent-this': ['error', 'that'], + curly: 'error', + 'default-case': 'error', + eqeqeq: 'error', + 'guard-for-in': 'error', + 'handle-callback-err': 'error', + 'new-parens': 'error', + 'no-array-constructor': 'error', + 'no-async-promise-executor': 'error', + 'no-buffer-constructor': 'error', + 'no-caller': 'error', + 'no-case-declarations': 'error', + 'no-catch-shadow': 'error', + 'no-console': 'error', + 'no-constant-condition': 'error', + 'no-debugger': 'error', + 'no-div-regex': 'error', + + 'no-duplicate-imports': [ + 'error', + { + allowSeparateTypeImports: true, + }, + ], + + 'no-empty': 'error', + 'no-eq-null': 'error', + 'no-eval': 'error', + 'no-extend-native': 'error', + 'no-extra-bind': 'error', + 'no-extra-boolean-cast': 'error', + 'no-extra-label': 'error', + 'no-extra-semi': 'error', + 'no-global-assign': 'error', + 'no-implicit-globals': 'error', + 'no-implied-eval': 'error', + 'no-iterator': 'error', + 'no-label-var': 'error', + 'no-labels': 'error', + 'no-lone-blocks': 'error', + 'no-loop-func': 'error', + 'no-mixed-requires': 'error', + 'no-multi-str': 'error', + 'no-native-reassign': 'error', + 'no-new-object': 'error', + 'no-new-require': 'error', + 'no-new-wrappers': 'error', + 'no-octal-escape': 'error', + 'no-path-concat': 'error', + 'no-proto': 'error', + 'no-prototype-builtins': 'error', + 'no-redeclare': 'off', + 'no-regex-spaces': 'error', + 'no-script-url': 'error', + 'no-self-assign': 'error', + 'no-self-compare': 'error', + 'no-sequences': 'error', + 'no-shadow-restricted-names': 'error', + 'no-shadow': 'off', + 'no-sparse-arrays': 'error', + 'no-template-curly-in-string': 'error', + 'no-throw-literal': 'error', + 'no-undef-init': 'error', + 'no-undef': 'off', + 'no-unneeded-ternary': 'error', + 'no-unreachable': 'error', + 'no-unsafe-negation': 'error', + 'no-unused-expressions': 'error', + 'no-unused-vars': 'off', + 'no-useless-call': 'error', + 'no-useless-computed-key': 'error', + 'no-useless-concat': 'error', + 'no-useless-constructor': 'error', + 'no-void': 'error', + 'no-with': 'error', + 'one-var-declaration-per-line': 'error', + 'prefer-spread': 'error', + quotes: 'off', + radix: 'error', + 'require-yield': 'off', + yoda: 'error', + 'airtable/background-image-url-double-quotes': 'warn', + 'airtable/is-returns-boolean': 'warn', + 'airtable/no-missing-async-suffix': 'warn', + 'airtable/no-missing-await': 'warn', + 'airtable/no-process-domain': 'warn', + 'airtable/noopener-noreferrer': 'off', + '@airtable/blocks/no-throw-new': 'error', + '@airtable/blocks/no-node-modules-invariant': 'error', + + '@airtable/blocks/no-error-interpolation': [ + 'error', + { + spawnError: 0, + invariant: 1, + }, + ], + + 'jsdoc/check-tag-names': [ + 'error', + { + definedTags: [ + 'hidden', + 'reactComponent', + 'noInheritDoc', + 'docsPath', + 'groupPath', + 'component', + 'hook', + ], + }, + ], + + 'jsdoc/no-types': 'error', + 'import/first': 'error', + 'import/order': 'error', + 'react-hooks/exhaustive-deps': 'warn', + 'react-hooks/rules-of-hooks': 'error', + 'react/display-name': 'off', + 'react/jsx-boolean-value': ['error', 'always'], + 'react/jsx-fragments': ['error', 'element'], + 'react/no-unescaped-entities': 'off', + 'react/prop-types': 'off', + 'react/react-in-jsx-scope': 'error', + + '@typescript-eslint/no-unused-vars': [ + 'error', + { + vars: 'all', + args: 'none', + }, + ], + + '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], + + '@typescript-eslint/consistent-type-exports': ['error'], + '@typescript-eslint/consistent-type-imports': [ + 'error', + { + fixStyle: 'inline-type-imports', + }, + ], + }, + }, +]); 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/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..67b35d7de 100644 --- a/packages/sdk/models.js +++ b/packages/sdk/models.js @@ -1 +1 @@ -module.exports = require('./dist/cjs/models/models'); +export * from './dist/esm/base/models/models'; diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 393f6cfdb..973cfed04 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,14 +1,51 @@ { "name": "@airtable/blocks", - "version": "1.18.2", + "version": "1.19.0", "description": "Airtable Blocks SDK", + "type": "module", "repository": { "type": "git", "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/esm/shared/unstable_private_utils.js" + }, + "./unstable_testing_utils": { + "types": "./dist/types/src/base/unstable_testing_utils.d.ts", + "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/esm/base/ui/unstable_standalone_ui.js" + }, + "./base/types": { + "types": "./types.d.ts", + "default": "./types.js" + }, + "./base/models": { + "types": "./dist/types/src/base/models/models.d.ts", + "default": "./dist/esm/base/models/models.js" + }, + "./base/ui": { + "types": "./dist/types/src/base/ui/ui.d.ts", + "default": "./dist/esm/base/ui/ui.js" + }, + "./base": { + "types": "./dist/types/src/base/index.d.ts", + "default": "./dist/esm/base/index.js" + }, + "./interface/models": { + "types": "./dist/types/src/interface/models/models.d.ts", + "default": "./dist/esm/interface/models/models.js" + }, + "./interface/ui": { + "types": "./dist/types/src/interface/ui/ui.d.ts", + "default": "./dist/esm/interface/ui/ui.js" + } + }, "files": [ "dist", "types", @@ -30,98 +67,70 @@ "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.cjs", "types": "tsc", - "lint": "ESLINT_USE_FLAT_CONFIG=false eslint --report-unused-disable-directives --ext .js,.ts,.tsx src test", + "lint": "eslint --report-unused-disable-directives --ext .js,.ts,.tsx src test", "lint:quiet": "yarn run lint --quiet", "jest": "node --unhandled-rejections=strict ./node_modules/.bin/jest", "jest:watch": "jest --watch", "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", "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", - "deploy-storybook": "storybook-to-ghpages" + "watch": "yarn run build:clean && concurrently yarn:watch:babel yarn:watch:types" }, "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", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.7.4", - "@babel/plugin-proposal-optional-chaining": "^7.7.5", - "@babel/plugin-transform-runtime": "^7.7.6", "@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/storybook-deployer": "^2.8.1", - "@types/enzyme": "^3.10.4", - "@types/enzyme-adapter-react-16": "^1.0.5", - "@types/fs-extra": "^8.0.1", - "@types/glob": "^7.1.1", - "@types/hoist-non-react-statics": "^3.3.5", + "@eslint/compat": "^1.3.2", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.7.0", + "@testing-library/react": "^16.3.0", "@types/jest": "^24.0.23", - "@types/lodash.omit": "^4.5.6", + "@types/node": "^22.0.0", "@types/prettier": "^1.19.0", - "@types/react-dom": "^16.9.24", - "@types/react-window": "^1.8.8", - "@types/styled-system": "^5.1.4", - "@typescript-eslint/eslint-plugin": "^7.13.0", - "@typescript-eslint/parser": "^7.13.0", + "@types/react": "^19.1.10", + "@types/react-dom": "^19.1.7", "babel-eslint": "^11.0.0-beta.0", - "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", + "core-js": "^3.4.8", + "eslint": "^9.34.0", "eslint-plugin-airtable": "github:hyperbase/eslint-plugin-airtable#01bbfe0", "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", - "glob": "^7.1.6", + "eslint-plugin-react-hooks": "^5.2.0", + "globals": "^16.3.0", "jest": "^24.9.0", - "prettier": "^1.19.1", - "prism-react-renderer": "^1.0.2", - "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", - "@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", - "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" + "jest-environment-jsdom-sixteen": "^2.0.0", + "lodash.capitalize": "^4.2.1", + "lodash.clamp": "^4.0.3", + "prettier": "^3.6.2", + "typescript": "^5.4.5", + "typescript-eslint": "^8.42.0" }, + "dependencies": {}, "peerDependencies": { - "react": "^16.14.0 || ^17.0.0", - "react-dom": "^16.9.24 || ^17.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/**/*", @@ -135,19 +144,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", - "after:release": "../../tools/git-mirror/bin/git-mirror sync @airtable/blocks@${version}" - }, - "npm": { - "access": "public" - } } } 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..731f5cf30 100755 --- a/packages/sdk/scripts/check_typescript_when_installed_in_block.sh +++ b/packages/sdk/scripts/check_typescript_when_installed_in_block.sh @@ -11,8 +11,9 @@ cd "$work_dir" cat - > tsconfig.json <<'EOF' { "compilerOptions": { - "module": "commonjs", - "target": "es2018", + "module": "esnext", + "moduleResolution": "bundler", + "target": "es2019", "allowSyntheticDefaultImports": true }, "include": ["source.ts"] @@ -23,15 +24,15 @@ 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 {colorUtils} from '@airtable/blocks/base/unstable_standalone_ui'; console.log(sdk); EOF diff --git a/packages/sdk/scripts/interface-alpha-release.cjs b/packages/sdk/scripts/interface-alpha-release.cjs new file mode 100644 index 000000000..e586de462 --- /dev/null +++ b/packages/sdk/scripts/interface-alpha-release.cjs @@ -0,0 +1,219 @@ +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) { + // Update and then restore and make sure it doesn't result in any unexpected changes + restorePackageJsonVersion(updatePackageJsonVersion(versionString)); + // Verify there are no uncommitted changes after the modification and restoration + 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) { + // git tag --list returns matching tag names, empty string if none exist + 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(); + + 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); + } + + // Verify that the git tag doesn't already exist + const gitTagName = `@airtable/blocks@${versionString}`; + checkGitTag(gitTagName); + + verifyPackageJsonModification(versionString); + + // Update package.json with the new version + originalVersion = updatePackageJsonVersion(versionString); + + console.log('Building and testing...'); + execCommand('yarn build'); + execCommand('yarn test'); + execCommand('rm -rf dist/types/{stories,test}'); + + 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('Running git mirror sync...'); + execCommand(`../../tools/git-mirror/bin/git-mirror sync @airtable/blocks@${versionString}`); + console.log(`✅ Git mirror sync completed for @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); + // Restore package.json version if we modified it + if (originalVersion) { + restorePackageJsonVersion(originalVersion); + } + process.exit(1); + } finally { + rl.close(); + } +} + +main(); diff --git a/packages/sdk/src/base/assert_run_context.ts b/packages/sdk/src/base/assert_run_context.ts new file mode 100644 index 000000000..6f3f7e685 --- /dev/null +++ b/packages/sdk/src/base/assert_run_context.ts @@ -0,0 +1,14 @@ +import getAirtableInterface from '../injected/airtable_interface'; +import {type BaseSdkMode} from '../sdk_mode'; +import {spawnError} from '../shared/error_utils'; +import {BlockRunContextType} from './types/airtable_interface'; + +if ((window as any).__getAirtableInterfaceAtVersion) { + const runContextType = getAirtableInterface().sdkInitData.runContext.type; + if ( + runContextType !== BlockRunContextType.DASHBOARD_APP && + runContextType !== BlockRunContextType.VIEW + ) { + throw spawnError('Unexpected import when running block in interface'); + } +} diff --git a/packages/sdk/src/base/index.ts b/packages/sdk/src/base/index.ts new file mode 100644 index 000000000..865e4b06f --- /dev/null +++ b/packages/sdk/src/base/index.ts @@ -0,0 +1,43 @@ +import './assert_run_context'; + +import {__injectSdkIntoWarning} from '../shared/warning'; +import getAirtableInterface from '../injected/airtable_interface'; +import {type BaseSdkMode} from '../sdk_mode'; +import {__injectSdkIntoPerformRecordAction} from './perform_record_action'; +import Sdk from './sdk'; +import {__injectSdkIntoInitializeBlock} from './ui/initialize_block'; + +/** @internal */ +export let __sdk: Sdk; +export let base: Sdk['base']; +export let globalConfig: Sdk['globalConfig']; +export let installationId: Sdk['installationId']; +export let reload: Sdk['reload']; +export let runInfo: Sdk['runInfo']; +export let settingsButton: Sdk['settingsButton']; +export let undoRedo: Sdk['undoRedo']; +export let viewport: Sdk['viewport']; +export let unstable_fetchAsync: Sdk['unstable_fetchAsync']; + +/** @internal */ +export function __reset() { + __sdk = new Sdk(getAirtableInterface()); + + ({ + base, + globalConfig, + installationId, + reload, + runInfo, + settingsButton, + undoRedo, + viewport, + unstable_fetchAsync, + } = __sdk); + + __injectSdkIntoPerformRecordAction(__sdk); + __injectSdkIntoInitializeBlock(__sdk); + __injectSdkIntoWarning(__sdk); +} + +__reset(); 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 92% 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..6ff668ebb 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 type Sdk from '../sdk'; +import { + fireAndForgetPromise, + type FlowAnyFunction, + type FlowAnyObject, + type TimeoutId, +} from '../../shared/private_utils'; +import {invariant} from '../../shared/error_utils'; +import AbstractModel from '../../shared/models/abstract_model'; +import {type BaseSdkMode} from '../../sdk_mode'; /** * Abstract superclass for all Blocks SDK models that need to fetch async data. @@ -11,8 +17,8 @@ import AbstractModel from './abstract_model'; */ abstract class AbstractModelWithAsyncData< DataType, - WatchableKey extends string -> extends AbstractModel { + WatchableKey extends string, +> extends AbstractModel { /** @internal */ static __DATA_UNLOAD_DELAY_MS = 1000; /** @internal */ @@ -132,7 +138,7 @@ abstract class AbstractModelWithAsyncData< return; } if (!this._pendingDataLoadPromise) { - this._pendingDataLoadPromise = this._loadDataAsync().then(changedKeys => { + this._pendingDataLoadPromise = this._loadDataAsync().then((changedKeys) => { this._isDataLoaded = true; this._pendingDataLoadPromise = null; diff --git a/packages/sdk/src/base/models/base.ts b/packages/sdk/src/base/models/base.ts new file mode 100644 index 000000000..40534eabd --- /dev/null +++ b/packages/sdk/src/base/models/base.ts @@ -0,0 +1,233 @@ +/** @module @airtable/blocks/models: Base */ /** */ +import {BaseCore} from '../../shared/models/base_core'; +import {MutationTypes} from '../types/mutations'; +import {type FieldType} from '../../shared/types/field_core'; +import {type PermissionCheckResult} from '../../shared/types/mutations_core'; +import {type BaseSdkMode} from '../../sdk_mode'; +import {type TableId} from '../../shared/types/hyper_ids'; +import type BaseBlockSdk from '../sdk'; +import RecordStore from './record_store'; +import Table from './table'; +import createAggregators, {type Aggregators} from './create_aggregators'; + +/** + * 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/base'; + * + * console.log('The name of your base is', base.name); + * ``` + * @docsPath models/Base + */ +class Base extends BaseCore { + /** @internal */ + static _className = 'Base'; + + /** @internal */ + _aggregators: Aggregators | null = null; + + /** @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); + } + + /** + * 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. + * + * 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); + } +} + +export default Base; diff --git a/packages/sdk/src/models/create_aggregators.ts b/packages/sdk/src/base/models/create_aggregators.ts similarity index 78% rename from packages/sdk/src/models/create_aggregators.ts rename to packages/sdk/src/base/models/create_aggregators.ts index 510b1cf93..37d929a05 100644 --- a/packages/sdk/src/models/create_aggregators.ts +++ b/packages/sdk/src/base/models/create_aggregators.ts @@ -1,22 +1,22 @@ /** @module @airtable/blocks/models: Aggregators */ /** */ -import {AggregatorKey} from '../types/aggregators'; -import {spawnError} from '../error_utils'; -import Sdk from '../sdk'; -import Record from './record'; -import Field from './field'; +import {type AggregatorKey} from '../types/aggregators'; +import {spawnError} from '../../shared/error_utils'; +import type Sdk from '../sdk'; +import type Record from './record'; +import type Field from './field'; /** * Aggregators can be used to compute aggregates for cell values. * * @example * ```js - * import {aggregators} from '@airtable/blocks/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', @@ -58,7 +63,7 @@ const aggregate = (aggregatorKey: AggregatorKey, records: Array, field: } const {__appInterface: appInterface, __airtableInterface: airtableInterface} = sdk; - const cellValues = records.map(record => record._getRawCellValue(field)); + const cellValues = records.map((record) => record._getRawCellValue(field)); return airtableInterface.aggregators.aggregate( appInterface, aggregatorKey, @@ -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', @@ -77,7 +87,7 @@ const aggregateToString = (aggregatorKey: AggregatorKey, records: Array, } const {__appInterface: appInterface, __airtableInterface: airtableInterface} = sdk; - const cellValues = records.map(record => record._getRawCellValue(field)); + const cellValues = records.map((record) => record._getRawCellValue(field)); return airtableInterface.aggregators.aggregateToString( appInterface, aggregatorKey, @@ -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/models/cursor.ts b/packages/sdk/src/base/models/cursor.ts similarity index 96% rename from packages/sdk/src/models/cursor.ts rename to packages/sdk/src/base/models/cursor.ts index 6b96f7eb5..d79be6244 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 type Sdk from '../sdk'; +import {type ModelChange} from '../../shared/types/base_core'; +import {isEnumValue, entries, type ObjectValues, type ObjectMap} from '../../shared/private_utils'; +import {invariant} from '../../shared/error_utils'; +import {type RecordId, type FieldId, type TableId, type ViewId} from '../../shared/types/hyper_ids'; import AbstractModelWithAsyncData from './abstract_model_with_async_data'; import Table from './table'; import View from './view'; @@ -47,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(); @@ -63,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/models/field.ts b/packages/sdk/src/base/models/field.ts similarity index 55% rename from packages/sdk/src/models/field.ts rename to packages/sdk/src/base/models/field.ts index 0f95c4170..b5668ea26 100644 --- a/packages/sdk/src/models/field.ts +++ b/packages/sdk/src/base/models/field.ts @@ -1,39 +1,19 @@ /** @module @airtable/blocks/models: Field */ /** */ -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; +import {FieldCore} from '../../shared/models/field_core'; +import {type FieldOptions} from '../../shared/types/field_core'; +import {type UpdateFieldOptionsOpts, MutationTypes} from '../types/mutations'; +import {type BaseSdkMode} from '../../sdk_mode'; +import {type PermissionCheckResult} from '../../shared/types/mutations_core'; +import {values} from '../../shared/private_utils'; +import {type AggregatorKey} from '../types/aggregators'; +import {type Aggregator} from './create_aggregators'; /** * Model class representing a field in a table. * * @example * ```js - * import {base} from '@airtable/blocks'; + * import {base} from '@airtable/blocks/base'; * * const table = base.getTableByName('Table 1'); * const field = table.getFieldByName('Name'); @@ -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. - * - * @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. + * A list of available aggregators given this field's configuration. * - * @see {@link FieldType} * @example * ```js - * import {FieldType} from '@airtable/blocks/models'; - * - * if (myField.type === FieldType.CURRENCY) { - * console.log(myField.options.symbol); - * // => '$' - * } + * const fieldAggregators = myField.availableAggregators; * ``` */ - get options(): FieldOptions | null { - const {options} = this._getCachedConfigFromFieldTypeProvider(); - - return options ? cloneDeep(options) : null; - } - - _getCachedConfigFromFieldTypeProvider(): FieldTypeConfig { - if (this._cachedFieldTypeConfigOrNull !== null) { - return this._cachedFieldTypeConfigOrNull; - } + get availableAggregators(): Array { const airtableInterface = this._sdk.__airtableInterface; - const appInterface = this._sdk.__appInterface; - - this._cachedFieldTypeConfigOrNull = airtableInterface.fieldTypeProvider.getConfig( - appInterface, - this._data, - this.parentTable.__getFieldNamesById(), + const availableAggregatorKeysSet = new Set( + airtableInterface.aggregators.getAvailableAggregatorKeysForField(this._data), ); - 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, - }, + const base = this.parentTable.parentBase; + return values(base.aggregators).filter((aggregator) => { + return availableAggregatorKeysSet.has(aggregator.key); }); } - /** - * 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 + * Checks if the given aggregator is available for this field. * + * @param aggregator The aggregator object or aggregator key. * @example * ```js - * const canUpdateField = field.hasPermissionToUpdateOptions(); + * import {base} from '@airtable/blocks/base'; * - * 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. + * const aggregator = base.aggregators.totalAttachmentSize; * - * 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}, - * ] - * }; + * // Using an aggregator object + * console.log(myAttachmentField.isAggregatorAvailable(aggregator)); + * // => true * - * if (selectField.hasPermissionToUpdateOptions(updatedOptions)) { - * await selectField.updateOptionsAsync(updatedOptions); - * } - * } + * // Using an aggregator key + * console.log(myTextField.isAggregatorAvailable('totalAttachmentSize')); + * // => false * ``` */ - 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, - }); + 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); } /** @@ -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 93% 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..47fa65eb7 100644 --- a/packages/sdk/src/models/grouped_record_query_result.ts +++ b/packages/sdk/src/base/models/grouped_record_query_result.ts @@ -1,20 +1,19 @@ /** @module @airtable/blocks/models: RecordQueryResult */ /** */ -import {FieldId} from '../types/field'; -import Sdk from '../sdk'; -import {FlowAnyFunction, FlowAnyObject, ObjectMap} from '../private_utils'; -import {invariant} from '../error_utils'; -import {RecordId} from '../types/record'; -import {GroupData} from '../types/view'; -import {NormalizedGroupLevel} from '../types/airtable_interface'; +import {type FieldId, type RecordId} from '../../shared/types/hyper_ids'; +import type Sdk from '../sdk'; +import {type FlowAnyFunction, type FlowAnyObject, type ObjectMap} from '../../shared/private_utils'; +import {invariant} from '../../shared/error_utils'; +import {type GroupData} from '../types/view'; +import {type NormalizedGroupLevel} from '../types/airtable_interface'; import RecordQueryResult, { - WatchableRecordQueryResultKey, - NormalizedRecordQueryResultOpts, + type WatchableRecordQueryResultKey, + type NormalizedRecordQueryResultOpts, } from './record_query_result'; -import Table from './table'; -import Field from './field'; +import type Table from './table'; +import type Field from './field'; import ObjectPool from './object_pool'; -import TableOrViewQueryResult from './table_or_view_query_result'; +import type TableOrViewQueryResult from './table_or_view_query_result'; /** @hidden */ @@ -81,7 +80,7 @@ class GroupedRecordQueryResult extends RecordQueryResult { + ? groupData.groups.map((singleGroupData) => { const group = this.__groupedRecordQueryResultPool.getObjectForReuse( this, singleGroupData, 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 94% 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..9138bb7cd 100644 --- a/packages/sdk/src/models/linked_records_query_result.ts +++ b/packages/sdk/src/base/models/linked_records_query_result.ts @@ -1,19 +1,19 @@ /** @module @airtable/blocks/models: RecordQueryResult */ /** */ -import {FieldType, FieldId} from '../types/field'; -import Sdk from '../sdk'; -import {FlowAnyFunction, FlowAnyObject, ObjectMap} from '../private_utils'; -import {invariant} from '../error_utils'; -import {RecordId} from '../types/record'; +import {FieldType} from '../../shared/types/field_core'; +import type Sdk from '../sdk'; +import {type FlowAnyFunction, type FlowAnyObject, type ObjectMap} from '../../shared/private_utils'; +import {invariant} from '../../shared/error_utils'; +import {type FieldId, type RecordId} from '../../shared/types/hyper_ids'; import RecordQueryResult, { - WatchableRecordQueryResultKey, - NormalizedRecordQueryResultOpts, + type WatchableRecordQueryResultKey, + type NormalizedRecordQueryResultOpts, } from './record_query_result'; -import TableOrViewQueryResult from './table_or_view_query_result'; +import type TableOrViewQueryResult from './table_or_view_query_result'; -import Table from './table'; -import Field from './field'; -import Record from './record'; -import RecordStore from './record_store'; +import type Table from './table'; +import type Field from './field'; +import type Record from './record'; +import type RecordStore from './record_store'; export const getLinkedTableId = (field: Field): string => { const options = field.options; @@ -135,7 +135,7 @@ class LinkedRecordsQueryResult extends RecordQueryResult { invariant(this.isValid, 'LinkedRecordsQueryResult is no longer valid'); - return this.recordIds.map(recordId => { + return this.recordIds.map((recordId) => { const record = this._linkedRecordStore.getRecordByIdIfExists(recordId); invariant(record, 'No record for id: %s', recordId); return record; @@ -181,9 +181,9 @@ class LinkedRecordsQueryResult extends RecordQueryResult { - const arrayKeys = (Array.isArray(keys) ? keys : [keys]) as ReadonlyArray< - WatchableRecordQueryResultKey - >; + const arrayKeys = ( + Array.isArray(keys) ? keys : [keys] + ) as ReadonlyArray; for (const key of arrayKeys) { if (key === RecordQueryResult.WatchableKeys.cellValues) { @@ -236,7 +236,7 @@ class LinkedRecordsQueryResult extends RecordQueryResult field.id); + this.parentTable.fields.map((field) => field.id); for (const fieldId of fieldIds) { changedKeys.push(RecordQueryResult.WatchableCellValuesInFieldKeyPrefix + fieldId); @@ -290,7 +290,7 @@ class LinkedRecordsQueryResult extends RecordQueryResult { const countByFieldId: ObjectMap = {}; - const watchKeys = Object.keys(this._changeWatchersByKey).filter(key => { + const watchKeys = Object.keys(this._changeWatchersByKey).filter((key) => { return key.startsWith(RecordQueryResult.WatchableCellValuesInFieldKeyPrefix); }); for (const watchKey of watchKeys) { @@ -427,7 +427,7 @@ class LinkedRecordsQueryResult extends RecordQueryResult recordIdsSet[id] === true); + const recordIds = changes.recordIds.filter((id) => recordIdsSet[id] === true); if (recordIds.length) { this._onChange('cellValues', {fieldIds: changes.fieldIds, recordIds}); } @@ -455,7 +455,7 @@ class LinkedRecordsQueryResult extends RecordQueryResult typeof id === 'string' && recordIdsSet[id] === true, + (id) => typeof id === 'string' && recordIdsSet[id] === true, ); if (filteredRecordIds.length) { this._onChange( @@ -573,7 +573,7 @@ class LinkedRecordsQueryResult extends RecordQueryResult recordIdsSet[recordId] === true, + (recordId) => recordIdsSet[recordId] === true, ); } else { this._computedFilteredSortedRecordIds = Object.keys(recordIdsSet); diff --git a/packages/sdk/src/models/models.ts b/packages/sdk/src/base/models/models.ts similarity index 61% rename from packages/sdk/src/models/models.ts rename to packages/sdk/src/base/models/models.ts index ecddbd4a3..b2d36a971 100644 --- a/packages/sdk/src/models/models.ts +++ b/packages/sdk/src/base/models/models.ts @@ -1,7 +1,9 @@ /** @ignore */ /** */ +import '../assert_run_context'; + import * as recordColoring from './record_coloring'; -import createAggregators from './create_aggregators'; -export {FieldType, FieldConfig} from '../types/field'; +export type {FieldConfig} from '../../shared/types/field_core'; +export {FieldType} from '../../shared/types/field_core'; export {ViewType} from '../types/view'; export {default as Base} from './base'; export {default as Table} from './table'; @@ -16,16 +18,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 new file mode 100644 index 000000000..d56bacd6d --- /dev/null +++ b/packages/sdk/src/base/models/mutations.ts @@ -0,0 +1,394 @@ +import {BlockRunContextType} from '../types/airtable_interface'; +import {type ModelChange} from '../../shared/types/base_core'; +import {type Mutation, MutationTypes} from '../types/mutations'; +import {spawnError, spawnUnknownSwitchCaseError} from '../../shared/error_utils'; +import {type 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 '../../shared/types/mutation_constants'; +import {MutationsCore} from '../../shared/models/mutations_core'; +import {type BaseSdkMode} from '../../sdk_mode'; +import {type RecordData} from '../types/record'; +import type Table from './table'; +import type RecordStore from './record_store'; + +/** @hidden */ +class Mutations extends MutationsCore { + /** @internal */ + _isRecordStoreReadyForMutations(recordStore: RecordStore): boolean { + return recordStore.isRecordMetadataLoaded; + } + + /** @internal */ + _isFieldAvailableForMutation(recordStore: RecordStore, fieldId: FieldId): boolean { + return recordStore.areCellValuesLoadedForFieldId(fieldId); + } + + /** @internal */ + _getDefaultRecordProperties(): Partial { + return { + commentCount: 0, + createdTime: new Date().toJSON(), + }; + } + + /** @internal */ + _assertMutationIsValid(mutation: Mutation): void { + + const appInterface = this._sdk.__appInterface; + const billingPlanGrouping = this._base.__billingPlanGrouping; + + switch (mutation.type) { + case MutationTypes.SET_MULTIPLE_GLOBAL_CONFIG_PATHS: + case MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES: + case MutationTypes.CREATE_MULTIPLE_RECORDS: + case MutationTypes.DELETE_MULTIPLE_RECORDS: { + super._assertMutationIsValid(mutation); + return; + } + + case MutationTypes.CREATE_SINGLE_FIELD: { + const {tableId, name, config, description} = mutation; + const table = this._base.getTableByIdIfExists(tableId); + if (!table) { + throw spawnError("Can't create field: No table with id %s exists", tableId); + } + + if (table.fields.length >= MAX_NUM_FIELDS_PER_TABLE) { + throw spawnError( + "Can't create field: table already has the maximum of %s fields", + MAX_NUM_FIELDS_PER_TABLE, + ); + } + + this._assertFieldNameIsValidForMutation(name, table); + + const validationResult = + this._airtableInterface.fieldTypeProvider.validateConfigForUpdate( + appInterface, + config, + null, + null, + billingPlanGrouping, + ); + + if (!validationResult.isValid) { + throw spawnError( + "Can't create field: invalid field config.\n%s", + validationResult.reason, + ); + } + + if (description && description.length > MAX_FIELD_DESCRIPTION_LENGTH) { + throw spawnError( + "Can't create field: description exceeds maximum length of %s characters", + MAX_FIELD_DESCRIPTION_LENGTH, + ); + } + return; + } + + case MutationTypes.UPDATE_SINGLE_FIELD_CONFIG: { + const {tableId, id, config, opts} = mutation; + const table = this._base.getTableByIdIfExists(tableId); + if (!table) { + throw spawnError("Can't update field: No table with id %s exists", tableId); + } + + const field = table.getFieldByIdIfExists(id); + if (!field) { + throw spawnError("Can't update field: No field with id %s exists", id); + } + + const currentConfig = this._airtableInterface.fieldTypeProvider.getConfig( + appInterface, + field._data, + field.parentTable.__getFieldNamesById(), + ); + const validationResult = + this._airtableInterface.fieldTypeProvider.validateConfigForUpdate( + appInterface, + config, + currentConfig, + field._data, + billingPlanGrouping, + opts, + ); + + if (!validationResult.isValid) { + throw spawnError( + "Can't update field: invalid field config.\n%s", + validationResult.reason, + ); + } + return; + } + + case MutationTypes.UPDATE_SINGLE_FIELD_DESCRIPTION: { + const {tableId, id, description} = mutation; + const table = this._base.getTableByIdIfExists(tableId); + if (!table) { + throw spawnError("Can't update field: No table with id %s exists", tableId); + } + + const field = table.getFieldByIdIfExists(id); + if (!field) { + throw spawnError("Can't update field: No field with id %s exists", id); + } + + if (description && description.length > MAX_FIELD_DESCRIPTION_LENGTH) { + throw spawnError( + "Can't update field: description exceeds maximum length of %s characters", + MAX_FIELD_DESCRIPTION_LENGTH, + ); + } + return; + } + + case MutationTypes.UPDATE_SINGLE_FIELD_NAME: { + const {tableId, id, name} = mutation; + const table = this._base.getTableByIdIfExists(tableId); + if (!table) { + throw spawnError("Can't update field: No table with id %s exists", tableId); + } + + const field = table.getFieldByIdIfExists(id); + if (!field) { + throw spawnError("Can't update field: No field with id %s exists", id); + } + + if (field.name.toLowerCase() !== name.toLowerCase()) { + this._assertFieldNameIsValidForMutation(name, table); + } + return; + } + + case MutationTypes.CREATE_SINGLE_TABLE: { + const {name, fields} = mutation; + + if (!name) { + throw spawnError("Can't create table: must provide non-empty name"); + } + + if (name.length > MAX_TABLE_NAME_LENGTH) { + throw spawnError( + "Can't create table: name '%s' exceeds maximum length of %s characters", + name, + MAX_TABLE_NAME_LENGTH, + ); + } + + const existingLowercaseTableNames = this._base.tables.map((table) => + table.name.toLowerCase(), + ); + + if (existingLowercaseTableNames.includes(name.toLowerCase())) { + throw spawnError( + "Can't create table: table with name '%s' already exists", + name, + ); + } + + if (fields.length === 0) { + throw spawnError("Can't create table: must specify at least one field"); + } + + if (fields.length > MAX_NUM_FIELDS_PER_TABLE) { + throw spawnError( + "Can't create table: number of fields exceeds maximum of %s", + MAX_NUM_FIELDS_PER_TABLE, + ); + } + + const lowercaseFieldNames = new Set(); + for (const field of fields) { + if (!field.name) { + throw spawnError( + "Can't create table: must provide non-empty name for every field", + ); + } + + if (field.name.length > MAX_FIELD_NAME_LENGTH) { + throw spawnError( + "Can't create table: field name '%s' exceeds maximum length of %s characters", + field.name, + MAX_FIELD_NAME_LENGTH, + ); + } + + const lowercaseFieldName = field.name.toLowerCase(); + if (lowercaseFieldNames.has(lowercaseFieldName)) { + throw spawnError( + "Can't create table: duplicate field name '%s'", + field.name, + ); + } + lowercaseFieldNames.add(lowercaseFieldName); + + const validationResult = + this._airtableInterface.fieldTypeProvider.validateConfigForUpdate( + appInterface, + field.config, + null, + null, + billingPlanGrouping, + ); + + if (!validationResult.isValid) { + throw spawnError( + "Can't create table: invalid field config for field '%s'.\n%s", + field.name, + validationResult.reason, + ); + } + + if ( + field.description && + field.description.length > MAX_FIELD_DESCRIPTION_LENGTH + ) { + throw spawnError( + "Can't create table: description for field '%s' exceeds maximum length of %s characters", + field.name, + MAX_FIELD_DESCRIPTION_LENGTH, + ); + } + } + + const primaryField = fields[0]; + if ( + !this._airtableInterface.fieldTypeProvider.canBePrimary( + appInterface, + primaryField.config, + billingPlanGrouping, + ) + ) { + throw spawnError( + "Can't create table: first field '%s' has type '%s' which cannot be a primary field", + primaryField.name, + primaryField.config.type, + ); + } + + return; + } + case MutationTypes.UPDATE_VIEW_METADATA: { + const {tableId, viewId} = mutation; + const {runContext} = this._airtableInterface.sdkInitData; + + if (runContext.type !== BlockRunContextType.VIEW) { + throw spawnError('Setting view metadata is only valid for views'); + } + + if (runContext.viewId !== viewId || runContext.tableId !== tableId) { + throw spawnError('Custom views can only set view metadata on themselves'); + } + + const table = this._base.getTableByIdIfExists(tableId); + if (!table) { + throw spawnError("Can't update metadata: No table with id %s exists", tableId); + } + + const view = table.getViewByIdIfExists(viewId); + if (!view) { + throw spawnError("Can't update metadata: No view with id %s exists", viewId); + } + return; + } + + default: + throw spawnUnknownSwitchCaseError('mutation type', mutation, 'type'); + } + } + + /** @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 */ + _getOptimisticModelChangesForMutation(mutation: Mutation): Array { + switch (mutation.type) { + 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); + + if (!recordStore.isRecordMetadataLoaded) { + return []; + } + + const superChanges = super._getOptimisticModelChangesForMutation(mutation); + return [ + ...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), + ); + }), + ]; + } + 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 [ + ...superChanges, + ...this._base.getTableById(tableId).views.flatMap((view) => { + const viewDataStore = recordStore.getViewDataStore(view.id); + if (!viewDataStore.isDataLoaded) { + return []; + } + return viewDataStore.__generateChangesForParentTableDeleteMultipleRecords( + recordIds, + ); + }), + ]; + } + + case MutationTypes.CREATE_SINGLE_FIELD: + case MutationTypes.UPDATE_SINGLE_FIELD_CONFIG: + case MutationTypes.UPDATE_SINGLE_FIELD_DESCRIPTION: + case MutationTypes.UPDATE_SINGLE_FIELD_NAME: + case MutationTypes.UPDATE_VIEW_METADATA: + case MutationTypes.CREATE_SINGLE_TABLE: { + return []; + } + + default: + throw spawnUnknownSwitchCaseError('mutation type', mutation, 'type'); + } + } +} + +export default Mutations; diff --git a/packages/sdk/src/models/object_pool.ts b/packages/sdk/src/base/models/object_pool.ts similarity index 95% rename from packages/sdk/src/models/object_pool.ts rename to packages/sdk/src/base/models/object_pool.ts index c92f88a4d..cb6e2738a 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 {type TimeoutId} from '../../shared/private_utils'; const WEAK_RETAIN_TIME_MS = 10000; @@ -71,7 +71,7 @@ class ObjectPool T> { if (!pooledObjects) { return false; } - const index = pooledObjects.findIndex(stored => stored.object === object); + const index = pooledObjects.findIndex((stored) => stored.object === object); if (index === -1) { return false; } diff --git a/packages/sdk/src/models/record.ts b/packages/sdk/src/base/models/record.ts similarity index 59% rename from packages/sdk/src/models/record.ts rename to packages/sdk/src/base/models/record.ts index 07e36f4de..e0432e783 100644 --- a/packages/sdk/src/models/record.ts +++ b/packages/sdk/src/base/models/record.ts @@ -1,25 +1,29 @@ /** @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 ObjectPool from './object_pool'; -import Table from './table'; -import View from './view'; -import RecordQueryResult, {RecordQueryResultOpts} from './record_query_result'; +import {type Color} from '../../shared/colors'; +import {RecordCore, WatchableRecordKeysCore} from '../../shared/models/record_core'; +import {type ViewId, type FieldId} from '../../shared/types/hyper_ids'; +import {type BaseSdkMode} from '../../sdk_mode'; +import { + isEnumValue, + type ObjectValues, + type FlowAnyObject, + isObjectEmpty, +} from '../../shared/private_utils'; +import type BlockSdk from '../sdk'; +import {invariant} from '../../shared/error_utils'; +import colorUtils from '../../shared/color_utils'; +import {type FieldType} from '../../shared/types/field_core'; import LinkedRecordsQueryResult from './linked_records_query_result'; -import RecordStore from './record_store'; +import ObjectPool from './object_pool'; +import type RecordStore from './record_store'; +import type Field from './field'; +import type Table from './table'; +import type View from './view'; +import RecordQueryResult, {type RecordQueryResultOpts} from './record_query_result'; 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 +45,7 @@ type WatchableRecordKey = ObjectValues | string; * * @docsPath models/Record */ -class Record extends AbstractModel { +class Record extends RecordCore { /** @internal */ static _className = 'Record'; /** @internal */ @@ -53,10 +57,6 @@ class Record extends AbstractModel { ); } /** @internal */ - _parentRecordStore: RecordStore; - /** @internal */ - _parentTable: Table; - /** @internal */ __linkedRecordsQueryResultPool: ObjectPool< LinkedRecordsQueryResult, typeof LinkedRecordsQueryResult @@ -65,47 +65,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 +81,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 +94,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: {id: FieldId; type: FieldType}): 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 +244,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 +259,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 92% rename from packages/sdk/src/models/record_coloring.ts rename to packages/sdk/src/base/models/record_coloring.ts index 35897cca3..17a5a10a2 100644 --- a/packages/sdk/src/models/record_coloring.ts +++ b/packages/sdk/src/base/models/record_coloring.ts @@ -1,7 +1,7 @@ /** @module @airtable/blocks/models: Record Coloring */ /** */ -import {ObjectValues} from '../private_utils'; -import Field from './field'; -import View from './view'; +import {type ObjectValues} from '../../shared/private_utils'; +import type Field from './field'; +import type View from './view'; /** * An enum of the different types of {@link recordColoring.modes} @@ -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/models/record_query_result.ts b/packages/sdk/src/base/models/record_query_result.ts similarity index 96% rename from packages/sdk/src/models/record_query_result.ts rename to packages/sdk/src/base/models/record_query_result.ts index 28eeca6c6..06c1781d6 100644 --- a/packages/sdk/src/models/record_query_result.ts +++ b/packages/sdk/src/base/models/record_query_result.ts @@ -1,31 +1,31 @@ /** @module @airtable/blocks/models: RecordQueryResult */ /** */ -import Colors, {Color} from '../colors'; -import Sdk from '../sdk'; -import {RecordId} from '../types/record'; -import {FieldType, FieldId} from '../types/field'; +import Colors, {type Color} from '../../shared/colors'; +import type Sdk from '../sdk'; +import {type FieldId, type RecordId} from '../../shared/types/hyper_ids'; +import {FieldType} from '../../shared/types/field_core'; import { isEnumValue, assertEnumValue, getLocallyUniqueId, - ObjectValues, - ObjectMap, + type ObjectValues, + type ObjectMap, cast, - FlowAnyFunction, - FlowAnyObject, -} from '../private_utils'; -import {spawnUnknownSwitchCaseError, spawnError, invariant} from '../error_utils'; -import Watchable from '../watchable'; -import {NormalizedGroupLevel} from '../types/airtable_interface'; + type FlowAnyFunction, + type FlowAnyObject, +} from '../../shared/private_utils'; +import {spawnUnknownSwitchCaseError, spawnError, invariant} from '../../shared/error_utils'; +import type Watchable from '../../shared/watchable'; +import {type NormalizedGroupLevel} from '../types/airtable_interface'; import AbstractModelWithAsyncData from './abstract_model_with_async_data'; -import Table from './table'; +import type Table from './table'; import Field from './field'; -import Record from './record'; -import RecordStore from './record_store'; +import type Record from './record'; +import type RecordStore from './record_store'; import { ModeTypes as RecordColorModeTypes, modes as recordColorModes, serialize as serializeColorMode, - RecordColorMode, + type RecordColorMode, } from './record_coloring'; const WatchableRecordQueryResultKeys = Object.freeze({ @@ -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(), @@ -277,7 +277,7 @@ export function normalizeSortsOrGroups( if (sortsOrGroups === undefined || sortsOrGroups === null) { return sortsOrGroups; } - return sortsOrGroups.map(sortOrGroup => _normalizeSortOrGroup(table, sortOrGroup)); + return sortsOrGroups.map((sortOrGroup) => _normalizeSortOrGroup(table, sortOrGroup)); } /** @@ -495,7 +495,7 @@ abstract class RecordQueryResult extends AbstractModelWithAsyncDa * Can be watched. */ get records(): Array { - return this.recordIds.map(recordId => { + return this.recordIds.map((recordId) => { const record = this._recordStore.getRecordByIdIfExists(recordId); invariant(record, 'Record missing in table'); return record; @@ -669,8 +669,8 @@ abstract class RecordQueryResult extends AbstractModelWithAsyncDa this._changeWatchersByKey[WatchableRecordQueryResultKeys.recordColors], 'method may only be called when `recordColors` key has been watched', ); - const watchCount = this._changeWatchersByKey[WatchableRecordQueryResultKeys.recordColors] - .length; + const watchCount = + this._changeWatchersByKey[WatchableRecordQueryResultKeys.recordColors].length; if (!this._recordColorChangeHandler && watchCount >= 1) { this._watchRecordColors(); } diff --git a/packages/sdk/src/models/record_store.ts b/packages/sdk/src/base/models/record_store.ts similarity index 70% rename from packages/sdk/src/models/record_store.ts rename to packages/sdk/src/base/models/record_store.ts index 53658b49c..808cff331 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, + type ObjectMap, + type FlowAnyFunction, + type FlowAnyObject, cast, keys as objectKeys, -} from '../private_utils'; -import {invariant, logErrorToSentry} from '../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 {AirtableInterface} from '../types/airtable_interface'; + type ObjectValues, + isEnumValue, +} from '../../shared/private_utils'; +import {invariant, logErrorToSentry} from '../../shared/error_utils'; +import type Sdk from '../sdk'; +import {type TableData} from '../types/table'; +import {type TableId, type FieldId, type ViewId, type RecordId} from '../../shared/types/hyper_ids'; +import {type RecordData} from '../types/record'; +import {type AirtableInterface} from '../types/airtable_interface'; +import {type ChangedPathsForType} from '../../shared/models/base_core'; +import {type 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 type 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>> = []; @@ -248,23 +299,21 @@ class RecordStore extends AbstractModelWithAsyncData 0) { - const loadFieldsWhichAreNotAlreadyLoadedOrLoadingPromise = this._loadCellValuesInFieldIdsAsync( - fieldIdsWhichAreNotAlreadyLoadedOrLoading, - ); + const loadFieldsWhichAreNotAlreadyLoadedOrLoadingPromise = + this._loadCellValuesInFieldIdsAsync(fieldIdsWhichAreNotAlreadyLoadedOrLoading); pendingLoadPromises.push(loadFieldsWhichAreNotAlreadyLoadedOrLoadingPromise); for (const fieldId of fieldIdsWhichAreNotAlreadyLoadedOrLoading) { - this._pendingCellValuesLoadPromiseByFieldId[ - fieldId - ] = loadFieldsWhichAreNotAlreadyLoadedOrLoadingPromise; + this._pendingCellValuesLoadPromiseByFieldId[fieldId] = + loadFieldsWhichAreNotAlreadyLoadedOrLoadingPromise; } - loadFieldsWhichAreNotAlreadyLoadedOrLoadingPromise.then(changedKeys => { + loadFieldsWhichAreNotAlreadyLoadedOrLoadingPromise.then((changedKeys) => { for (const fieldId of fieldIdsWhichAreNotAlreadyLoadedOrLoading) { this._areCellValuesLoadedByFieldId[fieldId] = true; this._pendingCellValuesLoadPromiseByFieldId[fieldId] = undefined; } for (const key of changedKeys) { - this._onChange(key); + onChange(key); } }); } @@ -275,12 +324,11 @@ class RecordStore extends AbstractModelWithAsyncData, ): Promise> { - const { - recordsById: newRecordsById, - } = await this._airtableInterface.fetchAndSubscribeToCellValuesInFieldsAsync( - this.tableId, - fieldIds, - ); + const {recordsById: newRecordsById} = + await this._airtableInterface.fetchAndSubscribeToCellValuesInFieldsAsync( + this.tableId, + fieldIds, + ); if (!this._data.recordsById) { this._data.recordsById = {}; @@ -323,7 +371,9 @@ class RecordStore extends AbstractModelWithAsyncData WatchableCellValuesInFieldKeyPrefix + fieldId); + const changedKeys = fieldIds.map( + (fieldId) => WatchableCellValuesInFieldKeyPrefix + fieldId, + ); changedKeys.push(WatchableRecordStoreKeys.records); changedKeys.push(WatchableRecordStoreKeys.recordIds); changedKeys.push(WatchableRecordStoreKeys.cellValues); @@ -361,7 +411,7 @@ class RecordStore extends AbstractModelWithAsyncData 0) { this._timeoutForRemovingFieldIds = setTimeout(() => { - const fieldIdsToUnload = fieldIdsWithZeroRetainCount.filter(fieldId => { + const fieldIdsToUnload = fieldIdsWithZeroRetainCount.filter((fieldId) => { return ( this._cellValuesRetainCountByFieldId[fieldId] === 0 && this._areCellValuesLoadedByFieldId[fieldId] @@ -420,7 +470,7 @@ class RecordStore extends AbstractModelWithAsyncData) { const areAnyFieldsLoaded = this.isDataLoaded || - values(this._areCellValuesLoadedByFieldId).some(isLoaded => isLoaded); + values(this._areCellValuesLoadedByFieldId).some((isLoaded) => isLoaded); if (!this.isDeleted) { if (!areAnyFieldsLoaded) { @@ -433,7 +483,7 @@ class RecordStore extends AbstractModelWithAsyncData !this._areCellValuesLoadedByFieldId[fieldId], + (fieldId) => !this._areCellValuesLoadedByFieldId[fieldId], ); } const {recordsById} = this._data; @@ -448,79 +498,12 @@ 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 52% rename from packages/sdk/src/models/session.ts rename to packages/sdk/src/base/models/session.ts index 1fbeba9b4..d3a9b318b 100644 --- a/packages/sdk/src/models/session.ts +++ b/packages/sdk/src/base/models/session.ts @@ -1,40 +1,14 @@ -/** @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 {type BaseSdkMode} from '../../sdk_mode'; +import {SessionCore} from '../../shared/models/session_core'; +import {type PermissionCheckResult} from '../../shared/types/mutations_core'; +import {MutationTypes} from '../types/mutations'; /** * Model class representing the current user's session. * * @example * ```js - * import {useSession} from '@airtable/blocks/ui'; + * import {useSession} from '@airtable/blocks/base/ui'; * * function Username() { * const session = useSession(); @@ -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}. @@ -129,7 +35,7 @@ class Session extends AbstractModel { * * @example * ```js - * import {useSession} from '@airtable/blocks/ui'; + * import {useSession} from '@airtable/blocks/base/ui'; * * function UpdateButton({onClick}) { * const session = useSession(); @@ -170,7 +76,7 @@ class Session extends AbstractModel { * * @example * ```js - * import {useSession} from '@airtable/blocks/ui'; + * import {useSession} from '@airtable/blocks/base/ui'; * * function CreateButton({onClick}) { * const session = useSession(); @@ -211,7 +117,7 @@ class Session extends AbstractModel { * * @example * ```js - * import {useSession} from '@airtable/blocks/ui'; + * import {useSession} from '@airtable/blocks/base/ui'; * * function DeleteButton({onClick}) { * const session = useSession(); @@ -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/base/models/table.ts b/packages/sdk/src/base/models/table.ts new file mode 100644 index 000000000..0d7e8ce34 --- /dev/null +++ b/packages/sdk/src/base/models/table.ts @@ -0,0 +1,561 @@ +/** @module @airtable/blocks/models: Table */ /** */ +import {TableCore, WatchableTableKeysCore} from '../../shared/models/table_core'; +import {type ViewType} from '../types/view'; +import {spawnError} from '../../shared/error_utils'; +import {entries, cast, isEnumValue, type ObjectValues} from '../../shared/private_utils'; +import type BlockSdk from '../sdk'; +import {type FieldId, type ViewId} from '../../shared/types/hyper_ids'; +import {type TableData} from '../types/table'; +import {type FieldType, type FieldOptions} from '../../shared/types/field_core'; +import {MutationTypes} from '../types/mutations'; +import {type BaseSdkMode} from '../../sdk_mode'; +import {type PermissionCheckResult} from '../../shared/types/mutations_core'; +import {type ChangedPathsForType} from '../../shared/models/base_core'; +import type RecordStore from './record_store'; +import RecordQueryResult, {type RecordQueryResultOpts} from './record_query_result'; +import Field from './field'; +import TableOrViewQueryResult from './table_or_view_query_result'; +import View from './view'; +import ObjectPool from './object_pool'; +import type Base from './base'; + +export const WatchableTableKeys = Object.freeze({ + ...WatchableTableKeysCore, + views: 'views' as const, +}); + +/** + * A key in {@link Table} that can be watched. + * - `name` + * - `description` + * - `fields` + * - `views` + */ +export type WatchableTableKey = ObjectValues; + +/** + * Model class representing a table. Every {@link Base} has one or more tables. + * + * @docsPath models/Table + */ +class Table extends TableCore { + /** @internal */ + static _className = 'Table'; + /** @internal */ + static _isWatchableKey(key: string): boolean { + return isEnumValue(WatchableTableKeys, key); + } + /** @internal */ + _viewModelsById: {[key: string]: View}; + /** @internal */ + __tableOrViewQueryResultPool: ObjectPool; + + /** @internal */ + constructor(parentBase: Base, recordStore: RecordStore, tableId: string, sdk: BlockSdk) { + super(parentBase, recordStore, tableId, sdk); + + this._viewModelsById = {}; + this.__tableOrViewQueryResultPool = new ObjectPool(TableOrViewQueryResult); + } + + /** @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. + * + * @example + * ```js + * console.log(myTable.url); + * // => 'https://airtable.com/appxxxxxxxxxxxxxx/tblxxxxxxxxxxxxxx' + * ``` + */ + get url(): string { + return this._sdk.__airtableInterface.urlConstructor.getTableUrl(this.id); + } + + /** + * @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; + } + + /** + * The views in this table. Can be watched to know when views are created, + * deleted, or reordered. + * + * @example + * ```js + * console.log(`This table has ${myTable.views.length} views`); + * ``` + */ + get views(): Array { + const views: Array = []; + this._data.viewOrder.forEach((viewId) => { + const view = this.getViewById(viewId); + views.push(view); + }); + return views; + } + /** + * Gets the view matching the given ID, or `null` if that view does not exist in this table. + * + * @param viewId The ID of the view. + * @example + * ```js + * const viewId = 'viwxxxxxxxxxxxxxx'; + * const view = myTable.getViewByIdIfExists(viewId); + * if (view !== null) { + * console.log(view.name); + * } else { + * console.log('No view exists with that ID'); + * } + * ``` + */ + getViewByIdIfExists(viewId: ViewId): View | null { + if (!this._data.viewsById[viewId]) { + return null; + } else { + if (!this._viewModelsById[viewId]) { + this._viewModelsById[viewId] = new View( + this._sdk, + this, + this._recordStore.getViewDataStore(viewId), + viewId, + ); + } + return this._viewModelsById[viewId]; + } + } + /** + * Gets the view matching the given ID. Throws if that view does not exist in this table. Use + * {@link getViewByIdIfExists} instead if you are unsure whether a view exists with the given + * ID. + * + * @param viewId The ID of the view. + * @example + * ```js + * const viewId = 'viwxxxxxxxxxxxxxx'; + * const view = myTable.getViewById(viewId); + * console.log(view.name); + * // => 'Grid view' + * ``` + */ + getViewById(viewId: ViewId): View { + const view = this.getViewByIdIfExists(viewId); + if (!view) { + throw spawnError("No view with ID %s in table '%s'", viewId, this.name); + } + return view; + } + /** + * Gets the view matching the given name, or `null` if no view exists with that name in this + * table. + * + * @param viewName The name of the view you're looking for. + * @example + * ```js + * const view = myTable.getViewByNameIfExists('Name'); + * if (view !== null) { + * console.log(view.id); + * } else { + * console.log('No view exists with that name'); + * } + * ``` + */ + getViewByNameIfExists(viewName: string): View | null { + for (const [viewId, viewData] of entries(this._data.viewsById)) { + if (viewData.name === viewName) { + return this.getViewByIdIfExists(viewId); + } + } + return null; + } + /** + * Gets the view matching the given name. Throws if no view exists with that name in this table. + * Use {@link getViewByNameIfExists} instead if you are unsure whether a view exists with the + * given name. + * + * @param viewName The name of the view you're looking for. + * @example + * ```js + * const view = myTable.getViewByName('Name'); + * console.log(view.id); + * // => 'viwxxxxxxxxxxxxxx' + * ``` + */ + getViewByName(viewName: string): View { + const view = this.getViewByNameIfExists(viewName); + if (!view) { + throw spawnError("No view named '%s' in table '%s'", viewName, this.name); + } + return view; + } + /** + * The view matching the given ID or name. Returns `null` if no matching view 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 getViewByIdIfExists} or + * {@link getViewByNameIfExists} methods instead. + * + * @param viewIdOrName The ID or name of the view you're looking for. + */ + getViewIfExists(viewIdOrName: ViewId | string): View | null { + return this.getViewByIdIfExists(viewIdOrName) ?? this.getViewByNameIfExists(viewIdOrName); + } + /** + * The view matching the given ID or name. Throws if no matching view exists within this table. + * Use {@link getViewIfExists} instead if you are unsure whether a view 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 getViewById} or {@link getViewByName} methods + * instead. + * + * @param viewIdOrName The ID or name of the view you're looking for. + */ + getView(viewIdOrName: ViewId | string): View { + const view = this.getViewIfExists(viewIdOrName); + if (!view) { + throw spawnError("No view with ID or name '%s' in table '%s'", viewIdOrName, this.name); + } + return view; + } + /** + * Select records from the table. Returns a {@link RecordQueryResult}. + * + * Consider using {@link useRecords} or {@link useRecordIds} instead, unless you need the + * features of a QueryResult (e.g. `queryResult.getRecordById`). Record hooks handle + * loading/unloading and updating your UI automatically, but manually `select`ing records is + * useful for one-off data processing. + * + * @param opts Options for the query, such as sorts and fields. + * @example + * ```js + * import {useBase, useRecords} from '@airtable/blocks/base/ui'; + * import React from 'react'; + * + * function TodoList() { + * const base = useBase(); + * const table = base.getTableByName('Tasks'); + * + * const queryResult = table.selectRecords(); + * const records = useRecords(queryResult); + * + * return ( + *
    + * {records.map(record => ( + *
  • + * {record.name || 'Unnamed record'} + *
  • + * ))} + *
+ * ); + * } + * ``` + */ + selectRecords(opts?: RecordQueryResultOpts): TableOrViewQueryResult { + const normalizedOpts = RecordQueryResult._normalizeOpts( + this, + this._recordStore, + opts || {}, + ); + return this.__tableOrViewQueryResultPool.getObjectForReuse(this._sdk, this, normalizedOpts); + } + /** + * Select and load records from the table. Returns a {@link RecordQueryResult} promise where + * record data has been loaded. + * + * Consider using {@link useRecords} or {@link useRecordIds} instead, unless you need the + * features of a QueryResult (e.g. `queryResult.getRecordById`). Record hooks handle + * loading/unloading and updating your UI automatically, but manually `select`ing records is + * useful for one-off data processing. + * + * Once you've finished with your query, remember to call `queryResult.unloadData()`. + * + * @param opts Options for the query, such as sorts and fields. + * @example + * ```js + * async function logRecordCountAsync(table) { + * const query = await table.selectRecordsAsync(); + * console.log(query.recordIds.length); + * query.unloadData(); + * } + * ``` + */ + async selectRecordsAsync(opts?: RecordQueryResultOpts): Promise { + const queryResult = this.selectRecords(opts); + await queryResult.loadDataAsync(); + return queryResult; + } + /** + * Returns the first view in the table where the type is one of `allowedViewTypes`, or `null` if + * no such view exists in the table. + * + * @param allowedViewTypes An array of view types or a single view type to match against. + * @param preferredViewOrViewId If a view or view ID is supplied and that view exists & has the + * correct type, that view will be returned before checking the other views in the table. + * @example + * ```js + * import {ViewType} from '@airtable/blocks/base/models'; + * const firstCalendarView = myTable.getFirstViewOfType(ViewType.CALENDAR); + * if (firstCalendarView !== null) { + * console.log(firstCalendarView.name); + * } else { + * console.log('No calendar views exist in the table'); + * } + * ``` + */ + getFirstViewOfType( + allowedViewTypes: Array | ViewType, + preferredViewOrViewId?: View | ViewId | null, + ): View | null { + if (!Array.isArray(allowedViewTypes)) { + allowedViewTypes = cast>([allowedViewTypes]); + } + + if (preferredViewOrViewId) { + const preferredView = this.getViewByIdIfExists( + typeof preferredViewOrViewId === 'string' + ? preferredViewOrViewId + : preferredViewOrViewId.id, + ); + if (preferredView && allowedViewTypes.includes(preferredView.type)) { + return preferredView; + } + } + + 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); + } + + /** @internal */ + __triggerOnChangeForDirtyPaths(dirtyPaths: ChangedPathsForType): boolean { + let didTableSchemaChange = false; + if (super.__triggerOnChangeForDirtyPaths(dirtyPaths)) { + didTableSchemaChange = true; + } + if (dirtyPaths.viewOrder) { + this._onChange(WatchableTableKeys.views); + didTableSchemaChange = true; + + for (const [viewId, viewModel] of entries(this._viewModelsById)) { + if (viewModel.isDeleted) { + delete this._viewModelsById[viewId]; + } + } + } + if (dirtyPaths.viewsById) { + for (const [viewId, dirtyViewPaths] of entries(dirtyPaths.viewsById)) { + const view = this._viewModelsById[viewId]; + if (view) { + const didViewSchemaChange = view.__triggerOnChangeForDirtyPaths(dirtyViewPaths); + if (didViewSchemaChange) { + didTableSchemaChange = true; + } + } + } + } + + return didTableSchemaChange; + } +} + +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 94% 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..470329d01 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,33 +1,33 @@ /** @module @airtable/blocks/models: RecordQueryResult */ /** */ -import Sdk from '../sdk'; -import {FieldId} from '../types/field'; +import type Sdk from '../sdk'; +import {type FieldId, type RecordId} from '../../shared/types/hyper_ids'; import { has, arrayDifference, - FlowAnyObject, - FlowAnyExistential, - FlowAnyFunction, - ObjectMap, -} from '../private_utils'; -import {invariant, spawnError} from '../error_utils'; -import {VisList, NormalizedGroupLevel} from '../types/airtable_interface'; -import {RecordId} from '../types/record'; -import {GroupLevelData, GroupData} from '../types/view'; + type FlowAnyObject, + type FlowAnyExistential, + type FlowAnyFunction, + type ObjectMap, +} from '../../shared/private_utils'; +import {invariant, spawnError} from '../../shared/error_utils'; +import {type VisList, type NormalizedGroupLevel} from '../types/airtable_interface'; +import {type GroupLevelData, type GroupData} from '../types/view'; import Table, {WatchableTableKeys} from './table'; -import View from './view'; +import type View from './view'; import RecordQueryResult, { - WatchableRecordQueryResultKey, - NormalizedRecordQueryResultOpts, - NormalizedSortConfig, + type WatchableRecordQueryResultKey, + type NormalizedRecordQueryResultOpts, + type NormalizedSortConfig, } from './record_query_result'; import {ModeTypes as RecordColorModeTypes} from './record_coloring'; -import Field from './field'; -import Record from './record'; +import type Field from './field'; +import type Record from './record'; import ObjectPool from './object_pool'; -import RecordStore, {WatchableRecordStoreKeys} from './record_store'; +import type RecordStore from './record_store'; +import {WatchableRecordStoreKeys} from './record_store'; import ViewDataStore, {WatchableViewDataStoreKeys} from './view_data_store'; import GroupedRecordQueryResult from './grouped_record_query_result'; -import {GroupLevels} from './view_metadata_query_result'; +import {type GroupLevels} from './view_metadata_query_result'; /** @hidden */ interface TableOrViewQueryResultData { @@ -204,7 +204,7 @@ class TableOrViewQueryResult extends RecordQueryResult ({ + ? groupLevels.map((singleLevel) => ({ ...singleLevel, field: this.parentTable.getFieldById(singleLevel.fieldId), })) @@ -249,12 +249,12 @@ class TableOrViewQueryResult extends RecordQueryResult { - return this._sorts ? this._sorts.map(sort => `cellValuesInField:${sort.fieldId}`) : []; + return this._sorts ? this._sorts.map((sort) => `cellValuesInField:${sort.fieldId}`) : []; } /** @internal */ get _cellValuesForGroupWatchKeys(): Array { return this._groupLevels - ? this._groupLevels.map(group => `cellValuesInField:${group.fieldId}`) + ? this._groupLevels.map((group) => `cellValuesInField:${group.fieldId}`) : []; } /** @internal */ @@ -444,7 +444,7 @@ class TableOrViewQueryResult extends RecordQueryResult field.id); + this._table.fields.map((field) => field.id); for (const fieldId of fieldIds) { changedKeys.push(RecordQueryResult.WatchableCellValuesInFieldKeyPrefix + fieldId); @@ -633,8 +633,9 @@ class TableOrViewQueryResult extends RecordQueryResult 0, 'field ID set without a corresponding record ID'); const visListRecordIdsSet = new Set(visList.getOrderedRecordIds()); - const recordIdsToMove = recordIds.filter(recordId => visListRecordIdsSet.has(recordId)); + const recordIdsToMove = recordIds.filter((recordId) => visListRecordIdsSet.has(recordId)); visList.removeRecordIds(recordIdsToMove); this._addRecordIdsToVisList(recordIdsToMove); @@ -724,7 +725,7 @@ class TableOrViewQueryResult extends RecordQueryResult sort.fieldId)); + const fieldIdsSet = new Set(this._sorts.map((sort) => sort.fieldId)); let wereAnyFieldsCreatedOrDeleted = false; for (const fieldId of addedFieldIds) { @@ -738,7 +739,7 @@ class TableOrViewQueryResult extends RecordQueryResult + wereAnyFieldsCreatedOrDeleted = removedFieldIds.some((fieldId) => fieldIdsSet.has(fieldId), ); } @@ -791,7 +792,7 @@ class TableOrViewQueryResult extends RecordQueryResult { + return this._sourceModelGroups.map((groupData) => { const group = this.__groupedRecordQueryResultPool.getObjectForReuse( this, groupData, @@ -829,8 +830,8 @@ class TableOrViewQueryResult extends RecordQueryResult record._data); - const fieldDatas = this._table.fields.map(field => field._data); + const recordDatas = this._sourceModelRecords.map((record) => record._data); + const fieldDatas = this._table.fields.map((field) => field._data); const filteredSorts = this._getSortsWithDeletedFieldsFiltered(); this._visList = airtableInterface.createVisList( @@ -844,7 +845,7 @@ class TableOrViewQueryResult extends RecordQueryResult { invariant(this._sorts, 'No sorts'); - return this._sorts.filter(sort => { + return this._sorts.filter((sort) => { const field = this._table.getFieldByIdIfExists(sort.fieldId); return !!field; }); diff --git a/packages/sdk/src/models/view.ts b/packages/sdk/src/base/models/view.ts similarity index 93% rename from packages/sdk/src/models/view.ts rename to packages/sdk/src/base/models/view.ts index ac4698ccd..7cef66f32 100644 --- a/packages/sdk/src/models/view.ts +++ b/packages/sdk/src/base/models/view.ts @@ -1,18 +1,20 @@ /** @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 type Sdk from '../sdk'; +import {type ViewData, type ViewType} from '../types/view'; +import {isEnumValue, type ObjectValues, type FlowAnyObject} from '../../shared/private_utils'; +import {MutationTypes} from '../types/mutations'; +import AbstractModel from '../../shared/models/abstract_model'; +import {type PermissionCheckResult} from '../../shared/types/mutations_core'; +import {type BaseSdkMode} from '../../sdk_mode'; import ObjectPool from './object_pool'; -import Table from './table'; +import type Table from './table'; import RecordQueryResult, { normalizeSortsOrGroups, - RecordQueryResultOpts, - ViewMetadataForUpdate, + type RecordQueryResultOpts, + type ViewMetadataForUpdate, } from './record_query_result'; -import TableOrViewQueryResult from './table_or_view_query_result'; -import ViewDataStore from './view_data_store'; +import type TableOrViewQueryResult from './table_or_view_query_result'; +import type ViewDataStore from './view_data_store'; import ViewMetadataQueryResult from './view_metadata_query_result'; import * as RecordColoring from './record_coloring'; @@ -32,7 +34,7 @@ export type WatchableViewKey = ObjectValues; * * @docsPath models/View */ -class View extends AbstractModel { +class View extends AbstractModel { /** @internal */ static _className = 'View'; /** @internal */ @@ -144,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/models/view_data_store.ts b/packages/sdk/src/base/models/view_data_store.ts similarity index 92% rename from packages/sdk/src/models/view_data_store.ts rename to packages/sdk/src/base/models/view_data_store.ts index 21e9f295a..a37d533e4 100644 --- a/packages/sdk/src/models/view_data_store.ts +++ b/packages/sdk/src/base/models/view_data_store.ts @@ -1,23 +1,22 @@ import { isEnumValue, - ObjectValues, - FlowAnyExistential, - FlowAnyObject, + type ObjectValues, + type FlowAnyExistential, + type FlowAnyObject, has, - ObjectMap, + type ObjectMap, cloneDeep, -} from '../private_utils'; -import {invariant} from '../error_utils'; -import {ModelChange} from '../types/base'; -import Sdk from '../sdk'; -import {FieldId} from '../types/field'; -import {GroupData, GroupLevelData, ViewData, ViewId} from '../types/view'; -import {RecordId} from '../types/record'; -import {AirtableInterface} from '../types/airtable_interface'; -import {Color} from '../colors'; +} from '../../shared/private_utils'; +import {invariant} from '../../shared/error_utils'; +import type Sdk from '../sdk'; +import {type FieldId, type ViewId, type RecordId} from '../../shared/types/hyper_ids'; +import {type GroupData, type GroupLevelData, type ViewData} from '../types/view'; +import {type AirtableInterface} from '../types/airtable_interface'; +import {type Color} from '../../shared/colors'; +import {type ModelChange} from '../../shared/types/base_core'; import AbstractModelWithAsyncData from './abstract_model_with_async_data'; -import RecordStore from './record_store'; -import Record from './record'; +import type RecordStore from './record_store'; +import type Record from './record'; export const WatchableViewDataStoreKeys = Object.freeze({ visibleRecords: 'visibleRecords' as const, @@ -160,7 +159,7 @@ class ViewDataStore extends AbstractModelWithAsyncData !recordIdsToDeleteSet[recordId], + (recordId) => !recordIdsToDeleteSet[recordId], ); const changePayload = [ @@ -203,10 +202,10 @@ class ViewDataStore extends AbstractModelWithAsyncData { + return groups.map((group) => { if (group.visibleRecordIds) { group.visibleRecordIds = group.visibleRecordIds.filter( - id => !recordIdsToDeleteSet[id], + (id) => !recordIdsToDeleteSet[id], ); } this.__recursivelyRemoveRecordsFromGroupsInPlace(group.groups, recordIdsToDeleteSet); @@ -241,7 +240,7 @@ class ViewDataStore extends AbstractModelWithAsyncData { + return visibleRecordIds.map((recordId) => { const record = this.parentRecordStore.getRecordByIdIfExists(recordId); invariant(record, 'Record in view does not exist'); return 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 89% 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..d2a96feb6 100644 --- a/packages/sdk/src/models/view_metadata_query_result.ts +++ b/packages/sdk/src/base/models/view_metadata_query_result.ts @@ -1,13 +1,13 @@ /** @module @airtable/blocks/models: View */ /** */ -import Sdk from '../sdk'; -import {FieldId} from '../types/field'; -import {NormalizedGroupLevel} from '../types/airtable_interface'; -import {invariant} from '../error_utils'; -import {isEnumValue, getLocallyUniqueId, ObjectValues} from '../private_utils'; +import type Sdk from '../sdk'; +import {type FieldId} from '../../shared/types/hyper_ids'; +import {type NormalizedGroupLevel} from '../types/airtable_interface'; +import {invariant} from '../../shared/error_utils'; +import {isEnumValue, getLocallyUniqueId, type ObjectValues} from '../../shared/private_utils'; import AbstractModelWithAsyncData from './abstract_model_with_async_data'; -import ViewDataStore from './view_data_store'; -import View from './view'; -import Field from './field'; +import type ViewDataStore from './view_data_store'; +import type View from './view'; +import type Field from './field'; const WatchableViewMetadataKeys = { allFields: 'allFields' as const, @@ -173,7 +173,7 @@ class ViewMetadataQueryResult extends AbstractModelWithAsyncData< get allFields(): Array { const allFieldIds = this._data.allFieldIds; invariant(allFieldIds, 'view meta data is not loaded'); - return allFieldIds.map(fieldId => this.parentView.parentTable.getFieldById(fieldId)); + return allFieldIds.map((fieldId) => this.parentView.parentTable.getFieldById(fieldId)); } /** @@ -182,7 +182,7 @@ class ViewMetadataQueryResult extends AbstractModelWithAsyncData< get visibleFields(): Array { const visibleFieldIds = this._data.visibleFieldIds; invariant(visibleFieldIds, 'view meta data is not loaded'); - return visibleFieldIds.map(fieldId => this.parentView.parentTable.getFieldById(fieldId)); + return visibleFieldIds.map((fieldId) => this.parentView.parentTable.getFieldById(fieldId)); } /** @@ -193,7 +193,7 @@ class ViewMetadataQueryResult extends AbstractModelWithAsyncData< get groupLevels(): GroupLevels | null { const groupLevels = this._data.groupLevels; return groupLevels - ? groupLevels.map(singleConfig => ({ + ? groupLevels.map((singleConfig) => ({ ...singleConfig, field: this.parentView.parentTable.getFieldById(singleConfig.fieldId), })) diff --git a/packages/sdk/src/perform_record_action.ts b/packages/sdk/src/base/perform_record_action.ts similarity index 93% rename from packages/sdk/src/perform_record_action.ts rename to packages/sdk/src/base/perform_record_action.ts index 07cac1ce8..ac9713c0e 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 {AirtableInterface} from './types/airtable_interface'; -import {RecordActionData, RecordActionDataCallback} from './types/record_action_data'; +import {invariant} from '../shared/error_utils'; +import {isEnumValue, type ObjectValues} from '../shared/private_utils'; +import {type AirtableInterface} from './types/airtable_interface'; +import {type RecordActionData, type 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'; +import type Sdk from './sdk'; /** @hidden */ export const WatchablePerformRecordActionKeys = Object.freeze({ @@ -120,9 +120,10 @@ export class PerformRecordAction extends AbstractModelWithAsyncData< async _loadDataAsync(): Promise<[]> { if (!this._hasRegisteredHandler) { this._hasRegisteredHandler = true; - this.recordActionData = await this._airtableInterface.fetchAndSubscribeToPerformRecordActionAsync( - this._handlePerformRecordAction, - ); + this.recordActionData = + await this._airtableInterface.fetchAndSubscribeToPerformRecordActionAsync( + this._handlePerformRecordAction, + ); } return []; @@ -164,7 +165,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/sdk.ts b/packages/sdk/src/base/sdk.ts similarity index 57% rename from packages/sdk/src/sdk.ts rename to packages/sdk/src/base/sdk.ts index 7da831c23..87c75d5c8 100644 --- a/packages/sdk/src/sdk.ts +++ b/packages/sdk/src/base/sdk.ts @@ -1,45 +1,20 @@ /** @hidden */ /** */ -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 {type ModelChange} from '../shared/types/base_core'; +import {type GlobalConfigUpdate} from '../shared/types/global_config'; +import {BlockSdkCore} from '../shared/sdk_core'; +import {type 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 {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; -} +import {type AirtableInterface, type BlockRunContext} from './types/airtable_interface'; +import {type RequestJson, type ResponseJson} from './types/backend_fetch_types'; /** @hidden */ type UpdateBatcher = (applyUpdates: () => void) => void; @@ -54,51 +29,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 +54,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 +139,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 91% rename from packages/sdk/src/settings_button.ts rename to packages/sdk/src/base/settings_button.ts index 60900bd52..c09b89520 100644 --- a/packages/sdk/src/settings_button.ts +++ b/packages/sdk/src/base/settings_button.ts @@ -1,7 +1,7 @@ /** @module @airtable/blocks: settingsButton */ /** */ -import Watchable from './watchable'; -import {isEnumValue, ObjectValues} from './private_utils'; -import {AirtableInterface} from './types/airtable_interface'; +import Watchable from '../shared/watchable'; +import {isEnumValue, type ObjectValues} from '../shared/private_utils'; +import {type AirtableInterface} from './types/airtable_interface'; const WatchableSettingsButtonKeys = Object.freeze({ isVisible: 'isVisible' as const, @@ -25,8 +25,8 @@ type WatchableSettingsButtonKey = ObjectValues { 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 58% rename from packages/sdk/src/types/airtable_interface.ts rename to packages/sdk/src/base/types/airtable_interface.ts index a451022b3..ab88e4aae 100644 --- a/packages/sdk/src/types/airtable_interface.ts +++ b/packages/sdk/src/base/types/airtable_interface.ts @@ -1,36 +1,29 @@ -import {ObjectMap} from '../private_utils'; -import {NormalizedSortConfig} from '../models/record_query_result'; -import {Stat} from './stat'; -import {AggregatorKey} from './aggregators'; -import {BaseData, BasePermissionData, ModelChange} from './base'; -import {BlockInstallationId} from './block'; -import {CursorData} from './cursor'; -import {FieldData, FieldId, FieldType} from './field'; -import {RecordActionData, RecordActionDataCallback} from './record_action_data'; +import {type NormalizedSortConfig} from '../models/record_query_result'; +import {type TableId, type RecordId, type ViewId} from '../../shared/types/hyper_ids'; import { - GlobalConfigUpdate, - GlobalConfigData, - GlobalConfigPath, - GlobalConfigPathValidationResult, -} from './global_config'; -import {RecordData, RecordId} from './record'; -import {UndoRedoMode} from './undo_redo'; -import {ViewportSizeConstraint} from './viewport'; + type AirtableInterfaceCore, + type AppInterface, + type FieldTypeProviderCore, + type FieldTypeConfig, + type SdkInitDataCore, +} from '../../shared/types/airtable_interface_core'; +import {type BaseSdkMode} from '../../sdk_mode'; +import {type FieldData} from './field'; +import {type RecordData} from './record'; +import {type ViewportSizeConstraint} from './viewport'; +import {type AggregatorKey} from './aggregators'; +import {type BaseData} from './base'; +import {type CursorData} from './cursor'; +import {type RecordActionData, type RecordActionDataCallback} from './record_action_data'; +import {type UndoRedoMode} from './undo_redo'; +import {type UpdateFieldOptionsOpts} from './mutations'; import { - Mutation, - PartialMutation, - PermissionCheckResult, - UpdateFieldOptionsOpts, -} from './mutations'; -import {TableId} from './table'; -import { - GroupData, - ViewColorsByRecordIdData, - ViewFieldOrderData, - ViewId, - GroupLevelData, + type GroupData, + type ViewColorsByRecordIdData, + type ViewFieldOrderData, + type GroupLevelData, } from './view'; -import {RequestJson, ResponseJson} from './backend_fetch_types'; +import {type RequestJson, type ResponseJson} from './backend_fetch_types'; /** @hidden */ export enum BlockRunContextType { @@ -53,6 +46,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 +91,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 +144,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; @@ -222,21 +153,16 @@ export interface VisList { /** * 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. + * Block SDK and Airtable. * * @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 +187,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 +205,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 +221,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..b49a97b0b --- /dev/null +++ b/packages/sdk/src/base/types/base.ts @@ -0,0 +1,13 @@ +import {type BaseDataCore, type BasePermissionDataCore} from '../../shared/types/base_core'; +import {type TableId} from '../../shared/types/hyper_ids'; +import {type TableData, type TablePermissionData} from './table'; +import {type CursorData} from './cursor'; + +/** @hidden */ +export interface BaseData extends BaseDataCore { + activeTableId: TableId | null; + cursorData: CursorData | null; +} + +/** @hidden */ +export interface BasePermissionData extends BasePermissionDataCore {} diff --git a/packages/sdk/src/types/cursor.ts b/packages/sdk/src/base/types/cursor.ts similarity index 53% rename from packages/sdk/src/types/cursor.ts rename to packages/sdk/src/base/types/cursor.ts index aed9771f5..872e8d5d6 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 {type ObjectMap} from '../../shared/private_utils'; +import {type FieldId, type RecordId} from '../../shared/types/hyper_ids'; /** @hidden */ export interface CursorData { diff --git a/packages/sdk/src/base/types/field.ts b/packages/sdk/src/base/types/field.ts new file mode 100644 index 000000000..7e05e4215 --- /dev/null +++ b/packages/sdk/src/base/types/field.ts @@ -0,0 +1,7 @@ +import {type FieldDataCore, type FieldPermissionDataCore} from '../../shared/types/field_core'; + +/** @hidden */ +export interface FieldData extends FieldDataCore {} + +/** @hidden */ +export interface FieldPermissionData extends FieldPermissionDataCore {} diff --git a/packages/sdk/src/types/mutations.ts b/packages/sdk/src/base/types/mutations.ts similarity index 57% rename from packages/sdk/src/types/mutations.ts rename to packages/sdk/src/base/types/mutations.ts index 579e86030..2705e70a5 100644 --- a/packages/sdk/src/types/mutations.ts +++ b/packages/sdk/src/base/types/mutations.ts @@ -1,18 +1,17 @@ /** @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 {type ObjectValues} from '../../shared/private_utils'; +import {type TableId, type FieldId, type ViewId} from '../../shared/types/hyper_ids'; +import { + MutationTypesCore, + type MutationCore, + type PartialMutationCore, +} from '../../shared/types/mutations_core'; +import {type FieldTypeConfig} from '../../shared/types/airtable_interface_core'; +import {type NormalizedViewMetadata} from './airtable_interface'; /** @hidden */ export const MutationTypes = Object.freeze({ - 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, + ...MutationTypesCore, CREATE_SINGLE_FIELD: 'createSingleField' as const, UPDATE_SINGLE_FIELD_CONFIG: 'updateSingleFieldConfig' as const, UPDATE_SINGLE_FIELD_DESCRIPTION: 'updateSingleFieldDescription' as const, @@ -24,115 +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 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 +202,7 @@ export interface PartialUpdateViewMetadataMutation { /** @hidden */ export type Mutation = - | SetMultipleRecordsCellValuesMutation - | DeleteMultipleRecordsMutation - | CreateMultipleRecordsMutation - | SetMultipleGlobalConfigPathsMutation + | MutationCore | CreateSingleFieldMutation | UpdateSingleFieldConfigMutation | UpdateSingleFieldDescriptionMutation @@ -325,36 +212,10 @@ export type Mutation = /** @hidden */ export type PartialMutation = - | PartialSetMultipleRecordsCellValuesMutation - | PartialDeleteMultipleRecordsMutation - | PartialCreateMultipleRecordsMutation - | PartialSetMultipleGlobalConfigPathsMutation + | PartialMutationCore | 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..9aa4a60ec --- /dev/null +++ b/packages/sdk/src/base/types/record.ts @@ -0,0 +1,7 @@ +/** @module @airtable/blocks/models: Record */ /** */ +import {type 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..26d072b13 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 {type TableId, type RecordId, type 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..92b7e179e --- /dev/null +++ b/packages/sdk/src/base/types/table.ts @@ -0,0 +1,20 @@ +import {type TableDataCore, type TablePermissionDataCore} from '../../shared/types/table_core'; +import {type FieldId, type RecordId, type ViewId} from '../../shared/types/hyper_ids'; +import {type ObjectMap} from '../../shared/private_utils'; +import {type ViewData} from './view'; +import {type RecordData} from './record'; +import {type FieldData, type 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/types/undo_redo.ts b/packages/sdk/src/base/types/undo_redo.ts similarity index 74% rename from packages/sdk/src/types/undo_redo.ts rename to packages/sdk/src/base/types/undo_redo.ts index 95db6b1a2..467abbedb 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 {type 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 85% rename from packages/sdk/src/types/view.ts rename to packages/sdk/src/base/types/view.ts index c3006097c..6944ecfc1 100644 --- a/packages/sdk/src/types/view.ts +++ b/packages/sdk/src/base/types/view.ts @@ -1,18 +1,14 @@ /** @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 {type ObjectMap} from '../../shared/private_utils'; +import {type Color} from '../../shared/colors'; +import {type FieldId, type RecordId, type ViewId} from '../../shared/types/hyper_ids'; /** * An enum of Airtable's view types * * @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/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 86% rename from packages/sdk/src/ui/base_provider.tsx rename to packages/sdk/src/base/ui/base_provider.tsx index 849c3e302..0f6bc0089 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 type Base from '../models/base'; +import {SdkContext} from '../../shared/ui/sdk_context'; /** * Props for the {@link BaseProvider} component. @@ -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/block_wrapper.tsx b/packages/sdk/src/base/ui/block_wrapper.tsx new file mode 100644 index 000000000..258316b37 --- /dev/null +++ b/packages/sdk/src/base/ui/block_wrapper.tsx @@ -0,0 +1,168 @@ +/** @hidden */ /** */ +import * as React from 'react'; +import type Sdk from '../sdk'; +import {SdkContext} from '../../shared/ui/sdk_context'; +import Loader from '../../shared/ui/loader'; +import {type GlobalAlert, globalAlert, useWatchable} from './ui'; +import { + getCssContentToAddToHead, + BUTTON_LINK_CLASS_NAME, + SPIN_SCALE_ANIMATION_NAME, +} from './global_css_helpers'; + +interface BlockWrapperProps { + sdk: Sdk; + children: React.ReactNode; +} + +const suspenseFallbackStyle: React.CSSProperties = { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}; + +const wrapperStyle: React.CSSProperties = { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + overflow: 'hidden', +}; + +const fullscreenMessageStyle: React.CSSProperties = { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: '1rem', + backgroundColor: '#ffffff', + zIndex: 2147483647, +}; + +const animateSpinnerStyle: React.CSSProperties = { + animationIterationCount: 'infinite', + animationName: SPIN_SCALE_ANIMATION_NAME, + animationDuration: '1800ms', + animationTimingFunction: 'cubic-bezier(0.785, 0.135, 0.15, 0.86)', +}; + +export default function BlockWrapper(props: BlockWrapperProps) { + const {sdk, children} = props; + + const [globalAlertInfoIfExists, setGlobalAlertInfoIfExists] = React.useState< + GlobalAlert['__alertInfo'] | null + >(null); + React.useEffect(() => { + if (globalAlertInfoIfExists) { + return () => {}; + } + + if (globalAlert.__alertInfo) { + setGlobalAlertInfoIfExists(globalAlert.__alertInfo); + } + + const onAlertInfoChange = (a: GlobalAlert) => setGlobalAlertInfoIfExists(a.__alertInfo); + globalAlert.watch('__alertInfo', onAlertInfoChange); + return () => { + globalAlert.unwatch('__alertInfo', onAlertInfoChange); + }; + }, [globalAlertInfoIfExists]); + + const viewport = sdk.viewport; + useWatchable(viewport, ['size', 'minSize']); + const minSizeBeforeFirstRender = React.useRef<{ + width: number | null; + height: number | null; + } | null>(viewport.minSize); + const [, forceUpdate] = React.useReducer((x) => x + 1, 0); + React.useEffect(() => { + if (minSizeBeforeFirstRender.current) { + const prevMinSize = minSizeBeforeFirstRender.current; + minSizeBeforeFirstRender.current = null; + const currentMinSize = viewport.minSize; + if ( + currentMinSize.width !== prevMinSize.width || + currentMinSize.height !== prevMinSize.height + ) { + forceUpdate(); + } + } + }, [viewport]); + + React.useLayoutEffect(() => { + const styleElement = document.createElement('style'); + styleElement.textContent = getCssContentToAddToHead(); + document.head.appendChild(styleElement); + + return () => { + if (document.head.contains(styleElement)) { + document.head.removeChild(styleElement); + } + }; + }, []); + + if (globalAlertInfoIfExists) { + return ( + +
{globalAlertInfoIfExists.content}
+
+ ); + } + + return ( + + + + + } + > +
+ {children} +
+
+ + {/* + TODO: if a modal is presented after we show this viewport + message, it will cover the message. We should fix this by + having this component manage the modal stack, so it can + guarantee that this viewport message is in front of all modals. + */} + {viewport.isSmallerThanMinSize && } +
+ ); +} + +function MakeBiggerOrFullscreenMessage({viewport}: {viewport: Sdk['viewport']}) { + return ( +
+ + Please make this extension bigger or + viewport.enterFullscreenIfPossible()} + > + fullscreen + + +
+ ); +} diff --git a/packages/sdk/src/base/ui/cell_renderer.tsx b/packages/sdk/src/base/ui/cell_renderer.tsx new file mode 100644 index 000000000..0583f9f85 --- /dev/null +++ b/packages/sdk/src/base/ui/cell_renderer.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import {CellRenderer as CellRendererSharedImpl} from '../../shared/ui/cell_renderer'; +import {type BaseSdkMode} from '../../sdk_mode'; +import type Record from '../models/record'; +import type Field from '../models/field'; + +/** + * Props for the {@link CellRenderer} component. + * + * @docsPath UI/components/CellRenderer + * @noInheritDoc + */ +interface CellRendererProps { + /** The {@link Record} from which to render a cell. Either `record` or `cellValue` must be provided to the CellRenderer. If both are provided, `record` will be used. */ + record?: Record | null | undefined; + /** The cell value to render. Either `record` or `cellValue` must be provided to the CellRenderer. If both are provided, `record` will be used. */ + cellValue?: unknown; + /** The {@link Field} for a given {@link Record} being rendered as a cell. */ + field: Field; + /** Whether to wrap cell contents. Defaults to true. */ + shouldWrap?: boolean; + /** Additional class names to apply to the cell renderer container, separated by spaces. */ + className?: string; + /** Additional styles to apply to the cell renderer container. */ + style?: React.CSSProperties; + /** Additional class names to apply to the cell itself, separated by spaces. */ + cellClassName?: string; + /** Additional styles to apply to the cell itself. */ + cellStyle?: React.CSSProperties; + /** Render function if provided and validation fails. */ + renderInvalidCellValue?: (cellValue: unknown, field: Field) => React.ReactElement; +} + +/** + * Displays the contents of a cell given a field and record. + * + * @component + * @docsPath UI/components/CellRenderer + */ +export default function CellRenderer(props: CellRendererProps) { + return {...props} />; +} diff --git a/packages/sdk/src/ui/expand_record.ts b/packages/sdk/src/base/ui/expand_record.ts similarity index 85% rename from packages/sdk/src/ui/expand_record.ts rename to packages/sdk/src/base/ui/expand_record.ts index d4ca78444..c31bda377 100644 --- a/packages/sdk/src/ui/expand_record.ts +++ b/packages/sdk/src/base/ui/expand_record.ts @@ -1,5 +1,5 @@ /** @module @airtable/blocks/ui: expandRecord */ /** */ -import Record from '../models/record'; +import type Record from '../models/record'; /** * Options object for expanding a record. @@ -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], * }); @@ -28,7 +28,7 @@ function expandRecord(record: Record, opts?: ExpandRecordOpts) { let recordIds = null; if (opts && opts.records) { - recordIds = opts.records.map(r => r.id); + recordIds = opts.records.map((r) => r.id); } record.parentTable.parentBase.__sdk.__airtableInterface.expandRecord( diff --git a/packages/sdk/src/ui/expand_record_list.ts b/packages/sdk/src/base/ui/expand_record_list.ts similarity index 83% rename from packages/sdk/src/ui/expand_record_list.ts rename to packages/sdk/src/base/ui/expand_record_list.ts index 25e2c4bc2..a135ab0b0 100644 --- a/packages/sdk/src/ui/expand_record_list.ts +++ b/packages/sdk/src/base/ui/expand_record_list.ts @@ -1,7 +1,7 @@ /** @module @airtable/blocks/ui: expandRecordList */ /** */ -import {invariant} from '../error_utils'; -import Record from '../models/record'; -import Field from '../models/field'; +import {invariant} from '../../shared/error_utils'; +import type Record from '../models/record'; +import type Field from '../models/field'; /** * Options object for expanding a record list. @@ -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], { @@ -35,7 +35,7 @@ function expandRecordList(records: Array, opts?: ExpandRecordListOpts) { const tableId = records[0].parentTable.id; - const recordIds = records.map(record => { + const recordIds = records.map((record) => { invariant(record.parentTable.id === tableId, 'all records must belong to the same table'); return record.id; @@ -43,7 +43,7 @@ function expandRecordList(records: Array, opts?: ExpandRecordListOpts) { const fieldIds = opts && opts.fields - ? opts.fields.map(field => { + ? opts.fields.map((field) => { invariant( field.parentTable.id === tableId, 'all fields must belong to the same table', 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 90% 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..64a97bb10 100644 --- a/packages/sdk/src/ui/expand_record_picker_async.ts +++ b/packages/sdk/src/base/ui/expand_record_picker_async.ts @@ -1,7 +1,7 @@ /** @module @airtable/blocks/ui: expandRecordPickerAsync */ /** */ -import {invariant} from '../error_utils'; -import Record from '../models/record'; -import Field from '../models/field'; +import {invariant} from '../../shared/error_utils'; +import type Record from '../models/record'; +import type Field from '../models/field'; /** * Options object for expanding a record picker. @@ -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]); @@ -55,7 +55,7 @@ async function expandRecordPickerAsync( const tableId = records[0].parentTable.id; const sdk = records[0].parentTable.parentBase.__sdk; - const recordIds = records.map(record => { + const recordIds = records.map((record) => { invariant(record.parentTable.id === tableId, 'all records must belong to the same table'); return record.id; @@ -63,7 +63,7 @@ async function expandRecordPickerAsync( const fieldIds = opts && opts.fields - ? opts.fields.map(field => { + ? opts.fields.map((field) => { invariant( field.parentTable.id === tableId, 'all fields must belong to the same table', diff --git a/packages/sdk/src/ui/global_alert.tsx b/packages/sdk/src/base/ui/global_alert.tsx similarity index 77% rename from packages/sdk/src/ui/global_alert.tsx rename to packages/sdk/src/base/ui/global_alert.tsx index 722e4e143..59cf68ba7 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 {baymax} from './baymax_utils'; -import {useSdk} from './sdk_context'; +import {isEnumValue, type ObjectValues} from '../../shared/private_utils'; +import Watchable from '../../shared/watchable'; +import {useSdk} from '../../shared/ui/sdk_context'; +import {BUTTON_LINK_CLASS_NAME} from './global_css_helpers'; const WatchableGlobalAlertKeys = Object.freeze({ __alertInfo: '__alertInfo' as const, @@ -20,9 +20,9 @@ const GlobalAlertInfo = () => { const sdk = useSdk(); return ( - + { sdk.reload(); }} @@ -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/global_css_helpers.ts b/packages/sdk/src/base/ui/global_css_helpers.ts new file mode 100644 index 000000000..87faed7ed --- /dev/null +++ b/packages/sdk/src/base/ui/global_css_helpers.ts @@ -0,0 +1,31 @@ +export const SPIN_SCALE_ANIMATION_NAME = `spinScale_${Math.random().toString(36).substring(2, 11)}`; +export const BUTTON_LINK_CLASS_NAME = `buttonLink_${Math.random().toString(36).substring(2, 11)}`; + +const spinScaleKeyframesStyle = ` + @keyframes ${SPIN_SCALE_ANIMATION_NAME} { + 0% { + transform: rotate(0) scale(1); + } + 50% { + transform: rotate(360deg) scale(0.9); + } + 100% { + transform: rotate(720deg) scale(1); + } + } +`; + +const buttonLinkStyle = ` + .${BUTTON_LINK_CLASS_NAME} { + cursor: pointer; + padding-bottom: 0.14rem; + border-bottom: 2px solid hsla(0, 0%, 0%, 0.1); + } + .${BUTTON_LINK_CLASS_NAME}:hover { + opacity: 1; + } +`; + +export const getCssContentToAddToHead = () => { + return `${spinScaleKeyframesStyle}\n${buttonLinkStyle}`; +}; diff --git a/packages/sdk/src/ui/initialize_block.tsx b/packages/sdk/src/base/ui/initialize_block.tsx similarity index 88% rename from packages/sdk/src/ui/initialize_block.tsx rename to packages/sdk/src/base/ui/initialize_block.tsx index e7e3a2c4b..9e831f111 100644 --- a/packages/sdk/src/ui/initialize_block.tsx +++ b/packages/sdk/src/base/ui/initialize_block.tsx @@ -1,12 +1,12 @@ /** @module @airtable/blocks/ui: initializeBlock */ /** */ import * as React from 'react'; -import ReactDOM from 'react-dom'; -import {spawnError} from '../error_utils'; -import Sdk from '../sdk'; -import getAirtableInterface from '../injected/airtable_interface'; +import {createRoot} from 'react-dom/client'; +import {spawnError} from '../../shared/error_utils'; +import type Sdk from '../sdk'; +import getAirtableInterface from '../../injected/airtable_interface'; import {BlockRunContextType} from '../types/airtable_interface'; -import Table from '../models/table'; -import View from '../models/view'; +import type Table from '../models/table'; +import type View from '../models/view'; import BlockWrapper from './block_wrapper'; let hasBeenInitialized = false; @@ -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,12 @@ export function initializeBlock(getEntryElement: DashboardOrEntryPoints) { ); } - sdk.__setBatchedUpdatesFn(ReactDOM.unstable_batchedUpdates); - const container = document.createElement('div'); + container.style.height = '100%'; + container.style.width = '100%'; body.appendChild(container); - ReactDOM.render({entryElement}, container); + + createRoot(container).render({entryElement}); } let sdk: Sdk; diff --git a/packages/sdk/src/base/ui/ui.ts b/packages/sdk/src/base/ui/ui.ts new file mode 100644 index 000000000..69d354081 --- /dev/null +++ b/packages/sdk/src/base/ui/ui.ts @@ -0,0 +1,30 @@ +/** @ignore */ +import '../assert_run_context'; + +import GlobalAlert from './global_alert'; +import '..'; + +export {default as BaseProvider} from './base_provider'; +export {initializeBlock} from './initialize_block'; +export {default as CellRenderer} from './cell_renderer'; +export {default as expandRecord} from './expand_record'; +export {default as expandRecordList} from './expand_record_list'; +export {default as expandRecordPickerAsync} from './expand_record_picker_async'; +export {default as GlobalAlert} from './global_alert'; +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'; +export {default as useSession} from './use_session'; +export {default as useSettingsButton} from './use_settings_button'; +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 '../../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'; +export * from './unstable_standalone_ui'; + +export const globalAlert = new GlobalAlert(); diff --git a/packages/sdk/src/base/ui/unstable_standalone_ui.ts b/packages/sdk/src/base/ui/unstable_standalone_ui.ts new file mode 100644 index 000000000..fdc4f5062 --- /dev/null +++ b/packages/sdk/src/base/ui/unstable_standalone_ui.ts @@ -0,0 +1,7 @@ +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/ui/use_base.ts b/packages/sdk/src/base/ui/use_base.ts similarity index 75% rename from packages/sdk/src/ui/use_base.ts rename to packages/sdk/src/base/ui/use_base.ts index 9544215bb..ed118d051 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 {type 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} @@ -17,7 +16,7 @@ import {useSdk} from './sdk_context'; * * @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() { @@ -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 78% rename from packages/sdk/src/ui/use_cursor.ts rename to packages/sdk/src/base/ui/use_cursor.ts index 6a89a5f21..a2d311eb4 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 type Cursor from '../models/cursor'; +import useWatchable from '../../shared/ui/use_watchable'; +import {useSdk} from '../../shared/ui/sdk_context'; +import {type BaseSdkMode} from '../../sdk_mode'; /** * A hook for connecting a React component to your base's cursor. This returns a {@link Cursor} @@ -16,7 +17,7 @@ import {useSdk} from './sdk_context'; * * @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() { @@ -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_loadable.ts b/packages/sdk/src/base/ui/use_loadable.ts similarity index 87% rename from packages/sdk/src/ui/use_loadable.ts rename to packages/sdk/src/base/ui/use_loadable.ts index 767693b88..b263048c4 100644 --- a/packages/sdk/src/ui/use_loadable.ts +++ b/packages/sdk/src/base/ui/use_loadable.ts @@ -1,9 +1,8 @@ /** @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 {useMemo, useEffect, useSyncExternalStore} from 'react'; +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 +58,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 +75,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 +88,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(); @@ -127,10 +126,10 @@ export default function useLoadable( return compacted; }, [constModels]); - const areAllModelsLoaded = compactModels.every(model => model.isDataLoaded); + const areAllModelsLoaded = compactModels.every((model) => model.isDataLoaded); if (shouldSuspend && !areAllModelsLoaded) { - const suspensePromise = Promise.all(compactModels.map(model => model.loadDataAsync())) + const suspensePromise = Promise.all(compactModels.map((model) => model.loadDataAsync())) .then(() => { setTimeout(() => { for (const model of compactModels) { @@ -138,7 +137,7 @@ export default function useLoadable( } }, SUSPENSE_CLEAN_UP_MS); }) - .catch(error => { + .catch((error) => { // eslint-disable-next-line no-console console.error(error); throw error; @@ -149,7 +148,7 @@ export default function useLoadable( const modelIsLoadedSubscription = useMemo( () => ({ - getCurrentValue: () => compactModels.map(model => model.isDataLoaded).join(','), + getCurrentValue: () => compactModels.map((model) => model.isDataLoaded).join(','), subscribe: (onChange: () => void) => { for (const model of compactModels) { model.watch('isDataLoaded', onChange); @@ -163,7 +162,10 @@ export default function useLoadable( }), [compactModels], ); - useSubscription(modelIsLoadedSubscription); + useSyncExternalStore( + modelIsLoadedSubscription.subscribe, + modelIsLoadedSubscription.getCurrentValue, + ); useEffect(() => { for (const model of compactModels) { 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 88% 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..4c30255dd 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 {type RecordActionData} from '../types/record_action_data'; import {WatchablePerformRecordActionKeys} from '../perform_record_action'; +import useWatchable from '../../shared/ui/use_watchable'; +import {useSdk} from '../../shared/ui/sdk_context'; +import {type BaseSdkMode} from '../../sdk_mode'; import useLoadable from './use_loadable'; -import useWatchable from './use_watchable'; -import {useSdk} from './sdk_context'; /** * A hook to watch "open extension" / "perform record action" events (from button field). Returns @@ -33,7 +34,7 @@ import {useSdk} from './sdk_context'; * @example * ```js * import React from 'react'; - * import {useRecordActionData} from '@airtable/blocks/ui'; + * import {useRecordActionData} from '@airtable/blocks/base/ui'; * * function LatestRecordAction() { * const recordActionData = useRecordActionData(); @@ -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 91% rename from packages/sdk/src/ui/use_records.ts rename to packages/sdk/src/base/ui/use_records.ts index 065e5c1a8..d7cb6bc88 100644 --- a/packages/sdk/src/ui/use_records.ts +++ b/packages/sdk/src/base/ui/use_records.ts @@ -1,19 +1,19 @@ /** @module @airtable/blocks/ui: useRecords */ /** */ -import {spawnError} from '../error_utils'; -import {RecordId} from '../types/record'; +import {spawnError} from '../../shared/error_utils'; +import {type 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'; +import type TableOrViewQueryResult from '../models/table_or_view_query_result'; +import type LinkedRecordsQueryResult from '../models/linked_records_query_result'; import RecordQueryResult, { - RecordQueryResultOpts, - RecordIdQueryResultOpts, - SingleRecordQueryResultOpts, + type RecordQueryResultOpts, + type RecordIdQueryResultOpts, + type SingleRecordQueryResultOpts, } from '../models/record_query_result'; -import Record from '../models/record'; +import type Record from '../models/record'; import * as RecordColoring from '../models/record_coloring'; import View from '../models/view'; +import useWatchable from '../../shared/ui/use_watchable'; import useLoadable from './use_loadable'; -import useWatchable from './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, @@ -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(); @@ -108,7 +108,7 @@ export function useRecordIds( } : opts; - const queryResult = _useUnwatchedRecordQueryResult( + const queryResult = useUnwatchedRecordQueryResult_( tableOrViewOrQueryResult, 'useRecordIds', generatedOpts, @@ -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(); @@ -202,7 +202,7 @@ export function useRecords( tableOrViewOrQueryResult: TableOrViewOrQueryResult | null, opts?: RecordQueryResultOpts, ): Array | null { - const queryResult = _useUnwatchedRecordQueryResult( + const queryResult = useUnwatchedRecordQueryResult_( tableOrViewOrQueryResult, 'useRecords', opts, @@ -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}) { @@ -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 77% rename from packages/sdk/src/ui/use_session.ts rename to packages/sdk/src/base/ui/use_session.ts index c398a99e2..367c5423c 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 {type 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} @@ -14,7 +13,7 @@ import {useSdk} from './sdk_context'; * * @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. @@ -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 78% rename from packages/sdk/src/ui/use_settings_button.ts rename to packages/sdk/src/base/ui/use_settings_button.ts index 32636d936..233a552eb 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 {type FlowAnyFunction} from '../../shared/private_utils'; +import useWatchable from '../../shared/ui/use_watchable'; +import {useSdk} from '../../shared/ui/sdk_context'; +import {type BaseSdkMode} from '../../sdk_mode'; /** * A hook for using the settings button that lives outside the extension's viewport. It will show @@ -13,7 +14,7 @@ import {useSdk} from './sdk_context'; * * @example * ```js - * import {useSettingsButton} from '@airtable/blocks/ui'; + * import {useSettingsButton} from '@airtable/blocks/base/ui'; * import {useState} from 'react'; * * function ComponentWithSettings() { @@ -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_view_metadata.ts b/packages/sdk/src/base/ui/use_view_metadata.ts similarity index 88% rename from packages/sdk/src/ui/use_view_metadata.ts rename to packages/sdk/src/base/ui/use_view_metadata.ts index 4c5fc98fd..b37e4d444 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 type ViewMetadataQueryResult from '../models/view_metadata_query_result'; import View from '../models/view'; +import useWatchable from '../../shared/ui/use_watchable'; import useLoadable from './use_loadable'; -import useWatchable from './use_watchable'; /** */ 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/ui/use_viewport.ts b/packages/sdk/src/base/ui/use_viewport.ts similarity index 75% rename from packages/sdk/src/ui/use_viewport.ts rename to packages/sdk/src/base/ui/use_viewport.ts index e3d97c188..0c3ac7051 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 type Viewport from '../viewport'; +import useWatchable from '../../shared/ui/use_watchable'; +import {useSdk} from '../../shared/ui/sdk_context'; +import {type BaseSdkMode} from '../../sdk_mode'; /** * Returns the current {@link Viewport} object and updates whenever the viewport size, constraints, @@ -9,7 +10,7 @@ import {useSdk} from './sdk_context'; * * @example * ```js - * import {useViewport} from '@airtable/blocks/ui'; + * import {useViewport} from '@airtable/blocks/base/ui'; * * function ViewportSize() { * const viewport = useViewport(); @@ -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/undo_redo.ts b/packages/sdk/src/base/undo_redo.ts similarity index 75% rename from packages/sdk/src/undo_redo.ts rename to packages/sdk/src/base/undo_redo.ts index 6eebb3423..b18b21bd5 100644 --- a/packages/sdk/src/undo_redo.ts +++ b/packages/sdk/src/base/undo_redo.ts @@ -1,7 +1,7 @@ -import {values} from './private_utils'; -import {spawnError} from './error_utils'; -import {UndoRedoModes, UndoRedoMode} from './types/undo_redo'; -import {AirtableInterface} from './types/airtable_interface'; +import {values} from '../shared/private_utils'; +import {spawnError} from '../shared/error_utils'; +import {UndoRedoModes, type UndoRedoMode} from './types/undo_redo'; +import {type AirtableInterface} from './types/airtable_interface'; /** @hidden */ class UndoRedo { diff --git a/packages/sdk/src/base/unstable_testing_utils.ts b/packages/sdk/src/base/unstable_testing_utils.ts new file mode 100644 index 000000000..d65783784 --- /dev/null +++ b/packages/sdk/src/base/unstable_testing_utils.ts @@ -0,0 +1,37 @@ +export type {ModelChange} from '../shared/types/base_core'; +export type {BaseData} from './types/base'; + +export type {Mutation} from './types/mutations'; +export {MutationTypes} from './types/mutations'; + +export type {AppInterface, GlobalConfigHelpers} from '../shared/types/airtable_interface_core'; +export type { + FieldTypeProvider, + IdGenerator, + SdkInitData, + PartialViewData, +} from './types/airtable_interface'; +export {BlockRunContextType} from './types/airtable_interface'; + +export type {RecordData} from './types/record'; + +export type {CursorData} from './types/cursor'; + +export type {FieldData} from './types/field'; +export {FieldType} from '../shared/types/field_core'; + +export {ViewType} from './types/view'; + +export type {ViewportSizeConstraint} from './types/viewport'; + +export type { + GlobalConfigUpdate, + GlobalConfigData, + GlobalConfigArray, + GlobalConfigObject, +} from '../shared/types/global_config'; + +export type {RequestJson, ResponseJson} from './types/backend_fetch_types'; + +export {default as Sdk} from './sdk'; +export {AbstractMockAirtableInterface} from '../testing/base/abstract_mock_airtable_interface'; diff --git a/packages/sdk/src/viewport.ts b/packages/sdk/src/base/viewport.ts similarity index 96% rename from packages/sdk/src/viewport.ts rename to packages/sdk/src/base/viewport.ts index 5fe8a276d..62dbe3f3a 100644 --- a/packages/sdk/src/viewport.ts +++ b/packages/sdk/src/base/viewport.ts @@ -1,9 +1,15 @@ /** @module @airtable/blocks: viewport */ /** */ -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'; +import Watchable from '../shared/watchable'; +import { + isEnumValue, + debounce, + type ObjectValues, + type FlowAnyFunction, + type FlowAnyObject, +} from '../shared/private_utils'; +import {invariant} from '../shared/error_utils'; +import {type ViewportSizeConstraint} from './types/viewport'; +import {type AirtableInterface} from './types/airtable_interface'; const WatchableViewportKeys = Object.freeze({ isFullscreen: 'isFullscreen' as const, @@ -47,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/index.ts b/packages/sdk/src/index.ts deleted file mode 100644 index 38fdf53af..000000000 --- a/packages/sdk/src/index.ts +++ /dev/null @@ -1,88 +0,0 @@ -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'; - -/** @internal */ -export let __sdk: Sdk; -export let base: Sdk['base']; -export let globalConfig: Sdk['globalConfig']; -export let installationId: Sdk['installationId']; -export let reload: Sdk['reload']; -export let runInfo: Sdk['runInfo']; -export let settingsButton: Sdk['settingsButton']; -export let undoRedo: Sdk['undoRedo']; -export let viewport: Sdk['viewport']; -export let unstable_fetchAsync: Sdk['unstable_fetchAsync']; - -export let cursor: Sdk['cursor']; -Object.defineProperty(module.exports, 'cursor', { - enumerable: true, - get() { - warn( - '`import {cursor} from "@airtable/blocks"` is deprecated. Use `import {useCursor} from "@airtable/blocks/ui"` instead.', - ); - - return __sdk.cursor; - }, -}); - -export let session: Sdk['session']; -Object.defineProperty(module.exports, 'session', { - enumerable: true, - get() { - warn( - '`import {session} from "@airtable/blocks"` is deprecated. Use `import {useSession} from "@airtable/blocks/ui"` instead.', - ); - - return __sdk.session; - }, -}); - -Object.defineProperty(module.exports, 'UI', { - enumerable: true, - get() { - warn( - '`import {UI} from "@airtable/blocks"` is deprecated. Use `import * as UI from "@airtable/blocks/ui/ui"` instead.', - ); - - return require('./ui/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()); - - ({ - base, - globalConfig, - installationId, - reload, - runInfo, - settingsButton, - undoRedo, - viewport, - unstable_fetchAsync, - } = __sdk); - - __injectSdkIntoCreateAggregators(__sdk); - __injectSdkIntoPerformRecordAction(__sdk); - __injectSdkIntoInitializeBlock(__sdk); - __injectSdkIntoWarning(__sdk); -} - -__reset(); diff --git a/packages/sdk/src/injected/airtable_interface.ts b/packages/sdk/src/injected/airtable_interface.ts index 322aae736..e7d2ae9f3 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 {type 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,10 +19,11 @@ const missingAirtableInterfaceErrorMessage = [ ].join(''); /** @hidden */ -export default function getAirtableInterface(): AirtableInterface { - const getAirtableInterfaceAtVersion: - | ((arg1: number) => AirtableInterface) - | void = (window as any).__getAirtableInterfaceAtVersion; +export default function getAirtableInterface< + SdkModeT extends SdkMode, +>(): SdkModeT['AirtableInterfaceT'] { + const getAirtableInterfaceAtVersion: ((arg1: number) => SdkModeT['AirtableInterfaceT']) | void = + (window as any).__getAirtableInterfaceAtVersion; if (!airtableInterface) { if (!getAirtableInterfaceAtVersion) { @@ -32,5 +33,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/assert_run_context.ts b/packages/sdk/src/interface/assert_run_context.ts new file mode 100644 index 000000000..3dc840155 --- /dev/null +++ b/packages/sdk/src/interface/assert_run_context.ts @@ -0,0 +1,11 @@ +import getAirtableInterface from '../injected/airtable_interface'; +import {type InterfaceSdkMode} from '../sdk_mode'; +import {spawnError} from '../shared/error_utils'; +import {BlockRunContextType} from './types/airtable_interface'; + +if ((window as any).__getAirtableInterfaceAtVersion) { + const runContextType = getAirtableInterface().sdkInitData.runContext.type; + if (runContextType !== BlockRunContextType.PAGE_ELEMENT_IN_QUERY_CONTAINER) { + throw spawnError('Unexpected import when running block in base'); + } +} diff --git a/packages/sdk/src/interface/index.ts b/packages/sdk/src/interface/index.ts new file mode 100644 index 000000000..d8611ad2c --- /dev/null +++ b/packages/sdk/src/interface/index.ts @@ -0,0 +1,15 @@ +import {__injectSdkIntoWarning} from '../shared/warning'; +import getAirtableInterface from '../injected/airtable_interface'; +import {type InterfaceSdkMode} from '../sdk_mode'; +import {InterfaceBlockSdk} from './sdk'; +import {__injectSdkIntoInitializeBlock} from './ui/initialize_block'; + +/** @internal */ +export function __reset() { + const __sdk = new InterfaceBlockSdk(getAirtableInterface()); + + __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..cf974ad53 --- /dev/null +++ b/packages/sdk/src/interface/models/base.ts @@ -0,0 +1,42 @@ +import {BaseCore, type ChangedPathsForType, WatchableBaseKeys} from '../../shared/models/base_core'; +import {type InterfaceSdkMode} from '../../sdk_mode'; +import {type TableId} from '../../shared/types/hyper_ids'; +import {type InterfaceBlockSdk} from '../sdk'; +import {type BaseData} from '../types/base'; +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 */ + _getAllTableDataForEditModeConfiguration(): BaseData['allTableDataForEditModeConfiguration'] { + return this._data.allTableDataForEditModeConfiguration; + } + + /** @internal */ + __triggerOnChangeForChangedPaths(changedPaths: ChangedPathsForType): void { + super.__triggerOnChangeForChangedPaths(changedPaths); + + if (changedPaths.allTableDataForEditModeConfiguration) { + this._onChange(WatchableBaseKeys.schema); + } + } +} diff --git a/packages/sdk/src/interface/models/field.ts b/packages/sdk/src/interface/models/field.ts new file mode 100644 index 000000000..e2eef9cf3 --- /dev/null +++ b/packages/sdk/src/interface/models/field.ts @@ -0,0 +1,23 @@ +import {type 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/models.ts b/packages/sdk/src/interface/models/models.ts new file mode 100644 index 000000000..7650867d8 --- /dev/null +++ b/packages/sdk/src/interface/models/models.ts @@ -0,0 +1,10 @@ +/** @ignore */ /** */ +import '../assert_run_context'; + +export type {FieldConfig} from '../../shared/types/field_core'; +export {FieldType} from '../../shared/types/field_core'; +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/models/mutations.ts b/packages/sdk/src/interface/models/mutations.ts new file mode 100644 index 000000000..045635af6 --- /dev/null +++ b/packages/sdk/src/interface/models/mutations.ts @@ -0,0 +1,51 @@ +import {type InterfaceSdkMode} from '../../sdk_mode'; +import {MutationsCore} from '../../shared/models/mutations_core'; +import {type ModelChange} from '../../shared/types/base_core'; +import {type Mutation, MutationTypes} from '../types/mutations'; +import {type RecordData} from '../types/record'; + +/** @hidden */ +export class Mutations extends MutationsCore { + /** @internal */ + _isRecordStoreReadyForMutations(): boolean { + return true; + } + + /** @internal */ + _isFieldAvailableForMutation(): boolean { + return true; + } + + /** @internal */ + _getDefaultRecordProperties(): Partial { + return { + createdTime: new Date().toJSON(), + }; + } + + /** @internal */ + _getOptimisticModelChangesForMutation(mutation: Mutation): Array { + switch (mutation.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 new file mode 100644 index 000000000..10c18b0ee --- /dev/null +++ b/packages/sdk/src/interface/models/record.ts @@ -0,0 +1,60 @@ +import {type InterfaceSdkMode} from '../../sdk_mode'; +import {spawnError} from '../../shared/error_utils'; +import {RecordCore, WatchableRecordKeysCore} from '../../shared/models/record_core'; +import {type ObjectValues} from '../../shared/private_utils'; +import {FieldType} from '../../shared/types/field_core'; +import {type RecordId} from '../../shared/types/hyper_ids'; +import {type Field} from './field'; + +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'; + + /** + * 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<{id: RecordId; name: 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/models/record_store.ts b/packages/sdk/src/interface/models/record_store.ts new file mode 100644 index 000000000..a70a7dfb6 --- /dev/null +++ b/packages/sdk/src/interface/models/record_store.ts @@ -0,0 +1,60 @@ +import {type RecordId} from '../../shared/types/hyper_ids'; +import {type InterfaceSdkMode} from '../../sdk_mode'; +import RecordStoreCore, { + WatchableCellValuesInFieldKeyPrefix, + WatchableRecordStoreKeysCore, +} from '../../shared/models/record_store_core'; +import {type TableData} from '../types/table'; +import {type ChangedPathsForType} from '../../shared/models/base_core'; +import {isEnumValue, type ObjectValues} from '../../shared/private_utils'; +import {type 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; + } + + __onDataDeletion(): void { + } + + 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..a219aa9fb --- /dev/null +++ b/packages/sdk/src/interface/models/session.ts @@ -0,0 +1,23 @@ +import {type 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..942b6e33a --- /dev/null +++ b/packages/sdk/src/interface/models/table.ts @@ -0,0 +1,98 @@ +import {TableCore} from '../../shared/models/table_core'; +import {type InterfaceSdkMode} from '../../sdk_mode'; +import {type FieldId, type RecordId} from '../../shared/types/hyper_ids'; +import {type PermissionCheckResult} from '../../shared/types/mutations_core'; +import {FieldType} from '../../shared/types/field_core'; +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); + } + + /** + * 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; + } + + /** @internal */ + _adjustCellValueForFieldIfNecessary( + field: Field, + cellValue: unknown, + onGenerateIdForNewForeignRecord: (recordId: RecordId) => void, + ): unknown { + if (field.type !== FieldType.MULTIPLE_RECORD_LINKS || !Array.isArray(cellValue)) { + return cellValue; + } + return cellValue.map((item) => { + if (typeof item !== 'object' || item === null) { + return item; + } + + if (!item.id) { + const newForeignRecordId = + this.parentBase.__sdk.__airtableInterface.idGenerator.generateRecordId(); + onGenerateIdForNewForeignRecord(newForeignRecordId); + return { + ...item, + id: newForeignRecordId, + }; + } + return item; + }); + } +} diff --git a/packages/sdk/src/interface/sdk.ts b/packages/sdk/src/interface/sdk.ts new file mode 100644 index 000000000..033599836 --- /dev/null +++ b/packages/sdk/src/interface/sdk.ts @@ -0,0 +1,81 @@ +import {type ModelChange} from '../shared/types/base_core'; +import {type GlobalConfigUpdate} from '../shared/types/global_config'; +import {BlockSdkCore} from '../shared/sdk_core'; +import {type InterfaceSdkMode} from '../sdk_mode'; +import {type AppInterface} from '../shared/types/airtable_interface_core'; +import {Session} from './models/session'; +import {Mutations} from './models/mutations'; +import {Base} from './models/base'; +import { + type BlockInstallationPageElementCustomPropertyForAirtableInterface, + type 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..ebb79164c --- /dev/null +++ b/packages/sdk/src/interface/types/airtable_interface.ts @@ -0,0 +1,93 @@ +import { + type AirtableInterfaceCore, + type SdkInitDataCore, +} from '../../shared/types/airtable_interface_core'; +import {type InterfaceSdkMode} from '../../sdk_mode'; +import {type TableId, type PageId, type FieldId, type RecordId} from '../../shared/types/hyper_ids'; +import {type BaseData} from './base'; + +/** @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: BaseData; +} + +/** @hidden */ +export interface IdGenerator { + generateRecordId(): string; +} + +/** @hidden */ +export enum BlockInstallationPageElementCustomPropertyTypeForAirtableInterface { + BOOLEAN = 'boolean', + STRING = 'string', + ENUM = 'enum', + FIELD_ID = 'fieldId', + TABLE_ID = 'tableId', +} + +/** @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; + } + | { + type: BlockInstallationPageElementCustomPropertyTypeForAirtableInterface.TABLE_ID; + defaultValue?: TableId; + } +); + +/** + * AirtableInterface is designed as the communication interface between the + * Block SDK and Airtable. + * + * @hidden + */ +export interface AirtableInterface extends AirtableInterfaceCore { + idGenerator: IdGenerator; + + expandRecord(tableId: string, recordId: string): void; + fetchForeignRecordsAsync( + tableId: string, + recordId: string, + fieldId: string, + filterString: string, + ): Promise<{records: ReadonlyArray<{id: RecordId; name: string}>}>; + 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..f04252ab9 --- /dev/null +++ b/packages/sdk/src/interface/types/base.ts @@ -0,0 +1,29 @@ +import {type BaseDataCore, type BasePermissionDataCore} from '../../shared/types/base_core'; +import {type ObjectMap} from '../../shared/private_utils'; +import {type PrivateColumnType} from '../../shared/types/field_core'; +import {type FieldId, type TableId} from '../../shared/types/hyper_ids'; +import {type TableData, type TablePermissionData} from './table'; + +/** @hidden */ +export interface BaseData extends BaseDataCore { + allTableDataForEditModeConfiguration?: ObjectMap< + TableId, + { + id: TableId; + name: string; + primaryFieldId: FieldId; + fieldsById: ObjectMap< + FieldId, + { + id: FieldId; + name: string; + type: PrivateColumnType; + typeOptions: {[key: string]: unknown} | null | undefined; + } + >; + } + >; +} + +/** @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..5e91fbd14 --- /dev/null +++ b/packages/sdk/src/interface/types/field.ts @@ -0,0 +1,17 @@ +import {type FieldDataCore, type FieldPermissionDataCore} from '../../shared/types/field_core'; + +/** @hidden */ +export interface FieldData extends FieldDataCore { + /** Set by interfaces properties panel. Does not reflect locks or sync. */ + isEditable: boolean; + /** Only populated for foreign key columns. */ + canCreateNewForeignRecords: boolean | undefined; +} + +/** @hidden */ +export interface FieldPermissionData extends FieldPermissionDataCore { + /** Set by interfaces properties panel. Does not reflect locks or sync. */ + readonly isEditable: boolean; + /** Only populated for foreign key columns. */ + readonly canCreateNewForeignRecords: boolean | undefined; +} diff --git a/packages/sdk/src/interface/types/mutations.ts b/packages/sdk/src/interface/types/mutations.ts new file mode 100644 index 000000000..f26914cd6 --- /dev/null +++ b/packages/sdk/src/interface/types/mutations.ts @@ -0,0 +1,20 @@ +import {type ObjectValues} from '../../shared/private_utils'; +import { + type MutationCore, + MutationTypesCore, + type 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..838f87877 --- /dev/null +++ b/packages/sdk/src/interface/types/record.ts @@ -0,0 +1,4 @@ +import {type 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..e44622069 --- /dev/null +++ b/packages/sdk/src/interface/types/table.ts @@ -0,0 +1,24 @@ +import {type TableDataCore, type TablePermissionDataCore} from '../../shared/types/table_core'; +import {type FieldId, type RecordId} from '../../shared/types/hyper_ids'; +import {type ObjectMap} from '../../shared/private_utils'; +import {type RecordData} from './record'; +import {type FieldData, type 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/interface/ui/block_wrapper.tsx b/packages/sdk/src/interface/ui/block_wrapper.tsx new file mode 100644 index 000000000..e6bce5131 --- /dev/null +++ b/packages/sdk/src/interface/ui/block_wrapper.tsx @@ -0,0 +1,57 @@ +/** @hidden */ /** */ +import * as React from 'react'; +import {type InterfaceBlockSdk} from '../sdk'; +import Loader from '../../shared/ui/loader'; +import {SdkContext} from '../../shared/ui/sdk_context'; +import {getCssContentToAddToHead, SPIN_SCALE_ANIMATION_NAME} from './global_css_helpers'; + +interface BlockWrapperProps { + sdk: InterfaceBlockSdk; + children: React.ReactNode; +} + +const suspenseFallbackStyle: React.CSSProperties = { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}; + +const animateSpinnerStyle: React.CSSProperties = { + animationIterationCount: 'infinite', + animationName: SPIN_SCALE_ANIMATION_NAME, + animationDuration: '1800ms', + animationTimingFunction: 'cubic-bezier(0.785, 0.135, 0.15, 0.86)', +}; + +export const BlockWrapper: React.FC = ({sdk, children}) => { + React.useLayoutEffect(() => { + const styleElement = document.createElement('style'); + styleElement.textContent = getCssContentToAddToHead(); + document.head.appendChild(styleElement); + + return () => { + if (document.head.contains(styleElement)) { + document.head.removeChild(styleElement); + } + }; + }, []); + + return ( + + + + + } + > + {children} + + + ); +}; diff --git a/packages/sdk/src/interface/ui/cell_renderer.tsx b/packages/sdk/src/interface/ui/cell_renderer.tsx new file mode 100644 index 000000000..06c155c12 --- /dev/null +++ b/packages/sdk/src/interface/ui/cell_renderer.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import {CellRenderer as CellRendererSharedImpl} from '../../shared/ui/cell_renderer'; +import {type InterfaceSdkMode} from '../../sdk_mode'; +import {type Record} from '../models/record'; +import {type Field} from '../models/field'; + +/** + * Props for the {@link CellRenderer} component. + * + * @docsPath UI/components/CellRenderer + * @noInheritDoc + */ +interface CellRendererProps { + /** The {@link Record} from which to render a cell. Either `record` or `cellValue` must be provided to the CellRenderer. If both are provided, `record` will be used. */ + record?: Record | null | undefined; + /** The cell value to render. Either `record` or `cellValue` must be provided to the CellRenderer. If both are provided, `record` will be used. */ + cellValue?: unknown; + /** The {@link Field} for a given {@link Record} being rendered as a cell. */ + field: Field; + /** Whether to wrap cell contents. Defaults to true. */ + shouldWrap?: boolean; + /** Additional class names to apply to the cell renderer container, separated by spaces. */ + className?: string; + /** Additional styles to apply to the cell renderer container. */ + style?: React.CSSProperties; + /** Additional class names to apply to the cell itself, separated by spaces. */ + cellClassName?: string; + /** Additional styles to apply to the cell itself. */ + cellStyle?: React.CSSProperties; + /** Render function if provided and validation fails. */ + renderInvalidCellValue?: (cellValue: unknown, field: Field) => React.ReactElement; +} + +/** + * Displays the contents of a cell given a field and record. + * + * @component + * @docsPath UI/components/CellRenderer + */ +export function CellRenderer(props: CellRendererProps) { + return {...props} />; +} 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..e2f354180 --- /dev/null +++ b/packages/sdk/src/interface/ui/expand_record.ts @@ -0,0 +1,22 @@ +/** @module @airtable/blocks/interface/ui: expandRecord */ /** */ +import {type 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'; + * + * + * ``` + * @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/global_css_helpers.ts b/packages/sdk/src/interface/ui/global_css_helpers.ts new file mode 100644 index 000000000..b9bc068a3 --- /dev/null +++ b/packages/sdk/src/interface/ui/global_css_helpers.ts @@ -0,0 +1,19 @@ +export const SPIN_SCALE_ANIMATION_NAME = `spinScale_${Math.random().toString(36).substring(2, 11)}`; + +const spinScaleKeyframesStyle = ` + @keyframes ${SPIN_SCALE_ANIMATION_NAME} { + 0% { + transform: rotate(0) scale(1); + } + 50% { + transform: rotate(360deg) scale(0.9); + } + 100% { + transform: rotate(720deg) scale(1); + } + } +`; + +export const getCssContentToAddToHead = () => { + return `${spinScaleKeyframesStyle}`; +}; 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..a5448a14e --- /dev/null +++ b/packages/sdk/src/interface/ui/initialize_block.tsx @@ -0,0 +1,94 @@ +/** @module @airtable/blocks/ui: initializeBlock */ /** */ +import * as React from 'react'; +import {createRoot} from 'react-dom/client'; +import {spawnError} from '../../shared/error_utils'; +import {type 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 + */ +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'); + container.style.height = '100%'; + container.style.width = '100%'; + body.appendChild(container); + + createRoot(container).render({entryElement}); +} + +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..d5af4f4cf --- /dev/null +++ b/packages/sdk/src/interface/ui/ui.ts @@ -0,0 +1,23 @@ +import '../assert_run_context'; + +import '..'; + +export {CellRenderer} from './cell_renderer'; +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'; +export {useSession} from './use_session'; +export {default as useGlobalConfig} from '../../shared/ui/use_global_config'; +export {default as useSynced} from '../../shared/ui/use_synced'; +export {default as useWatchable} from '../../shared/ui/use_watchable'; +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_base.ts b/packages/sdk/src/interface/ui/use_base.ts new file mode 100644 index 000000000..f81ec9a8a --- /dev/null +++ b/packages/sdk/src/interface/ui/use_base.ts @@ -0,0 +1,38 @@ +/** @module @airtable/blocks/interface/ui: useBase */ /** */ +import {type InterfaceSdkMode} from '../../sdk_mode'; +import useBaseInternal from '../../shared/ui/use_base'; +import {type 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..c6cd299a3 --- /dev/null +++ b/packages/sdk/src/interface/ui/use_custom_properties.ts @@ -0,0 +1,323 @@ +/** @module @airtable/blocks/interface/ui: useCustomProperties */ /** */ +import {useState, useEffect} from 'react'; +import {type InterfaceSdkMode} from '../../sdk_mode'; +import {useSdk} from '../../shared/ui/sdk_context'; +import { + type BlockInstallationPageElementCustomPropertyForAirtableInterface, + BlockInstallationPageElementCustomPropertyTypeForAirtableInterface, +} from '../types/airtable_interface'; +import useGlobalConfig from '../../shared/ui/use_global_config'; +import {type Table} from '../models/table'; +import type GlobalConfig from '../../shared/global_config'; +import {type Base} from '../models/base'; +import {spawnUnknownSwitchCaseError} from '../../shared/error_utils'; +import {type Field} from '../models/field'; +import {type FieldConfig} from '../models/models'; +import {type FieldId} from '../../shared/types/hyper_ids'; +import {type BaseData} from '../types/base'; +import {type FieldDataCore} from '../../shared/types/field_core'; +import {type ObjectMap} from '../../shared/private_utils'; +import {type FieldTypeConfig} from '../../shared/types/airtable_interface_core'; + +/** + * 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; + * shouldFieldBeAllowed?: (field: {id: FieldId; config: FieldConfig}) => boolean; // If not provided, all fields in the table will be shown in the dropdown. + * defaultValue?: Field; + * } + * | { + * type: 'table'; + * defaultValue?: Table; + * } + * ); + * ``` + */ +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; + /** @deprecated Prefer `shouldFieldBeAllowed` instead. If not provided, all visible fields in the table will be shown in the dropdown. */ + possibleValues?: Array; + /** If provided, it will be used to filter the fields in the dropdown. */ + shouldFieldBeAllowed?: (field: {id: FieldId; config: FieldConfig}) => boolean; + defaultValue?: Field; + } + | { + type: 'table'; + defaultValue?: Table; + } +); + +/** + * 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 isNumberField = (field: {id: FieldId, config: FieldConfig}) => field.config.type === FieldType.NUMBER; + * return [ + * {key: 'title', label: 'Title', type: 'string', defaultValue: 'Chart'}, + * {key: 'xAxis', label: 'X-axis', type: 'field', table, shouldFieldBeAllowed: isNumberField}, + * {key: 'yAxis', label: 'Y-axis', type: 'field', table, shouldFieldBeAllowed: isNumberField}, + * {key: 'color', label: 'Color', type: 'enum', possibleValues: [{value: 'red', label: 'Red'}, {value: 'blue', label: 'Blue'}, {value: 'green', label: '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, +): {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((customProperty) => + convertBlockPageElementCustomPropertyToBlockInstallationPageElementCustomPropertyForAirtableInterface( + sdk.base._getAllTableDataForEditModeConfiguration(), + (fieldData, fieldNamesById) => + sdk.__airtableInterface.fieldTypeProvider.getConfig( + sdk.__appInterface, + fieldData, + fieldNamesById, + ), + customProperty, + ), + ); + 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( + allTableDataForEditModeConfiguration: + | BaseData['allTableDataForEditModeConfiguration'] + | undefined, + getFieldConfig: ( + fieldData: Pick, + fieldNamesById: ObjectMap, + ) => FieldTypeConfig, + 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': { + let possibleValues: Array | undefined; + if (!allTableDataForEditModeConfiguration) { + possibleValues = undefined; + } else if (property.possibleValues) { + possibleValues = property.possibleValues.map((field) => field.id); + } else if (property.shouldFieldBeAllowed) { + const shouldFieldBeAllowed = property.shouldFieldBeAllowed; + const tableData = allTableDataForEditModeConfiguration[property.table.id]; + if (!tableData) { + possibleValues = []; + } else { + const fieldNamesById = Object.fromEntries( + Object.entries(tableData.fieldsById).map(([id, field]) => [id, field.name]), + ); + possibleValues = Object.values(tableData.fieldsById) + .filter((field) => + shouldFieldBeAllowed({ + id: field.id, + config: getFieldConfig(field, fieldNamesById) as FieldConfig, + }), + ) + .map((field) => field.id); + } + } else { + possibleValues = undefined; + } + return { + key: property.key, + label: property.label, + type: BlockInstallationPageElementCustomPropertyTypeForAirtableInterface.FIELD_ID, + tableId: property.table.id, + possibleValues, + defaultValue: property.defaultValue?.id, + }; + } + case 'table': + return { + key: property.key, + label: property.label, + type: BlockInstallationPageElementCustomPropertyTypeForAirtableInterface.TABLE_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) { + if (property.possibleValues) { + if (property.possibleValues.includes(fieldModel)) { + return fieldModel; + } + return defaultValue; + } else if (property.shouldFieldBeAllowed) { + if ( + property.shouldFieldBeAllowed({ + id: fieldModel.id, + config: { + type: fieldModel.type, + options: fieldModel.options, + } as FieldConfig, + }) + ) { + return fieldModel; + } + return defaultValue; + } else { + return fieldModel; + } + } + } + return defaultValue; + } + case 'table': { + if (typeof rawValue === 'string') { + const tableIfExists = base.getTableByIdIfExists(rawValue); + if (tableIfExists) { + return tableIfExists; + } + } + 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..2a3b06e2e --- /dev/null +++ b/packages/sdk/src/interface/ui/use_records.ts @@ -0,0 +1,50 @@ +/** @module @airtable/blocks/interface/ui: useRecords */ /** */ +import {type InterfaceSdkMode} from '../../sdk_mode'; +import {useSdk} from '../../shared/ui/sdk_context'; +import useWatchable from '../../shared/ui/use_watchable'; +import {type Record} from '../models/record'; +import {type 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..28855ea39 --- /dev/null +++ b/packages/sdk/src/interface/ui/use_run_info.ts @@ -0,0 +1,37 @@ +/** @module @airtable/blocks/interface/ui: useRunInfo */ /** */ +import {type 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..c85fe89c5 --- /dev/null +++ b/packages/sdk/src/interface/ui/use_session.ts @@ -0,0 +1,35 @@ +/** @module @airtable/blocks/ui: useSession */ /** */ +import {type InterfaceSdkMode} from '../../sdk_mode'; +import useSessionInternal from '../../shared/ui/use_session'; +import {type 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/models/mutations.ts b/packages/sdk/src/models/mutations.ts deleted file mode 100644 index 511ae147e..000000000 --- a/packages/sdk/src/models/mutations.ts +++ /dev/null @@ -1,672 +0,0 @@ -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 { - 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, - ); - } - } - - /** @internal */ - _doesMutationExceedBatchSizeLimit(mutation: Mutation) { - 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; - } - } - - /** @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) { - - const appInterface = this._sdk.__appInterface; - 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.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: { - return; - } - - case MutationTypes.CREATE_SINGLE_FIELD: { - const {tableId, name, config, description} = mutation; - const table = this._base.getTableByIdIfExists(tableId); - if (!table) { - throw spawnError("Can't create field: No table with id %s exists", tableId); - } - - if (table.fields.length >= MAX_NUM_FIELDS_PER_TABLE) { - throw spawnError( - "Can't create field: table already has the maximum of %s fields", - MAX_NUM_FIELDS_PER_TABLE, - ); - } - - this._assertFieldNameIsValidForMutation(name, table); - - const validationResult = this._airtableInterface.fieldTypeProvider.validateConfigForUpdate( - appInterface, - config, - null, - null, - billingPlanGrouping, - ); - - if (!validationResult.isValid) { - throw spawnError( - "Can't create field: invalid field config.\n%s", - validationResult.reason, - ); - } - - if (description && description.length > MAX_FIELD_DESCRIPTION_LENGTH) { - throw spawnError( - "Can't create field: description exceeds maximum length of %s characters", - MAX_FIELD_DESCRIPTION_LENGTH, - ); - } - return; - } - - case MutationTypes.UPDATE_SINGLE_FIELD_CONFIG: { - const {tableId, id, config, opts} = mutation; - const table = this._base.getTableByIdIfExists(tableId); - if (!table) { - throw spawnError("Can't update field: No table with id %s exists", tableId); - } - - const field = table.getFieldByIdIfExists(id); - if (!field) { - throw spawnError("Can't update field: No field with id %s exists", id); - } - - const currentConfig = this._airtableInterface.fieldTypeProvider.getConfig( - appInterface, - field._data, - field.parentTable.__getFieldNamesById(), - ); - const validationResult = this._airtableInterface.fieldTypeProvider.validateConfigForUpdate( - appInterface, - config, - currentConfig, - field._data, - billingPlanGrouping, - opts, - ); - - if (!validationResult.isValid) { - throw spawnError( - "Can't update field: invalid field config.\n%s", - validationResult.reason, - ); - } - return; - } - - case MutationTypes.UPDATE_SINGLE_FIELD_DESCRIPTION: { - const {tableId, id, description} = mutation; - const table = this._base.getTableByIdIfExists(tableId); - if (!table) { - throw spawnError("Can't update field: No table with id %s exists", tableId); - } - - const field = table.getFieldByIdIfExists(id); - if (!field) { - throw spawnError("Can't update field: No field with id %s exists", id); - } - - if (description && description.length > MAX_FIELD_DESCRIPTION_LENGTH) { - throw spawnError( - "Can't update field: description exceeds maximum length of %s characters", - MAX_FIELD_DESCRIPTION_LENGTH, - ); - } - return; - } - - case MutationTypes.UPDATE_SINGLE_FIELD_NAME: { - const {tableId, id, name} = mutation; - const table = this._base.getTableByIdIfExists(tableId); - if (!table) { - throw spawnError("Can't update field: No table with id %s exists", tableId); - } - - const field = table.getFieldByIdIfExists(id); - if (!field) { - throw spawnError("Can't update field: No field with id %s exists", id); - } - - if (field.name.toLowerCase() !== name.toLowerCase()) { - this._assertFieldNameIsValidForMutation(name, table); - } - return; - } - - case MutationTypes.CREATE_SINGLE_TABLE: { - const {name, fields} = mutation; - - if (!name) { - throw spawnError("Can't create table: must provide non-empty name"); - } - - if (name.length > MAX_TABLE_NAME_LENGTH) { - throw spawnError( - "Can't create table: name '%s' exceeds maximum length of %s characters", - name, - MAX_TABLE_NAME_LENGTH, - ); - } - - const existingLowercaseTableNames = this._base.tables.map(table => - table.name.toLowerCase(), - ); - - if (existingLowercaseTableNames.includes(name.toLowerCase())) { - throw spawnError( - "Can't create table: table with name '%s' already exists", - name, - ); - } - - if (fields.length === 0) { - throw spawnError("Can't create table: must specify at least one field"); - } - - if (fields.length > MAX_NUM_FIELDS_PER_TABLE) { - throw spawnError( - "Can't create table: number of fields exceeds maximum of %s", - MAX_NUM_FIELDS_PER_TABLE, - ); - } - - const lowercaseFieldNames = new Set(); - for (const field of fields) { - if (!field.name) { - throw spawnError( - "Can't create table: must provide non-empty name for every field", - ); - } - - if (field.name.length > MAX_FIELD_NAME_LENGTH) { - throw spawnError( - "Can't create table: field name '%s' exceeds maximum length of %s characters", - field.name, - MAX_FIELD_NAME_LENGTH, - ); - } - - const lowercaseFieldName = field.name.toLowerCase(); - if (lowercaseFieldNames.has(lowercaseFieldName)) { - throw spawnError( - "Can't create table: duplicate field name '%s'", - field.name, - ); - } - lowercaseFieldNames.add(lowercaseFieldName); - - const validationResult = this._airtableInterface.fieldTypeProvider.validateConfigForUpdate( - appInterface, - field.config, - null, - null, - billingPlanGrouping, - ); - - if (!validationResult.isValid) { - throw spawnError( - "Can't create table: invalid field config for field '%s'.\n%s", - field.name, - validationResult.reason, - ); - } - - if ( - field.description && - field.description.length > MAX_FIELD_DESCRIPTION_LENGTH - ) { - throw spawnError( - "Can't create table: description for field '%s' exceeds maximum length of %s characters", - field.name, - MAX_FIELD_DESCRIPTION_LENGTH, - ); - } - } - - const primaryField = fields[0]; - if ( - !this._airtableInterface.fieldTypeProvider.canBePrimary( - appInterface, - primaryField.config, - billingPlanGrouping, - ) - ) { - throw spawnError( - "Can't create table: first field '%s' has type '%s' which cannot be a primary field", - primaryField.name, - primaryField.config.type, - ); - } - - return; - } - case MutationTypes.UPDATE_VIEW_METADATA: { - const {tableId, viewId} = mutation; - const {runContext} = this._airtableInterface.sdkInitData; - - if (runContext.type !== BlockRunContextType.VIEW) { - throw spawnError('Setting view metadata is only valid for views'); - } - - if (runContext.viewId !== viewId || runContext.tableId !== tableId) { - throw spawnError('Custom views can only set view metadata on themselves'); - } - - const table = this._base.getTableByIdIfExists(tableId); - if (!table) { - throw spawnError("Can't update metadata: No table with id %s exists", tableId); - } - - const view = table.getViewByIdIfExists(viewId); - if (!view) { - throw spawnError("Can't update metadata: No view with id %s exists", viewId); - } - return; - } - - default: - throw spawnUnknownSwitchCaseError('mutation type', mutation, 'type'); - } - } - - /** @internal */ - _applyOptimisticUpdatesForMutation(mutation: Mutation): boolean { - if (mutation.type === MutationTypes.SET_MULTIPLE_GLOBAL_CONFIG_PATHS) { - this._applyGlobalConfigUpdates(mutation.updates); - return true; - } - - const modelChanges = this._getOptimisticModelChangesForMutation(mutation); - - if (modelChanges.length > 0) { - this._applyModelChanges(modelChanges); - return true; - } - - return false; - } - - /** @internal */ - _getOptimisticModelChangesForMutation(mutation: Mutation): Array { - switch (mutation.type) { - case MutationTypes.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 => 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 []; - } - - return [ - ...recordIds.map(recordId => ({ - path: ['tablesById', tableId, 'recordsById', recordId], - value: undefined, - })), - ...this._base.getTableById(tableId).views.flatMap(view => { - const viewDataStore = recordStore.getViewDataStore(view.id); - if (!viewDataStore.isDataLoaded) { - return []; - } - return viewDataStore.__generateChangesForParentTableDeleteMultipleRecords( - recordIds, - ); - }), - ]; - } - - case MutationTypes.CREATE_MULTIPLE_RECORDS: { - const {tableId, records} = mutation; - const recordStore = this._base.__getRecordStore(tableId); - - if (!recordStore.isRecordMetadataLoaded) { - return []; - } - - 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(), - }, - }; - }), - ...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), - ); - }), - ]; - } - - 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: - case MutationTypes.UPDATE_SINGLE_FIELD_NAME: - case MutationTypes.UPDATE_VIEW_METADATA: - case MutationTypes.CREATE_SINGLE_TABLE: { - return []; - } - - default: - throw spawnUnknownSwitchCaseError('mutation type', mutation, 'type'); - } - } -} - -export default Mutations; diff --git a/packages/sdk/src/sdk_mode.ts b/packages/sdk/src/sdk_mode.ts new file mode 100644 index 000000000..69a235fd3 --- /dev/null +++ b/packages/sdk/src/sdk_mode.ts @@ -0,0 +1,105 @@ +import type BaseBlockSdk from './base/sdk'; +import { + type BaseData as BaseDataForBaseSdkMode, + type BasePermissionData as BasePermissionDataForBaseSdkMode, +} from './base/types/base'; +import {type TableData as TableDataForBaseSdkMode} from './base/types/table'; +import {type FieldData as FieldDataForBaseSdkMode} from './base/types/field'; +import {type RecordData as RecordDataForBaseSdkMode} from './base/types/record'; +import { + type BaseData as BaseDataForInterfaceSdkMode, + type BasePermissionData as BasePermissionDataForInterfaceSdkMode, +} from './interface/types/base'; +import {type TableData as TableDataForInterfaceSdkMode} from './interface/types/table'; +import {type FieldData as FieldDataForInterfaceSdkMode} from './interface/types/field'; +import {type RecordData as RecordDataForInterfaceSdkMode} from './interface/types/record'; +import type BaseForBaseSdkMode from './base/models/base'; +import {type Base as BaseForInterfaceSdkMode} from './interface/models/base'; +import type FieldForBaseSdkMode from './base/models/field'; +import {type InterfaceBlockSdk} from './interface/sdk'; +import type TableForBaseSdkMode from './base/models/table'; +import {type Table as TableForInterfaceSdkMode} from './interface/models/table'; +import {type Field as FieldForInterfaceSdkMode} from './interface/models/field'; +import {type Record as RecordForInterfaceSdkMode} from './interface/models/record'; +import type RecordForBaseSdkMode from './base/models/record'; +import type RecordStoreForBaseSdkMode from './base/models/record_store'; +import {type RecordStore as RecordStoreForInterfaceSdkMode} from './interface/models/record_store'; +import { + type AirtableInterface as AirtableInterfaceForBaseSdkMode, + type BlockRunContext as BlockRunContextForBaseSdkMode, + type SdkInitData as SdkInitDataForBaseSdkMode, +} from './base/types/airtable_interface'; +import { + type AirtableInterface as AirtableInterfaceForInterfaceSdkMode, + type BlockRunContext as BlockRunContextForInterfaceSdkMode, + type SdkInitData as SdkInitDataForInterfaceSdkMode, +} from './interface/types/airtable_interface'; +import type MutationsForBaseSdkMode from './base/models/mutations'; +import {type Mutations as MutationsForInterfaceSdkMode} from './interface/models/mutations'; +import { + type Mutation as MutationForBaseSdkMode, + type PartialMutation as PartialMutationForBaseSdkMode, +} from './base/types/mutations'; +import { + type Mutation as MutationForInterfaceSdkMode, + type PartialMutation as PartialMutationForInterfaceSdkMode, +} from './interface/types/mutations'; +import type SesssionForBaseSdkMode from './base/models/session'; +import {type 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; + FieldDataT: FieldDataForBaseSdkMode; + RecordDataT: RecordDataForBaseSdkMode; + + BasePermissionDataT: BasePermissionDataForBaseSdkMode; + + 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; + FieldDataT: FieldDataForInterfaceSdkMode; + RecordDataT: RecordDataForInterfaceSdkMode; + + BasePermissionDataT: BasePermissionDataForInterfaceSdkMode; + + 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 90% rename from packages/sdk/src/color_utils.ts rename to packages/sdk/src/shared/color_utils.ts index ed8419e95..a344fdeb6 100644 --- a/packages/sdk/src/color_utils.ts +++ b/packages/sdk/src/shared/color_utils.ts @@ -1,6 +1,6 @@ /** @module @airtable/blocks/ui: colorUtils */ /** */ import {getEnumValueIfExists, has} from './private_utils'; -import Colors, {Color, rgbTuplesByColor} from './colors'; +import Colors, {type Color, rgbTuplesByColor} from './colors'; /** A red/green/blue color object. Each property is a number from 0 to 255. */ interface RGB { @@ -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/colors.ts b/packages/sdk/src/shared/colors.ts similarity index 96% rename from packages/sdk/src/colors.ts rename to packages/sdk/src/shared/colors.ts index 9d5de5a05..6e64b7a9d 100644 --- a/packages/sdk/src/colors.ts +++ b/packages/sdk/src/shared/colors.ts @@ -1,5 +1,5 @@ /** @module @airtable/blocks/ui: colors */ /** */ -import {ObjectValues} from './private_utils'; +import {type ObjectValues} from './private_utils'; /** * Airtable color names. * @@ -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/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 77% rename from packages/sdk/src/global_config.ts rename to packages/sdk/src/shared/global_config.ts index fb0d23787..e90c565b8 100644 --- a/packages/sdk/src/global_config.ts +++ b/packages/sdk/src/shared/global_config.ts @@ -1,20 +1,21 @@ /** @module @airtable/blocks: globalConfig */ /** */ +import {type SdkMode} from '../sdk_mode'; import Watchable from './watchable'; -import {AirtableInterface} from './types/airtable_interface'; import {spawnError} from './error_utils'; import { - GlobalConfigPath, - GlobalConfigKey, - PartialGlobalConfigKey, - GlobalConfigValue, - GlobalConfigData, - GlobalConfigUpdate, - PartialGlobalConfigUpdate, - GlobalConfigPathValidationResult, + type GlobalConfigPath, + type GlobalConfigKey, + type PartialGlobalConfigKey, + type GlobalConfigValue, + type GlobalConfigData, + type GlobalConfigUpdate, + type PartialGlobalConfigUpdate, + type GlobalConfigPathValidationResult, } from './types/global_config'; -import {MutationTypes, PermissionCheckResult} from './types/mutations'; +import {type PermissionCheckResult, MutationTypesCore} from './types/mutations_core'; import {getValueAtOwnPath} from './private_utils'; -import Sdk from './sdk'; +import {type BlockSdkCore} from './sdk_core'; +import {type AirtableInterfaceCore} from './types/airtable_interface_core'; /** * You can watch any top-level key in global config. Use '*' to watch every change. @@ -37,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 { @@ -51,15 +48,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; @@ -110,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 { @@ -209,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'); * } * ``` */ @@ -265,7 +268,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, @@ -312,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'); * } * ``` */ @@ -350,7 +356,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, }); } @@ -359,13 +365,11 @@ class GlobalConfig extends Watchable { * this shouldn't be called directly - instead, use this._sdk.__applyGlobalConfigUpdates() */ __setMultipleKvPaths(updates: ReadonlyArray) { - const { - newKvStore, - changedTopLevelKeys, - } = this._airtableInterface.globalConfigHelpers.validateAndApplyUpdates( - updates, - this._kvStore, - ); + const {newKvStore, changedTopLevelKeys} = + this._airtableInterface.globalConfigHelpers.validateAndApplyUpdates( + updates, + this._kvStore, + ); this._kvStore = newKvStore; 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..d9f67e381 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 {type 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 57% rename from packages/sdk/src/models/base.ts rename to packages/sdk/src/shared/models/base_core.ts index 2e7243d21..3448941b2 100644 --- a/packages/sdk/src/models/base.ts +++ b/packages/sdk/src/shared/models/base_core.ts @@ -1,18 +1,20 @@ -/** @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 {type ModelChange} from '../types/base_core'; +import {type CollaboratorData} from '../types/collaborator'; +import {type TableId, type UserId} from '../types/hyper_ids'; +import { + isEnumValue, + entries, + isDeepEqual, + type ObjectValues, + has, + type ObjectMap, +} from '../private_utils'; import {spawnError, invariant} from '../error_utils'; -import Sdk from '../sdk'; -import Table from './table'; -import RecordStore from './record_store'; +import {type 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, @@ -37,35 +39,25 @@ type ChangedPathsForObject = {[K in keyof T]?: ChangedPathsFor export type ChangedPathsForType = T extends string | number | boolean | ReadonlyArray ? {_isDirty?: true} : T extends {} - ? ChangedPathsForObject - : never; + ? 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 +65,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 +77,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; } /** @@ -100,8 +92,12 @@ class Base 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 { @@ -113,8 +109,12 @@ class Base 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 { @@ -126,9 +126,16 @@ class Base 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 { @@ -140,31 +147,48 @@ class Base 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
    { - const tables: Array
    = []; - this._data.tableOrder.forEach(tableId => { + get tables(): Array { + const tables: Array = []; + for (const tableId of this._data.tableOrder) { 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']; + /** * The users who have access to this base. * * @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 { - return this._data.activeCollaboratorIds.map(collaboratorId => + return this._data.activeCollaboratorIds.map((collaboratorId) => this.getCollaboratorById(collaboratorId), ); } @@ -253,19 +277,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 +285,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 +302,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 +314,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 +329,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 +346,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 +362,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 +375,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); @@ -568,14 +427,17 @@ class Base extends AbstractModel { } 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 +457,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 +470,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 +505,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..a79c13a70 --- /dev/null +++ b/packages/sdk/src/shared/models/field_core.ts @@ -0,0 +1,281 @@ +import {FieldType, type FieldOptions, type FieldConfig} from '../types/field_core'; +import {isEnumValue, cloneDeep, type ObjectValues, type FlowAnyObject} from '../private_utils'; +import {type SdkMode} from '../../sdk_mode'; +import {type 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, + SdkModeT['FieldDataT'], + 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(): SdkModeT['FieldDataT'] | 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. + * See {@link FieldType} for more information on the options for each field + * type. Can be watched. + * + * @see {@link FieldType} + * @example + * ```js + * import {FieldType} from '@airtable/blocks/[placeholder-path]/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 FieldType} for more information on the options for each field type. + * + * @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..77fc1476b --- /dev/null +++ b/packages/sdk/src/shared/models/mutations_core.ts @@ -0,0 +1,378 @@ +import {type SdkMode} from '../../sdk_mode'; +import {type ModelChange} from '../types/base_core'; +import {type GlobalConfigUpdate} from '../types/global_config'; +import { + type PermissionCheckResult, + MutationTypesCore, + type SetMultipleGlobalConfigPathsMutation, +} from '../types/mutations_core'; +import {spawnError} from '../error_utils'; +import {type AirtableInterfaceCore} from '../types/airtable_interface_core'; +import {entries, type ObjectMap} from '../private_utils'; +import {type FieldId} from '../types/hyper_ids'; + +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 */ + 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 */ + _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 */ + protected _getOptimisticModelChangesForMutation( + mutation: SdkModeT['MutationT'], + ): 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: { + ...this._getDefaultRecordProperties(), + id: record.id, + cellValuesByFieldId: filteredCellValuesByFieldId, + }, + }; + }); + } + + 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; + /** @internal */ + abstract _getDefaultRecordProperties(): Partial; +} 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..519dd7710 --- /dev/null +++ b/packages/sdk/src/shared/models/record_core.ts @@ -0,0 +1,228 @@ +import {type SdkMode} from '../../sdk_mode'; +import { + cloneDeep, + type FlowAnyObject, + isEnumValue, + isObjectEmpty, + type ObjectValues, +} from '../private_utils'; +import {invariant} from '../error_utils'; +import {type FieldId, type RecordId} from '../types/hyper_ids'; +import {FieldType} from '../types/field_core'; +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/base/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: {id: FieldId; type: FieldType}): 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..b1f8e5efb --- /dev/null +++ b/packages/sdk/src/shared/models/record_store_core.ts @@ -0,0 +1,192 @@ +import { + isEnumValue, + entries, + has, + type ObjectValues, + type ObjectMap, + type FlowAnyFunction, + type FlowAnyObject, +} from '../../shared/private_utils'; +import {invariant} from '../../shared/error_utils'; +import {type TableId, type FieldId, type RecordId} from '../../shared/types/hyper_ids'; +import {type ChangedPathsForType} from '../../shared/models/base_core'; +import AbstractModel from '../../shared/models/abstract_model'; +import {type 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; + + abstract __onDataDeletion(): void; + + 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..33a3d3b98 --- /dev/null +++ b/packages/sdk/src/shared/models/session_core.ts @@ -0,0 +1,160 @@ +/** @module @airtable/blocks/models: Session */ /** */ +import {invariant} from '../error_utils'; +import {type CollaboratorData} from '../types/collaborator'; +import {type PermissionLevel} from '../types/permission_levels'; +import {isEnumValue, entries, type ObjectValues, type ObjectMap} from '../private_utils'; +import {type UserId} from '../types/hyper_ids'; +import {type SdkMode} from '../../sdk_mode'; +import {type 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/[placeholder-path]/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/models/table.ts b/packages/sdk/src/shared/models/table_core.ts similarity index 69% rename from packages/sdk/src/models/table.ts rename to packages/sdk/src/shared/models/table_core.ts index c7a3c4571..eb00d591b 100644 --- a/packages/sdk/src/models/table.ts +++ b/packages/sdk/src/shared/models/table_core.ts @@ -1,81 +1,61 @@ -/** @module @airtable/blocks/models: Table */ /** */ -import Sdk from '../sdk'; -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 {type FieldId, type RecordId} from '../types/hyper_ids'; +import {isEnumValue, entries, has, type ObjectValues, type ObjectMap, keys} from '../private_utils'; import {spawnError} from '../error_utils'; +import {type SdkMode} from '../../sdk_mode'; +import {MutationTypesCore, type PermissionCheckResult} from '../types/mutations_core'; import AbstractModel from './abstract_model'; -import View from './view'; -import Field from './field'; -import Base, {ChangedPathsForType} from './base'; -import ObjectPool from './object_pool'; -import Record from './record'; -import RecordQueryResult, {RecordQueryResultOpts} from './record_query_result'; -import TableOrViewQueryResult from './table_or_view_query_result'; -import RecordStore from './record_store'; +import {type ChangedPathsForType} from './base_core'; +import {FieldCore} from './field_core'; -export const WatchableTableKeys = Object.freeze({ +export const WatchableTableKeysCore = Object.freeze({ name: 'name' as const, description: 'description' as const, - views: 'views' as const, fields: 'fields' as const, }); -/** - * A key in {@link Table} that can be watched. - * - `name` - * - `description` - * - `views` - * - `fields` - */ -export type WatchableTableKey = ObjectValues; +/** @hidden */ +type WatchableTableKeyCore = ObjectValues; -/** - * Model class representing a table. Every {@link Base} has one or more tables. - * - * @docsPath models/Table - */ -class Table extends AbstractModel { +/** @hidden */ +export abstract class TableCore< + SdkModeT extends SdkMode, + WatchableKeys extends string = WatchableTableKeyCore, +> extends AbstractModel { /** @internal */ - static _className = 'Table'; + static _className = 'TableCore'; /** @internal */ static _isWatchableKey(key: string): boolean { - return isEnumValue(WatchableTableKeys, key); + return isEnumValue(WatchableTableKeysCore, key); } /** @internal */ - _parentBase: Base; + _parentBase: SdkModeT['BaseT']; /** @internal */ - _viewModelsById: {[key: string]: View}; + _recordStore: SdkModeT['RecordStoreT']; /** @internal */ - _fieldModelsById: {[key: string]: Field}; + _fieldModelsById: {[key: string]: SdkModeT['FieldT']}; /** @internal */ _cachedFieldNamesById: {[key: string]: string} | null; - /** @internal */ - _recordStore: RecordStore; - /** @internal */ - __tableOrViewQueryResultPool: ObjectPool; /** * @internal */ - constructor(parentBase: Base, recordStore: RecordStore, tableId: string, sdk: Sdk) { + constructor( + parentBase: SdkModeT['BaseT'], + recordStore: SdkModeT['RecordStoreT'], + tableId: string, + sdk: SdkModeT['SdkT'], + ) { super(sdk, tableId); this._parentBase = parentBase; this._recordStore = recordStore; - this._viewModelsById = {}; this._fieldModelsById = {}; this._cachedFieldNamesById = null; - - this.__tableOrViewQueryResultPool = new ObjectPool(TableOrViewQueryResult); } /** * @internal */ - get _dataOrNullIfDeleted(): TableData | null { + get _dataOrNullIfDeleted(): SdkModeT['TableDataT'] | null { return this._baseData.tablesById[this._id] ?? null; } /** @@ -84,13 +64,13 @@ class Table extends AbstractModel { * @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 * ``` */ - get parentBase(): Base { + get parentBase(): SdkModeT['BaseT'] { return this._parentBase; } /** @@ -117,18 +97,6 @@ class Table extends AbstractModel { get description(): string | null { return this._data.description; } - /** - * The URL for the table. You can visit this URL in the browser to be taken to the table in the Airtable UI. - * - * @example - * ```js - * console.log(myTable.url); - * // => 'https://airtable.com/appxxxxxxxxxxxxxx/tblxxxxxxxxxxxxxx' - * ``` - */ - 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. @@ -139,7 +107,7 @@ class Table extends AbstractModel { * // => 'Name' * ``` */ - get primaryField(): Field { + get primaryField(): SdkModeT['FieldT'] { const primaryField = this.getFieldById(this._data.primaryFieldId); return primaryField; } @@ -154,7 +122,7 @@ class Table extends AbstractModel { * console.log(`This table has ${myTable.fields.length} fields`); * ``` */ - get fields(): Array { + get fields(): Array { const fields = []; for (const fieldId of Object.keys(this._data.fieldsById)) { const field = this.getFieldById(fieldId); @@ -177,16 +145,18 @@ class Table extends AbstractModel { * } * ``` */ - getFieldByIdIfExists(fieldId: FieldId): Field | null { + getFieldByIdIfExists(fieldId: FieldId): SdkModeT['FieldT'] | null { if (!this._data.fieldsById[fieldId]) { return null; } else { if (!this._fieldModelsById[fieldId]) { - this._fieldModelsById[fieldId] = new Field(this._sdk, this, 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 @@ -201,7 +171,7 @@ class Table extends AbstractModel { * // => 'Name' * ``` */ - getFieldById(fieldId: FieldId): Field { + 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); @@ -223,7 +193,7 @@ class Table extends AbstractModel { * } * ``` */ - getFieldByNameIfExists(fieldName: string): Field | null { + getFieldByNameIfExists(fieldName: string): SdkModeT['FieldT'] | null { for (const [fieldId, fieldData] of entries(this._data.fieldsById)) { if (fieldData.name === fieldName) { return this.getFieldByIdIfExists(fieldId); @@ -244,7 +214,7 @@ class Table extends AbstractModel { * // => 'fldxxxxxxxxxxxxxx' * ``` */ - getFieldByName(fieldName: string): Field { + getFieldByName(fieldName: string): SdkModeT['FieldT'] { const field = this.getFieldByNameIfExists(fieldName); if (!field) { throw spawnError("No field named '%s' in table '%s'", fieldName, this.name); @@ -261,7 +231,7 @@ class Table extends AbstractModel { * * @param fieldIdOrName The ID or name of the field you're looking for. */ - getFieldIfExists(fieldIdOrName: FieldId | string): Field | null { + getFieldIfExists(fieldIdOrName: FieldId | string): SdkModeT['FieldT'] | null { return ( this.getFieldByIdIfExists(fieldIdOrName) ?? this.getFieldByNameIfExists(fieldIdOrName) ); @@ -277,7 +247,7 @@ class Table extends AbstractModel { * * @param fieldIdOrName The ID or name of the field you're looking for. */ - getField(fieldIdOrName: FieldId | string): Field { + getField(fieldIdOrName: FieldId | string): SdkModeT['FieldT'] { const field = this.getFieldIfExists(fieldIdOrName); if (!field) { throw spawnError( @@ -288,271 +258,67 @@ class Table extends AbstractModel { } return field; } - /** - * The views in this table. Can be watched to know when views are created, - * deleted, or reordered. - * - * @example - * ```js - * console.log(`This table has ${myTable.views.length} views`); - * ``` - */ - get views(): Array { - const views: Array = []; - this._data.viewOrder.forEach(viewId => { - const view = this.getViewById(viewId); - views.push(view); - }); - return views; - } - /** - * Gets the view matching the given ID, or `null` if that view does not exist in this table. - * - * @param viewId The ID of the view. - * @example - * ```js - * const viewId = 'viwxxxxxxxxxxxxxx'; - * const view = myTable.getViewByIdIfExists(viewId); - * if (view !== null) { - * console.log(view.name); - * } else { - * console.log('No view exists with that ID'); - * } - * ``` - */ - getViewByIdIfExists(viewId: ViewId): View | null { - if (!this._data.viewsById[viewId]) { - return null; - } else { - if (!this._viewModelsById[viewId]) { - this._viewModelsById[viewId] = new View( - this._sdk, - this, - this._recordStore.getViewDataStore(viewId), - viewId, - ); - } - return this._viewModelsById[viewId]; - } - } - /** - * Gets the view matching the given ID. Throws if that view does not exist in this table. Use - * {@link getViewByIdIfExists} instead if you are unsure whether a view exists with the given - * ID. - * - * @param viewId The ID of the view. - * @example - * ```js - * const viewId = 'viwxxxxxxxxxxxxxx'; - * const view = myTable.getViewById(viewId); - * console.log(view.name); - * // => 'Grid view' - * ``` - */ - getViewById(viewId: ViewId): View { - const view = this.getViewByIdIfExists(viewId); - if (!view) { - throw spawnError("No view with ID %s in table '%s'", viewId, this.name); - } - return view; - } - /** - * Gets the view matching the given name, or `null` if no view exists with that name in this - * table. - * - * @param viewName The name of the view you're looking for. - * @example - * ```js - * const view = myTable.getViewByNameIfExists('Name'); - * if (view !== null) { - * console.log(view.id); - * } else { - * console.log('No view exists with that name'); - * } - * ``` - */ - getViewByNameIfExists(viewName: string): View | null { - for (const [viewId, viewData] of entries(this._data.viewsById)) { - if (viewData.name === viewName) { - return this.getViewByIdIfExists(viewId); - } - } - return null; - } - /** - * Gets the view matching the given name. Throws if no view exists with that name in this table. - * Use {@link getViewByNameIfExists} instead if you are unsure whether a view exists with the - * given name. - * - * @param viewName The name of the view you're looking for. - * @example - * ```js - * const view = myTable.getViewByName('Name'); - * console.log(view.id); - * // => 'viwxxxxxxxxxxxxxx' - * ``` - */ - getViewByName(viewName: string): View { - const view = this.getViewByNameIfExists(viewName); - if (!view) { - throw spawnError("No view named '%s' in table '%s'", viewName, this.name); - } - return view; - } - /** - * The view matching the given ID or name. Returns `null` if no matching view 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 getViewByIdIfExists} or - * {@link getViewByNameIfExists} methods instead. - * - * @param viewIdOrName The ID or name of the view you're looking for. - */ - getViewIfExists(viewIdOrName: ViewId | string): View | null { - return this.getViewByIdIfExists(viewIdOrName) ?? this.getViewByNameIfExists(viewIdOrName); - } - /** - * The view matching the given ID or name. Throws if no matching view exists within this table. - * Use {@link getViewIfExists} instead if you are unsure whether a view 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 getViewById} or {@link getViewByName} methods - * instead. - * - * @param viewIdOrName The ID or name of the view you're looking for. - */ - getView(viewIdOrName: ViewId | string): View { - const view = this.getViewIfExists(viewIdOrName); - if (!view) { - throw spawnError("No view with ID or name '%s' in table '%s'", viewIdOrName, this.name); - } - return view; - } - /** - * Select records from the table. Returns a {@link RecordQueryResult}. - * - * Consider using {@link useRecords} or {@link useRecordIds} instead, unless you need the - * features of a QueryResult (e.g. `queryResult.getRecordById`). Record hooks handle - * loading/unloading and updating your UI automatically, but manually `select`ing records is - * useful for one-off data processing. - * - * @param opts Options for the query, such as sorts and fields. - * @example - * ```js - * import {useBase, useRecords} from '@airtable/blocks/ui'; - * import React from 'react'; - * - * function TodoList() { - * const base = useBase(); - * const table = base.getTableByName('Tasks'); - * - * const queryResult = table.selectRecords(); - * const records = useRecords(queryResult); - * - * return ( - *
      - * {records.map(record => ( - *
    • - * {record.name || 'Unnamed record'} - *
    • - * ))} - *
    - * ); - * } - * ``` - */ - selectRecords(opts?: RecordQueryResultOpts): TableOrViewQueryResult { - const normalizedOpts = RecordQueryResult._normalizeOpts( - this, - this._recordStore, - opts || {}, + /** @internal */ + _cellValuesByFieldIdOrNameToCellValuesByFieldId( + cellValuesByFieldIdOrName: ObjectMap, + onGenerateIdForNewForeignRecord: (recordId: RecordId) => void, + ): ObjectMap { + return Object.fromEntries( + entries(cellValuesByFieldIdOrName).map(([fieldIdOrName, cellValue]) => { + const field = this.__getFieldMatching(fieldIdOrName); + return [ + field.id, + this._adjustCellValueForFieldIfNecessary( + field, + cellValue, + onGenerateIdForNewForeignRecord, + ), + ]; + }), ); - return this.__tableOrViewQueryResultPool.getObjectForReuse(this._sdk, this, normalizedOpts); } - /** - * Select and load records from the table. Returns a {@link RecordQueryResult} promise where - * record data has been loaded. - * - * Consider using {@link useRecords} or {@link useRecordIds} instead, unless you need the - * features of a QueryResult (e.g. `queryResult.getRecordById`). Record hooks handle - * loading/unloading and updating your UI automatically, but manually `select`ing records is - * useful for one-off data processing. - * - * Once you've finished with your query, remember to call `queryResult.unloadData()`. - * - * @param opts Options for the query, such as sorts and fields. - * @example - * ```js - * async function logRecordCountAsync(table) { - * const query = await table.selectRecordsAsync(); - * console.log(query.recordIds.length); - * query.unloadData(); - * } - * ``` - */ - async selectRecordsAsync(opts?: RecordQueryResultOpts): Promise { - const queryResult = this.selectRecords(opts); - await queryResult.loadDataAsync(); - return queryResult; + /** @internal */ + _adjustCellValueForFieldIfNecessary( + field: SdkModeT['FieldT'], + cellValue: unknown, + onGenerateIdForNewForeignRecord: (recordId: RecordId) => void, + ): unknown { + return cellValue; } /** - * Returns the first view in the table where the type is one of `allowedViewTypes`, or `null` if - * no such view exists in the table. - * - * @param allowedViewTypes An array of view types or a single view type to match against. - * @param preferredViewOrViewId If a view or view ID is supplied and that view exists & has the - * correct type, that view will be returned before checking the other views in the table. - * @example - * ```js - * import {ViewType} from '@airtable/blocks/models'; - * const firstCalendarView = myTable.getFirstViewOfType(ViewType.CALENDAR); - * if (firstCalendarView !== null) { - * console.log(firstCalendarView.name); - * } else { - * console.log('No calendar views exist in the table'); - * } - * ``` + * @internal */ - getFirstViewOfType( - allowedViewTypes: Array | ViewType, - preferredViewOrViewId?: View | ViewId | null, - ): View | null { - if (!Array.isArray(allowedViewTypes)) { - allowedViewTypes = cast>([allowedViewTypes]); - } + __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 (preferredViewOrViewId) { - const preferredView = this.getViewByIdIfExists( - typeof preferredViewOrViewId === 'string' - ? preferredViewOrViewId - : preferredViewOrViewId.id, - ); - if (preferredView && allowedViewTypes.includes(preferredView.type)) { - return preferredView; + if (field === null) { + throw spawnError( + "Field '%s' does not exist in table '%s'", + fieldOrFieldIdOrFieldName, + this.name, + ); } } - 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; + if (field.isDeleted) { + throw spawnError("Field '%s' was deleted from table '%s'", field.name, this.name); + } + return field; } + /** * Updates cell values for a record. * @@ -608,7 +374,7 @@ class Table extends AbstractModel { * ``` */ async updateRecordAsync( - recordOrRecordId: Record | RecordId, + recordOrRecordId: SdkModeT['RecordT'] | RecordId, fields: ObjectMap, ): Promise { const recordId = @@ -677,7 +443,7 @@ class Table extends AbstractModel { * ``` */ checkPermissionsForUpdateRecord( - recordOrRecordId?: Record | RecordId, + recordOrRecordId?: SdkModeT['RecordT'] | RecordId, fields?: ObjectMap, ): PermissionCheckResult { const recordId = @@ -743,7 +509,7 @@ class Table extends AbstractModel { * ``` */ hasPermissionToUpdateRecord( - recordOrRecordId?: Record | RecordId, + recordOrRecordId?: SdkModeT['RecordT'] | RecordId, fields?: ObjectMap, ): boolean { return this.checkPermissionsForUpdateRecord(recordOrRecordId, fields).hasPermission; @@ -829,18 +595,25 @@ class Table extends AbstractModel { readonly fields: ObjectMap; }>, ): Promise { - const recordsWithCellValuesByFieldId = records.map(record => ({ + let includesForeignRowsThatShouldBeCreated = false; + const recordsWithCellValuesByFieldId = records.map((record) => ({ id: record.id, cellValuesByFieldId: this._cellValuesByFieldIdOrNameToCellValuesByFieldId( record.fields, + () => { + includesForeignRowsThatShouldBeCreated = true; + }, ), })); await this._sdk.__mutations.applyMutationAsync({ - type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + type: MutationTypesCore.SET_MULTIPLE_RECORDS_CELL_VALUES, tableId: this.id, records: recordsWithCellValuesByFieldId, - opts: {parseDateCellValueInColumnTimeZone: true}, + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated, + }, }); } /** @@ -908,17 +681,26 @@ class Table extends AbstractModel { readonly fields?: ObjectMap | void; }>, ): PermissionCheckResult { + let includesForeignRowsThatShouldBeCreated = false; + const recordsWithCellValuesByFieldId = records + ? records.map((record) => ({ + id: record.id || undefined, + cellValuesByFieldId: record.fields + ? this._cellValuesByFieldIdOrNameToCellValuesByFieldId(record.fields, () => { + includesForeignRowsThatShouldBeCreated = true; + }) + : undefined, + })) + : undefined; + return this._sdk.__mutations.checkPermissionsForMutation({ - type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + 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, + records: recordsWithCellValuesByFieldId, + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated, + }, }); } /** @@ -1016,7 +798,7 @@ class Table extends AbstractModel { * } * ``` */ - async deleteRecordAsync(recordOrRecordId: Record | RecordId): Promise { + async deleteRecordAsync(recordOrRecordId: SdkModeT['RecordT'] | RecordId): Promise { await this.deleteRecordsAsync([recordOrRecordId]); } /** @@ -1046,7 +828,9 @@ class Table extends AbstractModel { * table.checkPermissionsForDeleteRecord(); * ``` */ - checkPermissionsForDeleteRecord(recordOrRecordId?: Record | RecordId): PermissionCheckResult { + checkPermissionsForDeleteRecord( + recordOrRecordId?: SdkModeT['RecordT'] | RecordId, + ): PermissionCheckResult { return this.checkPermissionsForDeleteRecords( recordOrRecordId ? [recordOrRecordId] : undefined, ); @@ -1074,7 +858,7 @@ class Table extends AbstractModel { * const canDeleteUnknownRecord = table.hasPermissionToDeleteRecord(); * ``` */ - hasPermissionToDeleteRecord(recordOrRecordId?: Record | RecordId): boolean { + hasPermissionToDeleteRecord(recordOrRecordId?: SdkModeT['RecordT'] | RecordId): boolean { return this.checkPermissionsForDeleteRecord(recordOrRecordId).hasPermission; } /** @@ -1113,13 +897,15 @@ class Table extends AbstractModel { * } * ``` */ - async deleteRecordsAsync(recordsOrRecordIds: ReadonlyArray): Promise { - const recordIds = recordsOrRecordIds.map(recordOrRecordId => + 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, + type: MutationTypesCore.DELETE_MULTIPLE_RECORDS, tableId: this.id, recordIds, }); @@ -1153,13 +939,13 @@ class Table extends AbstractModel { * ``` */ checkPermissionsForDeleteRecords( - recordsOrRecordIds?: ReadonlyArray, + recordsOrRecordIds?: ReadonlyArray, ): PermissionCheckResult { return this._sdk.__mutations.checkPermissionsForMutation({ - type: MutationTypes.DELETE_MULTIPLE_RECORDS, + type: MutationTypesCore.DELETE_MULTIPLE_RECORDS, tableId: this.id, recordIds: recordsOrRecordIds - ? recordsOrRecordIds.map(recordOrRecordId => + ? recordsOrRecordIds.map((recordOrRecordId) => typeof recordOrRecordId === 'string' ? recordOrRecordId : recordOrRecordId.id, ) : undefined, @@ -1190,7 +976,9 @@ class Table extends AbstractModel { * const canDeleteUnknownRecords = table.hasPermissionToDeleteRecords(); * ``` */ - hasPermissionToDeleteRecords(recordsOrRecordIds?: ReadonlyArray): boolean { + hasPermissionToDeleteRecords( + recordsOrRecordIds?: ReadonlyArray, + ): boolean { return this.checkPermissionsForDeleteRecords(recordsOrRecordIds).hasPermission; } @@ -1411,7 +1199,8 @@ class Table extends AbstractModel { async createRecordsAsync( records: ReadonlyArray<{fields: ObjectMap}>, ): Promise> { - const recordsToCreate = records.map(recordDef => { + let includesForeignRowsThatShouldBeCreated = false; + const recordsToCreate = records.map((recordDef) => { const recordDefKeys = keys(recordDef); let fields: ObjectMap; if (recordDefKeys.length === 1 && recordDefKeys[0] === 'fields') { @@ -1423,18 +1212,26 @@ class Table extends AbstractModel { } return { id: this._sdk.__airtableInterface.idGenerator.generateRecordId(), - cellValuesByFieldId: this._cellValuesByFieldIdOrNameToCellValuesByFieldId(fields), + cellValuesByFieldId: this._cellValuesByFieldIdOrNameToCellValuesByFieldId( + fields, + () => { + includesForeignRowsThatShouldBeCreated = true; + }, + ), }; }); await this._sdk.__mutations.applyMutationAsync({ - type: MutationTypes.CREATE_MULTIPLE_RECORDS, + type: MutationTypesCore.CREATE_MULTIPLE_RECORDS, tableId: this.id, records: recordsToCreate, - opts: {parseDateCellValueInColumnTimeZone: true}, + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated, + }, }); - return recordsToCreate.map(record => record.id); + return recordsToCreate.map((record) => record.id); } /** * Checks whether the current user has permission to create the specified records. @@ -1484,17 +1281,26 @@ class Table extends AbstractModel { readonly fields?: ObjectMap | void; }>, ): PermissionCheckResult { + let includesForeignRowsThatShouldBeCreated = false; + const recordsWithCellValuesByFieldId = records + ? records.map((record) => ({ + id: undefined, + cellValuesByFieldId: record.fields + ? this._cellValuesByFieldIdOrNameToCellValuesByFieldId(record.fields, () => { + includesForeignRowsThatShouldBeCreated = true; + }) + : undefined, + })) + : undefined; + return this._sdk.__mutations.checkPermissionsForMutation({ - type: MutationTypes.CREATE_MULTIPLE_RECORDS, + type: MutationTypesCore.CREATE_MULTIPLE_RECORDS, tableId: this.id, - records: records - ? records.map(record => ({ - id: undefined, - cellValuesByFieldId: record.fields - ? this._cellValuesByFieldIdOrNameToCellValuesByFieldId(record.fields) - : undefined, - })) - : undefined, + records: recordsWithCellValuesByFieldId, + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated, + }, }); } /** @@ -1544,249 +1350,17 @@ 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 */ - __triggerOnChangeForDirtyPaths(dirtyPaths: ChangedPathsForType): boolean { + __triggerOnChangeForDirtyPaths( + dirtyPaths: ChangedPathsForType, + ): boolean { let didTableSchemaChange = false; if (dirtyPaths.name) { - this._onChange(WatchableTableKeys.name); - didTableSchemaChange = true; - } - if (dirtyPaths.viewOrder) { - this._onChange(WatchableTableKeys.views); + this._onChange(WatchableTableKeysCore.name); didTableSchemaChange = true; - - for (const [viewId, viewModel] of entries(this._viewModelsById)) { - if (viewModel.isDeleted) { - delete this._viewModelsById[viewId]; - } - } } if (dirtyPaths.lock) { didTableSchemaChange = true; @@ -1795,20 +1369,9 @@ class Table extends AbstractModel { didTableSchemaChange = true; } if (dirtyPaths.description) { - this._onChange(WatchableTableKeys.description); + this._onChange(WatchableTableKeysCore.description); didTableSchemaChange = true; } - if (dirtyPaths.viewsById) { - for (const [viewId, dirtyViewPaths] of entries(dirtyPaths.viewsById)) { - const view = this._viewModelsById[viewId]; - if (view) { - const didViewSchemaChange = view.__triggerOnChangeForDirtyPaths(dirtyViewPaths); - if (didViewSchemaChange) { - didTableSchemaChange = true; - } - } - } - } if (dirtyPaths.fieldsById) { didTableSchemaChange = true; @@ -1835,7 +1398,7 @@ class Table extends AbstractModel { } if (addedFieldIds.length > 0 || removedFieldIds.length > 0) { - this._onChange(WatchableTableKeys.fields, { + this._onChange(WatchableTableKeysCore.fields, { addedFieldIds, removedFieldIds, }); @@ -1843,7 +1406,9 @@ class Table extends AbstractModel { this._cachedFieldNamesById = null; } + this._recordStore.triggerOnChangeForDirtyPaths(dirtyPaths); + return didTableSchemaChange; } /** @@ -1860,5 +1425,3 @@ class Table extends AbstractModel { return this._cachedFieldNamesById; } } - -export default Table; diff --git a/packages/sdk/src/private_utils.ts b/packages/sdk/src/shared/private_utils.ts similarity index 82% rename from packages/sdk/src/private_utils.ts rename to packages/sdk/src/shared/private_utils.ts index 15b493970..46e0dc9a8 100644 --- a/packages/sdk/src/private_utils.ts +++ b/packages/sdk/src/shared/private_utils.ts @@ -1,10 +1,5 @@ -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'; /** @hidden */ export type FlowAnyObject = any; @@ -25,25 +20,6 @@ export type ObjectValues = T[keyof T]; /** @hidden */ export type TimeoutId = ReturnType; -/** - * Extract the type of `ref` from a react component - * - * @hidden - */ -export type ReactRefType = C extends React.Component - ? C - : C extends new (props: any) => React.Component - ? C - : C extends React.RefForwardingComponent - ? T - : (C extends React.JSXElementConstructor<{ref?: infer R}> - ? R - : C extends keyof JSX.IntrinsicElements - ? JSX.IntrinsicElements[C]['ref'] - : unknown) extends React.Ref | string | undefined - ? T - : unknown; - /** * Allows creating an object map type with a dynamic key type. * @@ -74,32 +50,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`. * @@ -130,6 +80,62 @@ export function cloneDeep(obj: T): T { return JSON.parse(jsonString); } +/** + * This is basically taken from https://github.com/epoberezkin/fast-deep-equal/blob/a8e7172b6c411ec320d6045fd4afbd2abc1b4bde/src/index.jst + * with some minor adjustments based on our needs. + * Differences: + * - Removed RegExp support because we don't need it + * - Removed .valueOf() and .toString() support because we don't need it + */ +export function isDeepEqual(a: any, b: any): boolean { + if (a === b) { + return true; + } + + if (a && b && typeof a === 'object' && typeof b === 'object') { + if (a.constructor !== b.constructor) { + return false; + } + + if (Array.isArray(a)) { + const length = a.length; + if (length !== b.length) { + return false; + } + for (let i = length - 1; i >= 0; i--) { + if (!isDeepEqual(a[i], b[i])) { + return false; + } + } + return true; + } + + const keys = Object.keys(a); + const length = keys.length; + if (length !== Object.keys(b).length) { + return false; + } + + for (let i = length - 1; i >= 0; i--) { + if (!Object.prototype.hasOwnProperty.call(b, keys[i])) { + return false; + } + } + + for (let i = length - 1; i >= 0; i--) { + const key = keys[i]; + if (!isDeepEqual(a[key], b[key])) { + return false; + } + } + + return true; + } + + // eslint-disable-next-line no-self-compare + return a !== a && b !== b; +} + /** * @hidden */ @@ -156,7 +162,7 @@ export function entries(obj: Obj): Array<[keyof Obj, ObjectV * @hidden */ export function fireAndForgetPromise(fn: () => Promise) { - fn().catch(err => { + fn().catch((err) => { setTimeout(() => { throw err; }, 0); @@ -372,7 +378,7 @@ export function getValueAtOwnPath(value: unknown, path: ReadonlyArray): /** @hidden */ export function arrayDifference(a: ReadonlyArray, b: ReadonlyArray): Array { const bSet = new Set(b); - return a.filter(item => !bSet.has(item)); + return a.filter((item) => !bSet.has(item)); } /** @hidden */ @@ -406,7 +412,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..2f24d5faf --- /dev/null +++ b/packages/sdk/src/shared/sdk_core.ts @@ -0,0 +1,121 @@ +import {type SdkMode} from '../sdk_mode'; +import GlobalConfig from './global_config'; +import {type AppInterface} from './types/airtable_interface_core'; +import {type BlockInstallationId} from './types/hyper_ids'; + +/** + * @hidden + * @example + * ```js + * 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 + * // 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/base'; + * 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/base'; + * import {Button, initializeBlock} from '@airtable/blocks/base/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..0badb6c45 --- /dev/null +++ b/packages/sdk/src/shared/types/airtable_interface_core.ts @@ -0,0 +1,112 @@ +import {type ObjectMap} from '../private_utils'; +import {type SdkMode} from '../../sdk_mode'; +import {type Stat} from './stat'; +import {type FieldId, type BlockInstallationId} from './hyper_ids'; +import {type FieldType, type FieldDataCore} from './field_core'; +import { + type GlobalConfigUpdate, + type GlobalConfigData, + type GlobalConfigPath, + type GlobalConfigPathValidationResult, +} from './global_config'; +import {type BaseDataCore, type ModelChange} from './base_core'; +import {type TableDataCore} from './table_core'; +import {type 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: FieldDataCore): boolean; + validateCellValueForUpdate( + appInterface: AppInterface, + newCellValue: unknown, + currentCellValue: unknown, + fieldData: FieldDataCore, + ): CellValueValidationResult; + getConfig( + appInterface: AppInterface, + fieldData: Pick, + fieldNamesById: ObjectMap, + ): FieldTypeConfig; + convertStringToCellValue( + appInterface: AppInterface, + string: string, + fieldData: FieldDataCore, + opts?: {parseDateCellValueInColumnTimeZone?: boolean}, + ): unknown; + convertCellValueToString( + appInterface: AppInterface, + cellValue: unknown, + fieldData: FieldDataCore, + ): string; + getCellRendererData( + appInterface: AppInterface, + cellValue: unknown, + fieldData: FieldDataCore, + 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: SdkModeT['BasePermissionDataT'], + ): 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..63272bfc0 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 {type 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 52% rename from packages/sdk/src/types/base.ts rename to packages/sdk/src/shared/types/base_core.ts index 880372269..0472be387 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 {PermissionLevel} from './permission_levels'; -import {TableData, TablePermissionData, TableId} from './table'; -import {CursorData} from './cursor'; -import {CollaboratorData, UserId} from './collaborator'; - -/** */ -export type BaseId = string; +import {type ObjectMap} from '../private_utils'; +import {type AppInterface} from './airtable_interface_core'; +import {type PermissionLevel} from './permission_levels'; +import {type TableDataCore, type TablePermissionDataCore} from './table_core'; +import {type CollaboratorData} from './collaborator'; +import {type TableId, type UserId, type BaseId} from './hyper_ids'; /** @hidden */ export interface ModelChange { @@ -16,13 +13,12 @@ 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 +29,6 @@ export interface BaseData { currentUserId: UserId | null; permissionLevel: PermissionLevel; enabledFeatureNames: Array; - cursorData: CursorData | null; billingPlanGrouping: string; isBlockDevelopmentRestrictionEnabled: boolean; @@ -42,7 +37,7 @@ export interface BaseData { } /** @hidden */ -export interface BasePermissionData { +export interface BasePermissionDataCore { readonly permissionLevel: PermissionLevel; - readonly tablesById: ObjectMap; + readonly tablesById: ObjectMap; } diff --git a/packages/sdk/src/types/collaborator.ts b/packages/sdk/src/shared/types/collaborator.ts similarity index 92% rename from packages/sdk/src/types/collaborator.ts rename to packages/sdk/src/shared/types/collaborator.ts index c4f4b9b30..97bb4a0fe 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 {type 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_core.ts similarity index 98% rename from packages/sdk/src/types/field.ts rename to packages/sdk/src/shared/types/field_core.ts index 81d80db21..f7349ca45 100644 --- a/packages/sdk/src/types/field.ts +++ b/packages/sdk/src/shared/types/field_core.ts @@ -1,10 +1,7 @@ /** @module @airtable/blocks/models: Field */ /** */ -import {Color} from '../colors'; -import {TableId} from './table'; -import {ViewId} from './view'; +import {type Color} from '../colors'; +import {type TableId, type FieldId, type ViewId} from './hyper_ids'; -/** */ -export type FieldId = string; /** @hidden */ export type PrivateColumnType = string; @@ -13,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 * )); @@ -399,9 +396,27 @@ export enum FieldType { * The currently linked record IDs and their primary cell values from the linked table. * * **Cell write format** + * {base-only} * ```js - * Array<{ id: RecordId }> + * Array<{ id: RecordId; name?: string }> * ``` + * {/base-only} + * {interface-only} + * ```js + * Array<{ id: RecordId; name?: string } | { name: string }> + * ``` + * {/interface-only} + * + * Pass an array of objects with an `id` property that is the RecordId of the records + * in the linked table. The `name` property is optional and does not affect which + * record(s) are linked, but is recommended for better user experience. The block will + * initially use the `name` to optimistically update the cell value, before the response + * from the server is received. + * + * {interface-only} + * If you pass an object without an `id` property, the `name` will be used to create a + * new record in the linked table. + * {/interface-only} * * **Field options read format** * ```js @@ -1186,7 +1201,7 @@ export enum FieldType { /** @hidden */ export type FieldLock = unknown; /** @hidden */ -export interface FieldData { +export interface FieldDataCore { id: FieldId; name: string; type: PrivateColumnType; @@ -1197,7 +1212,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/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..9fa5149d8 --- /dev/null +++ b/packages/sdk/src/shared/types/mutations_core.ts @@ -0,0 +1,171 @@ +/** @module @airtable/blocks: mutations */ /** */ +import {type ObjectMap} from '../private_utils'; +import {type FieldId, type RecordId, type TableId} from './hyper_ids'; +import {type GlobalConfigUpdate, type 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, +}); + + +/** + * 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; +} + +/** + * 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?: { + readonly parseDateCellValueInColumnTimeZone?: boolean; + readonly includesForeignRowsThatShouldBeCreated?: 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?: { + readonly parseDateCellValueInColumnTimeZone?: boolean; + readonly includesForeignRowsThatShouldBeCreated?: 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?: { + readonly parseDateCellValueInColumnTimeZone?: boolean; + readonly includesForeignRowsThatShouldBeCreated?: 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?: { + readonly parseDateCellValueInColumnTimeZone?: boolean; + readonly includesForeignRowsThatShouldBeCreated?: boolean; + }; +} + +/** @hidden */ +export type MutationCore = + | SetMultipleGlobalConfigPathsMutation + | SetMultipleRecordsCellValuesMutation + | DeleteMultipleRecordsMutation + | CreateMultipleRecordsMutation; + +/** @hidden */ +export type PartialMutationCore = + | PartialSetMultipleGlobalConfigPathsMutation + | PartialSetMultipleRecordsCellValuesMutation + | PartialDeleteMultipleRecordsMutation + | PartialCreateMultipleRecordsMutation; + +/** */ +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 86% rename from packages/sdk/src/types/permission_levels.ts rename to packages/sdk/src/shared/types/permission_levels.ts index b57389326..e5b0ec668 100644 --- a/packages/sdk/src/types/permission_levels.ts +++ b/packages/sdk/src/shared/types/permission_levels.ts @@ -1,4 +1,4 @@ -import {ObjectValues} from '../private_utils'; +import {type ObjectValues} from '../private_utils'; /** @hidden */ export const PermissionLevels = Object.freeze({ diff --git a/packages/sdk/src/types/record.ts b/packages/sdk/src/shared/types/record.ts similarity index 57% rename from packages/sdk/src/types/record.ts rename to packages/sdk/src/shared/types/record.ts index 422ca2596..0d6ae2bfe 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 {type ObjectMap} from '../private_utils'; +import {type FieldId, type 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/shared/types/table_core.ts b/packages/sdk/src/shared/types/table_core.ts new file mode 100644 index 000000000..5fa967c42 --- /dev/null +++ b/packages/sdk/src/shared/types/table_core.ts @@ -0,0 +1,25 @@ +/** @module @airtable/blocks/models: Table */ /** */ +import {type TableId} from './hyper_ids'; + +/** @hidden */ +export type TableLock = unknown; +/** @hidden */ +export type ExternalSyncById = unknown; + +/** @hidden */ +export interface TableDataCore { + id: TableId; + name: string; + primaryFieldId: string; + description: string | null; + lock: TableLock | null; + externalSyncById: ExternalSyncById | null; +} + +/** @hidden */ +export interface TablePermissionDataCore { + readonly id: TableId; + readonly name: string; + readonly lock: TableLock | null; + readonly externalSyncById: ExternalSyncById | null; +} diff --git a/packages/sdk/src/shared/ui/cell_renderer.tsx b/packages/sdk/src/shared/ui/cell_renderer.tsx new file mode 100644 index 000000000..12b6a6492 --- /dev/null +++ b/packages/sdk/src/shared/ui/cell_renderer.tsx @@ -0,0 +1,195 @@ +/** @module @airtable/blocks/ui: CellRenderer */ /** */ +import * as React from 'react'; +import {spawnError} from '../../shared/error_utils'; +import {FieldType} from '../../shared/types/field_core'; +import {type RecordId} from '../../shared/types/hyper_ids'; +import {type ObjectMap} from '../../shared/private_utils'; +import useWatchable from '../../shared/ui/use_watchable'; +import {useSdk} from '../../shared/ui/sdk_context'; +import {type SdkMode} from '../../sdk_mode'; + +/** + * @hidden + */ +interface CellRendererProps { + /** The {@link Record} from which to render a cell. Either `record` or `cellValue` must be provided to the CellRenderer. If both are provided, `record` will be used. */ + record?: SdkModeT['RecordT'] | null | undefined; + /** The cell value to render. Either `record` or `cellValue` must be provided to the CellRenderer. If both are provided, `record` will be used. */ + cellValue?: unknown; + /** The {@link Field} for a given {@link Record} being rendered as a cell. */ + field: SdkModeT['FieldT']; + /** Whether to wrap cell contents. Defaults to true. */ + shouldWrap?: boolean; + /** Additional class names to apply to the cell renderer container, separated by spaces. */ + className?: string; + /** Additional styles to apply to the cell renderer container. */ + style?: React.CSSProperties; + /** Additional class names to apply to the cell itself, separated by spaces. */ + cellClassName?: string; + /** Additional styles to apply to the cell itself. */ + cellStyle?: React.CSSProperties; + /** Render function if provided and validation fails. */ + renderInvalidCellValue?: (cellValue: unknown, field: SdkModeT['FieldT']) => React.ReactElement; + /** @internal May be injected by Tooltip */ + onMouseEnter?: () => void; + /** @internal May be injected by Tooltip */ + onMouseLeave?: () => void; + /** @internal May be injected by Tooltip */ + onClick?: () => void; +} + +/** + * @internal + */ +function validateRecordAndFieldProps(props: { + record: SdkModeT['RecordT'] | null | undefined; + field: SdkModeT['FieldT']; +}) { + if ( + props.record && + !props.record.isDeleted && + !props.field.isDeleted && + props.record.parentTable.id !== props.field.parentTable.id + ) { + throw spawnError( + 'CellRenderer: record %s and field %s do not have the same parent table', + props.record.parentTable.id, + props.field.parentTable.id, + ); + } +} + +/** + * @hidden + */ +export function CellRenderer(props: CellRendererProps) { + const { + record, + cellValue, + field, + shouldWrap = true, + onMouseEnter, + onMouseLeave, + onClick, + className, + style, + cellClassName, + cellStyle, + renderInvalidCellValue, + } = props; + validateRecordAndFieldProps({record, field}); + + const sdk = useSdk(); + useWatchable(record, [`cellValueInField:${field.id}`]); + useWatchable(field, ['type', 'options']); + + if (field.isDeleted) { + return null; + } + + const airtableInterface = sdk.__airtableInterface; + const appInterface = sdk.__appInterface; + + let cellValueToRender; + if (record) { + if (cellValue !== undefined) { + // eslint-disable-next-line + console.warn( + 'CellRenderer was given both record and cellValue, choosing to render record value', + ); + } + + if (record.isDeleted) { + return null; + } + + cellValueToRender = record.getCellValue(field.id); + } else { + if (!field.isComputed) { + const validationResult = airtableInterface.fieldTypeProvider.validateCellValueForUpdate( + appInterface, + cellValue, + null, + field._data, + ); + if (!validationResult.isValid) { + if (renderInvalidCellValue) { + return ( +
    + {renderInvalidCellValue(cellValue, field)} +
    + ); + } else { + throw spawnError( + 'Cannot render invalid cell value %s: %s', + cellValue, + validationResult.reason, + ); + } + } + } + + cellValueToRender = cellValue; + } + + if ( + cellValueToRender && + field.type === FieldType.MULTIPLE_LOOKUP_VALUES && + !airtableInterface.sdkInitData.isUsingNewLookupCellValueFormat + ) { + const originalCellValue = cellValueToRender as Array<{ + linkedRecordId: RecordId; + value: unknown; + }>; + const linkedRecordIdsSet = new Set(); + const valuesByLinkedRecordId = {} as ObjectMap>; + + for (const {linkedRecordId, value} of originalCellValue) { + linkedRecordIdsSet.add(linkedRecordId); + if (!valuesByLinkedRecordId[linkedRecordId]) { + valuesByLinkedRecordId[linkedRecordId] = []; + } + valuesByLinkedRecordId[linkedRecordId].push(value); + } + + cellValueToRender = { + linkedRecordIds: Array.from(linkedRecordIdsSet), + valuesByLinkedRecordId, + }; + } + + const {cellValueHtml, attributes} = airtableInterface.fieldTypeProvider.getCellRendererData( + appInterface, + cellValueToRender, + field._data, + !!shouldWrap, + ); + + return ( +
    +
    +
    + ); +} 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 63% 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..ae43e01cc 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,15 +1,11 @@ -import PropTypes from 'prop-types'; -import {BlockRunContextType} from '../types/airtable_interface'; -import {GlobalConfigKey} from '../types/global_config'; +import {BlockRunContextType} from '../../base/types/airtable_interface'; +import {type GlobalConfigKey} from '../types/global_config'; +import {type BaseSdkMode} from '../../sdk_mode'; import useWatchable from './use_watchable'; import {useSdk} from './sdk_context'; /** @hidden */ const globalConfigSyncedComponentHelpers = { - globalConfigKeyPropType: PropTypes.oneOfType([ - PropTypes.string.isRequired, - PropTypes.arrayOf(PropTypes.string.isRequired).isRequired, - ]).isRequired, useDefaultWatchesForSyncedComponent(globalConfigKey: GlobalConfigKey): void { const sdk = useSdk(); const {globalConfig, session} = sdk; @@ -19,7 +15,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..042759a46 --- /dev/null +++ b/packages/sdk/src/shared/ui/loader.tsx @@ -0,0 +1,52 @@ +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 = ({fillColor = '#888', scale = 0.3, className, style}: LoaderProps) => { + return ( + + + + + + + + ); +}; + +export default Loader; diff --git a/packages/sdk/src/ui/remote_utils.ts b/packages/sdk/src/shared/ui/remote_utils.ts similarity index 91% rename from packages/sdk/src/ui/remote_utils.ts rename to packages/sdk/src/shared/ui/remote_utils.ts index 09aaf119c..90638af15 100644 --- a/packages/sdk/src/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/ui/sdk_context.ts b/packages/sdk/src/shared/ui/sdk_context.ts similarity index 60% rename from packages/sdk/src/ui/sdk_context.ts rename to packages/sdk/src/shared/ui/sdk_context.ts index e4df9380f..4acef57a9 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 {type 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..9877ec4ec --- /dev/null +++ b/packages/sdk/src/shared/ui/use_base.ts @@ -0,0 +1,15 @@ +import {type 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/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/src/ui/use_global_config.ts b/packages/sdk/src/shared/ui/use_global_config.ts similarity index 60% rename from packages/sdk/src/ui/use_global_config.ts rename to packages/sdk/src/shared/ui/use_global_config.ts index 00214de42..a7379dee9 100644 --- a/packages/sdk/src/ui/use_global_config.ts +++ b/packages/sdk/src/shared/ui/use_global_config.ts @@ -1,5 +1,5 @@ /** @module @airtable/blocks/ui: useGlobalConfig */ /** */ -import GlobalConfig from '../global_config'; +import type GlobalConfig from '../global_config'; import useWatchable from './use_watchable'; import {useSdk} from './sdk_context'; @@ -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_session.ts b/packages/sdk/src/shared/ui/use_session.ts new file mode 100644 index 000000000..3cabca340 --- /dev/null +++ b/packages/sdk/src/shared/ui/use_session.ts @@ -0,0 +1,13 @@ +import {type 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 89% rename from packages/sdk/src/ui/use_synced.ts rename to packages/sdk/src/shared/ui/use_synced.ts index e47c90dba..80d169865 100644 --- a/packages/sdk/src/ui/use_synced.ts +++ b/packages/sdk/src/shared/ui/use_synced.ts @@ -1,4 +1,4 @@ -import {GlobalConfigKey, GlobalConfigValue} from '../types/global_config'; +import {type GlobalConfigKey, type GlobalConfigValue} from '../types/global_config'; import globalConfigSyncedComponentHelpers from './global_config_synced_component_helpers'; import {useSdk} from './sdk_context'; @@ -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/ui/use_watchable.ts b/packages/sdk/src/shared/ui/use_watchable.ts similarity index 73% rename from packages/sdk/src/ui/use_watchable.ts rename to packages/sdk/src/shared/ui/use_watchable.ts index 35e6a430d..a8bc4435e 100644 --- a/packages/sdk/src/ui/use_watchable.ts +++ b/packages/sdk/src/shared/ui/use_watchable.ts @@ -1,9 +1,8 @@ /** @module @airtable/blocks/ui: useWatchable */ /** */ -import {useMemo, useRef} from 'react'; -import {useSubscription} from 'use-subscription'; +import {useMemo, useRef, useSyncExternalStore} from 'react'; import {spawnError} from '../error_utils'; import {compact} from '../private_utils'; -import Watchable from '../watchable'; +import type Watchable from '../watchable'; import useArrayIdentity from './use_array_identity'; /** @@ -15,10 +14,12 @@ import useArrayIdentity from './use_array_identity'; * This is a low-level tool that you should only use when you specifically need it. There are more * convenient model-specific hooks available: * - * * For {@link Base}, {@link Table}, {@link View}, or {@link Field}, use {@link useBase}. - * * For {@link RecordQueryResult} or {@link Record}, use {@link useRecords}, {@link useRecordIds}, or {@link useRecordById}. + * * For {@link Base}, {@link Table}, {base-only} {@link View},{/base-only}or {@link Field}, use {@link useBase}. + * * For {base-only}{@link RecordQueryResult} or {/base-only}{@link Record}, use {@link useRecords}, {@link useRecordIds}, or {@link useRecordById}. + * {base-only} * * For {@link Viewport}, use {@link useViewport}. * * For {@link SettingsButton}, use {@link useSettingsButton}. + * {/base-only} * * If you're writing a class component and still want to be able to use hooks, try {@link withHooks}. * @@ -28,39 +29,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 */ @@ -89,7 +71,8 @@ export default function useWatchable( const watchSubscription = useMemo(() => { return { - getCurrentValue: () => compactModels.map(model => model.__getWatchableKey()).join(','), + getCurrentValue: () => + compactModels.map((model) => model.__getWatchableKey()).join(','), subscribe: (notifyChange: () => void) => { let isDisabled = false; @@ -120,5 +103,5 @@ export default function useWatchable( }; }, [compactModels, compactKeys]); - useSubscription(watchSubscription); + useSyncExternalStore(watchSubscription.subscribe, watchSubscription.getCurrentValue); } diff --git a/packages/sdk/src/ui/with_hooks.tsx b/packages/sdk/src/shared/ui/with_hooks.tsx similarity index 89% rename from packages/sdk/src/ui/with_hooks.tsx rename to packages/sdk/src/shared/ui/with_hooks.tsx index da68017a3..74f9a1616 100644 --- a/packages/sdk/src/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: * @@ -94,13 +94,11 @@ import {spawnError} from '../error_utils'; export default function withHooks( Component: | ((new (props: Props) => React.Component) & {displayName?: string}) - | React.RefForwardingComponent - | React.FunctionComponent, + | React.ComponentType, getAdditionalPropsToInject: ( - props: Omit, + props: React.PropsWithoutRef>, ) => InjectedProps & {ref?: React.Ref}, -): React.RefForwardingComponent< - Instance, +): React.ForwardRefExoticComponent< Omit & React.RefAttributes > { if (!getAdditionalPropsToInject) { @@ -125,13 +123,13 @@ export default function withHooks { if (injectedRef && typeof injectedRef === 'object') { - (injectedRef as React.MutableRefObject).current = instance; + (injectedRef as React.RefObject).current = instance; } else if (typeof injectedRef === 'function') { injectedRef(instance); } if (forwardedRef && typeof forwardedRef === 'object') { - (forwardedRef as React.MutableRefObject).current = instance; + (forwardedRef as React.RefObject).current = instance; } else if (typeof forwardedRef === 'function') { forwardedRef(instance); } 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 66% rename from packages/sdk/src/warning.ts rename to packages/sdk/src/shared/warning.ts index 91f90531d..844bf4126 100644 --- a/packages/sdk/src/warning.ts +++ b/packages/sdk/src/shared/warning.ts @@ -1,5 +1,6 @@ -import {ObjectMap} from './private_utils'; -import Sdk from './sdk'; +import {type SdkMode} from '../sdk_mode'; +import {type ObjectMap} from './private_utils'; +import {type 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 96% rename from packages/sdk/src/watchable.ts rename to packages/sdk/src/shared/watchable.ts index b2874d7ba..66832f6b7 100644 --- a/packages/sdk/src/watchable.ts +++ b/packages/sdk/src/shared/watchable.ts @@ -1,5 +1,5 @@ /** @module @airtable/blocks/models: Abstract models */ /** */ -import {getLocallyUniqueId, FlowAnyExistential, FlowAnyObject} from './private_utils'; +import {getLocallyUniqueId, type FlowAnyExistential, type FlowAnyObject} from './private_utils'; import {spawnError} from './error_utils'; /** @@ -124,7 +124,7 @@ class Watchable { for (const key of validKeys) { const watchers = this._changeWatchersByKey[key]; if (watchers) { - const filteredWatchers = watchers.filter(watcher => { + const filteredWatchers = watchers.filter((watcher) => { return watcher.callback !== callback || watcher.context !== context; }); if (filteredWatchers.length > 0) { diff --git a/packages/sdk/src/testing/abstract_mock_airtable_interface.ts b/packages/sdk/src/testing/base/abstract_mock_airtable_interface.ts similarity index 82% rename from packages/sdk/src/testing/abstract_mock_airtable_interface.ts rename to packages/sdk/src/testing/base/abstract_mock_airtable_interface.ts index c255a7082..cfbee66d4 100644 --- a/packages/sdk/src/testing/abstract_mock_airtable_interface.ts +++ b/packages/sdk/src/testing/base/abstract_mock_airtable_interface.ts @@ -1,32 +1,34 @@ -import {AggregatorKey} from '../types/aggregators'; +import EventEmitter from 'events'; +import {type AggregatorKey} from '../../base/types/aggregators'; 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'; -const EventEmitter = require('events'); + type AppInterface, + type FieldTypeConfig, + type GlobalConfigHelpers, +} from '../../shared/types/airtable_interface_core'; +import { + type AggregatorConfig, + type Aggregators, + type AirtableInterface, + type FieldTypeProvider, + type SdkInitData, + type UrlConstructor, + type PartialViewData, + type IdGenerator, + type VisList, +} from '../../base/types/airtable_interface'; +import {type TableId, type FieldId, type ViewId, type RecordId} from '../../shared/types/hyper_ids'; +import {cloneDeep, type ObjectMap} from '../../shared/private_utils'; +import {spawnError} from '../../shared/error_utils'; +import {type ModelChange} from '../../shared/types/base_core'; +import {type FieldData} from '../../base/types/field'; +import {type RecordData} from '../../base/types/record'; +import {type ViewportSizeConstraint} from '../../base/types/viewport'; +import {type PermissionCheckResult} from '../../shared/types/mutations_core'; +import {type Mutation} from '../../base/types/mutations'; +import {type NormalizedSortConfig} from '../../base/models/record_query_result'; +import {type RequestJson, type ResponseJson} from '../../base/types/backend_fetch_types'; +import {type CursorData} from '../../base/types/cursor'; +import {type RecordActionData} from '../../base/types/record_action_data'; /** @internal */ const aggregators: Aggregators = { @@ -152,8 +154,10 @@ const idGenerator: IdGenerator = { * * @hidden */ -export abstract class AbstractMockAirtableInterface extends EventEmitter - implements AirtableInterface { +export abstract class AbstractMockAirtableInterface + extends EventEmitter + implements AirtableInterface +{ sdkInitData!: SdkInitData; private _initData: SdkInitData; @@ -222,7 +226,7 @@ export abstract class AbstractMockAirtableInterface extends EventEmitter }; } - subscribeToModelUpdates(fn: Function) { + subscribeToModelUpdates(fn: (...args: any[]) => void) { this.on('modelupdates', fn); } diff --git a/packages/sdk/src/testing/interface/abstract_mock_airtable_interface.ts b/packages/sdk/src/testing/interface/abstract_mock_airtable_interface.ts new file mode 100644 index 000000000..9c05abb1f --- /dev/null +++ b/packages/sdk/src/testing/interface/abstract_mock_airtable_interface.ts @@ -0,0 +1,163 @@ +import EventEmitter from 'events'; +import { + type AppInterface, + type FieldTypeConfig, + type FieldTypeProviderCore, + type GlobalConfigHelpers, +} from '../../shared/types/airtable_interface_core'; +import { + type AirtableInterface, + type SdkInitData, + type IdGenerator, + type BlockInstallationPageElementCustomPropertyForAirtableInterface, +} from '../../interface/types/airtable_interface'; +import {type FieldId, type RecordId} from '../../shared/types/hyper_ids'; +import {cloneDeep, type ObjectMap} from '../../shared/private_utils'; +import {spawnError} from '../../shared/error_utils'; +import {type ModelChange} from '../../shared/types/base_core'; +import {type FieldData} from '../../interface/types/field'; +import {type PermissionCheckResult} from '../../shared/types/mutations_core'; +import {type Mutation} from '../../interface/types/mutations'; + +/** @internal */ +const fieldTypeProvider: FieldTypeProviderCore = { + isComputed(fieldData: FieldData): boolean { + return false; + }, + getConfig: ( + appInterface: AppInterface, + fieldData: FieldData, + fieldNamesById: ObjectMap, + ) => { + return { + type: fieldData.type, + options: fieldData.typeOptions, + } as FieldTypeConfig; + }, + convertStringToCellValue(appInterface: AppInterface, string: string, fieldData: FieldData) { + return ''; + }, + convertCellValueToString(appInterface: AppInterface, cellValue: unknown, fieldData: FieldData) { + return ''; + }, + getCellRendererData( + appInterface: AppInterface, + cellValue: unknown, + fieldData: FieldData, + shouldWrap: boolean, + ) { + return {cellValueHtml: `
    ${JSON.stringify(cellValue)}
    `, attributes: {}}; + }, + validateCellValueForUpdate( + appInterface: AppInterface, + newCellValue: unknown, + currentCellValue: unknown, + fieldData: FieldData, + ) { + return {isValid: true}; + }, +}; + +/** @internal */ +const globalConfigHelpers: GlobalConfigHelpers = { + validatePath(path, store) { + return {isValid: true}; + }, + validateAndApplyUpdates(updates, store) { + throw spawnError('validateAndApplyUpdates unimplemented'); + }, +}; + +/** @internal */ +const idGenerator: IdGenerator = { + generateRecordId: () => 'recGeneratedMockId', +}; + +/** + * An abstract base class with a common interface exposed to both Blocks SDK's + * internal automated test suite and the blocks-testing public repo. + * + * @hidden + */ +export abstract class AbstractMockAirtableInterface + extends EventEmitter + implements AirtableInterface +{ + sdkInitData!: SdkInitData; + + private _initData: SdkInitData; + + constructor(initData: SdkInitData) { + super(); + this._initData = cloneDeep(initData); + this.reset(); + } + + /** + * Revert the mock interface to its initial state. This includes: + * + * - removing all event listeners + * - restoring the database schema + */ + reset() { + this.removeAllListeners(); + this.sdkInitData = cloneDeep(this._initData); + } + + get fieldTypeProvider() { + return fieldTypeProvider; + } + + get globalConfigHelpers() { + return globalConfigHelpers; + } + + get idGenerator() { + return idGenerator; + } + + assertAllowedSdkPackageVersion() {} + + applyMutationAsync(mutation: Mutation, opts?: {holdForMs?: number}): Promise { + return Promise.resolve(); + } + + checkPermissionsForMutation(mutation: Mutation): PermissionCheckResult { + return { + hasPermission: true, + }; + } + + fetchForeignRecordsAsync( + tableId: string, + recordId: string, + fieldId: string, + filterString: string, + ): Promise<{records: ReadonlyArray<{id: RecordId; name: string}>}> { + return Promise.resolve({records: []}); + } + + setCustomPropertiesAsync( + properties: Array, + ): Promise { + return Promise.resolve(true); + } + + subscribeToModelUpdates(fn: (...args: any[]) => void) { + this.on('modelupdates', fn); + } + + subscribeToGlobalConfigUpdates() {} + + triggerModelUpdates(changes: ReadonlyArray) { + this.emit('modelupdates', {changes}); + } + + triggerGlobalConfigUpdates() {} + + abstract expandRecord(tableId: string, recordId: string): void; + abstract reloadFrame(): void; + abstract trackEvent(): void; + abstract trackExposure(): void; + abstract sendStat(): void; +} 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/src/types/table.ts b/packages/sdk/src/types/table.ts deleted file mode 100644 index b4a7d051c..000000000 --- a/packages/sdk/src/types/table.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** @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'; - -/** */ -export type TableId = string; -/** @hidden */ -export type TableLock = unknown; -/** @hidden */ -export type ExternalSyncById = unknown; - -/** @hidden */ -export interface TableData { - 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; -} - -/** @hidden */ -export interface TablePermissionData { - readonly id: TableId; - readonly name: string; - readonly fieldsById: {readonly [key: string]: FieldPermissionData}; - readonly lock: TableLock | null; -} diff --git a/packages/sdk/src/ui/baymax_utils.ts b/packages/sdk/src/ui/baymax_utils.ts deleted file mode 100644 index ad65659f6..000000000 --- a/packages/sdk/src/ui/baymax_utils.ts +++ /dev/null @@ -1,464 +0,0 @@ -import {css, keyframes} from 'emotion'; -import {has} from '../private_utils'; -import {spawnError} from '../error_utils'; - -const bounceIn = keyframes` - from, 50%, to { - -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); - animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); - } - 0% { - opacity: 0.9; - -webkit-transform: scale3d(.98, .98, .98); - transform: scale3d(.98, .98, .98); - } - 70% { - opacity: 1; - -webkit-transform: scale3d(1.01, 1.01, 1.01); - transform: scale3d(1.01, 1.01, 1.01); - } - to { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } -`; - -const spinScale = keyframes` - 0% { - transform: rotate(0) scale(1); - } - 50% { - transform: rotate(360deg) scale(0.9); - } - 100% { - transform: rotate(720deg) scale(1); - } -`; - -// TODO (stephen): import values from theme object? -const emotionClassNameByBaymaxClassName = { - absolute: css` - position: absolute; - `, - 'align-top': css` - vertical-align: top; - `, - 'all-0': css` - top: 0; - left: 0; - right: 0; - bottom: 0; - `, - animate: css` - transition: 0.085s all ease-in; - `, - 'animate-bounce-in': css` - animation-name: ${bounceIn}; - animation-duration: 240ms; - `, - 'animate-infinite': css` - animation-iteration-count: infinite; - `, - 'animate-spin-scale': css` - animation-name: ${spinScale}; - animation-duration: 1800ms; - animation-timing-function: cubic-bezier(0.785, 0.135, 0.15, 0.86); - `, - appFontColorLight: css` - color: hsl(0, 0%, 33%); - `, - 'background-center': css` - background-position: center center; - `, - 'background-cover': css` - background-size: cover; - `, - 'background-transparent': css` - background-color: transparent; - `, - big: css` - font-size: 0.9rem; - `, - block: css` - display: block; - `, - blue: css` - background-color: rgb(45, 127, 249); - `, - 'border-box': css` - box-sizing: border-box; - `, - 'bottom-0': css` - bottom: 0; - `, - caps: css` - text-transform: uppercase; - letter-spacing: 0.1em; - `, - cardBoxShadow: css` - box-shadow: 0 0 0 2px hsla(0, 0%, 0%, 0.1); - transition-property: box-shadow; - -webkit-transition-property: box-shadow; - -moz-transition-property: box-shadow; - transition-duration: 0.15s; - -webkit-transition-duration: 0.15s; - -moz-transition-duration: 0.15s; - transition-timing-function: ease-out; - -webkit-transition-timing-function: ease-out; - -moz-transition-timing-function: ease-out; - &:hover { - box-shadow: 0 0 0 2px hsla(0, 0%, 0%, 0.25); - } - `, - center: css` - text-align: center; - `, - circle: css` - border-radius: 50%; - `, - dark: css` - background-color: hsl(0, 0%, 20%); - `, - darken1: css` - background-color: hsla(0, 0%, 0%, 0.05); - `, - 'darken1-focus': css` - &:focus { - background-color: hsla(0, 0%, 0%, 0.05); - } - `, - 'darken1-hover': css` - &:hover { - background-color: hsla(0, 0%, 0%, 0.05); - } - `, - darken2: css` - background-color: hsla(0, 0%, 0%, 0.1); - `, - darken3: css` - background-color: hsla(0, 0%, 0%, 0.25); - `, - darken4: css` - background-color: hsla(0, 0%, 0%, 0.5); - `, - fixed: css` - position: fixed; - `, - flex: css` - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - `, - 'flex-auto': css` - -webkit-box-flex: 1; - -webkit-flex: 1 1 auto; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - min-width: 0; - min-height: 0; - `, - 'flex-inline': css` - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; - `, - 'flex-none': css` - -webkit-box-flex: 0; - -webkit-flex: none; - -ms-flex: none; - flex: none; - `, - 'flex-reverse': css` - -webkit-flex-direction: row-reverse; - flex-direction: row-reverse; - `, - 'flex-wrap': css` - -webkit-flex-wrap: wrap; - -ms-flex-wrap: wrap; - flex-wrap: wrap; - `, - focusable: css` - &:focus { - outline: 0; - box-shadow: inset 0 0 0 2px rgba(0, 0, 0, 0.1); - } - `, - gray: css` - background-color: rgb(102, 102, 102); - `, - grayLight2: css` - background-color: rgb(238, 238, 238); - `, - green: css` - background-color: rgb(32, 201, 51); - `, - 'height-full': css` - height: 100%; - `, - inline: css` - display: inline; - `, - 'inline-block': css` - display: inline-block; - `, - 'items-center': css` - -webkit-box-align: center; - -webkit-align-items: center; - -ms-flex-align: center; - -ms-grid-row-align: center; - align-items: center; - `, - 'justify-center': css` - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; - `, - 'justify-end': css` - -webkit-box-pack: end; - -webkit-justify-content: flex-end; - -ms-flex-pack: end; - justify-content: flex-end; - `, - 'justify-start': css` - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; - `, - 'left-0': css` - left: 0; - `, - 'light-scrollbar': css` - &::-webkit-scrollbar { - width: 12px; - height: 12px; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - } - &::-webkit-scrollbar-button { - display: none; - height: 0; - width: 0; - } - &::-webkit-scrollbar-thumb { - background-color: hsla(0, 0%, 0%, 0.35); - background-clip: padding-box; - border: 3px solid rgba(0, 0, 0, 0); - border-radius: 6px; - min-height: 36px; - } - &::-webkit-scrollbar-thumb:hover { - background-color: hsla(0, 0%, 0%, 0.4); - } - `, - 'line-height-4': css` - line-height: 1.5; - `, - 'link-quiet': css` - &:hover { - opacity: 0.85; - } - `, - 'link-unquiet': css` - &:hover { - opacity: 1; - } - `, - 'link-unquiet-focusable': css` - &:hover { - opacity: 1; - } - &:focus { - opacity: 1; - } - `, - m0: css` - margin: 0; - `, - m2: css` - margin: 1rem; - `, - mb1: css` - margin-bottom: 0.5rem; - `, - ml1: css` - margin-left: 0.5rem; - `, - 'mr-half': css` - margin-right: 0.25rem; - `, - mr1: css` - margin-right: 0.5rem; - `, - mt0: css` - margin-top: 0; - `, - mt1: css` - margin-top: 0.5rem; - `, - mt2: css` - margin-top: 1rem; - `, - 'no-outline': css` - outline: 0; - `, - 'no-user-select': css` - user-select: none; - -webkit-user-select: none; - `, - noevents: css` - pointer-events: none; - `, - normal: css` - font-size: 0.8rem; - `, - nowrap: css` - white-space: nowrap; - `, - 'overflow-auto': css` - overflow: auto; - `, - 'overflow-hidden': css` - overflow: hidden; - `, - 'p-half': css` - padding: 0.25rem; - `, - p1: css` - padding: 0.5rem; - `, - p2: css` - padding: 1rem; - `, - pill: css` - border-radius: 9999px; - `, - pointer: css` - cursor: pointer; - `, - pr1: css` - padding-right: 0.5rem; - `, - 'print-color-exact': css` - -webkit-print-color-adjust: exact; - `, - px1: css` - padding-left: 0.5rem; - padding-right: 0.5rem; - `, - quiet: css` - opacity: 0.75; - `, - quieter: css` - opacity: 0.5; - `, - red: css` - background-color: rgb(248, 43, 96); - `, - relative: css` - position: relative; - `, - 'right-0': css` - right: 0; - `, - rounded: css` - border-radius: 3px; - `, - 'rounded-big': css` - border-radius: 6px; - `, - 'self-end': css` - -webkit-align-self: flex-end; - -ms-flex-item-align: end; - align-self: flex-end; - `, - small: css` - font-size: 11px; - `, - stroked1: css` - box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1); - `, - strong: css` - font-weight: 500; - `, - 'styled-input': css` - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - border-style: solid; - border-width: 2px; - border-color: transparent; - outline: none; - &:active { - border-color: hsla(0, 0%, 0%, 0.25); - } - &:focus { - border-color: hsla(0, 0%, 0%, 0.25); - } - `, - 'text-blue': css` - color: rgb(45, 127, 249); - fill: rgb(45, 127, 249); - `, - 'text-blue-focus': css` - &:focus { - color: rgb(45, 127, 249); - fill: rgb(45, 127, 249); - } - `, - 'text-dark': css` - color: hsl(0, 0%, 20%); - fill: hsl(0, 0%, 20%); - `, - 'text-white': css` - color: hsl(0, 0%, 100%); - fill: hsl(0, 0%, 100%); - `, - 'top-0': css` - top: 0; - `, - truncate: css` - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - `, - understroke: css` - padding-bottom: 0.14rem; - border-bottom: 2px solid hsla(0, 0%, 0%, 0.1); - `, - white: css` - background-color: hsl(0, 0%, 100%); - `, - 'width-full': css` - width: 100%; - `, - yellow: css` - background-color: rgb(252, 180, 0); - `, -}; - -/** - * @internal - */ -export function baymax(baymaxClassNames: string): string { - return baymaxClassNames - .split(/\s+/) - .filter(Boolean) - .map(baymaxClassName => { - if (has(emotionClassNameByBaymaxClassName, baymaxClassName)) { - return emotionClassNameByBaymaxClassName[baymaxClassName]; - } else { - throw spawnError( - 'Baymax class not found: %s. If required, add the definition to baymax_utils.js.`', - baymaxClassName, - ); - } - }) - .join(' '); -} diff --git a/packages/sdk/src/ui/block_wrapper.tsx b/packages/sdk/src/ui/block_wrapper.tsx deleted file mode 100644 index 056752bc9..000000000 --- a/packages/sdk/src/ui/block_wrapper.tsx +++ /dev/null @@ -1,137 +0,0 @@ -/** @hidden */ /** */ -import * as React from 'react'; -import {invariant} from '../error_utils'; -import Sdk from '../sdk'; -import {globalAlert} from './ui'; -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'; - -interface BlockWrapperProps { - sdk: Sdk; - children: React.ReactNode; -} - -class BlockWrapper extends React.Component { - /** @internal */ - _minSizeBeforeRender: {width: number | null; height: number | null} | null = null; - /** @hidden */ - constructor(props: BlockWrapperProps) { - super(props); - - globalAlert.watch('__alertInfo', () => this.forceUpdate()); - } - /** @hidden */ - UNSAFE_componentWillMount() { - this._snapshotMinSizeBeforeRender(); - } - /** @hidden */ - componentDidMount() { - this._checkMinSizeConstraintUnchangedAfterRender(); - } - /** @hidden */ - UNSAFE_componentWillUpdate() { - this._snapshotMinSizeBeforeRender(); - } - /** @hidden */ - componentDidUpdate() { - this._checkMinSizeConstraintUnchangedAfterRender(); - } - /** @internal */ - _snapshotMinSizeBeforeRender() { - this._minSizeBeforeRender = this.props.sdk.viewport.minSize; - } - /** @internal */ - _checkMinSizeConstraintUnchangedAfterRender() { - const prevMinSize = this._minSizeBeforeRender; - invariant(prevMinSize, 'prevMinSize must be set'); - const currentMinSize = this.props.sdk.viewport.minSize; - if ( - currentMinSize.width !== prevMinSize.width || - currentMinSize.height !== prevMinSize.height - ) { - this.forceUpdate(); - } - } - /** @hidden */ - render() { - const {viewport} = this.props.sdk; - - const globalAlertInfo = globalAlert.__alertInfo; - if (globalAlertInfo) { - return ( - - - {globalAlertInfo.content} - - - ); - } - - return ( - - - - - } - > -
    - {this.props.children} -
    -
    - - {/* - TODO: if a modal is presented after we show this viewport - message, it will cover the message. We should fix this by - having this component manage the modal stack, so it can - guarantee that this viewport message is in front of all modals. - */} - {viewport.isSmallerThanMinSize && ( -
    - - Please make this extension bigger or - viewport.enterFullscreenIfPossible()} - > - fullscreen - - -
    - )} -
    - ); - } -} - -export default withHooks<{}, BlockWrapperProps, BlockWrapper>(BlockWrapper, ({sdk}) => { - useWatchable(sdk.viewport, ['size', 'minSize']); - return {}; -}); diff --git a/packages/sdk/src/ui/box.tsx b/packages/sdk/src/ui/box.tsx deleted file mode 100644 index 1d009e812..000000000 --- a/packages/sdk/src/ui/box.tsx +++ /dev/null @@ -1,154 +0,0 @@ -/** @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'; - -/** - * Props for the Box component. Also accepts: - * * {@link AllStylesProps} - * * {@link AriaProps} - * - * @docsPath UI/components/Box - * @noInheritDoc - */ -export interface BoxProps extends AllStylesProps, AriaProps, TooltipAnchorProps { - /** The element that is rendered. Defaults to `div`. */ - as?: - | 'div' - | 'span' - | 'section' - | 'main' - | 'nav' - | 'header' - | 'footer' - | 'aside' - | 'article' - | 'address' - | 'hgroup' - | 'blockquote' - | 'figure' - | 'figcaption' - | 'ol' - | 'ul' - | 'li' - | 'pre'; - /** The contents of the box. */ - children?: React.ReactNode | string; - /** The `tabIndex` attribute. */ - tabIndex?: number; - /** The `role` attribute. */ - role?: string; - /** The `id` attribute. */ - id?: string; - /** Additional class names to apply, separated by spaces. */ - className?: string; - /** Additional styles. */ - style?: React.CSSProperties; - /** Data attributes that are spread onto the element, e.g. `dataAttributes={{'data-*': '...'}}`. */ - dataAttributes?: DataAttributesProp; -} - -/** - * A box component for creating layouts. - * - * [[ Story id="box--example" title="Box example" ]] - * - * @component - * @docsPath UI/components/Box - */ -const Box = (props: BoxProps, ref: React.Ref) => { - const { - as: Component = 'div', - id, - children, - className, - style, - tabIndex, - role, - onClick, - onMouseEnter, - onMouseLeave, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - hasOnClick, - dataAttributes = {}, - 'aria-label': ariaLabel, - 'aria-labelledby': ariaLabelledBy, - 'aria-describedby': ariaDescribedBy, - 'aria-controls': ariaControls, - 'aria-expanded': ariaExpanded, - 'aria-haspopup': ariaHasPopup, - 'aria-hidden': ariaHidden, - 'aria-live': ariaLive, - ...styleProps - } = props; - const classNameForStyleProps = useStyledSystem(styleProps); - - return ( - - {children} - - ); -}; - -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/ui/button.tsx b/packages/sdk/src/ui/button.tsx deleted file mode 100644 index 020b9d8c2..000000000 --- a/packages/sdk/src/ui/button.tsx +++ /dev/null @@ -1,241 +0,0 @@ -/** @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, createPropTypeFromEnum} from '../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 Icon from './icon'; -import cssHelpers from './css_helpers'; -import Box from './box'; - -/** - * Style props for the {@link Button} component. Also accepts: - * * {@link FlexItemSetProps} - * * {@link MarginProps} - * * {@link MaxWidthProps} - * * {@link MinWidthProps} - * * {@link PositionSetProps} - * * {@link WidthProps} - * - * @noInheritDoc - */ -interface ButtonStyleProps - extends MaxWidthProps, - MinWidthProps, - WidthProps, - FlexItemSetProps, - PositionSetProps, - MarginProps { - /** Defines the display type of an element, which consists of the two basic qualities of how an element generates boxes — the outer display type defining how the box participates in flow layout, and the inner display type defining how the children of the box are laid out. */ - display?: OptionalResponsiveProp<'inline-flex' | 'flex' | 'none'>; -} - -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: - * - * • **default** - * - * Gray button for toolbars and other generic actions. - * - * • **primary** - * - * Blue button used for primary actions and CTAs. There should only be one primary button present at a time. Often used in {@link Dialog} and bottom bars. - * - * • **secondary** - * - * Transparent button that pairs with the primary button. This is typically used for cancel or back buttons. - * - * • **danger** - * - * Red button that replaces primary buttons for dangerous or otherwise difficult-to-reverse actions like record deletion. - */ -type ButtonVariant = EnumType; -const ButtonVariant = createEnum('default', 'primary', 'secondary', 'danger'); -const buttonVariantPropType = createPropTypeFromEnum(ButtonVariant); - -/** @internal */ -function useButtonVariant(variant: ButtonVariant = ButtonVariant.default): string { - const {buttonVariants} = useTheme(); - return buttonVariants[variant]; -} - -/** - * Props for the {@link Button} component. Also accepts: - * * {@link AriaProps} - * * {@link ButtonStyleProps} - * - * @noInheritDoc - * @docsPath UI/components/Button - */ -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`. */ - variant?: ButtonVariant; - /** The name of the icon or a React node. For more details, see the {@link IconName|list of supported icons}. */ - icon?: IconName | React.ReactElement; - /** The type of the button. Defaults to `button`. */ - type?: 'button' | 'submit' | 'reset'; - /** The `id` attribute. */ - id?: string; - /** Indicates whether or not the user can interact with the button. */ - disabled?: boolean; - /** Indicates if the button can be focused and if/where it participates in sequential keyboard navigation. */ - tabIndex?: number; - /** The contents of the button. */ - children?: React.ReactNode | string; - /** Click event handler. Also handles Space and Enter keypress events. */ - onClick?: (e?: React.MouseEvent) => unknown; - /** Extra `className`s to apply to the button, separated by spaces. */ - className?: string; - /** Extra styles to apply to the button. */ - style?: React.CSSProperties; - /** The `aria-selected` attribute. */ - 'aria-selected'?: boolean; -} - -/** - * Clickable button component. - * - * [[ Story id="button--example" title="Box example" ]] - * - * @component - * @docsPath UI/components/Button - */ -const Button = (props: ButtonProps, ref: React.Ref) => { - const { - size = ControlSize.default, - variant = ButtonVariant.default, - icon, - id, - className, - style, - onMouseEnter, - onMouseLeave, - onClick, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - hasOnClick, - type = 'button', - disabled, - tabIndex, - children, - 'aria-label': ariaLabel, - 'aria-selected': ariaSelected, - ...styleProps - } = props; - - const classNameForButtonSize = useButtonSize(size); - const classNameForButtonVariant = useButtonVariant(variant); - const classNameForStyleProps = useStyledSystem( - {display: 'inline-flex', ...styleProps}, - styleParser, - ); - const hasIcon = icon !== undefined; - const hasChildren = !!children; - - if (!hasChildren && !ariaLabel) { - // eslint-disable-next-line no-console - console.error(' - ); -}; - -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/ui/cell_renderer.tsx b/packages/sdk/src/ui/cell_renderer.tsx deleted file mode 100644 index 1e748eb3a..000000000 --- a/packages/sdk/src/ui/cell_renderer.tsx +++ /dev/null @@ -1,304 +0,0 @@ -/** @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'; -import {spawnError} from '../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 { - 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 {useSdk} from './sdk_context'; - -/** - * Style props for the {@link CellRenderer} component. Also accepts: - * * {@link FlexItemSetProps} - * * {@link MarginProps} - * * {@link MaxWidthProps} - * * {@link MinWidthProps} - * * {@link PositionSetProps} - * * {@link WidthProps} - * - * @noInheritDoc - */ -interface CellRendererStyleProps - extends FlexItemSetProps, - MarginProps, - MaxWidthProps, - MinWidthProps, - PositionSetProps, - WidthProps { - /** Defines the display type of an element, which consists of the two basic qualities of how an element generates boxes — the outer display type defining how the box participates in flow layout, and the inner display type defining how the children of the box are laid out. */ - display?: OptionalResponsiveProp<'block' | 'inline' | 'inline-block'>; -} - -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} - * - * @docsPath UI/components/CellRenderer - * @noInheritDoc - */ -interface CellRendererProps extends CellRendererStyleProps, TooltipAnchorProps { - /** The {@link Record} from which to render a cell. Either `record` or `cellValue` must be provided to the CellRenderer. If both are provided, `record` will be used. */ - record?: Record | null | undefined; - /** The cell value to render. Either `record` or `cellValue` must be provided to the CellRenderer. If both are provided, `record` will be used. */ - cellValue?: unknown; - /** The {@link Field} for a given {@link Record} being rendered as a cell. */ - field: Field; - /** Whether to wrap cell contents. Defaults to true. */ - shouldWrap?: boolean; - /** Additional class names to apply to the cell renderer container, separated by spaces. */ - className?: string; - /** Additional styles to apply to the cell renderer container. */ - style?: React.CSSProperties; - /** Additional class names to apply to the cell itself, separated by spaces. */ - cellClassName?: string; - /** Additional styles to apply to the cell itself. */ - cellStyle?: React.CSSProperties; - /** Render function if provided and validation fails. */ - renderInvalidCellValue?: (cellValue: unknown, field: Field) => React.ReactElement; - /** @internal injected by withHooks */ - sdk: Sdk; -} - -/** - * Displays the contents of a cell given a field and record. - * - * [[ Story id="cellrenderer--example" title="Cell renderer example" ]] - * - * @component - * @docsPath UI/components/CellRenderer - */ -export class CellRenderer extends React.Component { - /** @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, - }; - - /** @hidden */ - constructor(props: CellRendererProps) { - super(props); - - this._validateProps(props); - } - /** @hidden */ - UNSAFE_componentWillReceiveProps(nextProps: CellRendererProps) { - this._validateProps(nextProps); - } - /** @internal */ - _validateProps(props: CellRendererProps) { - if ( - props.record && - !props.record.isDeleted && - !props.field.isDeleted && - props.record.parentTable.id !== props.field.parentTable.id - ) { - throw spawnError( - 'CellRenderer: record %s and field %s do not have the same parent table', - props.record.parentTable.id, - props.field.parentTable.id, - ); - } - } - /** @hidden */ - render() { - const { - record, - cellValue, - field, - shouldWrap, - onMouseEnter, - onMouseLeave, - onClick, - className, - style, - cellClassName, - cellStyle, - renderInvalidCellValue, - sdk, - } = this.props; - - if (field.isDeleted) { - return null; - } - - const airtableInterface = sdk.__airtableInterface; - const appInterface = sdk.__appInterface; - - let cellValueToRender; - if (record) { - if (cellValue !== undefined) { - // eslint-disable-next-line - console.warn( - 'CellRenderer was given both record and cellValue, choosing to render record value', - ); - } - - if (record.isDeleted) { - return null; - } - - cellValueToRender = record.getCellValue(field.id); - } else { - if (!field.isComputed) { - const validationResult = airtableInterface.fieldTypeProvider.validateCellValueForUpdate( - appInterface, - cellValue, - null, - field._data, - ); - if (!validationResult.isValid) { - if (renderInvalidCellValue) { - return ( -
    - {renderInvalidCellValue(cellValue, field)} -
    - ); - } else { - throw spawnError( - 'Cannot render invalid cell value %s: %s', - cellValue, - validationResult.reason, - ); - } - } - } - - cellValueToRender = cellValue; - } - - if ( - cellValueToRender && - field.type === FieldType.MULTIPLE_LOOKUP_VALUES && - !airtableInterface.sdkInitData.isUsingNewLookupCellValueFormat - ) { - const originalCellValue = cellValueToRender as Array<{ - linkedRecordId: RecordId; - value: unknown; - }>; - const linkedRecordIdsSet = new Set(); - const valuesByLinkedRecordId = {} as ObjectMap>; - - for (const {linkedRecordId, value} of originalCellValue) { - linkedRecordIdsSet.add(linkedRecordId); - if (!valuesByLinkedRecordId[linkedRecordId]) { - valuesByLinkedRecordId[linkedRecordId] = []; - } - valuesByLinkedRecordId[linkedRecordId].push(value); - } - - cellValueToRender = { - linkedRecordIds: Array.from(linkedRecordIdsSet), - valuesByLinkedRecordId, - }; - } - - const {cellValueHtml, attributes} = airtableInterface.fieldTypeProvider.getCellRendererData( - appInterface, - cellValueToRender, - field._data, - !!shouldWrap, - ); - - return ( -
    -
    -
    - ); - } -} - -export default withHooks<{className?: string; sdk: Sdk}, CellRendererProps, CellRenderer>( - CellRenderer, - props => { - const {styleProps, nonStyleProps} = splitStyleProps< - Omit, - CellRendererStyleProps - >(props, styleParser.propNames, {display: 'block'}); - const {className} = nonStyleProps; - const classNameForStyleProps = useStyledSystem( - styleProps, - styleParser, - ); - 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/ui/choice_token.tsx deleted file mode 100644 index d0ef2dbe3..000000000 --- a/packages/sdk/src/ui/choice_token.tsx +++ /dev/null @@ -1,134 +0,0 @@ -/** @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'; -import {Color} from '../colors'; -import {baymax} from './baymax_utils'; -import Box from './box'; -import Text from './text'; -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'; - -/** - * Style props for the {@link ChoiceToken} component. Accepts: - * * {@link FlexItemSetProps} - * * {@link MarginProps} - * * {@link PositionSetProps} - * - * @noInheritDoc - */ -interface ChoiceTokenStyleProps extends FlexItemSetProps, PositionSetProps, MarginProps {} - -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. */ -interface ChoiceOption { - /** The ID of the select option. */ - id?: string; - /** The name of the select option. */ - name: string; - /** The color of the select option. */ - color?: Color; -} - -/** - * Props for the {@link ChoiceToken} component. Also accepts: - * * {@link ChoiceTokenStyleProps} - * - * @docsPath UI/components/ChoiceToken - * @noInheritDoc - */ -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. */ - style?: React.CSSProperties; - /** Additional class names to apply to the choice token. */ - className?: string; -} - -/** - * A component that shows a single choice in a small token, to be displayed inline or in a list of choices. - * - * [[ Story id="choicetoken--example" title="Choice token example" ]] - * - * @component - * @docsPath UI/components/ChoiceToken - */ -const ChoiceToken = (props: ChoiceTokenProps) => { - const { - choice, - onMouseEnter, - onMouseLeave, - onClick, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - hasOnClick, - className, - style, - ...styleProps - } = props; - const classNameForStyleProps = useStyledSystem(styleProps, styleParser); - const color = choice.color || DEFAULT_CHOICE_COLOR; - const textColor = useTextColorForBackgroundColor(color); - - return ( - - - - {choice.name} - - - - ); -}; - -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/ui/collaborator_token.tsx b/packages/sdk/src/ui/collaborator_token.tsx deleted file mode 100644 index a18d51653..000000000 --- a/packages/sdk/src/ui/collaborator_token.tsx +++ /dev/null @@ -1,166 +0,0 @@ -/** @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'; -import {CollaboratorData} from '../types/collaborator'; -import Box from './box'; -import Text from './text'; -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 useBase from './use_base'; - -const UNKNOWN_PROFILE_PIC_URL = - 'https://static.airtable.com/images/userIcons/user_icon_unknown.png'; - -/** - * Style props for the {@link CollaboratorToken} component. Accepts: - * * {@link FlexItemSetProps} - * * {@link MarginProps} - * * {@link PositionSetProps} - * - * @noInheritDoc - */ -interface CollaboratorTokenStyleProps extends FlexItemSetProps, PositionSetProps, MarginProps {} - -const styleParser = compose(flexItemSet, positionSet, margin); - -export const collaboratorTokenStylePropTypes = { - ...flexItemSetPropTypes, - ...positionSetPropTypes, - ...marginPropTypes, -}; - -/** - * Props for the {@link CollaboratorToken} component. Also accepts: - * * {@link CollaboratorTokenStyleProps} - * - * @noInheritDoc - * @docsPath UI/components/CollaboratorToken - */ -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. */ - className?: string; - /** Additional styles to apply to the collaborator token. */ - style?: React.CSSProperties; -} - -/** - * A version of `Collaborator` that doesn't connect to base. Not part of the public API. - * - * @hidden - */ -const StaticCollaboratorToken = (props: CollaboratorTokenProps & {isActive?: boolean}) => { - const { - collaborator, - isActive = true, - onMouseEnter, - onMouseLeave, - onClick, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - hasOnClick, - className, - style, - ...styleProps - } = props; - const classNameForStyledProps = useStyledSystem( - styleProps, - styleParser, - ); - - const userName = collaborator.name || collaborator.email || 'Unknown'; - const profilePicUrl = collaborator.profilePicUrl || UNKNOWN_PROFILE_PIC_URL; - - return ( - - {profilePicUrl && ( - - )} - - {userName} - - - ); -}; - -/** - * A component that shows a single collaborator in a small token, to be displayed inline or in a list of choices. - * - * [[ Story id="collaboratortoken--example" title="Collaborator token example" ]] - * - * @component - * @docsPath UI/components/CollaboratorToken - */ -const CollaboratorToken = (props: CollaboratorTokenProps) => { - const base = useBase(); - - const activeCollaborators = base.activeCollaborators; - const isActive = activeCollaborators.some(activeCollaborator => { - return activeCollaborator.id === props.collaborator.id; - }); - - 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/ui/color_palette.tsx b/packages/sdk/src/ui/color_palette.tsx deleted file mode 100644 index 262ec6a60..000000000 --- a/packages/sdk/src/ui/color_palette.tsx +++ /dev/null @@ -1,291 +0,0 @@ -/** @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'; -import colorUtils from '../color_utils'; -import {invariant} from '../error_utils'; -import {baymax} from './baymax_utils'; -import Box from './box'; -import Icon from './icon'; -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'; - -const MIN_COLOR_SQUARE_SIZE = 16; -const DEFAULT_COLOR_SQUARE_SIZE = 24; -const MAX_COLOR_SQUARE_SIZE = 32; - - -/** - * Style props shared between the {@link ColorPalette} and {@link ColorPaletteSynced} components. Accepts: - * * {@link FlexItemSetProps} - * * {@link MarginProps} - * * {@link MaxWidthProps} - * * {@link MinWidthProps} - * * {@link PositionSetProps} - * * {@link WidthProps} - * - * @noInheritDoc - */ -export interface ColorPaletteStyleProps - extends MaxWidthProps, - MinWidthProps, - WidthProps, - FlexItemSetProps, - PositionSetProps, - MarginProps {} - -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. - */ -export interface SharedColorPaletteProps extends ColorPaletteStyleProps, TooltipAnchorProps { - /** The list of {@link Color|colors} to display in the color palette. */ - allowedColors: Array; - /** A function to be called when the selected color changes. */ - onChange?: (arg1: string) => unknown; - /** The margin between color squares in the color palette. */ - squareMargin?: number; - /** Additional class names to apply to the color palette, separated by spaces. */ - className?: string; - /** Additional styles to apply to the color palette. */ - style?: React.CSSProperties; - /** If set to `true`, the color palette will not allow color selection. */ - 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} - * - * @docsPath UI/components/ColorPalette - */ -interface ColorPaletteProps extends SharedColorPaletteProps { - /** The current selected {@link Color} option. */ - color?: string | null; -} - -/** @hidden */ -interface ColorPaletteState { - squareSize: number; -} - -/** - * A color selection component. Accepts a list of `allowedColors` to be displayed - * as selectable color squares. - * - * [[ Story id="colorpalette--example" title="Color palette example" ]] - * - * @component - * @docsPath UI/components/ColorPalette - */ -export class ColorPalette extends React.Component { - /** @hidden */ - static propTypes = { - color: PropTypes.string, - ...sharedColorPalettePropTypes, - }; - /** @hidden */ - static defaultProps = { - squareMargin: 4, - className: '', - style: {}, - }; - /** @internal */ - _detectElementResize: { - addResizeListener: (element: HTMLElement, fn: () => void) => void; - removeResizeListener: (element: HTMLElement, fn: () => void) => void; - }; - /** @internal */ - _colorPaletteContainerRef: {current: HTMLElement | null}; - /** @hidden */ - constructor(props: ColorPaletteProps) { - super(props); - - this._detectElementResize = createDetectElementResize(); - this._colorPaletteContainerRef = React.createRef(); - this._setColorSquareSize = this._setColorSquareSize.bind(this); - this.state = { - squareSize: DEFAULT_COLOR_SQUARE_SIZE, - }; - } - /** @hidden */ - componentDidMount() { - if (this._colorPaletteContainerRef.current) { - this._detectElementResize.addResizeListener( - this._colorPaletteContainerRef.current, - this._setColorSquareSize, - ); - } - this._setColorSquareSize(); - } - /** @hidden */ - componentWillUnmount() { - if (this._colorPaletteContainerRef.current) { - this._detectElementResize.removeResizeListener( - this._colorPaletteContainerRef.current, - this._setColorSquareSize, - ); - } - } - /** @hidden */ - componentDidUpdate(prevProps: ColorPaletteProps) { - if (this.props.allowedColors.length !== prevProps.allowedColors.length) { - this._setColorSquareSize(); - } - } - /** @internal */ - _setColorSquareSize() { - const {squareMargin} = this.props; - - invariant( - squareMargin !== null && squareMargin !== undefined, - 'colorPalette.squareMargin must be a number', - ); - - invariant(this._colorPaletteContainerRef.current, 'No container to set color square size'); - - const calculateSquareSize = (numSquares: number) => { - return (containerWidth - squareMargin * 2 * numSquares) / numSquares; - }; - const containerWidth = this._colorPaletteContainerRef.current.getBoundingClientRect().width; - const numColors = this.props.allowedColors.length; - const calculatedSquareSize = calculateSquareSize(numColors); - let squareSize; - if (calculatedSquareSize < MIN_COLOR_SQUARE_SIZE) { - const numColorsThatWillFitAsDefaultSize = Math.round( - (containerWidth + 2 * squareMargin) / - (DEFAULT_COLOR_SQUARE_SIZE + 2 * squareMargin), - ); - squareSize = squareSize = - numColorsThatWillFitAsDefaultSize === 0 - ? DEFAULT_COLOR_SQUARE_SIZE - : calculateSquareSize(numColorsThatWillFitAsDefaultSize); - } else { - squareSize = Math.min(MAX_COLOR_SQUARE_SIZE, calculatedSquareSize); - } - this.setState({squareSize}); - } - /** @internal */ - _onChange(color: string) { - if (this.props.onChange) { - this.props.onChange(color); - } - } - /** @hidden */ - render() { - const { - color, - allowedColors, - squareMargin, - onMouseEnter, - onMouseLeave, - onClick, - className, - style, - disabled, - } = this.props; - const {squareSize} = this.state; - - invariant( - squareMargin !== null && squareMargin !== undefined, - 'colorPalette.squareMargin must be a number', - ); - - return ( - - - {allowedColors.map(allowedColor => ( - - ))} - - - ); - } -} - -export default withStyledSystem< - Omit, - ColorPaletteStyleProps, - ColorPalette, - {} ->(ColorPalette, styleParser, colorPaletteStylePropTypes); diff --git a/packages/sdk/src/ui/color_palette_synced.tsx b/packages/sdk/src/ui/color_palette_synced.tsx deleted file mode 100644 index 0d2b18877..000000000 --- a/packages/sdk/src/ui/color_palette_synced.tsx +++ /dev/null @@ -1,75 +0,0 @@ -/** @module @airtable/blocks/ui: ColorPalette */ /** */ -import * as React from 'react'; -import {spawnError} from '../error_utils'; -import {GlobalConfigKey} from '../types/global_config'; -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: - * * {@link ColorPaletteStyleProps} - * - * @docsPath UI/components/ColorPaletteSynced - * @groupPath UI/components/ColorPalette - */ -interface ColorPaletteSyncedProps extends SharedColorPaletteProps { - /** A string key or array key path in {@link GlobalConfig}. The selected color will always reflect the value stored in {@link GlobalConfig} for this key. Selecting a new color will update {@link GlobalConfig}. */ - globalConfigKey: GlobalConfigKey; -} - -/** - * A wrapper around the {@link ColorPalette} component that syncs with {@link GlobalConfig}. - * - * [[ Story id="colorpalette--synced-example" title="Synced color palette example" ]] - * - * @component - * @docsPath UI/components/ColorPaletteSynced - * @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; - return ( - { - let currentColor; - if (typeof value === 'string' || value === null || value === undefined) { - currentColor = value; - } else { - spawnError( - 'ColorPaletteSynced only works with a global config value that is a string, null, or undefined', - ); - } - return ( - { - setValue(newValue); - if (onChange) { - onChange(newValue); - } - }} - disabled={disabled || !canSetValue} - /> - ); - }} - /> - ); - } -} - -export default ColorPaletteSynced; diff --git a/packages/sdk/src/ui/confirmation_dialog.tsx b/packages/sdk/src/ui/confirmation_dialog.tsx deleted file mode 100644 index 8da4a39b8..000000000 --- a/packages/sdk/src/ui/confirmation_dialog.tsx +++ /dev/null @@ -1,154 +0,0 @@ -/** @module @airtable/blocks/ui: Dialog */ /** */ -import PropTypes from 'prop-types'; -import * as React from 'react'; -import Dialog, {DialogStyleProps, dialogStylePropTypes} from './dialog'; -import Heading from './heading'; -import Text from './text'; -import Button from './button'; -import Box from './box'; - -/** - * Props for the {@link ConfirmationDialog} component. Also accepts: - * * {@link DialogStyleProps} - * - * @noInheritDoc - * @docsPath UI/components/ConfirmationDialog - */ -interface ConfirmationDialogProps extends DialogStyleProps { - /** Extra styles to apply to the dialog element. */ - style?: React.CSSProperties; - /** The title of the dialog. */ - title: string; - /** The label for the cancel button. Defaults to 'Cancel'. */ - cancelButtonText?: string; - /** The label for the confirm button. Defaults to 'Confirm'. */ - confirmButtonText?: string; - /** Whether the action is dangerous (potentially destructive or not easily reversible). Defaults to `false`. */ - isConfirmActionDangerous: boolean; - /** Extra `className`s to apply to the dialog element, separated by spaces. */ - className?: string; - /** The body of the dialog. When it’s a string it will automatically be wrapped in a Text component. */ - body?: React.ReactNode | string; - /** Extra `className`s to apply to the background element, separated by spaces. */ - backgroundClassName?: string; - /** Extra styles to apply to the background element. */ - backgroundStyle?: React.CSSProperties; - /** Cancel button event handler. Handles click events and Space/Enter keypress events. */ - onCancel: () => unknown; - /** Confirm button event handler. Handles click events and Space/Enter keypress events. */ - onConfirm: () => unknown; - /** Whether the cancel button can be interacted with. Defaults to `false`. */ - isCancelButtonDisabled?: boolean; - /** Whether the confirm button can be interacted with. Defaults to `false`. */ - isConfirmButtonDisabled?: boolean; -} - -/** - * A styled modal dialog component that prompts the user to confirm or cancel an action. - * - * [[ Story id="confirmationdialog--example" title="Confirmation dialog example" ]] - * - * By default, this component will focus the "Confirm" button on mount, so that pressing - * the Enter key will confirm the action. - * - * @component - * @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', - confirmButtonText: 'Confirm', - isConfirmActionDangerous: false, - isCancelButtonDisabled: false, - isConfirmButtonDisabled: false, - width: '400px', - }; - /** @internal */ - _confirmButtonRef = React.createRef(); - /** @hidden */ - componentDidMount() { - if (this._confirmButtonRef.current !== null) { - this._confirmButtonRef.current.focus(); - } - } - /** @hidden */ - render() { - const { - title, - body, - cancelButtonText, - confirmButtonText, - isConfirmActionDangerous, - className, - style, - backgroundClassName, - backgroundStyle, - onCancel, - onConfirm, - isCancelButtonDisabled, - isConfirmButtonDisabled, - ...restOfProps - } = this.props; - - return ( - - - {title} - {typeof body === 'string' ? {body} : body} - - - - - - ); - } -} - -export default ConfirmationDialog; diff --git a/packages/sdk/src/ui/control_sizes.ts b/packages/sdk/src/ui/control_sizes.ts deleted file mode 100644 index 96287e2a2..000000000 --- a/packages/sdk/src/ui/control_sizes.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** @module @airtable/blocks/ui/system: Control sizes */ /** */ -import {compose, system} from '@styled-system/core'; -import {createEnum, createResponsivePropTypeFromEnum, EnumType} from '../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/'; - -/** - * Sizes for the {@link Button}, {@link Input}, {@link Select}, {@link SelectButtons}, and {@link Switch} components. - */ -export type ControlSize = EnumType; -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 { - const {buttonSizes} = useTheme(); - let styleProps; - if (typeof controlSizeProp === 'string') { - styleProps = buttonSizes[controlSizeProp]; - } else { - styleProps = getStylePropsForResponsiveProp(controlSizeProp, buttonSizes); - } - return useStyledSystem(styleProps); -} - -const selectSizeStyleParser = compose(allStylesParser, system({backgroundPosition: true})); - -/** @internal */ -export function useSelectSize(controlSizeProp: ControlSizeProp): string { - const {selectSizes} = useTheme(); - let styleProps; - if (typeof controlSizeProp === 'string') { - styleProps = selectSizes[controlSizeProp]; - } else { - styleProps = getStylePropsForResponsiveProp(controlSizeProp, selectSizes); - } - return useStyledSystem(styleProps, selectSizeStyleParser); -} - -/** @internal */ -export function useInputSize(controlSizeProp: ControlSizeProp): string { - const {inputSizes} = useTheme(); - let styleProps; - if (typeof controlSizeProp === 'string') { - styleProps = inputSizes[controlSizeProp]; - } else { - styleProps = getStylePropsForResponsiveProp(controlSizeProp, inputSizes); - } - return useStyledSystem(styleProps); -} - -/** @internal */ -export function useSelectButtonsSize(controlSizeProp: ControlSizeProp): string { - const {selectButtonsSizes} = useTheme(); - let styleProps; - if (typeof controlSizeProp === 'string') { - styleProps = selectButtonsSizes[controlSizeProp]; - } else { - styleProps = getStylePropsForResponsiveProp( - controlSizeProp, - selectButtonsSizes, - ); - } - return useStyledSystem(styleProps); -} - -/** @internal */ -export function useSwitchSize(controlSizeProp: ControlSizeProp): string { - const {switchSizes} = useTheme(); - let styleProps; - if (typeof controlSizeProp === 'string') { - styleProps = switchSizes[controlSizeProp]; - } else { - styleProps = getStylePropsForResponsiveProp(controlSizeProp, switchSizes); - } - return useStyledSystem(styleProps); -} diff --git a/packages/sdk/src/ui/create_detect_element_resize.d.ts b/packages/sdk/src/ui/create_detect_element_resize.d.ts deleted file mode 100644 index f5fed6999..000000000 --- a/packages/sdk/src/ui/create_detect_element_resize.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** @hidden */ -interface DetectElementResize { - addResizeListener(element: HTMLElement, callback: () => void): void; - removeResizeListener(element: HTMLElement, callback: () => void): void; -} - -/** @hidden */ -export default function createDetectElementResize(): DetectElementResize; diff --git a/packages/sdk/src/ui/create_detect_element_resize.js b/packages/sdk/src/ui/create_detect_element_resize.js deleted file mode 100644 index a03112bf9..000000000 --- a/packages/sdk/src/ui/create_detect_element_resize.js +++ /dev/null @@ -1,233 +0,0 @@ -// @ts-nocheck - -/** - * @internal - */ -export default function createDetectElementResize(nonce) { - var _window; - if (typeof window !== 'undefined') { - _window = window; - } else if (typeof self !== 'undefined') { - _window = self; - } else { - _window = this; // eslint-disable-line consistent-this - } - - var attachEvent = typeof document !== 'undefined' && document.attachEvent; - - var animationName; - var animationKeyframes; - var animationStyle; - var resetTriggers; - var checkTriggers; - var scrollListener; - var animation; - var keyframeprefix; - var animationstartevent; - var domPrefixes; - var startEvents; - var pfx; - if (!attachEvent) { - var requestFrame = (function() { - var raf = - _window.requestAnimationFrame || - _window.mozRequestAnimationFrame || - _window.webkitRequestAnimationFrame || - function(fn) { - return _window.setTimeout(fn, 20); - }; - return function(fn) { - return raf(fn); - }; - })(); - - var cancelFrame = (function() { - var cancel = - _window.cancelAnimationFrame || - _window.mozCancelAnimationFrame || - _window.webkitCancelAnimationFrame || - _window.clearTimeout; - return function(id) { - return cancel(id); - }; - })(); - - resetTriggers = function(element) { - var triggers = element.__resizeTriggers__, - expand = triggers.firstElementChild, - contract = triggers.lastElementChild, - expandChild = expand.firstElementChild; - contract.scrollLeft = contract.scrollWidth; - contract.scrollTop = contract.scrollHeight; - expandChild.style.width = expand.offsetWidth + 1 + 'px'; - expandChild.style.height = expand.offsetHeight + 1 + 'px'; - expand.scrollLeft = expand.scrollWidth; - expand.scrollTop = expand.scrollHeight; - }; - - checkTriggers = function(element) { - return ( - element.offsetWidth !== element.__resizeLast__.width || - element.offsetHeight !== element.__resizeLast__.height - ); - }; - - scrollListener = function(e) { - if ( - e.target.className.indexOf('contract-trigger') < 0 && - e.target.className.indexOf('expand-trigger') < 0 - ) { - return; - } - - var element = this; // eslint-disable-line consistent-this - resetTriggers(this); - if (this.__resizeRAF__) { - cancelFrame(this.__resizeRAF__); - } - this.__resizeRAF__ = requestFrame(function() { - if (checkTriggers(element)) { - element.__resizeLast__.width = element.offsetWidth; - element.__resizeLast__.height = element.offsetHeight; - element.__resizeListeners__.forEach(function(fn) { - fn.call(element, e); - }); - } - }); - }; - - /* Detect CSS Animations support to detect element display/re-attach */ - animation = false; - keyframeprefix = ''; - animationstartevent = 'animationstart'; - domPrefixes = 'Webkit Moz O ms'.split(' '); - startEvents = 'webkitAnimationStart animationstart oAnimationStart MSAnimationStart'.split( - ' ', - ); - pfx = ''; - - // eslint-disable-next-line no-lone-blocks - { - var elm = document.createElement('fakeelement'); - if (elm.style.animationName !== undefined) { - animation = true; - } - - if (animation === false) { - for (var i = 0; i < domPrefixes.length; i++) { - if (elm.style[domPrefixes[i] + 'AnimationName'] !== undefined) { - pfx = domPrefixes[i]; - keyframeprefix = '-' + pfx.toLowerCase() + '-'; - animationstartevent = startEvents[i]; - animation = true; - break; - } - } - } - } - - animationName = 'resizeanim'; - animationKeyframes = - '@' + - keyframeprefix + - 'keyframes ' + - animationName + - ' { from { opacity: 0; } to { opacity: 0; } } '; - animationStyle = keyframeprefix + 'animation: 1ms ' + animationName + '; '; - } - - var createStyles = function() { - if (!document.getElementById('detectElementResize')) { - var css = - (animationKeyframes ? animationKeyframes : '') + - '.resize-triggers { ' + - (animationStyle ? animationStyle : '') + - 'visibility: hidden; opacity: 0; } ' + - '.resize-triggers, .resize-triggers > div, .contract-trigger:before { content: " "; display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; z-index: -1; } .resize-triggers > div { background: #eee; overflow: auto; } .contract-trigger:before { width: 200%; height: 200%; }', - head = document.head || document.getElementsByTagName('head')[0], - style = document.createElement('style'); - - style.id = 'detectElementResize'; - style.type = 'text/css'; - - if (nonce !== null) { - style.setAttribute('nonce', nonce); - } - - if (style.styleSheet) { - style.styleSheet.cssText = css; - } else { - style.appendChild(document.createTextNode(css)); - } - - head.appendChild(style); - } - }; - - var addResizeListener = function(element, fn) { - if (attachEvent) { - element.attachEvent('onresize', fn); - } else { - if (!element.__resizeTriggers__) { - var elementStyle = _window.getComputedStyle(element); - if (elementStyle && elementStyle.position === 'static') { - element.style.position = 'relative'; - } - createStyles(); - element.__resizeLast__ = {}; - element.__resizeListeners__ = []; - (element.__resizeTriggers__ = document.createElement('div')).className = - 'resize-triggers'; - element.__resizeTriggers__.innerHTML = - '
    ' + - '
    '; - element.appendChild(element.__resizeTriggers__); - resetTriggers(element); - element.addEventListener('scroll', scrollListener, true); - - /* Listen for a css animation to detect element display/re-attach */ - if (animationstartevent) { - element.__resizeTriggers__.__animationListener__ = function animationListener( - e, - ) { - if (e.animationName === animationName) { - resetTriggers(element); - } - }; - element.__resizeTriggers__.addEventListener( - animationstartevent, - element.__resizeTriggers__.__animationListener__, - ); - } - } - element.__resizeListeners__.push(fn); - } - }; - - var removeResizeListener = function(element, fn) { - if (attachEvent) { - element.detachEvent('onresize', fn); - } else { - element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1); - if (!element.__resizeListeners__.length) { - element.removeEventListener('scroll', scrollListener, true); - if (element.__resizeTriggers__.__animationListener__) { - element.__resizeTriggers__.removeEventListener( - animationstartevent, - element.__resizeTriggers__.__animationListener__, - ); - element.__resizeTriggers__.__animationListener__ = null; - } - try { - element.__resizeTriggers__ = !element.removeChild(element.__resizeTriggers__); - } catch (e) { - } - } - } - }; - - return { - addResizeListener, - removeResizeListener, - }; -} diff --git a/packages/sdk/src/ui/css_helpers.ts b/packages/sdk/src/ui/css_helpers.ts deleted file mode 100644 index 2dc596620..000000000 --- a/packages/sdk/src/ui/css_helpers.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {css} from 'emotion'; - -const cssHelpers = { - TRUNCATE: css({ - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - }), - VISUALLY_HIDDEN: css({ - position: 'absolute', - height: '1px', - width: '1px', - overflow: 'hidden', - clip: ['rect(1px, 1px, 1px, 1px)', 'rect(1px 1px 1px 1px)'] /* fallback for IE6, IE7 */, - whiteSpace: 'nowrap' /* added line */, - }), -}; - -export default cssHelpers; diff --git a/packages/sdk/src/ui/dialog.tsx b/packages/sdk/src/ui/dialog.tsx deleted file mode 100644 index 86f16ceb1..000000000 --- a/packages/sdk/src/ui/dialog.tsx +++ /dev/null @@ -1,143 +0,0 @@ -/** @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 {OptionalResponsiveProp} from './system/utils/types'; - -/** - * Style props shared between the {@link Dialog} and {@link ConfirmationDialog} components. Also accepts: - * * {@link DimensionsSetProps} - * * {@link FlexContainerSetProps} - * * {@link SpacingSetProps} - * - * @noInheritDoc - */ - -export interface DialogStyleProps - extends DimensionsSetProps, - FlexContainerSetProps, - SpacingSetProps { - /** Defines the display type of an element, which consists of the two basic qualities of how an element generates boxes — the outer display type defining how the box participates in flow layout, and the inner display type defining how the children of the box are laid out. */ - display?: OptionalResponsiveProp<'block' | 'flex'>; -} - -export const dialogStylePropTypes = { - ...dimensionsSetPropTypes, - ...displayPropTypes, - ...flexContainerSetPropTypes, - ...spacingSetPropTypes, -}; - -/** - * Props for the {@link Dialog} component. Also accepts: - * * {@link DialogStyleProps} - * - * @noInheritDoc - * @docsPath UI/components/Dialog - */ -interface DialogProps extends DialogStyleProps { - /** Callback function to fire when the dialog is closed. */ - onClose: () => unknown; - /** Extra `className`s to apply to the dialog element, separated by spaces. */ - className?: string; - /** Extra styles to apply to the dialog element. */ - style?: React.CSSProperties; - /** Extra `className`s to apply to the background element, separated by spaces. */ - backgroundClassName?: string; - /** Extra styles to apply to the background element. */ - backgroundStyle?: React.CSSProperties; - /** The contents of the dialog element. */ - children: React.ReactNode; -} - -/** - * A styled modal dialog component. - * - * [[ Story id="dialog--example" title="Dialog example" ]] - * - * @docsPath UI/components/Dialog - * @component - */ - -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); - } - /** @hidden */ - componentWillUnmount() { - window.removeEventListener('keydown', this._onKeyDown, false); - } - /** @internal */ - _onKeyDown(e: KeyboardEvent) { - if (e.key === 'Escape') { - this.props.onClose(); - } - } - /** @hidden */ - render() { - const { - onClose, - className, - style, - backgroundClassName, - backgroundStyle, - children, - ...restOfProps - } = this.props; - - return ( - - {children} - - ); - } -} - -export default Dialog; diff --git a/packages/sdk/src/ui/dialog_close_button.tsx b/packages/sdk/src/ui/dialog_close_button.tsx deleted file mode 100644 index 7855a3669..000000000 --- a/packages/sdk/src/ui/dialog_close_button.tsx +++ /dev/null @@ -1,170 +0,0 @@ -/** @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'; -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 Icon from './icon'; - -/** - * Props for the {@link DialogCloseButton} component. Also accepts: - * * {@link DialogCloseButtonStyleProps} - * - * @noInheritDoc - */ -export interface DialogCloseButtonProps extends TooltipAnchorProps { - /** `className`s to apply to the close button, separated by spaces. */ - className?: string; - /** Styles to apply to the close button. */ - style?: React.CSSProperties; - /** Indicates if the button can be focused and if/where it participates in sequential keyboard navigation. */ - tabIndex?: number; - /** The contents of the close button. */ - children?: React.ReactNode | string; -} - -/** - * Style props for the {@link DialogCloseButton} component. Accepts: - * * {@link BorderRadiusProps} - * * {@link DimensionsSetProps} - * * {@link DisplayProps} - * * {@link FlexContainerSetProps} - * * {@link FlexItemSetProps} - * * {@link PositionSetProps} - * * {@link SpacingSetProps} - * - * @noInheritDoc - */ -export interface DialogCloseButtonStyleProps - extends BorderRadiusProps, - DimensionsSetProps, - DisplayProps, - FlexContainerSetProps, - FlexItemSetProps, - PositionSetProps, - SpacingSetProps {} - -const styleParser = compose( - borderRadius, - dimensionsSet, - display, - flexContainerSet, - flexItemSet, - positionSet, - 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); - } - this.context.onDialogClose(); - } - /** @internal */ - _onKeyDown(e: React.KeyboardEvent) { - if (e.ctrlKey || e.altKey || e.metaKey) { - return; - } - if (['Enter', ' '].includes(e.key)) { - e.preventDefault(); - this.context.onDialogClose(); - } - } - /** @hidden */ - render() { - const {onMouseEnter, onMouseLeave, className, style, tabIndex, children} = this.props; - return ( -
    - {children ? children : } -
    - ); - } -} - -export default withStyledSystem< - DialogCloseButtonProps, - DialogCloseButtonStyleProps, - DialogCloseButton, - {} ->(DialogCloseButton, styleParser, dialogCloseButtonStylePropTypes, { - position: 'absolute', - top: 0, - right: 0, - marginTop: 2, - marginRight: 2, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - width: '24px', - height: '24px', - borderRadius: 'circle', -}); diff --git a/packages/sdk/src/ui/field_icon.tsx b/packages/sdk/src/ui/field_icon.tsx deleted file mode 100644 index ed58a7304..000000000 --- a/packages/sdk/src/ui/field_icon.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/** @module @airtable/blocks/ui: FieldIcon */ /** */ -import PropTypes from 'prop-types'; -import * as React from 'react'; -import Field from '../models/field'; -import Icon, {sharedIconPropTypes, SharedIconProps} from './icon'; -import {IconName} from './icon_config'; -import {useSdk} from './sdk_context'; - -/** - * Props for the {@link FieldIcon} component. Also accepts: - * * {@link IconStyleProps} - * - * @docsPath UI/components/FieldIcon - */ - -interface FieldIconProps extends SharedIconProps { - /** The field model to display an icon for. */ - field: Field; -} - -/** - * A vector icon for a field’s type. - * - * [[ Story id="fieldicon--example" title="FieldIcon example" ]] - * - * @docsPath UI/components/FieldIcon - * @component - */ -const FieldIcon = (props: FieldIconProps) => { - const {field, ...restOfProps} = props; - const sdk = useSdk(); - - const airtableInterface = sdk.__airtableInterface; - const appInterface = sdk.__appInterface; - - const uiConfig = airtableInterface.fieldTypeProvider.getUiConfig(appInterface, field._data); - - const name = uiConfig.iconName; - return ; -}; - -FieldIcon.propTypes = { - field: PropTypes.instanceOf(Field).isRequired, - ...sharedIconPropTypes, -}; - -export default FieldIcon; diff --git a/packages/sdk/src/ui/field_picker.tsx b/packages/sdk/src/ui/field_picker.tsx deleted file mode 100644 index 2abe62037..000000000 --- a/packages/sdk/src/ui/field_picker.tsx +++ /dev/null @@ -1,132 +0,0 @@ -/** @module @airtable/blocks/ui: FieldPicker */ /** */ -import PropTypes from 'prop-types'; -import * as React from 'react'; -import {values, ObjectMap, has} from '../private_utils'; -import Field from '../models/field'; -import Table from '../models/table'; -import {FieldType} from '../types/field'; -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. - */ -export interface SharedFieldPickerProps extends SharedSelectBaseProps { - /** The parent table model to select fields from. If `null` or `undefined`, the picker won't render. */ - table?: Table | null; - /** An array indicating which field types can be selected. */ - allowedTypes?: Array; - /** If set to `true`, the user can unset the selected field. */ - shouldAllowPickingNone?: boolean; - /** The placeholder text when no field is selected. */ - placeholder?: string; - /** A function to be called when the selected field changes. */ - 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} - * - * @docsPath UI/components/FieldPicker - */ -interface FieldPickerProps extends SharedFieldPickerProps { - /** The selected field model. */ - field?: Field | null; -} - -/** - * Dropdown menu component for selecting fields. - * - * [[ Story id="modelpickers--fieldpicker-example" title="Field picker example" ]] - * - * @docsPath UI/components/FieldPicker - * @component - */ -const FieldPicker = (props: FieldPickerProps, ref: React.Ref) => { - const { - table, - field: selectedField, - shouldAllowPickingNone, - allowedTypes, - placeholder, - onChange, - ...restOfProps - } = props; - const sdk = useSdk(); - - useWatchable(sdk.base, ['tables']); - useWatchable(table, ['fields']); - - if (!table || table.isDeleted) { - return null; - } - - function _onChange(fieldId: string | null) { - if (onChange) { - const field = - table && !table.isDeleted && fieldId ? table.getFieldByIdIfExists(fieldId) : null; - onChange(field); - } - } - - let placeholderToUse; - if (placeholder === undefined) { - placeholderToUse = shouldAllowPickingNone ? 'None' : 'Pick a field...'; - } else { - placeholderToUse = placeholder; - } - - const allowedTypesSet = {} as ObjectMap; - if (allowedTypes) { - for (const allowedType of allowedTypes) { - allowedTypesSet[allowedType] = true; - } - } - const shouldAllowPickingFieldFn = (field: Field) => { - return !allowedTypes || has(allowedTypesSet, field.type); - }; - - const models = table.fields - .filter(field => field !== table.primaryField) - .sort((a, b) => { - return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; - }); - models.unshift(table.primaryField); - - return ( - - ); -}; - -const ForwardedRefFieldPicker = React.forwardRef(FieldPicker); - -ForwardedRefFieldPicker.displayName = 'FieldPicker'; - -ForwardedRefFieldPicker.propTypes = { - field: PropTypes.instanceOf(Field), - ...sharedFieldPickerPropTypes, -}; - -export default ForwardedRefFieldPicker; diff --git a/packages/sdk/src/ui/field_picker_synced.tsx b/packages/sdk/src/ui/field_picker_synced.tsx deleted file mode 100644 index 9e59fa4f8..000000000 --- a/packages/sdk/src/ui/field_picker_synced.tsx +++ /dev/null @@ -1,75 +0,0 @@ -/** @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 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: - * * {@link SelectStyleProps} - * - * @docsPath UI/components/FieldPickerSynced - * @groupPath UI/components/FieldPicker - */ -interface FieldPickerSyncedProps extends SharedFieldPickerProps { - /** A string key or array key path in {@link GlobalConfig}. The selected field will always reflect the field id stored in {@link GlobalConfig} for this key. Selecting a new field will update {@link GlobalConfig}. */ - globalConfigKey: GlobalConfigKey; -} - -/** - * A wrapper around the {@link FieldPicker} component that syncs with {@link GlobalConfig}. - * - * [[ Story id="modelpickers--fieldpickersynced-example" title="Synced field picker example" ]] - * - * @docsPath UI/components/FieldPickerSynced - * @groupPath UI/components/FieldPicker - * @component - */ -const FieldPickerSynced = (props: FieldPickerSyncedProps, ref: React.Ref) => { - const {globalConfigKey, onChange, disabled, table, ...restOfProps} = props; - const [fieldId, setFieldId, canSetFieldId] = useSynced(globalConfigKey); - const sdk = useSdk(); - - useWatchable(sdk.base, ['tables']); - useWatchable(table, ['fields']); - - function _getFieldFromGlobalConfigValue(): Field | null { - if (!table || table.isDeleted) { - return null; - } - return typeof fieldId === 'string' && table ? table.getFieldByIdIfExists(fieldId) : null; - } - - return ( - { - setFieldId(field ? field.id : null); - if (onChange) { - onChange(field); - } - }} - disabled={disabled || !canSetFieldId} - /> - ); -}; - -const ForwardedRefFieldPickerSynced = React.forwardRef( - FieldPickerSynced, -); - -ForwardedRefFieldPickerSynced.displayName = 'FieldPickerSynced'; - -ForwardedRefFieldPickerSynced.propTypes = { - globalConfigKey: globalConfigSyncedComponentHelpers.globalConfigKeyPropType, - ...sharedFieldPickerPropTypes, -}; - -export default ForwardedRefFieldPickerSynced; diff --git a/packages/sdk/src/ui/form_field.tsx b/packages/sdk/src/ui/form_field.tsx deleted file mode 100644 index ef2375982..000000000 --- a/packages/sdk/src/ui/form_field.tsx +++ /dev/null @@ -1,169 +0,0 @@ -/** @module @airtable/blocks/ui: FormField */ /** */ -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 Box from './box'; -import Text, {TextSize} from './text'; -import Label from './label'; -import {FormFieldContext} from './use_form_field'; -import useStyledSystem from './use_styled_system'; -import { - maxWidth, - maxWidthPropTypes, - MaxWidthProps, - minWidth, - minWidthPropTypes, - MinWidthProps, - width, - widthPropTypes, - WidthProps, - flexItemSet, - flexItemSetPropTypes, - FlexItemSetProps, - positionSet, - positionSetPropTypes, - PositionSetProps, - spacingSet, - spacingSetPropTypes, - SpacingSetProps, -} from './system'; - -/** - * Style props for the {@link FormField} component. Accepts: - * * {@link FlexItemSetProps} - * * {@link MaxWidthProps} - * * {@link MinWidthProps} - * * {@link PositionSetProps} - * * {@link SpacingSetProps} - * * {@link WidthProps} - * - * @noInheritDoc - */ -interface FormFieldStyleProps - extends MaxWidthProps, - MinWidthProps, - WidthProps, - FlexItemSetProps, - PositionSetProps, - SpacingSetProps {} - -const styleParser = compose(maxWidth, minWidth, width, flexItemSet, positionSet, spacingSet); - -export const formFieldStylePropTypes = { - ...maxWidthPropTypes, - ...minWidthPropTypes, - ...widthPropTypes, - ...flexItemSetPropTypes, - ...positionSetPropTypes, - ...spacingSetPropTypes, -}; - -/** - * Props for the {@link FormField} component. Also accepts: - * * {@link FormFieldStyleProps} - * - * @noInheritDoc - * @docsPath UI/components/FormField - */ -interface FormFieldProps extends FormFieldStyleProps { - /** The `id` attribute. */ - id?: string; - /** Additional class names to apply to the form field. */ - className?: string; - /** Additional styles to apply to the form field. */ - style?: React.CSSProperties; - /** The label content for the form field. */ - label?: React.ReactNode; - /** The `for` attribute to be applied to the inner label. By default, the form field will automatically generate a random ID and set it on both the label and the wrapped input/select. Only use this property if you want to override the generated ID with your own custom ID. */ - htmlFor?: string; - /** The description content for the form field. Displayed beneath the label and above the wrapped control field. */ - description?: React.ReactNode | string | null; - /** The contents of the form field. */ - children?: React.ReactNode; -} - -/** - * A form field component that wraps any control field, supplying a provided label and optional - * description. - * - * [[ Story id="formfield--example" title="Form field example" ]] - * - * This component will automatically set up the `for` attribute on the outputted label with the `id` attribute - * on the wrapped control field for the following UI components: Label, Select, FieldPicker, - * ModelPicker, and ViewPicker. If you'd like to manually override this behavior, you can provide an - * `htmlFor` prop to this component and manually set the `id` attribute on your wrapped control to - * the same value. - * - * @docsPath UI/components/FormField - * @component - */ -const FormField = (props: FormFieldProps, ref: React.Ref) => { - const { - id, - className, - style, - label = TextSize.default, - htmlFor, - description, - children, - ...styleProps - } = props; - const classNameForStyleProps = useStyledSystem({width: '100%', ...styleProps}, styleParser); - const [generatedId] = useState(getLocallyUniqueId('form-field-')); - const controlId = htmlFor || generatedId; - const [generatedDescriptionId] = useState(getLocallyUniqueId('input-description-')); - const descriptionId = description ? generatedDescriptionId : ''; - - let optionalLabelProps; - if (description) { - optionalLabelProps = {marginBottom: 1}; - } - return ( - - - {description && ( - - {description} - - )} - - {children} - - - ); -}; - -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/ui/geometry/geometry.ts b/packages/sdk/src/ui/geometry/geometry.ts deleted file mode 100644 index 1cb650efb..000000000 --- a/packages/sdk/src/ui/geometry/geometry.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Everything in Geometry is lifted from Hyperbase. - * Once we refactor/deprecate Popover we can deprecate this as well. - * - * @internal - */ /** */ -export {default as Rect} from './rect'; -export {default as Size} from './size'; -export {default as Point} from './point'; diff --git a/packages/sdk/src/ui/geometry/point.ts b/packages/sdk/src/ui/geometry/point.ts deleted file mode 100644 index c974b7d23..000000000 --- a/packages/sdk/src/ui/geometry/point.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** @internal */ -export default class Point { - /** @internal */ - x: number; - /** @internal */ - y: number; - - /** @internal */ - constructor(x: number, y: number) { - this.x = x; - this.y = y; - } -} diff --git a/packages/sdk/src/ui/geometry/rect.ts b/packages/sdk/src/ui/geometry/rect.ts deleted file mode 100644 index 3c6efa55a..000000000 --- a/packages/sdk/src/ui/geometry/rect.ts +++ /dev/null @@ -1,50 +0,0 @@ -import Point from './point'; - -/** @internal */ -export default class Rect { - /** @internal */ - x: number; - /** @internal */ - y: number; - /** @internal */ - width: number; - /** @internal */ - height: number; - - /** @internal */ - constructor(x: number, y: number, width: number, height: number) { - this.x = x; - this.y = y; - this.width = width; - this.height = height; - } - /** @internal */ - growBy(amount: number) { - return new Rect( - this.x - amount, - this.y - amount, - this.width + 2 * amount, - this.height + 2 * amount, - ); - } - /** @internal */ - top(): number { - return this.y; - } - /** @internal */ - left(): number { - return this.x; - } - /** @internal */ - right(): number { - return this.x + this.width; - } - /** @internal */ - bottom(): number { - return this.y + this.height; - } - /** @internal */ - centerPoint(): Point { - return new Point(this.x + this.width / 2, this.y + this.height / 2); - } -} diff --git a/packages/sdk/src/ui/geometry/size.ts b/packages/sdk/src/ui/geometry/size.ts deleted file mode 100644 index b2c1972e4..000000000 --- a/packages/sdk/src/ui/geometry/size.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** @internal */ -export default class Size { - /** @internal */ - width: number; - /** @internal */ - height: number; - - /** @internal */ - constructor(width: number, height: number) { - this.width = width; - this.height = height; - } -} diff --git a/packages/sdk/src/ui/heading.tsx b/packages/sdk/src/ui/heading.tsx deleted file mode 100644 index ca23366be..000000000 --- a/packages/sdk/src/ui/heading.tsx +++ /dev/null @@ -1,195 +0,0 @@ -/** @module @airtable/blocks/ui: Heading */ /** */ -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 useStyledSystem from './use_styled_system'; -import {allStylesPropTypes, 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'; - -/** - * Sizes for the {@link Heading} component. - */ -type HeadingSize = EnumType; -const HeadingSize = createEnum('xsmall', 'small', 'default', 'large', 'xlarge', 'xxlarge'); - -/** - * Size prop for the {@link Heading} component. - */ -type HeadingSizeProp = ResponsiveProp; -const headingSizePropType = createResponsivePropTypeFromEnum(HeadingSize); - -/** - * Variant prop for the {@link Heading} component. - * • **default** - Headings typically used for titles. - * • **caps** - All-caps headings typically used for field names and smaller section headings. - */ -type HeadingVariant = EnumType; -const HeadingVariant = createEnum('default', 'caps'); -const headingVariantPropType = createPropTypeFromEnum(HeadingVariant); - -/** @internal */ -function warnIfHeadingSizeOutOfRangeForVariant( - size: HeadingSize, - variant: HeadingVariant, - headingSizesForVariant: {[Size in HeadingSize]?: object}, -): void { - if (process.env.NODE_ENV === 'development' && !has(headingSizesForVariant, size)) { - // eslint-disable-next-line - console.warn( - `[@airtable/blocks/ui] warning: only supports the following values for the size prop: ${Object.keys( - headingSizesForVariant, - ).join(', ')}. The component will fall back to the default size.`, - ); - } -} - -/** @internal */ -function useHeadingStyle(headingSizeProp: HeadingSizeProp, variant: HeadingVariant): string { - const {headingStyles} = useTheme(); - const headingSizesForVariant = headingStyles[variant]; - - let styleProps; - if (typeof headingSizeProp === 'string') { - warnIfHeadingSizeOutOfRangeForVariant(headingSizeProp, variant, headingSizesForVariant); - styleProps = (headingSizesForVariant[headingSizeProp] || - headingSizesForVariant[HeadingSize.default]) as Partial; - } else { - const responsiveSizePropObject = {} as ObjectMap; - for (const sizeKey of keys(headingSizeProp)) { - const sizeProp = headingSizeProp[sizeKey]; - invariant(sizeProp, 'sizeProp'); - - warnIfHeadingSizeOutOfRangeForVariant(sizeProp, variant, headingSizesForVariant); - responsiveSizePropObject[sizeKey] = sizeProp || HeadingSize.default; - } - - styleProps = getStylePropsForResponsiveProp( - responsiveSizePropObject, - headingSizesForVariant as any, - ); - } - - return useStyledSystem(styleProps); -} - -/** - * Props for the {@link Heading} component. Also supports: - * * {@link AllStylesProps} - * * {@link AriaProps} - * - * @docsPath UI/components/Heading - * @noInheritDoc - */ -interface HeadingProps extends AllStylesProps, AriaProps { - /** The `role` attribute. */ - role?: string; - /** The element that is rendered. Defaults to `h3`. */ - as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; - /** The variant of the heading. Defaults to `default`. */ - variant?: HeadingVariant; - /** The contents of the heading. */ - children?: React.ReactNode | string; - /** The `id` attribute. */ - id?: string; - /** The size of the heading. Defaults to `default`. Can be a responsive prop object. */ - size?: HeadingSizeProp; - /** Data attributes that are spread onto the element, e.g. `dataAttributes={{'data-*': '...'}}`. */ - dataAttributes?: DataAttributesProp; - /** Additional class names to apply, separated by spaces. */ - className?: string; - /** Additional styles. */ - style?: React.CSSProperties; -} - -/** - * A heading component with sizes and variants. - * - * [[ Story id="heading--example" title="Heading example" ]] - * - * @docsPath UI/components/Heading - * @component - */ -const Heading = (props: HeadingProps, ref: React.Ref) => { - const { - as: Component = 'h3', - size = HeadingSize.default, - variant = HeadingVariant.default, - children, - id, - role, - dataAttributes, - className, - style, - 'aria-label': ariaLabel, - 'aria-labelledby': ariaLabelledBy, - 'aria-describedby': ariaDescribedBy, - 'aria-controls': ariaControls, - 'aria-expanded': ariaExpanded, - 'aria-haspopup': ariaHasPopup, - 'aria-hidden': ariaHidden, - 'aria-live': ariaLive, - ...styleProps - } = props; - - const classNameForHeadingSize = useHeadingStyle(size, variant); - const classNameForStyleProps = useStyledSystem({ - fontFamily: 'default', - textColor: 'default', - ...styleProps, - }); - - return ( - - {children} - - ); -}; - -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/ui/icon.tsx b/packages/sdk/src/ui/icon.tsx deleted file mode 100644 index de63e0531..000000000 --- a/packages/sdk/src/ui/icon.tsx +++ /dev/null @@ -1,174 +0,0 @@ -/** @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 '../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 { - iconNamePropType, - IconName, - legacyIconNameToPhosphorIconName, - phosphorIconConfig, - deprecatedIconNameToReplacementName, -} from './icon_config'; - -/** - * Style props shared between the {@link Icon} and {@link FieldIcon} components. Accepts: - * * {@link FlexItemSetProps} - * * {@link PositionSetProps} - * * {@link MarginProps} - * - * @noInheritDoc - */ -export interface IconStyleProps extends FlexItemSetProps, PositionSetProps, MarginProps {} - -const styleParser = compose(flexItemSet, positionSet, margin, width, height); - -export const iconStylePropTypes = { - ...flexItemSetPropTypes, - ...positionSetPropTypes, - ...marginPropTypes, -}; - -/** - * Props shared between the {@link Icon} and {@link FieldIcon} components. - * - * @noInheritDoc - */ - -export interface SharedIconProps extends IconStyleProps, TooltipAnchorProps { - /** The width/height of the icon. Defaults to 16. */ - size?: number | string; - /** The color of the icon. */ - fillColor?: string; - /** Additional class names to apply to the icon. */ - className?: string; - /** Additional styles to apply to the icon. */ - style?: React.CSSProperties; - /** Additional class names to apply to the icon path. */ - pathClassName?: string; - /** Additional styles to apply to the icon path. */ - pathStyle?: React.CSSProperties; -} - -export const sharedIconPropTypes = { - size: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - fillColor: PropTypes.string, - className: PropTypes.string, - style: PropTypes.object, - pathClassName: PropTypes.string, - pathStyle: PropTypes.object, - ...tooltipAnchorPropTypes, - ...iconStylePropTypes, -}; - -/** - * Props for the {@link Icon} component. Also accepts: - * * {@link IconStyleProps} - */ -interface IconProps extends SharedIconProps { - /** The name of the icon. For more details, see the {@link IconName|list of supported icons}. */ - name: IconName; - /** @internal */ - suppressWarning?: boolean; -} - -/** - * A vector icon from the Airtable icon set. - * - * [[ Story id="icon--example" title="Icon example" height="576px"]] - * - * @component - * @docsPath UI/components/Icon - */ -const Icon = (props: IconProps, ref: React.Ref) => { - const { - name, - size = 16, - fillColor = 'currentColor', - onMouseEnter, - onMouseLeave, - onClick, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - hasOnClick, - className, - style, - pathClassName, - pathStyle, - suppressWarning = false, - ...styleProps - } = props; - - const classNameForStyleProps = useStyledSystem( - {...styleProps, width: size, height: size}, - styleParser, - ); - - const phosphorIconName = legacyIconNameToPhosphorIconName[name]; - const pathData = phosphorIconName ? phosphorIconConfig[phosphorIconName] : null; - - if (!pathData) { - return null; - } - - if (deprecatedIconNameToReplacementName.has(name) && !suppressWarning) { - const alternative = deprecatedIconNameToReplacementName.get(name); - let alternativeText = ''; - if (alternative) { - alternativeText = `Use instead.`; - } - warning(`'${name}' as an icon name is deprecated. ${alternativeText}`); - } - - return ( - - - - ); -}; - -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/ui/icon_config.ts b/packages/sdk/src/ui/icon_config.ts deleted file mode 100644 index df7638217..000000000 --- a/packages/sdk/src/ui/icon_config.ts +++ /dev/null @@ -1,831 +0,0 @@ -import {createEnum, EnumType, createPropTypeFromEnum} from '../private_utils'; - -export const iconNamesArray = [ - 'aiAssistant', - 'android', - 'apple', - 'apps', - 'applyRowTemplate', - 'ascending', - 'attachment', - 'automations', - 'autonumber', - 'barcode', - 'bell', - 'blocks', - 'bold', - 'bolt', - 'boltList', - 'book', - 'calendar', - 'calendarDay', - 'caret', - 'chart', - 'chat', - 'check', - 'checkbox', - 'checkboxChecked', - 'checkboxUnchecked', - 'checklist', - 'chevronDown', - 'chevronLeft', - 'chevronRight', - 'chevronUp', - 'clipboard', - 'code', - 'cog', - 'collapse', - 'collapseSidebar', - 'contacts', - 'count', - 'count1', - 'cube', - 'cursor', - 'day', - 'dayAuto', - 'dedent', - 'descending', - 'dollar', - 'down', - 'download', - 'dragHandle', - 'drive', - 'duplicate', - 'edit', - 'envelope', - 'envelope1', - 'expand', - 'expand1', - 'expandSidebar', - 'feed', - 'file', - 'filter', - 'flag', - 'form', - 'formula', - 'fullscreen', - 'gallery', - 'gantt', - 'gift', - 'grid', - 'grid1', - 'group', - 'heart', - 'help', - 'hide', - 'hide1', - 'history', - 'home', - 'hyperlink', - 'hyperlinkCancel', - 'indent', - 'info', - 'italic', - 'kanban', - 'laptop', - 'left', - 'lightbulb', - 'link', - 'link1', - 'lock', - 'logout', - 'lookup', - 'mapPin', - 'markdown', - 'megaphone', - 'menu', - 'minus', - 'mobile', - 'multicollaborator', - 'multiselect', - 'number', - 'ol', - 'overflow', - 'overlay', - 'paint', - 'paragraph', - 'paragraph1', - 'pause', - 'percent', - 'personal', - 'personalAuto', - 'personalCloseup', - 'phone', - 'pivot', - 'play', - 'plus', - 'plusFilled', - 'premium', - 'print', - 'public', - 'publish', - 'quote', - 'quote1', - 'radio', - 'radioSelected', - 'redo', - 'redo1', - 'richText', - 'right', - 'rollup', - 'rollup1', - 'rowHeightSmall', - 'rowHeightMedium', - 'rowHeightLarge', - 'rowHeightExtraLarge', - 'search', - 'select', - 'selectCaret', - 'settings', - 'shapes', - 'share', - 'share1', - 'shareWithBolt', - 'show', - 'show1', - 'slack', - 'smiley', - 'sort', - 'stack', - 'star', - 'strikethrough', - 'switcher', - 'tabs', - 'team', - 'teamLocked', - 'text', - 'thumbsUp', - 'time', - 'timeline', - 'toggle', - 'trash', - 'twitter', - 'ul', - 'underline', - 'undo', - 'up', - 'upload', - 'video', - 'view', - 'warning', - 'windows', - 'x', - 'xCheckbox', -] as const; -export const iconNames = createEnum(...iconNamesArray); -export const deprecatedIconNameToReplacementName = new Map([['blocks', 'apps']]); -/** - * List of all icon names. If you need to render an icon use the {@link Icon|Icon component}. - * - * [[ Story id="icon--example" title="Icon example" height="576px"]] - */ -export type IconName = EnumType; -export const iconNamePropType = createPropTypeFromEnum(iconNames); - -export const phosphorIconConfig = { - AddressBook: - 'M8.5 4.5C7.12522 4.5 6 5.62522 6 7C6 7.73367 6.32641 8.38947 6.83423 8.8479C6.15661 9.13725 5.55366 9.59508 5.09998 10.2C5.06057 10.2525 5.03189 10.3123 5.01559 10.3759C4.99928 10.4395 4.99567 10.5057 5.00495 10.5707C5.01424 10.6357 5.03624 10.6983 5.06969 10.7548C5.10315 10.8113 5.14741 10.8606 5.19995 10.9C5.25248 10.9394 5.31226 10.9681 5.37587 10.9844C5.43949 11.0007 5.50569 11.0043 5.5707 10.995C5.63571 10.9858 5.69825 10.9638 5.75476 10.9303C5.81126 10.8968 5.86063 10.8526 5.90002 10.8C6.51409 9.98135 7.4766 9.50003 8.5 9.5C9.5234 9.50005 10.4859 9.98136 11.1 10.8C11.1394 10.8526 11.1887 10.8968 11.2452 10.9303C11.3017 10.9638 11.3643 10.9858 11.4293 10.995C11.4943 11.0043 11.5605 11.0007 11.6241 10.9844C11.6877 10.9681 11.7475 10.9394 11.8 10.9C11.8526 10.8606 11.8968 10.8113 11.9303 10.7548C11.9638 10.6983 11.9858 10.6357 11.995 10.5707C12.0043 10.5057 12.0007 10.4395 11.9844 10.3759C11.9681 10.3123 11.9394 10.2525 11.9 10.2C11.4463 9.5951 10.8434 9.13725 10.1658 8.8479C10.6736 8.38947 11 7.73367 11 7C11 5.62522 9.87478 4.5 8.5 4.5ZM8.5 5.5C9.33434 5.5 10 6.16566 10 7C10 7.83434 9.33434 8.5 8.5 8.5C7.66566 8.5 7 7.83434 7 7C7 6.16566 7.66566 5.5 8.5 5.5Z M4 1.5C3.45364 1.5 3 1.95364 3 2.5V13.5C3 14.0464 3.45364 14.5 4 14.5H13C13.5464 14.5 14 14.0464 14 13.5V2.5C14 1.95364 13.5464 1.5 13 1.5H4ZM4 2.5H13V13.5H4V2.5Z M2 3.75C1.86739 3.75 1.74021 3.80268 1.64645 3.89645C1.55268 3.99021 1.5 4.11739 1.5 4.25C1.5 4.38261 1.55268 4.50979 1.64645 4.60355C1.74021 4.69732 1.86739 4.75 2 4.75H3.5C3.63261 4.75 3.75979 4.69732 3.85355 4.60355C3.94732 4.50979 4 4.38261 4 4.25C4 4.11739 3.94732 3.99021 3.85355 3.89645C3.75979 3.80268 3.63261 3.75 3.5 3.75H2Z M2 6.25C1.86739 6.25 1.74021 6.30268 1.64645 6.39645C1.55268 6.49021 1.5 6.61739 1.5 6.75C1.5 6.88261 1.55268 7.00979 1.64645 7.10355C1.74021 7.19732 1.86739 7.25 2 7.25H3.5C3.63261 7.25 3.75979 7.19732 3.85355 7.10355C3.94732 7.00979 4 6.88261 4 6.75C4 6.61739 3.94732 6.49021 3.85355 6.39645C3.75979 6.30268 3.63261 6.25 3.5 6.25H2Z M2 8.75C1.86739 8.75 1.74021 8.80268 1.64645 8.89645C1.55268 8.99021 1.5 9.11739 1.5 9.25C1.5 9.38261 1.55268 9.50979 1.64645 9.60355C1.74021 9.69732 1.86739 9.75 2 9.75H3.5C3.63261 9.75 3.75979 9.69732 3.85355 9.60355C3.94732 9.50979 4 9.38261 4 9.25C4 9.11739 3.94732 8.99021 3.85355 8.89645C3.75979 8.80268 3.63261 8.75 3.5 8.75H2Z M2 11.25C1.86739 11.25 1.74021 11.3027 1.64645 11.3964C1.55268 11.4902 1.5 11.6174 1.5 11.75C1.5 11.8826 1.55268 12.0098 1.64645 12.1036C1.74021 12.1973 1.86739 12.25 2 12.25H3.5C3.63261 12.25 3.75979 12.1973 3.85355 12.1036C3.94732 12.0098 4 11.8826 4 11.75C4 11.6174 3.94732 11.4902 3.85355 11.3964C3.75979 11.3027 3.63261 11.25 3.5 11.25H2Z', - Apple: - 'M13.443 5.10467C13.3786 5.14227 11.8441 5.93585 11.8441 7.69538C11.9164 9.70202 13.7805 10.4057 13.8125 10.4057C13.7805 10.4433 13.5311 11.3644 12.7921 12.3298C12.2057 13.1615 11.5548 14 10.5664 14C9.62623 14 9.28873 13.4457 8.20391 13.4457C7.0389 13.4457 6.70926 14 5.81729 14C4.8289 14 4.12979 13.1166 3.51139 12.2927C2.70799 11.2144 2.02513 9.5223 2.00102 7.89755C1.98477 7.03659 2.16191 6.19028 2.61156 5.47142C3.24621 4.46786 4.37925 3.78661 5.61657 3.76414C6.56462 3.73435 7.40837 4.37068 7.98694 4.37068C8.54141 4.37068 9.57802 3.76414 10.7509 3.76414C11.2571 3.76463 12.6071 3.90674 13.443 5.10467Z M7.90676 3.59224C7.73801 2.806 8.20391 2.01975 8.63784 1.51821C9.1923 0.911681 10.068 0.5 10.8232 0.5C10.8714 1.28625 10.5659 2.05735 10.0198 2.61896C9.5298 3.22549 8.68605 3.6821 7.90676 3.59224Z', - AiAssistant: - 'M6.75004 6.00002C6.75004 5.72388 6.52618 5.50002 6.25004 5.50002C5.97389 5.50002 5.75004 5.72388 5.75004 6.00002V7.00002C5.75004 7.27616 5.97389 7.50002 6.25004 7.50002C6.52618 7.50002 6.75004 7.27616 6.75004 7.00002V6.00002Z M10.25 6.00002C10.25 5.72388 10.0262 5.50002 9.75004 5.50002C9.47389 5.50002 9.25004 5.72388 9.25004 6.00002V7.00002C9.25004 7.27616 9.47389 7.50002 9.75004 7.50002C10.0262 7.50002 10.25 7.27616 10.25 7.00002V6.00002Z M5.25681 9.06325C5.49805 8.92887 5.80255 9.01549 5.93693 9.25673C6.14942 9.63819 6.45288 9.95226 6.81496 10.1695C7.17686 10.3866 7.58548 10.5 8.00013 10.5C8.41477 10.5 8.82339 10.3866 9.18529 10.1695C9.54737 9.95226 9.85083 9.63819 10.0633 9.25673C10.1977 9.01549 10.5022 8.92887 10.7434 9.06325C10.9847 9.19763 11.0713 9.50213 10.9369 9.74337C10.6411 10.2744 10.2152 10.7178 9.69979 11.027C9.18421 11.3363 8.59803 11.5 8.00013 11.5C7.40222 11.5 6.81604 11.3363 6.30047 11.027C5.78506 10.7178 5.35914 10.2744 5.06332 9.74337C4.92894 9.50213 5.01557 9.19763 5.25681 9.06325Z M12.207 3.99986L12.0691 3.86195C11.341 3.13389 10.7675 2.56042 10.2638 2.14972C9.74904 1.73 9.26514 1.44414 8.7058 1.33288C8.25501 1.24321 7.79096 1.24321 7.34017 1.33288C6.78083 1.44414 6.29693 1.73 5.78215 2.14972C5.27843 2.56041 4.70497 3.13389 3.97691 3.86195L3.839 3.99986C3.11094 4.72792 2.53747 5.30138 2.12677 5.8051C1.70705 6.31988 1.42119 6.80378 1.30993 7.36312C1.22026 7.81391 1.22026 8.27795 1.30993 8.72875C1.42119 9.28809 1.70705 9.77198 2.12677 10.2868C2.53746 10.7905 3.11093 11.3639 3.839 12.092L3.97691 12.2299C4.70497 12.958 5.27843 13.5315 5.78215 13.9421C6.29693 14.3619 6.78083 14.6477 7.34017 14.759C7.79096 14.8487 8.25501 14.8487 8.7058 14.759C9.26514 14.6477 9.74904 14.3619 10.2638 13.9421C10.7675 13.5315 11.341 12.958 12.0691 12.2299L12.207 12.092C12.935 11.3639 13.5085 10.7905 13.9192 10.2868C14.3389 9.77199 14.6248 9.28809 14.736 8.72875C14.8257 8.27795 14.8257 7.81391 14.736 7.36312C14.6248 6.80378 14.3389 6.31988 13.9192 5.8051C13.5085 5.30138 12.935 4.72792 12.207 3.99986ZM8.51071 2.31366C8.84393 2.37994 9.17872 2.55526 9.63191 2.92476C10.0916 3.29956 10.6296 3.83666 11.3817 4.58885L11.4801 4.68718C12.2323 5.43936 12.7694 5.97732 13.1442 6.43701C13.5137 6.8902 13.689 7.22499 13.7553 7.55821C13.8193 7.8802 13.8193 8.21166 13.7553 8.53366C13.689 8.86688 13.5137 9.20167 13.1442 9.65485C12.7694 10.1145 12.2323 10.6525 11.4801 11.4047L11.3817 11.503C10.6296 12.2552 10.0916 12.7923 9.6319 13.1671C9.17872 13.5366 8.84393 13.7119 8.51071 13.7782C8.18871 13.8423 7.85725 13.8423 7.53526 13.7782C7.20204 13.7119 6.86725 13.5366 6.41406 13.1671C5.95437 12.7923 5.41641 12.2552 4.66423 11.503L4.5659 11.4047C3.81371 10.6525 3.27661 10.1145 2.90181 9.65485C2.53231 9.20167 2.35699 8.86688 2.29071 8.53366C2.22666 8.21166 2.22666 7.8802 2.29071 7.55821C2.35699 7.22499 2.53231 6.8902 2.90181 6.43701C3.27661 5.97732 3.81371 5.43936 4.5659 4.68718L4.66423 4.58885C5.41642 3.83666 5.95437 3.29956 6.41406 2.92476C6.86725 2.55526 7.20204 2.37994 7.53526 2.31366C7.85725 2.24961 8.18871 2.24961 8.51071 2.31366Z', - ArrowArcLeft: - 'M8.1427 4.99976C6.40211 4.96221 4.68 5.62488 3.40259 6.90283L1.40271 8.90271C1.35627 8.94914 1.31944 9.00426 1.29431 9.06493C1.26918 9.1256 1.25624 9.19062 1.25624 9.25629C1.25624 9.32195 1.26918 9.38698 1.29431 9.44764C1.31944 9.50831 1.35627 9.56343 1.40271 9.60986C1.44914 9.6563 1.50426 9.69314 1.56493 9.71827C1.6256 9.7434 1.69062 9.75633 1.75629 9.75633C1.82195 9.75633 1.88698 9.7434 1.94764 9.71827C2.00831 9.69314 2.06343 9.6563 2.10986 9.60986L4.10986 7.60986C4.10986 7.6099 4.10986 7.60982 4.10986 7.60986C5.68395 6.03511 8.04795 5.56457 10.105 6.41675C12.162 7.26893 13.5007 9.27332 13.5 11.4999C13.5 11.5655 13.5129 11.6306 13.538 11.6912C13.5631 11.7519 13.5999 11.807 13.6464 11.8535C13.6928 11.8999 13.7479 11.9368 13.8085 11.9619C13.8692 11.987 13.9342 12 13.9999 12C14.0655 12 14.1306 11.9871 14.1912 11.962C14.2519 11.9369 14.307 11.9001 14.3535 11.8536C14.3999 11.8072 14.4368 11.7521 14.4619 11.6915C14.487 11.6308 14.5 11.5658 14.5 11.5001C14.5008 8.87188 12.9158 6.49884 10.4877 5.49292C9.72888 5.17857 8.93388 5.01682 8.1427 4.99976Z M1.75623 4.75623C1.62362 4.75623 1.49644 4.8089 1.40267 4.90267C1.3089 4.99644 1.25623 5.12362 1.25623 5.25623L1.25624 9.25629C1.25625 9.38889 1.30894 9.5161 1.40271 9.60986C1.49647 9.70363 1.62368 9.75631 1.75629 9.75633L5.75623 9.75623C5.82189 9.75623 5.8869 9.74329 5.94757 9.71817C6.00823 9.69304 6.06335 9.65621 6.10978 9.60978C6.15621 9.56335 6.19304 9.50823 6.21817 9.44757C6.24329 9.3869 6.25623 9.32189 6.25623 9.25623C6.25623 9.19056 6.24329 9.12555 6.21817 9.06488C6.19304 9.00422 6.15621 8.9491 6.10978 8.90267C6.06335 8.85624 6.00823 8.81941 5.94757 8.79429C5.8869 8.76916 5.82189 8.75623 5.75623 8.75623H2.25623V5.25623C2.25623 5.12362 2.20355 4.99644 2.10978 4.90267C2.01601 4.8089 1.88883 4.75623 1.75623 4.75623Z', - ArrowArcRight: - 'M7.8573 4.99951C7.06611 5.01656 6.27102 5.17832 5.51221 5.49268C3.08402 6.49862 1.49903 8.87193 1.5 11.5002C1.50007 11.6329 1.55281 11.76 1.64662 11.8537C1.74043 11.9474 1.86764 12.0001 2.00024 12C2.06591 12 2.13092 11.987 2.19157 11.9618C2.25222 11.9367 2.30732 11.8998 2.35373 11.8534C2.40013 11.8069 2.43694 11.7518 2.46203 11.6911C2.48713 11.6304 2.50003 11.5654 2.5 11.4998C2.49918 9.27315 3.83795 7.2687 5.89502 6.4165C7.95209 5.56431 10.3161 6.03488 11.8901 7.60974C11.8901 7.60969 11.8901 7.60978 11.8901 7.60974L13.8901 9.60987C13.9366 9.6563 13.9917 9.69314 14.0524 9.71827C14.113 9.7434 14.178 9.75634 14.2437 9.75634C14.3094 9.75634 14.3744 9.7434 14.4351 9.71827C14.4957 9.69314 14.5509 9.6563 14.5973 9.60987C14.6437 9.56344 14.6806 9.50831 14.7057 9.44765C14.7308 9.38698 14.7438 9.32196 14.7438 9.25629C14.7438 9.19062 14.7308 9.1256 14.7057 9.06493C14.6806 9.00427 14.6437 8.94914 14.5973 8.90271L12.5974 6.90283C11.32 5.62486 9.59787 4.962 7.8573 4.99951Z M14.2438 4.75623C14.1112 4.75623 13.984 4.8089 13.8902 4.90267C13.7965 4.99644 13.7438 5.12362 13.7438 5.25623V8.75623H10.2438C10.1112 8.75623 9.98399 8.8089 9.89022 8.90267C9.79645 8.99644 9.74377 9.12362 9.74377 9.25623C9.74377 9.38883 9.79645 9.51601 9.89022 9.60978C9.98399 9.70355 10.1112 9.75623 10.2438 9.75623L14.2437 9.75634C14.3763 9.75632 14.5035 9.70364 14.5973 9.60987C14.6911 9.51611 14.7438 9.38889 14.7438 9.25629V5.25623C14.7438 5.12362 14.6911 4.99644 14.5973 4.90267C14.5036 4.8089 14.3764 4.75623 14.2438 4.75623Z', - ArrowCircleDown: - 'M8 5C7.86739 5 7.74021 5.05268 7.64645 5.14645C7.55268 5.24021 7.5 5.36739 7.5 5.5V9.29297L6.23486 8.02771C6.18843 7.98127 6.13331 7.94444 6.07264 7.91931C6.01198 7.89418 5.94695 7.88124 5.88129 7.88124C5.81562 7.88124 5.7506 7.89418 5.68993 7.91931C5.62926 7.94444 5.57414 7.98127 5.52771 8.02771C5.48127 8.07414 5.44444 8.12926 5.41931 8.18993C5.39418 8.2506 5.38124 8.31562 5.38124 8.38129C5.38124 8.44695 5.39418 8.51198 5.41931 8.57264C5.44444 8.63331 5.48127 8.68843 5.52771 8.73486L7.64648 10.8535C7.74026 10.9472 7.86741 10.9999 8 10.9999C8.13259 10.9999 8.25974 10.9472 8.35352 10.8535L10.4723 8.73486C10.5187 8.68843 10.5556 8.63331 10.5807 8.57264C10.6058 8.51198 10.6188 8.44695 10.6188 8.38129C10.6188 8.31562 10.6058 8.2506 10.5807 8.18993C10.5556 8.12926 10.5187 8.07414 10.4723 8.02771C10.4259 7.98127 10.3707 7.94444 10.3101 7.91931C10.2494 7.89418 10.1844 7.88124 10.1187 7.88124C10.053 7.88124 9.98802 7.89418 9.92736 7.91931C9.86669 7.94444 9.81157 7.98127 9.76514 8.02771L8.5 9.29297V5.5C8.5 5.36739 8.44732 5.24021 8.35355 5.14645C8.25979 5.05268 8.13261 5 8 5Z M8 1.5C4.41604 1.5 1.5 4.41604 1.5 8C1.5 11.5839 4.41603 14.5 8 14.5C11.5839 14.5 14.5 11.5839 14.5 8C14.5 4.41603 11.5839 1.5 8 1.5ZM8 2.5C11.0435 2.5 13.5 4.95647 13.5 8C13.5 11.0435 11.0435 13.5 8 13.5C4.95647 13.5 2.5 11.0435 2.5 8C2.5 4.95647 4.95647 2.5 8 2.5Z', - ArrowCircleUp: - 'M8 5C7.8674 5.00003 7.74024 5.05272 7.64648 5.14648L5.52771 7.26514C5.48127 7.31157 5.44444 7.36669 5.41931 7.42736C5.39418 7.48802 5.38124 7.55305 5.38124 7.61871C5.38124 7.68438 5.39418 7.7494 5.41931 7.81007C5.44444 7.87074 5.48127 7.92586 5.52771 7.97229C5.57414 8.01873 5.62926 8.05556 5.68993 8.08069C5.7506 8.10582 5.81562 8.11876 5.88129 8.11876C5.94695 8.11876 6.01198 8.10582 6.07264 8.08069C6.13331 8.05556 6.18843 8.01873 6.23486 7.97229L7.5 6.70703V10.5C7.5 10.6326 7.55268 10.7598 7.64645 10.8536C7.74021 10.9473 7.86739 11 8 11C8.13261 11 8.25979 10.9473 8.35355 10.8536C8.44732 10.7598 8.5 10.6326 8.5 10.5V6.70703L9.76514 7.97229C9.81157 8.01873 9.86669 8.05556 9.92736 8.08069C9.98802 8.10582 10.053 8.11876 10.1187 8.11876C10.1844 8.11876 10.2494 8.10582 10.3101 8.08069C10.3707 8.05556 10.4259 8.01873 10.4723 7.97229C10.5187 7.92586 10.5556 7.87074 10.5807 7.81007C10.6058 7.7494 10.6188 7.68438 10.6188 7.61871C10.6188 7.55305 10.6058 7.48802 10.5807 7.42736C10.5556 7.36669 10.5187 7.31157 10.4723 7.26514L8.35352 5.14648C8.34867 5.14437 8.34378 5.14234 8.33887 5.14038C8.24777 5.05235 8.12666 5.00218 8 5Z M8 1.5C4.41604 1.5 1.5 4.41604 1.5 8C1.5 11.5839 4.41603 14.5 8 14.5C11.5839 14.5 14.5 11.5839 14.5 8C14.5 4.41603 11.5839 1.5 8 1.5ZM8 2.5C11.0435 2.5 13.5 4.95647 13.5 8C13.5 11.0435 11.0435 13.5 8 13.5C4.95647 13.5 2.5 11.0435 2.5 8C2.5 4.95647 4.95647 2.5 8 2.5Z', - ArrowCounterClockwise: - 'M8.13367 2.00171C6.52708 1.96746 4.93757 2.57929 3.75879 3.75903L1.63452 5.87719C1.54063 5.97081 1.48777 6.09789 1.48756 6.23048C1.48736 6.36307 1.53982 6.49032 1.63342 6.58423C1.67978 6.63073 1.73485 6.66766 1.79547 6.69288C1.8561 6.71811 1.92111 6.73114 1.98677 6.73125C2.05244 6.73135 2.11748 6.71851 2.17819 6.69348C2.23889 6.66844 2.29407 6.63169 2.34058 6.58533L4.46558 4.46655C4.46537 4.46675 4.46578 4.46635 4.46558 4.46655C5.89626 3.0347 8.04552 2.60603 9.91565 3.38025C11.7858 4.15446 13.0029 5.97587 13.0029 8C13.0029 10.0241 11.7858 11.8455 9.91565 12.6197C8.04552 13.394 5.89687 12.9659 4.46619 11.5341C4.41977 11.4876 4.36466 11.4507 4.30401 11.4256C4.24335 11.4004 4.17833 11.3875 4.11266 11.3875C4.047 11.3874 3.98197 11.4003 3.92129 11.4255C3.86062 11.4506 3.80548 11.4874 3.75903 11.5338C3.71258 11.5802 3.67573 11.6353 3.65057 11.696C3.62542 11.7566 3.61246 11.8217 3.61244 11.8873C3.61242 11.953 3.62533 12.018 3.65044 12.0787C3.67555 12.1394 3.71237 12.1945 3.75879 12.241C5.47337 13.9569 8.05683 14.4716 10.2981 13.5437C12.5394 12.6158 14.0029 10.4257 14.0029 7.99999C14.0029 5.57425 12.5394 3.38415 10.2981 2.45629C9.5977 2.16633 8.86394 2.01728 8.13367 2.00171Z M1.98755 2.7312C1.85494 2.7312 1.72776 2.78388 1.634 2.87765C1.54023 2.97141 1.48755 3.09859 1.48755 3.2312L1.48756 6.23048C1.48736 6.36307 1.53982 6.49032 1.63342 6.58423C1.72719 6.67799 1.85416 6.73124 1.98677 6.73125L4.98755 6.7312C5.05321 6.7312 5.11823 6.71827 5.17889 6.69314C5.23955 6.66801 5.29467 6.63118 5.3411 6.58475C5.38753 6.53832 5.42436 6.4832 5.44949 6.42254C5.47462 6.36188 5.48755 6.29686 5.48755 6.2312C5.48755 6.09859 5.43487 5.97141 5.3411 5.87765C5.24733 5.78388 5.12016 5.7312 4.98755 5.7312H2.48755V3.2312C2.48755 3.09859 2.43487 2.97141 2.3411 2.87765C2.24733 2.78388 2.12016 2.7312 1.98755 2.7312Z', - ArrowDown: - 'M7.99999 2C7.86738 2 7.7402 2.05268 7.64643 2.14645C7.55266 2.24021 7.49999 2.36739 7.49999 2.5V12.293L3.8535 8.64648C3.75974 8.55274 3.63258 8.50008 3.49999 8.50008C3.3674 8.50008 3.24023 8.55274 3.14647 8.64648C3.05272 8.74025 3.00006 8.86741 3.00006 9C3.00006 9.13259 3.05272 9.25975 3.14647 9.35352L7.64647 13.8535C7.74022 13.9473 7.86739 14 7.99999 14C8.13259 14 8.25975 13.9473 8.3535 13.8535L12.8535 9.35352C12.9472 9.25975 12.9999 9.13259 12.9999 9C12.9999 8.86741 12.9472 8.74025 12.8535 8.64648C12.7597 8.55274 12.6326 8.50008 12.5 8.50008C12.3674 8.50008 12.2402 8.55274 12.1465 8.64648L8.49999 12.293V2.5C8.49999 2.36739 8.44731 2.24021 8.35354 2.14645C8.25977 2.05268 8.13259 2 7.99999 2Z', - ArrowElbowDownRight: - 'M3 1.125C2.90054 1.125 2.80516 1.16451 2.73484 1.23483C2.66451 1.30516 2.625 1.40054 2.625 1.5V8.25C2.62501 8.34945 2.66452 8.44483 2.73485 8.51515C2.80517 8.58548 2.90055 8.62499 3 8.625H9.75C9.84946 8.625 9.94484 8.58549 10.0152 8.51517C10.0855 8.44484 10.125 8.34946 10.125 8.25C10.125 8.15054 10.0855 8.05516 10.0152 7.98483C9.94484 7.91451 9.84946 7.875 9.75 7.875H3.375V1.5C3.375 1.40054 3.33549 1.30516 3.26516 1.23483C3.19484 1.16451 3.09946 1.125 3 1.125ZM7.5 5.625C7.40055 5.62502 7.30518 5.66453 7.23486 5.73486C7.16455 5.80519 7.12506 5.90056 7.12506 6C7.12506 6.09944 7.16455 6.19481 7.23486 6.26514L9.21973 8.25L7.23486 10.2349C7.16455 10.3052 7.12506 10.4006 7.12506 10.5C7.12506 10.5994 7.16455 10.6948 7.23486 10.7651C7.30519 10.8354 7.40056 10.8749 7.5 10.8749C7.59944 10.8749 7.69481 10.8354 7.76514 10.7651L10.0151 8.51514C10.0854 8.44481 10.1249 8.34944 10.1249 8.25C10.1249 8.15056 10.0854 8.05519 10.0151 7.98486L7.76514 5.73486C7.69482 5.66453 7.59945 5.62502 7.5 5.625Z', - ArrowLeft: - 'M7 3C6.8674 3.00002 6.74024 3.05271 6.64648 3.14648L2.14648 7.64648C2.05271 7.74024 2.00002 7.8674 2 8C2.00002 8.1326 2.05271 8.25976 2.14648 8.35352L6.64648 12.8535C6.74025 12.9473 6.86741 12.9999 7 12.9999C7.13259 12.9999 7.25975 12.9473 7.35352 12.8535C7.44726 12.7598 7.49992 12.6326 7.49992 12.5C7.49992 12.3674 7.44726 12.2402 7.35352 12.1465L3.70703 8.5H13.5C13.6326 8.5 13.7598 8.44732 13.8536 8.35355C13.9473 8.25979 14 8.13261 14 8C14 7.86739 13.9473 7.74021 13.8536 7.64645C13.7598 7.55268 13.6326 7.5 13.5 7.5H3.70703L7.35352 3.85352C7.44726 3.75975 7.49992 3.63259 7.49992 3.5C7.49992 3.36741 7.44726 3.24025 7.35352 3.14648C7.25976 3.05271 7.1326 3.00002 7 3Z', - ArrowLineLeft: - 'M2.5 2C2.36739 2 2.24021 2.05268 2.14645 2.14645C2.05268 2.24021 2 2.36739 2 2.5V13.5C2 13.6326 2.05268 13.7598 2.14645 13.8536C2.24021 13.9473 2.36739 14 2.5 14C2.63261 14 2.75979 13.9473 2.85355 13.8536C2.94732 13.7598 3 13.6326 3 13.5V2.5C3 2.36739 2.94732 2.24021 2.85355 2.14645C2.75979 2.05268 2.63261 2 2.5 2Z M9 3C8.8674 3.00002 8.74024 3.05271 8.64648 3.14648L4.14648 7.64648C4.05272 7.74024 4.00003 7.8674 4 8C4.00002 8.1326 4.05271 8.25976 4.14648 8.35352L8.64648 12.8535C8.74025 12.9473 8.86741 12.9999 9 12.9999C9.13259 12.9999 9.25975 12.9473 9.35352 12.8535C9.44726 12.7598 9.49992 12.6326 9.49992 12.5C9.49992 12.3674 9.44726 12.2402 9.35352 12.1465L5.70703 8.5H14C14.1326 8.5 14.2598 8.44732 14.3536 8.35355C14.4473 8.25979 14.5 8.13261 14.5 8C14.5 7.86739 14.4473 7.74021 14.3536 7.64645C14.2598 7.55268 14.1326 7.5 14 7.5H5.70703L9.35352 3.85352C9.44726 3.75975 9.49992 3.63259 9.49992 3.5C9.49992 3.36741 9.44726 3.24025 9.35352 3.14648C9.25976 3.05271 9.1326 3.00002 9 3Z', - ArrowLineRight: - 'M13.5 2C13.3674 2 13.2402 2.05268 13.1464 2.14645C13.0527 2.24021 13 2.36739 13 2.5V13.5C13 13.6326 13.0527 13.7598 13.1464 13.8536C13.2402 13.9473 13.3674 14 13.5 14C13.6326 14 13.7598 13.9473 13.8536 13.8536C13.9473 13.7598 14 13.6326 14 13.5V2.5C14 2.36739 13.9473 2.24021 13.8536 2.14645C13.7598 2.05268 13.6326 2 13.5 2Z M7 3C6.8674 3.00002 6.74024 3.05271 6.64648 3.14648C6.55274 3.24025 6.50008 3.36741 6.50008 3.5C6.50008 3.63259 6.55274 3.75975 6.64648 3.85352L10.293 7.5H2C1.86739 7.5 1.74021 7.55268 1.64645 7.64645C1.55268 7.74021 1.5 7.86739 1.5 8C1.5 8.13261 1.55268 8.25979 1.64645 8.35355C1.74021 8.44732 1.86739 8.5 2 8.5H10.293L6.64648 12.1465C6.55274 12.2402 6.50008 12.3674 6.50008 12.5C6.50008 12.6326 6.55274 12.7598 6.64648 12.8535C6.74025 12.9473 6.86741 12.9999 7 12.9999C7.13259 12.9999 7.25975 12.9473 7.35352 12.8535L11.8535 8.35352C11.9414 8.26249 11.9915 8.14153 11.9938 8.01501C11.9959 8.01004 11.998 8.00504 12 8C11.9985 7.98678 11.9964 7.97363 11.9938 7.96057C11.9917 7.93512 11.9877 7.90985 11.9818 7.88501C11.9757 7.85934 11.9675 7.8342 11.9574 7.80981C11.9476 7.78622 11.936 7.7634 11.9227 7.74158C11.9089 7.7191 11.8934 7.69776 11.8762 7.67773C11.8691 7.66703 11.8615 7.6566 11.8535 7.64648L7.35352 3.14648C7.25976 3.05271 7.1326 3.00002 7 3Z', - ArrowRight: - 'M9 3C8.8674 3.00002 8.74024 3.05271 8.64648 3.14648C8.55274 3.24025 8.50008 3.36741 8.50008 3.5C8.50008 3.63259 8.55274 3.75975 8.64648 3.85352L12.293 7.5H2.5C2.36739 7.5 2.24021 7.55268 2.14645 7.64645C2.05268 7.74021 2 7.86739 2 8C2 8.13261 2.05268 8.25979 2.14645 8.35355C2.24021 8.44732 2.36739 8.5 2.5 8.5H12.293L8.64648 12.1465C8.55274 12.2402 8.50008 12.3674 8.50008 12.5C8.50008 12.6326 8.55274 12.7598 8.64648 12.8535C8.74025 12.9473 8.86741 12.9999 9 12.9999C9.13259 12.9999 9.25975 12.9473 9.35352 12.8535L13.8535 8.35352C13.9414 8.26249 13.9915 8.14153 13.9938 8.01501C13.9959 8.01004 13.998 8.00504 14 8C13.9985 7.98678 13.9964 7.97363 13.9938 7.96057C13.9917 7.93512 13.9877 7.90985 13.9818 7.88501C13.9757 7.85934 13.9675 7.8342 13.9574 7.80981C13.9476 7.78622 13.936 7.7634 13.9227 7.74158C13.9089 7.7191 13.8934 7.69776 13.8762 7.67773C13.8691 7.66703 13.8615 7.6566 13.8535 7.64648L9.35352 3.14648C9.25976 3.05271 9.1326 3.00002 9 3Z', - ArrowRightList: - 'M3.5 3.5C3.22386 3.5 3 3.72386 3 4C3 4.27614 3.22386 4.5 3.5 4.5H13.5C13.7761 4.5 14 4.27614 14 4C14 3.72386 13.7761 3.5 13.5 3.5H3.5Z M3.5 11.5C3.22386 11.5 3 11.7239 3 12C3 12.2761 3.22386 12.5 3.5 12.5H13.5C13.7761 12.5 14 12.2761 14 12C14 11.7239 13.7761 11.5 13.5 11.5H3.5Z M9.25 7.5C8.97386 7.5 8.75 7.72386 8.75 8C8.75 8.27614 8.97386 8.5 9.25 8.5H13.5C13.7761 8.5 14 8.27614 14 8C14 7.72386 13.7761 7.5 13.5 7.5H9.25Z M5.85355 6.14645C5.65829 5.95118 5.34171 5.95118 5.14645 6.14645C4.95118 6.34171 4.95118 6.65829 5.14645 6.85355L5.79289 7.5H1.5C1.22386 7.5 1 7.72386 1 8C1 8.27614 1.22386 8.5 1.5 8.5H5.79289L5.14645 9.14645C4.95118 9.34171 4.95118 9.65829 5.14645 9.85355C5.34171 10.0488 5.65829 10.0488 5.85355 9.85355L7.35355 8.35355C7.40149 8.30562 7.43766 8.25036 7.46206 8.19139C7.48615 8.13331 7.4996 8.0697 7.49999 8.003L7.5 8L7.49999 7.997C7.4996 7.9303 7.48615 7.86669 7.46206 7.80861C7.43766 7.74964 7.40149 7.69439 7.35355 7.64645L5.85355 6.14645Z', - ArrowSquareDown: - 'M8 5C7.86739 5 7.74021 5.05268 7.64645 5.14645C7.55268 5.24021 7.5 5.36739 7.5 5.5V9.29297L6.23486 8.02771C6.18843 7.98127 6.13331 7.94444 6.07264 7.91931C6.01198 7.89418 5.94695 7.88124 5.88129 7.88124C5.81562 7.88124 5.7506 7.89418 5.68993 7.91931C5.62926 7.94444 5.57414 7.98127 5.52771 8.02771C5.48127 8.07414 5.44444 8.12926 5.41931 8.18993C5.39418 8.2506 5.38124 8.31562 5.38124 8.38129C5.38124 8.44695 5.39418 8.51198 5.41931 8.57264C5.44444 8.63331 5.48127 8.68843 5.52771 8.73486L7.64648 10.8535C7.74026 10.9472 7.86741 10.9999 8 10.9999C8.13259 10.9999 8.25974 10.9472 8.35352 10.8535L10.4723 8.73486C10.5187 8.68843 10.5556 8.63331 10.5807 8.57264C10.6058 8.51198 10.6188 8.44695 10.6188 8.38129C10.6188 8.31562 10.6058 8.2506 10.5807 8.18993C10.5556 8.12926 10.5187 8.07414 10.4723 8.02771C10.4259 7.98127 10.3707 7.94444 10.3101 7.91931C10.2494 7.89418 10.1844 7.88124 10.1187 7.88124C10.053 7.88124 9.98802 7.89418 9.92736 7.91931C9.86669 7.94444 9.81157 7.98127 9.76514 8.02771L8.5 9.29297V5.5C8.5 5.36739 8.44732 5.24021 8.35355 5.14645C8.25979 5.05268 8.13261 5 8 5Z M3 2C2.45364 2 2 2.45364 2 3V13C2 13.5464 2.45364 14 3 14H13C13.5464 14 14 13.5464 14 13V3C14 2.45364 13.5464 2 13 2H3ZM3 3H13V13H3V3Z', - ArrowSquareOut: - 'M9.75 2C9.61739 2 9.49021 2.05268 9.39645 2.14645C9.30268 2.24021 9.25 2.36739 9.25 2.5C9.25 2.63261 9.30268 2.75979 9.39645 2.85355C9.49021 2.94732 9.61739 3 9.75 3H12.293L8.64648 6.64648C8.55274 6.74025 8.50008 6.86741 8.50008 7C8.50008 7.13259 8.55274 7.25975 8.64648 7.35352C8.74025 7.44726 8.86741 7.49992 9 7.49992C9.13259 7.49992 9.25975 7.44726 9.35352 7.35352L13 3.70703V6.25C13 6.38261 13.0527 6.50979 13.1464 6.60355C13.2402 6.69732 13.3674 6.75 13.5 6.75C13.6326 6.75 13.7598 6.69732 13.8536 6.60355C13.9473 6.50979 14 6.38261 14 6.25V2.5C13.998 2.49504 13.996 2.49012 13.9939 2.48523C13.9917 2.35861 13.9415 2.23755 13.8535 2.14648C13.7598 2.05272 13.6326 2.00003 13.5 2H9.75Z M3 4C2.45364 4 2 4.45364 2 5V13C2.00007 13.5463 2.45357 13.9999 2.99988 14C2.99984 14 2.99992 14 2.99988 14H11C11.5464 14 12 13.5464 12 13V9C12 8.86739 11.9473 8.74021 11.8536 8.64645C11.7598 8.55268 11.6326 8.5 11.5 8.5C11.3674 8.5 11.2402 8.55268 11.1464 8.64645C11.0527 8.74021 11 8.86739 11 9V13H3.00012L3 5H7C7.1326 5 7.25978 4.94732 7.35355 4.85355C7.44732 4.75979 7.5 4.63261 7.5 4.5C7.5 4.36739 7.44732 4.24021 7.35355 4.14645C7.25978 4.05268 7.1326 4 7 4H3Z', - ArrowUp: - 'M7.99999 2C7.86739 2.00002 7.74022 2.05271 7.64647 2.14648L3.14647 6.64648C3.05272 6.74025 3.00006 6.86741 3.00006 7C3.00006 7.13259 3.05272 7.25975 3.14647 7.35352C3.24023 7.44726 3.3674 7.49992 3.49999 7.49992C3.63258 7.49992 3.75974 7.44726 3.8535 7.35352L7.49999 3.70703V13.5C7.49999 13.6326 7.55266 13.7598 7.64643 13.8536C7.7402 13.9473 7.86738 14 7.99999 14C8.13259 14 8.25977 13.9473 8.35354 13.8536C8.44731 13.7598 8.49999 13.6326 8.49999 13.5V3.70703L12.1465 7.35352C12.2402 7.44726 12.3674 7.49992 12.5 7.49992C12.6326 7.49992 12.7597 7.44726 12.8535 7.35352C12.9472 7.25975 12.9999 7.13259 12.9999 7C12.9999 6.86741 12.9472 6.74025 12.8535 6.64648L8.3535 2.14648C8.34865 2.14437 8.34377 2.14234 8.33885 2.14038C8.24776 2.05235 8.12665 2.00218 7.99999 2Z', - ArrowsClockwise: - 'M1.98657 9.2688C1.85399 9.26901 1.72691 9.32188 1.6333 9.41577C1.58693 9.46227 1.55017 9.51744 1.52512 9.57814C1.50007 9.63884 1.48723 9.70388 1.48732 9.76955C1.48741 9.83522 1.50043 9.90022 1.52565 9.96085C1.55087 10.0215 1.58778 10.0766 1.63428 10.1229L3.75928 12.2416C4.88334 13.367 6.40924 13.9995 7.99988 13.9994C9.59082 13.9994 11.117 13.3667 12.2411 12.2408C12.2875 12.1944 12.3243 12.1392 12.3494 12.0785C12.3745 12.0179 12.3874 11.9528 12.3873 11.8872C12.3873 11.8215 12.3743 11.7565 12.3491 11.6958C12.3239 11.6352 12.2871 11.5801 12.2406 11.5337C12.1941 11.4873 12.139 11.4505 12.0783 11.4254C12.0176 11.4004 11.9525 11.3875 11.8869 11.3875C11.8212 11.3876 11.7562 11.4006 11.6956 11.4258C11.6349 11.451 11.5798 11.4878 11.5334 11.5343C10.5966 12.4726 9.32574 12.9995 7.99987 12.9994C7.99991 12.9994 7.99982 12.9994 7.99987 12.9994C6.674 12.9995 5.40298 12.4726 4.46617 11.5343C4.46589 11.534 4.4656 11.5337 4.46532 11.5335L2.34032 9.41481C2.29383 9.36843 2.23866 9.33167 2.17796 9.30662C2.11726 9.28157 2.05223 9.26871 1.98657 9.2688Z M7.99976 2.00366C6.46339 2.00366 4.92703 2.58882 3.75842 3.75916C3.71202 3.80562 3.67522 3.86077 3.65013 3.92145C3.62504 3.98213 3.61215 4.04717 3.6122 4.11283C3.61224 4.1785 3.62522 4.24351 3.6504 4.30416C3.67557 4.36481 3.71244 4.41991 3.75891 4.46631C3.80537 4.51271 3.86052 4.54951 3.9212 4.5746C3.98189 4.59969 4.04692 4.61258 4.11259 4.61253C4.17825 4.61249 4.24327 4.59951 4.30392 4.57434C4.36457 4.54916 4.41967 4.51229 4.46606 4.46582C6.42104 2.50796 9.57847 2.50796 11.5334 4.46582C11.5337 4.46607 11.534 4.46631 11.5343 4.46655L13.6593 6.58533C13.7532 6.67893 13.8805 6.73139 14.013 6.73119C14.1456 6.73098 14.2727 6.67812 14.3663 6.58423C14.4599 6.49033 14.5124 6.3631 14.5123 6.23051C14.5121 6.09792 14.4592 5.97083 14.3654 5.8772L12.2411 3.75915L12.2404 3.75842C11.0718 2.58859 9.5358 2.00366 7.99976 2.00366Z M1.98657 9.2688C1.85399 9.26901 1.72691 9.32188 1.6333 9.41577C1.53953 9.50954 1.48734 9.63694 1.48732 9.76955L1.4873 12.7688C1.4873 12.9014 1.53998 13.0286 1.63375 13.1224C1.72752 13.2161 1.8547 13.2688 1.9873 13.2688C2.11991 13.2688 2.24709 13.2161 2.34086 13.1224C2.43463 13.0286 2.4873 12.9014 2.4873 12.7688V10.2688H4.9873C5.11991 10.2688 5.24709 10.2161 5.34086 10.1224C5.43463 10.0286 5.4873 9.90141 5.4873 9.7688C5.4873 9.63619 5.43463 9.50901 5.34086 9.41525C5.24709 9.32148 5.11991 9.2688 4.9873 9.2688H1.98657Z M14.0122 2.7312C13.8796 2.7312 13.7524 2.78388 13.6587 2.87765C13.5649 2.97142 13.5122 3.09859 13.5122 3.2312V5.7312H11.0122C10.8796 5.7312 10.7524 5.78388 10.6587 5.87765C10.5649 5.97142 10.5122 6.09859 10.5122 6.2312C10.5122 6.29686 10.5251 6.36188 10.5503 6.42254C10.5754 6.48321 10.6122 6.53833 10.6587 6.58475C10.7051 6.63118 10.7602 6.66801 10.8209 6.69314C10.8815 6.71827 10.9465 6.7312 11.0122 6.7312L14.013 6.73119C14.1456 6.73098 14.2727 6.67812 14.3663 6.58423C14.4599 6.49033 14.5124 6.3631 14.5123 6.23051L14.5122 3.2312C14.5122 3.09859 14.4595 2.97142 14.3658 2.87765C14.272 2.78388 14.1448 2.7312 14.0122 2.7312Z', - ArrowsDownUp: - 'M4.99999 2.5C4.86738 2.5 4.7402 2.55268 4.64643 2.64645C4.55266 2.74021 4.49999 2.86739 4.49999 3V11.793L3.3535 10.6465C3.25974 10.5527 3.13258 10.5001 2.99999 10.5001C2.8674 10.5001 2.74023 10.5527 2.64647 10.6465C2.55272 10.7402 2.50006 10.8674 2.50006 11C2.50006 11.1326 2.55272 11.2598 2.64647 11.3535L4.64647 13.3535C4.74022 13.4473 4.86738 13.5 4.99999 13.5C5.13259 13.5 5.25975 13.4473 5.3535 13.3535L7.3535 11.3535C7.44725 11.2598 7.49991 11.1326 7.49991 11C7.49991 10.8674 7.44725 10.7402 7.3535 10.6465C7.25974 10.5527 7.13258 10.5001 6.99999 10.5001C6.8674 10.5001 6.74024 10.5527 6.64647 10.6465L5.49999 11.793V3C5.49999 2.86739 5.44731 2.74021 5.35354 2.64645C5.25977 2.55268 5.13259 2.5 4.99999 2.5Z M11 2.5C10.8674 2.50003 10.7402 2.55272 10.6465 2.64648L8.64647 4.64648C8.55272 4.74025 8.50006 4.86741 8.50006 5C8.50006 5.13259 8.55272 5.25975 8.64647 5.35352C8.74024 5.44726 8.8674 5.49992 8.99999 5.49992C9.13258 5.49992 9.25974 5.44726 9.3535 5.35352L10.5 4.20703V13C10.5 13.1326 10.5527 13.2598 10.6464 13.3536C10.7402 13.4473 10.8674 13.5 11 13.5C11.1326 13.5 11.2598 13.4473 11.3535 13.3536C11.4473 13.2598 11.5 13.1326 11.5 13V4.20703L12.6465 5.35352C12.7402 5.44726 12.8674 5.49992 13 5.49992C13.1326 5.49992 13.2597 5.44726 13.3535 5.35352C13.4472 5.25975 13.4999 5.13259 13.4999 5C13.4999 4.86741 13.4472 4.74025 13.3535 4.64648L11.3535 2.64648C11.3487 2.64437 11.3438 2.64234 11.3389 2.64038C11.2478 2.55235 11.1266 2.50218 11 2.5Z', - ArrowsInSimple: - 'M13 2.5C12.8674 2.50002 12.7402 2.55271 12.6465 2.64648L9.99999 5.29297V3.5C9.99999 3.36739 9.94731 3.24021 9.85354 3.14645C9.75977 3.05268 9.63259 3 9.49999 3C9.36738 3 9.2402 3.05268 9.14643 3.14645C9.05266 3.24021 8.99999 3.36739 8.99999 3.5V6.5C9.00001 6.6326 9.0527 6.75976 9.14647 6.85352C9.24023 6.94728 9.36739 6.99997 9.49999 7H12.5C12.6326 7 12.7598 6.94732 12.8535 6.85355C12.9473 6.75979 13 6.63261 13 6.5C13 6.36739 12.9473 6.24021 12.8535 6.14645C12.7598 6.05268 12.6326 6 12.5 6H10.707L13.3535 3.35352C13.4472 3.25975 13.4999 3.13259 13.4999 3C13.4999 2.86741 13.4472 2.74025 13.3535 2.64648C13.2597 2.55271 13.1326 2.50002 13 2.5Z M3.49999 9C3.36738 9 3.2402 9.05268 3.14643 9.14645C3.05266 9.24021 2.99999 9.36739 2.99999 9.5C2.99999 9.63261 3.05266 9.75979 3.14643 9.85355C3.2402 9.94732 3.36738 10 3.49999 10H5.29295L2.64647 12.6465C2.55272 12.7402 2.50006 12.8674 2.50006 13C2.50006 13.1326 2.55272 13.2598 2.64647 13.3535C2.74023 13.4473 2.8674 13.4999 2.99999 13.4999C3.13258 13.4999 3.25974 13.4473 3.3535 13.3535L5.99999 10.707V12.5C5.99999 12.6326 6.05266 12.7598 6.14643 12.8536C6.2402 12.9473 6.36738 13 6.49999 13C6.63259 13 6.75977 12.9473 6.85354 12.8536C6.94731 12.7598 6.99999 12.6326 6.99999 12.5V9.5C6.99803 9.49504 6.99599 9.49012 6.99388 9.48523C6.99167 9.35861 6.9415 9.23755 6.8535 9.14648C6.75974 9.05272 6.63258 9.00003 6.49999 9H3.49999Z', - ArrowsOut: - 'M3 2.5C2.8674 2.50002 2.74024 2.55271 2.64648 2.64648C2.55271 2.74024 2.50002 2.8674 2.5 3V5.5C2.5 5.63261 2.55268 5.75979 2.64645 5.85355C2.74021 5.94732 2.86739 6 3 6C3.13261 6 3.25979 5.94732 3.35355 5.85355C3.44732 5.75979 3.5 5.63261 3.5 5.5V4.20703L6.14648 6.85352C6.24025 6.94726 6.36741 6.99992 6.5 6.99992C6.63259 6.99992 6.75975 6.94726 6.85352 6.85352C6.94726 6.75975 6.99992 6.63259 6.99992 6.5C6.99992 6.36741 6.94726 6.24025 6.85352 6.14648L4.20703 3.5H5.5C5.63261 3.5 5.75979 3.44732 5.85355 3.35355C5.94732 3.25979 6 3.13261 6 3C6 2.86739 5.94732 2.74021 5.85355 2.64645C5.75979 2.55268 5.63261 2.5 5.5 2.5H3Z M10.5 2.5C10.3674 2.5 10.2402 2.55268 10.1464 2.64645C10.0527 2.74021 10 2.86739 10 3C10 3.13261 10.0527 3.25979 10.1464 3.35355C10.2402 3.44732 10.3674 3.5 10.5 3.5H11.793L9.14648 6.14648C9.05274 6.24025 9.00008 6.36741 9.00008 6.5C9.00008 6.63259 9.05274 6.75975 9.14648 6.85352C9.24025 6.94726 9.36741 6.99992 9.5 6.99992C9.63259 6.99992 9.75975 6.94726 9.85352 6.85352L12.5 4.20703V5.5C12.5 5.63261 12.5527 5.75979 12.6464 5.85355C12.7402 5.94732 12.8674 6 13 6C13.1326 6 13.2598 5.94732 13.3536 5.85355C13.4473 5.75979 13.5 5.63261 13.5 5.5V3C13.498 2.99504 13.496 2.99012 13.4939 2.98523C13.4917 2.85861 13.4415 2.73755 13.3535 2.64648C13.2598 2.55272 13.1326 2.50003 13 2.5H10.5Z M6.5 9C6.3674 9.00002 6.24024 9.05271 6.14648 9.14648L3.5 11.793V10.5C3.5 10.3674 3.44732 10.2402 3.35355 10.1464C3.25979 10.0527 3.13261 10 3 10C2.86739 10 2.74021 10.0527 2.64645 10.1464C2.55268 10.2402 2.5 10.3674 2.5 10.5V13C2.50003 13.1326 2.55272 13.2598 2.64648 13.3535C2.74024 13.4473 2.8674 13.5 3 13.5H5.5C5.63261 13.5 5.75979 13.4473 5.85355 13.3536C5.94732 13.2598 6 13.1326 6 13C6 12.8674 5.94732 12.7402 5.85355 12.6464C5.75979 12.5527 5.63261 12.5 5.5 12.5H4.20703L6.85352 9.85352C6.94726 9.75975 6.99992 9.63259 6.99992 9.5C6.99992 9.36741 6.94726 9.24025 6.85352 9.14648C6.75976 9.05271 6.6326 9.00002 6.5 9Z M9.5 9C9.3674 9.00002 9.24024 9.05271 9.14648 9.14648C9.05274 9.24025 9.00008 9.36741 9.00008 9.5C9.00008 9.63259 9.05274 9.75975 9.14648 9.85352L11.793 12.5H10.5C10.3674 12.5 10.2402 12.5527 10.1464 12.6464C10.0527 12.7402 10 12.8674 10 13C10 13.1326 10.0527 13.2598 10.1464 13.3536C10.2402 13.4473 10.3674 13.5 10.5 13.5H13C13.1326 13.5 13.2598 13.4473 13.3535 13.3535C13.4415 13.2625 13.4917 13.1414 13.4939 13.0148C13.496 13.0099 13.498 13.005 13.5 13V10.5C13.5 10.3674 13.4473 10.2402 13.3536 10.1464C13.2598 10.0527 13.1326 10 13 10C12.8674 10 12.7402 10.0527 12.6464 10.1464C12.5527 10.2402 12.5 10.3674 12.5 10.5V11.793L9.85352 9.14648C9.75976 9.05271 9.6326 9.00002 9.5 9Z', - ArrowsOutSimple: - 'M10 2.5C9.86739 2.5 9.74021 2.55268 9.64645 2.64645C9.55268 2.74021 9.5 2.86739 9.5 3C9.5 3.13261 9.55268 3.25979 9.64645 3.35355C9.74021 3.44732 9.86739 3.5 10 3.5H11.793L9.14648 6.14648C9.05274 6.24025 9.00008 6.36741 9.00008 6.5C9.00008 6.63259 9.05274 6.75975 9.14648 6.85352C9.24025 6.94726 9.36741 6.99992 9.5 6.99992C9.63259 6.99992 9.75975 6.94726 9.85352 6.85352L12.5 4.20703V6C12.5 6.13261 12.5527 6.25979 12.6464 6.35355C12.7402 6.44732 12.8674 6.5 13 6.5C13.1326 6.5 13.2598 6.44732 13.3536 6.35355C13.4473 6.25979 13.5 6.13261 13.5 6V3C13.498 2.99504 13.496 2.99012 13.4939 2.98523C13.4917 2.85861 13.4415 2.73755 13.3535 2.64648C13.2598 2.55272 13.1326 2.50003 13 2.5H10Z M6.5 9C6.3674 9.00002 6.24024 9.05271 6.14648 9.14648L3.5 11.793V10C3.5 9.86739 3.44732 9.74021 3.35355 9.64645C3.25979 9.55268 3.13261 9.5 3 9.5C2.86739 9.5 2.74021 9.55268 2.64645 9.64645C2.55268 9.74021 2.5 9.86739 2.5 10V13C2.50002 13.1326 2.55271 13.2598 2.64648 13.3535C2.74024 13.4473 2.8674 13.5 3 13.5H6C6.13261 13.5 6.25979 13.4473 6.35355 13.3536C6.44732 13.2598 6.5 13.1326 6.5 13C6.5 12.8674 6.44732 12.7402 6.35355 12.6464C6.25979 12.5527 6.13261 12.5 6 12.5H4.20703L6.85352 9.85352C6.94726 9.75975 6.99992 9.63259 6.99992 9.5C6.99992 9.36741 6.94726 9.24025 6.85352 9.14648C6.75976 9.05271 6.6326 9.00002 6.5 9Z', - AssetsReview: - 'M4 2.5C4 2.22386 4.22386 2 4.5 2H13.5C13.7761 2 14 2.22386 14 2.5V8.5C14 8.77614 13.7761 9 13.5 9C13.2239 9 13 8.77614 13 8.5V3H4.5C4.22386 3 4 2.77614 4 2.5Z M5.56817 7.31822C5.90707 7.31822 6.1818 7.04348 6.1818 6.70458C6.1818 6.36568 5.90707 6.09094 5.56817 6.09094C5.22926 6.09094 4.95453 6.36568 4.95453 6.70458C4.95453 7.04348 5.22926 7.31822 5.56817 7.31822Z M2 5C2 4.44772 2.44772 4 3 4H11C11.5523 4 12 4.44772 12 5V9.93248C12 10.7803 11.0111 11.2435 10.3598 10.7007L8.57178 9.21067L7.23941 10.8761C6.86708 11.3415 6.17288 11.38 5.75143 10.9585L5 10.2071L3 12.2071V13H7.5C7.77614 13 8 13.2239 8 13.5C8 13.7761 7.77614 14 7.5 14H3C2.44771 14 2 13.5523 2 13V5ZM3 10.7929L4.29289 9.5C4.68342 9.10947 5.31658 9.10948 5.70711 9.5L6.45854 10.2514L7.79091 8.58597C8.14081 8.1486 8.78167 8.08387 9.21196 8.44245L11 9.93248V5H3V10.7929Z M14.8537 10.6466C15.0489 10.842 15.0487 11.1586 14.8534 11.3537L11.8505 14.3537C11.7567 14.4475 11.6294 14.5001 11.4968 14.5C11.3641 14.4999 11.2369 14.4471 11.1432 14.3532L9.64611 12.8532C9.45103 12.6578 9.45134 12.3412 9.64679 12.1461C9.84224 11.951 10.1588 11.9513 10.3539 12.1468L11.4976 13.2927L14.1466 10.6463C14.342 10.4511 14.6586 10.4513 14.8537 10.6466Z', - AssetsReviewElementFeature: - 'M4 2.5C4 2.22386 4.22386 2 4.5 2H13.5C13.7761 2 14 2.22386 14 2.5V8.5C14 8.77614 13.7761 9 13.5 9C13.2239 9 13 8.77614 13 8.5V3H4.5C4.22386 3 4 2.77614 4 2.5Z M5.56817 7.31822C5.90707 7.31822 6.1818 7.04348 6.1818 6.70458C6.1818 6.36568 5.90707 6.09094 5.56817 6.09094C5.22926 6.09094 4.95453 6.36568 4.95453 6.70458C4.95453 7.04348 5.22926 7.31822 5.56817 7.31822Z M2 5C2 4.44772 2.44772 4 3 4H11C11.5523 4 12 4.44772 12 5V9.93248C12 10.7803 11.0111 11.2435 10.3598 10.7007L8.57178 9.21067L7.23941 10.8761C6.86708 11.3415 6.17288 11.38 5.75143 10.9585L5 10.2071L3 12.2071V13H7.5C7.77614 13 8 13.2239 8 13.5C8 13.7761 7.77614 14 7.5 14H3C2.44771 14 2 13.5523 2 13V5ZM3 10.7929L4.29289 9.5C4.68342 9.10947 5.31658 9.10948 5.70711 9.5L6.45854 10.2514L7.79091 8.58597C8.14081 8.1486 8.78167 8.08387 9.21196 8.44245L11 9.93248V5H3V10.7929Z M14.8537 10.6466C15.0489 10.842 15.0487 11.1586 14.8534 11.3537L11.8505 14.3537C11.7567 14.4475 11.6294 14.5001 11.4968 14.5C11.3641 14.4999 11.2369 14.4471 11.1432 14.3532L9.64611 12.8532C9.45103 12.6578 9.45134 12.3412 9.64679 12.1461C9.84224 11.951 10.1588 11.9513 10.3539 12.1468L11.4976 13.2927L14.1466 10.6463C14.342 10.4511 14.6586 10.4513 14.8537 10.6466Z', - At: - 'M7.95653 1.50147C5.62075 1.51775 3.41484 2.79801 2.27172 4.92969C0.864707 7.55329 1.42287 10.8043 3.6245 12.8082C5.82601 14.8123 9.11517 15.0634 11.5953 13.4166C11.65 13.3803 11.697 13.3336 11.7337 13.2791C11.7703 13.2246 11.7959 13.1634 11.8089 13.0991C11.8219 13.0347 11.8221 12.9684 11.8095 12.904C11.7968 12.8395 11.7717 12.7782 11.7353 12.7235C11.699 12.6688 11.6523 12.6218 11.5978 12.5851C11.5433 12.5485 11.4821 12.523 11.4178 12.51C11.3534 12.497 11.2871 12.4968 11.2227 12.5094C11.1583 12.522 11.0969 12.5472 11.0422 12.5835C8.93971 13.9795 6.16402 13.7677 4.29772 12.0687C2.43131 10.3699 1.96017 7.62635 3.15294 5.40222C4.34565 3.17808 6.89156 2.05252 9.33934 2.66712C11.7871 3.28171 13.4994 5.47637 13.5 8.00012C13.5 8.63042 13.3765 9.17066 13.1692 9.50403C12.9619 9.8374 12.7258 10.0001 12.25 10.0001C11.7742 10.0001 11.5381 9.8374 11.3308 9.50403C11.1235 9.17066 11 8.63042 11 8.00012V5.50012C11 5.36751 10.9473 5.24034 10.8535 5.14657C10.7598 5.0528 10.6326 5.00012 10.5 5.00012C10.3674 5.00012 10.2402 5.0528 10.1464 5.14657C10.0527 5.24034 9.99999 5.36751 9.99999 5.50012V8.00012C9.99999 8.75108 10.1265 9.46067 10.4817 10.032C10.8369 10.6033 11.4758 11.0001 12.25 11.0001C13.0242 11.0001 13.6631 10.6033 14.0183 10.032C14.3735 9.46067 14.5 8.75108 14.5 8.00012C14.5 8.00016 14.5 8.00008 14.5 8.00012C14.4993 5.02309 12.4703 2.42212 9.58287 1.69714C9.04148 1.56121 8.49556 1.49771 7.95653 1.50147Z M7.99999 5C6.34907 5 4.99999 6.34908 4.99999 8C4.99999 9.65092 6.34907 11 7.99999 11C9.65091 11 11 9.65104 11 8.00012C11 6.3492 9.65091 5 7.99999 5ZM7.99999 6C9.11044 6 9.99999 6.88967 9.99999 8.00012C9.99999 9.11058 9.11044 10 7.99999 10C6.88953 10 5.99999 9.11046 5.99999 8C5.99999 6.88955 6.88953 6 7.99999 6Z', - Autonumber: - 'M11 3C11 2.72386 10.7761 2.5 10.5 2.5C10.2239 2.5 10 2.72386 10 3V11.2929L8.35355 9.64645C8.15829 9.45118 7.84171 9.45118 7.64645 9.64645C7.45118 9.84171 7.45118 10.1583 7.64645 10.3536L10.1464 12.8536C10.1944 12.9015 10.2496 12.9377 10.3086 12.9621C10.3667 12.9861 10.4303 12.9996 10.497 13H10.503C10.6399 12.9992 10.7637 12.9434 10.8536 12.8536L13.3536 10.3536C13.5488 10.1583 13.5488 9.84171 13.3536 9.64645C13.1583 9.45118 12.8417 9.45118 12.6464 9.64645L11 11.2929V3Z M4.99999 3.25C4.99999 3.07671 4.91026 2.91578 4.76285 2.82468C4.61544 2.73357 4.43137 2.72529 4.27638 2.80279L3.27638 3.30279C3.02939 3.42628 2.92928 3.72662 3.05277 3.97361C3.17627 4.2206 3.4766 4.32071 3.72359 4.19721L3.99999 4.05902V6.75C3.99999 7.02614 4.22385 7.25 4.49999 7.25C4.77613 7.25 4.99999 7.02614 4.99999 6.75V3.25Z M4.40777 9.4912C4.35519 9.4873 4.30237 9.49454 4.25277 9.51245C4.20317 9.53035 4.15791 9.55851 4.11993 9.59509C4.08195 9.63168 4.05212 9.67585 4.03237 9.72475C3.92897 9.9808 3.63757 10.1045 3.38152 10.0011C3.12547 9.89773 3.00173 9.60633 3.10513 9.35028C3.17753 9.171 3.28693 9.00901 3.42618 8.87488C3.56543 8.74075 3.7314 8.63749 3.91326 8.57184C4.09512 8.5062 4.28878 8.47965 4.4816 8.49393C4.67442 8.5082 4.86206 8.56298 5.03227 8.65469C5.20248 8.74639 5.35144 8.87296 5.46942 9.02614C5.5874 9.17931 5.67175 9.35565 5.71696 9.54363C5.76218 9.73162 5.76724 9.92702 5.73181 10.1171C5.69639 10.3072 5.62128 10.4876 5.51138 10.6467C5.50778 10.6519 5.50408 10.6571 5.50028 10.6621L4.49883 12H5.25C5.52615 12 5.75 12.2239 5.75 12.5C5.75 12.7762 5.52615 13 5.25 13H3.5C3.3107 13 3.13763 12.8931 3.05289 12.7238C2.96816 12.5546 2.98628 12.3519 3.09972 12.2004L4.69284 10.0721C4.72052 10.0302 4.73954 9.98325 4.74874 9.93388C4.7584 9.88204 4.75702 9.82875 4.74469 9.77748C4.73236 9.72621 4.70935 9.67812 4.67718 9.63635C4.645 9.59457 4.60438 9.56005 4.55796 9.53504C4.51153 9.51003 4.46036 9.49509 4.40777 9.4912Z', - BarChartElementFeature: - 'M9.75 2C9.47386 2 9.25 2.22386 9.25 2.5V5H6.25C5.97386 5 5.75 5.22386 5.75 5.5V8H2.75C2.47386 8 2.25 8.22386 2.25 8.5V12.5H1.75C1.47386 12.5 1.25 12.7239 1.25 13C1.25 13.2761 1.47386 13.5 1.75 13.5H14.25C14.5261 13.5 14.75 13.2761 14.75 13C14.75 12.7239 14.5261 12.5 14.25 12.5H13.75V2.5C13.75 2.22386 13.5261 2 13.25 2H9.75ZM12.75 12.5V3H10.25V12.5H12.75ZM9.25 12.5V6H6.75V12.5H9.25ZM3.25 9H5.75V12.5H3.25V9Z', - Barcode: - 'M2 2.5C1.8674 2.50001 1.74023 2.5527 1.64646 2.64646C1.5527 2.74023 1.50001 2.8674 1.5 3V5.5C1.5 5.63261 1.55268 5.75979 1.64645 5.85355C1.74021 5.94732 1.86739 6 2 6C2.13261 6 2.25979 5.94732 2.35355 5.85355C2.44732 5.75979 2.5 5.63261 2.5 5.5V3.5H4.5C4.63261 3.5 4.75979 3.44732 4.85355 3.35355C4.94732 3.25979 5 3.13261 5 3C5 2.86739 4.94732 2.74021 4.85355 2.64645C4.75979 2.55268 4.63261 2.5 4.5 2.5H2Z M11.5 2.5C11.3674 2.5 11.2402 2.55268 11.1464 2.64645C11.0527 2.74021 11 2.86739 11 3C11 3.13261 11.0527 3.25979 11.1464 3.35355C11.2402 3.44732 11.3674 3.5 11.5 3.5H13.5V5.5C13.5 5.63261 13.5527 5.75979 13.6464 5.85355C13.7402 5.94732 13.8674 6 14 6C14.1326 6 14.2598 5.94732 14.3536 5.85355C14.4473 5.75979 14.5 5.63261 14.5 5.5V3C14.5 2.8674 14.4473 2.74023 14.3535 2.64646C14.2598 2.5527 14.1326 2.50001 14 2.5H11.5Z M5 5C4.86739 5 4.74021 5.05268 4.64645 5.14645C4.55268 5.24021 4.5 5.36739 4.5 5.5V10.5C4.5 10.6326 4.55268 10.7598 4.64645 10.8536C4.74021 10.9473 4.86739 11 5 11C5.13261 11 5.25979 10.9473 5.35355 10.8536C5.44732 10.7598 5.5 10.6326 5.5 10.5V5.5C5.5 5.36739 5.44732 5.24021 5.35355 5.14645C5.25979 5.05268 5.13261 5 5 5Z M7 5C6.86739 5 6.74021 5.05268 6.64645 5.14645C6.55268 5.24021 6.5 5.36739 6.5 5.5V10.5C6.5 10.6326 6.55268 10.7598 6.64645 10.8536C6.74021 10.9473 6.86739 11 7 11C7.13261 11 7.25979 10.9473 7.35355 10.8536C7.44732 10.7598 7.5 10.6326 7.5 10.5V5.5C7.5 5.36739 7.44732 5.24021 7.35355 5.14645C7.25979 5.05268 7.13261 5 7 5Z M9 5C8.86739 5 8.74021 5.05268 8.64645 5.14645C8.55268 5.24021 8.5 5.36739 8.5 5.5V10.5C8.5 10.6326 8.55268 10.7598 8.64645 10.8536C8.74021 10.9473 8.86739 11 9 11C9.13261 11 9.25979 10.9473 9.35355 10.8536C9.44732 10.7598 9.5 10.6326 9.5 10.5V5.5C9.5 5.36739 9.44732 5.24021 9.35355 5.14645C9.25979 5.05268 9.13261 5 9 5Z M11 5C10.8674 5 10.7402 5.05268 10.6464 5.14645C10.5527 5.24021 10.5 5.36739 10.5 5.5V10.5C10.5 10.6326 10.5527 10.7598 10.6464 10.8536C10.7402 10.9473 10.8674 11 11 11C11.1326 11 11.2598 10.9473 11.3536 10.8536C11.4473 10.7598 11.5 10.6326 11.5 10.5V5.5C11.5 5.36739 11.4473 5.24021 11.3536 5.14645C11.2598 5.05268 11.1326 5 11 5Z M2 10C1.86739 10 1.74021 10.0527 1.64645 10.1464C1.55268 10.2402 1.5 10.3674 1.5 10.5V13C1.50001 13.1326 1.5527 13.2598 1.64646 13.3535C1.74023 13.4473 1.8674 13.5 2 13.5H4.5C4.63261 13.5 4.75979 13.4473 4.85355 13.3536C4.94732 13.2598 5 13.1326 5 13C5 12.8674 4.94732 12.7402 4.85355 12.6464C4.75979 12.5527 4.63261 12.5 4.5 12.5H2.5V10.5C2.5 10.3674 2.44732 10.2402 2.35355 10.1464C2.25979 10.0527 2.13261 10 2 10Z M14 10C13.8674 10 13.7402 10.0527 13.6464 10.1464C13.5527 10.2402 13.5 10.3674 13.5 10.5V12.5H11.5C11.3674 12.5 11.2402 12.5527 11.1464 12.6464C11.0527 12.7402 11 12.8674 11 13C11 13.1326 11.0527 13.2598 11.1464 13.3536C11.2402 13.4473 11.3674 13.5 11.5 13.5H14C14.1326 13.5 14.2598 13.4473 14.3535 13.3535C14.4473 13.2598 14.5 13.1326 14.5 13V10.5C14.5 10.3674 14.4473 10.2402 14.3536 10.1464C14.2598 10.0527 14.1326 10 14 10Z', - Bell: - 'M6 11.5C5.86739 11.5 5.74021 11.5527 5.64645 11.6464C5.55268 11.7402 5.5 11.8674 5.5 12V12.5C5.49987 13.8749 6.62514 15.0001 8 15C8.66281 15 9.29903 14.7365 9.7677 14.2678C10.2364 13.7991 10.5 13.1628 10.5 12.5V12C10.5 11.8674 10.4473 11.7402 10.3536 11.6464C10.2598 11.5527 10.1326 11.5 10 11.5C9.86739 11.5 9.74021 11.5527 9.64645 11.6464C9.55268 11.7402 9.5 11.8674 9.5 12V12.5C9.50001 12.898 9.34212 13.2793 9.06067 13.5607C8.77926 13.8421 8.398 14 8 14C7.16564 14.0001 6.49992 13.3344 6.5 12.5V12C6.5 11.8674 6.44732 11.7402 6.35355 11.6464C6.25978 11.5527 6.13261 11.5 6 11.5Z M8.03394 1.50012C5.26871 1.48474 3.00893 3.73483 3.01245 6.5V7.00014C3.01245 9.16781 2.56115 10.3731 2.19849 10.9995C2.19857 10.9994 2.1984 10.9997 2.19849 10.9995C2.11088 11.1513 2.06437 11.324 2.06421 11.4992C2.06387 12.0445 2.51528 12.498 3.06055 12.5C3.06116 12.5001 3.06177 12.5001 3.06238 12.5002H12.9374C12.938 12.5001 12.9386 12.5001 12.9392 12.5C13.1144 12.4994 13.2863 12.4529 13.4377 12.3649C13.9096 12.0911 14.0746 11.4723 13.8016 10.9999C13.8017 11.0001 13.8016 10.9998 13.8016 10.9999C13.4389 10.3735 12.9874 9.16781 12.9874 7.00015V6.5563C12.9874 3.80889 10.7849 1.52098 8.03503 1.50015C8.03466 1.50015 8.03431 1.50012 8.03394 1.50012ZM8.02734 2.5C8.02716 2.5 8.02753 2.5 8.02734 2.5C10.2272 2.51694 11.9874 4.34136 11.9874 6.55628V7.00012C11.9874 9.30695 12.4736 10.7013 12.9358 11.5C12.9361 11.499 12.9383 11.4986 12.9358 11.5L3.06434 11.5001C3.06426 11.5003 3.06443 11.5 3.06434 11.5001C3.52659 10.7015 4.01246 9.30699 4.01246 7.00013V6.50013C4.01242 6.50041 4.0125 6.49985 4.01246 6.50013C4.00929 4.27789 5.80529 2.48824 8.02734 2.5Z', - BellRinging: - 'M4.64771 1.01233C4.5184 0.983089 4.38276 1.0064 4.27064 1.07715C3.17507 1.76873 2.29071 2.74848 1.71461 3.90894C1.68541 3.96775 1.66809 4.03174 1.66363 4.09725C1.65917 4.16276 1.66765 4.22851 1.6886 4.29074C1.70955 4.35297 1.74255 4.41047 1.78572 4.45994C1.82889 4.50942 1.88138 4.54991 1.94019 4.5791C2.05896 4.63807 2.19629 4.64744 2.32197 4.60516C2.44765 4.56288 2.55138 4.4724 2.61036 4.35364C3.10491 3.35744 3.86396 2.51654 4.80445 1.92285C4.91658 1.85206 4.996 1.73963 5.02523 1.61029C5.05446 1.48094 5.03112 1.34529 4.96033 1.23315C4.92528 1.17762 4.87963 1.12953 4.82599 1.09164C4.77235 1.05375 4.71177 1.0268 4.64771 1.01233Z M11.3523 1.01233C11.2882 1.0268 11.2277 1.05375 11.174 1.09164C11.1204 1.12953 11.0747 1.17762 11.0397 1.23315C10.9689 1.34529 10.9456 1.48094 10.9748 1.61029C11.004 1.73963 11.0834 1.85206 11.1956 1.92285C12.1361 2.51652 12.8951 3.35731 13.3897 4.35352C13.4188 4.41233 13.4593 4.46482 13.5088 4.50799C13.5583 4.55116 13.6158 4.58416 13.678 4.60511C13.7402 4.62605 13.806 4.63454 13.8715 4.63008C13.937 4.62562 14.001 4.60829 14.0598 4.5791C14.1186 4.54991 14.1711 4.50942 14.2143 4.45994C14.2575 4.41047 14.2905 4.35297 14.3114 4.29074C14.3324 4.22851 14.3408 4.16276 14.3364 4.09725C14.3319 4.03174 14.3146 3.96775 14.2854 3.90894C13.7093 2.74847 12.825 1.76871 11.7294 1.07715C11.6173 1.0064 11.4816 0.983089 11.3523 1.01233Z M8.25367 2.01929C7.5282 1.98203 6.7897 2.10281 6.09132 2.39209C4.22899 3.1635 3.01248 4.98412 3.01246 6.99988C3.01246 6.99982 3.01246 6.99992 3.01246 6.99988C3.01244 9.16747 2.56115 10.373 2.19849 10.9994C2.19857 10.9992 2.19841 10.9996 2.19849 10.9994C2.11087 11.1511 2.06438 11.3238 2.06422 11.499C2.06422 11.4989 2.06422 11.4991 2.06422 11.499C2.06387 12.0442 2.51529 12.4979 3.06056 12.4999C3.06117 12.4999 3.06178 12.4999 3.06239 12.4999H5.50001C5.49988 13.8748 6.62514 15.0001 8.00001 15C8.66282 15 9.29904 14.7365 9.76771 14.2678C10.2364 13.7991 10.5 13.1628 10.5 12.5L12.9374 12.4999C12.938 12.4999 12.9386 12.4999 12.9392 12.4999C13.1144 12.4993 13.2863 12.4527 13.4378 12.3647C13.9095 12.0908 14.0746 11.4721 13.8016 10.9998C13.8017 10.9999 13.8016 10.9996 13.8016 10.9998C13.4389 10.3734 12.9874 9.16754 12.9874 6.99988C12.9874 5.67731 12.4618 4.40846 11.5266 3.47327C10.6358 2.58239 9.46279 2.08138 8.25367 2.01929ZM8.20338 3.01709C9.16957 3.06641 10.1061 3.46702 10.8195 4.18042C11.5674 4.92836 11.9874 5.94226 11.9874 7C11.9875 9.3067 12.4734 10.7012 12.9357 11.4999H3.06422C3.52645 10.7012 4.01246 9.30666 4.01246 6.99988C4.01244 5.38566 4.98266 3.93366 6.47401 3.31592C7.03326 3.08427 7.62366 2.9875 8.20338 3.01709ZM3.06422 11.4999C3.06414 11.5001 3.06431 11.4998 3.06422 11.4999ZM6.50001 12.4999H9.50001C9.50002 12.8979 9.34213 13.2793 9.06068 13.5607C8.77927 13.8421 8.39801 14 8.00001 14C7.16565 14.0001 6.49993 13.3343 6.50001 12.4999Z', - BellSimpleFill: - 'M13.819 12a.994.994 0 0 1-.869.5H3.05a.994.994 0 0 1-.869-.5.988 .988 0 0 1 .006-1.006c.369-.637.813-1.85.813-3.994v-.5a4.994 4.994 0 0 1 5-5h.037c2.737.019 4.963 2.288 4.963 5.056V7c0 2.144.444 3.356.813 3.994A.988.988 0 0 1 13.819 12Zm-3.825 1.5h-4a.5.5 0 1 0 0 1h4a.5.5 0 0 0 0-1Z', - BellSimpleRingingFill: - 'M2.163 4.631a.581.581 0 0 1-.219-.05.5 .5 0 0 1-.225-.669A6.981 6.981 0 0 1 4.269 1.075a.5.5 0 0 1 .531.85 6.063 6.063 0 0 0-2.187 2.425A.494.494 0 0 1 2.163 4.631ZM10 13.5H6a.5.5 0 0 0 0 1h4a.5.5 0 0 0 0-1Zm2.988-6.5a4.981 4.981 0 0 0-4.95-5H8a4.994 4.994 0 0 0-4.987 5c0 2.144-.444 3.356-.812 3.994a1.013 1.013 0 0 0-.006 1.006A.994.994 0 0 0 3.063 12.5H12.938a.994.994 0 0 0 .869-.5 1.013 1.013 0 0 0-.006-1.006C13.431 10.356 12.988 9.144 12.988 7Zm1.294-3.087a6.981 6.981 0 0 0-2.55-2.837.5 .5 0 0 0-.531.85 6.063 6.063 0 0 1 2.188 2.425.494 .494 0 0 0 .45.281 .581.581 0 0 0 .219-.05A.5.5 0 0 0 14.281 3.913Z', - BoltList: - 'M2.5 4C2.5 3.72386 2.72386 3.5 3 3.5H10.5C10.7761 3.5 11 3.72386 11 4C11 4.27614 10.7761 4.5 10.5 4.5H3C2.72386 4.5 2.5 4.27614 2.5 4Z M2.5 8C2.5 7.72386 2.72386 7.5 3 7.5H7C7.27614 7.5 7.5 7.72386 7.5 8C7.5 8.27614 7.27614 8.5 7 8.5H3C2.72386 8.5 2.5 8.27614 2.5 8Z M2.5 12C2.5 11.7239 2.72386 11.5 3 11.5H6C6.27614 11.5 6.5 11.7239 6.5 12C6.5 12.2761 6.27614 12.5 6 12.5H3C2.72386 12.5 2.5 12.2761 2.5 12Z M12.9178 5.04993C13.1208 5.14814 13.2329 5.37009 13.1915 5.59177L12.6667 8.40316L14.6864 9.21464C14.8366 9.27499 14.9483 9.40429 14.9861 9.56166C15.024 9.71902 14.9833 9.88497 14.877 10.007L10.677 14.8284C10.5289 14.9985 10.2853 15.0483 10.0823 14.9501C9.87925 14.8519 9.76712 14.63 9.8085 14.4083L10.3333 11.5969L8.3136 10.7854C8.16341 10.7251 8.05173 10.5958 8.01387 10.4384C7.97602 10.281 8.01668 10.1151 8.12299 9.99303L12.323 5.1716C12.4711 5.00156 12.7147 4.95172 12.9178 5.04993ZM9.3389 10.1197L11.0864 10.8218C11.3076 10.9106 11.4352 11.1432 11.3915 11.3775L11.1282 12.7881L13.6611 9.88039L11.9136 9.17826C11.6925 9.08941 11.5648 8.85684 11.6085 8.62256L11.8718 7.21198L9.3389 10.1197Z', - Book: - 'M4.50206 1.5C3.4005 1.49538 2.49537 2.40051 2.49999 3.50208V14C2.5 14.1326 2.55268 14.2598 2.64645 14.3535C2.74021 14.4473 2.86738 14.5 2.99999 14.5H12C12.1326 14.5 12.2598 14.4473 12.3535 14.3536C12.4473 14.2598 12.5 14.1326 12.5 14C12.5 13.8674 12.4473 13.7402 12.3535 13.6464C12.2598 13.5527 12.1326 13.5 12 13.5H3.49999V13.4979C3.49767 12.9398 3.93975 12.4977 4.49791 12.5H13C13.1326 12.5 13.2598 12.4473 13.3535 12.3535C13.4473 12.2598 13.5 12.1326 13.5 12V2C13.5 1.8674 13.4473 1.74023 13.3535 1.64646C13.2598 1.5527 13.1326 1.50002 13 1.5H4.50206ZM4.49791 2.5H12.5V11.5H4.50206C4.13616 11.4985 3.7964 11.6046 3.49999 11.7777V3.49793C3.49765 2.93976 3.93975 2.49766 4.49791 2.5Z', - BookOpen: - 'M2 3C1.45364 3 1 3.45364 1 4V12C1 12.5464 1.45364 13 2 13H6C6.398 13 6.77926 13.1579 7.06067 13.4393C7.34212 13.7207 7.50001 14.102 7.5 14.5C7.50186 14.6314 7.55535 14.7568 7.64892 14.849C7.74249 14.9413 7.8686 14.993 8 14.993C8.1314 14.993 8.25751 14.9413 8.35108 14.849C8.44465 14.7568 8.49814 14.6314 8.5 14.5V5.5C8.50013 4.12514 7.37486 2.99987 6 3H2ZM2 4H6C6.83436 3.99992 7.50008 4.66564 7.5 5.5V12.5127C7.06877 12.1874 6.54629 12 6 12H2V4Z M10 3C8.62514 2.99987 7.49987 4.12514 7.5 5.5C7.5 5.63261 7.55268 5.75979 7.64645 5.85355C7.74021 5.94732 7.86739 6 8 6C8.13261 6 8.25979 5.94732 8.35355 5.85355C8.44732 5.75979 8.5 5.63261 8.5 5.5C8.49994 4.66564 9.16564 3.99992 10 4H14V12H10C9.33719 12 8.70097 12.2635 8.2323 12.7322C7.76355 13.2009 7.49998 13.8371 7.5 14.5C7.5 14.6326 7.55268 14.7598 7.64645 14.8536C7.74021 14.9473 7.86739 15 8 15C8.13261 15 8.25979 14.9473 8.35355 14.8536C8.44732 14.7598 8.5 14.6326 8.5 14.5C8.49999 14.102 8.65788 13.7207 8.93933 13.4393C9.22074 13.1579 9.602 13 10 13H14C14.5464 13 15 12.5464 15 12V4C15 3.45364 14.5464 3 14 3H10Z', - Books: - 'M11.0402 1.81933C10.9739 1.82309 10.9072 1.83355 10.8409 1.85131L8.901 2.37133C8.90104 2.37133 8.90096 2.37133 8.901 2.37133C8.3711 2.51344 8.04895 3.07126 8.19092 3.60119C8.19088 3.60115 8.19096 3.60123 8.19092 3.60119L10.791 13.3013C10.9331 13.8312 11.491 14.1534 12.021 14.0114L13.9611 13.4913C14.491 13.3493 14.8131 12.7913 14.671 12.2614L12.071 2.5614C11.9467 2.09768 11.5039 1.79302 11.0402 1.81933ZM11.9209 4.14196L9.02087 4.91699C8.89279 4.95124 8.78356 5.03496 8.7172 5.14974C8.65083 5.26452 8.63278 5.40096 8.66699 5.52905C8.70122 5.65715 8.78493 5.76641 8.89971 5.8328C9.01449 5.89919 9.15094 5.91726 9.27905 5.88305L12.1791 5.10803C12.2425 5.09108 12.302 5.0618 12.3541 5.02186C12.4063 4.98193 12.45 4.93211 12.4829 4.87526C12.5158 4.81841 12.5371 4.75565 12.5457 4.69055C12.5543 4.62545 12.55 4.55929 12.5331 4.49585C12.5161 4.43241 12.4868 4.37294 12.4469 4.32083C12.4069 4.26871 12.3571 4.22498 12.3003 4.19212C12.2434 4.15926 12.1807 4.13791 12.1156 4.12931C12.0505 4.1207 11.9843 4.125 11.9209 4.14196ZM13.4698 9.93591L10.5698 10.7172C10.4418 10.7517 10.3327 10.8356 10.2666 10.9505C10.2005 11.0654 10.1827 11.2019 10.2172 11.33C10.2516 11.458 10.3356 11.5671 10.4505 11.6333C10.5654 11.6994 10.7019 11.7172 10.83 11.6827L13.73 10.9015C13.7934 10.8844 13.8528 10.855 13.9049 10.815C13.9569 10.7749 14.0006 10.725 14.0333 10.6681C14.0661 10.6112 14.0873 10.5484 14.0958 10.4833C14.1043 10.4181 14.0999 10.352 14.0828 10.2886C14.0657 10.2252 14.0363 10.1658 13.9962 10.1137C13.9562 10.0617 13.9063 10.018 13.8493 9.9853C13.7924 9.95255 13.7296 9.93133 13.6645 9.92286C13.5994 9.91438 13.5332 9.91882 13.4698 9.93591ZM11.101 2.82129L13.701 12.5214L11.761 13.0413L9.16101 3.34143L11.101 2.82129Z M3 2C2.45364 2 2 2.45363 2 3V13C2 13.5464 2.45364 14 3 14H5C5.18216 14 5.35183 13.946 5.5 13.8585C5.64817 13.946 5.81784 14 6 14H8C8.54636 14 9 13.5464 9 13V3C9 2.45363 8.54636 2 8 2H6C5.81784 2 5.64817 2.054 5.5 2.14148C5.35183 2.054 5.18216 2 5 2H3ZM3 3H5V4.5H3V3ZM6 3H8V10.5H6V3ZM3 5.5H5V13H3V5.5ZM6 11.5H8V13H6V11.5Z', - Buildings: - 'M4 4C3.86739 4 3.74021 4.05268 3.64645 4.14645C3.55268 4.24021 3.5 4.36739 3.5 4.5C3.5 4.63261 3.55268 4.75979 3.64645 4.85355C3.74021 4.94732 3.86739 5 4 5H6C6.13261 5 6.25979 4.94732 6.35355 4.85355C6.44732 4.75979 6.5 4.63261 6.5 4.5C6.5 4.36739 6.44732 4.24021 6.35355 4.14645C6.25979 4.05268 6.13261 4 6 4H4Z M5 8C4.86739 8 4.74021 8.05268 4.64645 8.14645C4.55268 8.24021 4.5 8.36739 4.5 8.5C4.5 8.63261 4.55268 8.75979 4.64645 8.85355C4.74021 8.94732 4.86739 9 5 9H7C7.13261 9 7.25979 8.94732 7.35355 8.85355C7.44732 8.75979 7.5 8.63261 7.5 8.5C7.5 8.36739 7.44732 8.24021 7.35355 8.14645C7.25979 8.05268 7.13261 8 7 8H5Z M4 10.5C3.86739 10.5 3.74021 10.5527 3.64645 10.6464C3.55268 10.7402 3.5 10.8674 3.5 11C3.5 11.1326 3.55268 11.2598 3.64645 11.3536C3.74021 11.4473 3.86739 11.5 4 11.5H6C6.13261 11.5 6.25979 11.4473 6.35355 11.3536C6.44732 11.2598 6.5 11.1326 6.5 11C6.5 10.8674 6.44732 10.7402 6.35355 10.6464C6.25979 10.5527 6.13261 10.5 6 10.5H4Z M11 8C10.8674 8 10.7402 8.05268 10.6464 8.14645C10.5527 8.24021 10.5 8.36739 10.5 8.5C10.5 8.63261 10.5527 8.75979 10.6464 8.85355C10.7402 8.94732 10.8674 9 11 9H12C12.1326 9 12.2598 8.94732 12.3536 8.85355C12.4473 8.75979 12.5 8.63261 12.5 8.5C12.5 8.36739 12.4473 8.24021 12.3536 8.14645C12.2598 8.05268 12.1326 8 12 8H11Z M11 10.5C10.8674 10.5 10.7402 10.5527 10.6464 10.6464C10.5527 10.7402 10.5 10.8674 10.5 11C10.5 11.1326 10.5527 11.2598 10.6464 11.3536C10.7402 11.4473 10.8674 11.5 11 11.5H12C12.1326 11.5 12.2598 11.4473 12.3536 11.3536C12.4473 11.2598 12.5 11.1326 12.5 11C12.5 10.8674 12.4473 10.7402 12.3536 10.6464C12.2598 10.5527 12.1326 10.5 12 10.5H11Z M2.5 1.5C1.95364 1.5 1.5 1.95364 1.5 2.5V13H1C0.867392 13 0.740215 13.0527 0.646447 13.1464C0.552678 13.2402 0.5 13.3674 0.5 13.5C0.5 13.6326 0.552678 13.7598 0.646447 13.8536C0.740215 13.9473 0.867392 14 1 14H15C15.1326 14 15.2598 13.9473 15.3536 13.8536C15.4473 13.7598 15.5 13.6326 15.5 13.5C15.5 13.3674 15.4473 13.2402 15.3536 13.1464C15.2598 13.0527 15.1326 13 15 13H14.5V6.5C14.5 5.95364 14.0464 5.5 13.5 5.5H9.5V2.5C9.5 2.50004 9.5 2.49996 9.5 2.5C9.49993 1.95369 9.04643 1.50007 8.50012 1.5C8.50016 1.5 8.50008 1.5 8.50012 1.5H2.5ZM2.5 2.5H8.5V13H2.5V2.5ZM9.5 6.5H13.5V13H9.5V6.5Z', - ButtonElementFeature: - 'M1 6C1 4.89543 1.89543 4 3 4H13C14.1046 4 15 4.89543 15 6V8C15 9.10457 14.1046 10 13 10H12.5C12.2239 10 12 9.77614 12 9.5C12 9.22386 12.2239 9 12.5 9H13C13.5523 9 14 8.55228 14 8V6C14 5.44772 13.5523 5 13 5H3C2.44772 5 2 5.44772 2 6V8C2 8.55228 2.44772 9 3 9H4C4.27614 9 4.5 9.22386 4.5 9.5C4.5 9.77614 4.27614 10 4 10H3C1.89543 10 1 9.10457 1 8V6Z M10.5262 8.74729L5.99134 7.04673C5.85944 6.99727 5.71606 6.9868 5.57838 7.01662C5.44069 7.04644 5.3145 7.11527 5.21488 7.21488C5.11527 7.3145 5.04644 7.44069 5.01662 7.57838C4.9868 7.71606 4.99727 7.85944 5.04673 7.99134L6.74672 12.5246L6.74729 12.5262C6.80034 12.6662 6.89498 12.7867 7.01851 12.8714C7.14204 12.9561 7.28853 13.001 7.4383 13C7.58808 12.999 7.73396 12.9522 7.85635 12.8658C7.97873 12.7795 8.07177 12.6577 8.12295 12.517L8.57266 11.2798L10.1464 12.8536C10.3417 13.0488 10.6583 13.0488 10.8536 12.8536C11.0488 12.6583 11.0488 12.3417 10.8536 12.1464L9.27977 10.5727L10.517 10.1229C10.6578 10.0718 10.7795 9.97873 10.8658 9.85635C10.9522 9.73396 10.999 9.58808 11 9.4383C11.001 9.28853 10.9561 9.14204 10.8714 9.01851C10.7867 8.89498 10.6662 8.80034 10.5262 8.74729ZM9.49745 9.42952L6.18877 8.18877L7.42952 11.4974L7.86445 10.301C7.9013 10.2009 7.95941 10.1101 8.03477 10.0348C8.11013 9.95941 8.20094 9.9013 8.30095 9.86445L9.49745 9.42952Z', - Calculator: - 'M5.5 10C5.91419 10 6.25 9.66419 6.25 9.25C6.25 8.83581 5.91419 8.5 5.5 8.5C5.08581 8.5 4.75 8.83581 4.75 9.25C4.75 9.66419 5.08581 10 5.5 10Z M8 10C8.41419 10 8.75 9.66419 8.75 9.25C8.75 8.83581 8.41419 8.5 8 8.5C7.58581 8.5 7.25 8.83581 7.25 9.25C7.25 9.66419 7.58581 10 8 10Z M10.5 10C10.9142 10 11.25 9.66419 11.25 9.25C11.25 8.83581 10.9142 8.5 10.5 8.5C10.0858 8.5 9.75 8.83581 9.75 9.25C9.75 9.66419 10.0858 10 10.5 10Z M5.5 12.5C5.91419 12.5 6.25 12.1642 6.25 11.75C6.25 11.3358 5.91419 11 5.5 11C5.08581 11 4.75 11.3358 4.75 11.75C4.75 12.1642 5.08581 12.5 5.5 12.5Z M8 12.5C8.41419 12.5 8.75 12.1642 8.75 11.75C8.75 11.3358 8.41419 11 8 11C7.58581 11 7.25 11.3358 7.25 11.75C7.25 12.1642 7.58581 12.5 8 12.5Z M10.5 12.5C10.9142 12.5 11.25 12.1642 11.25 11.75C11.25 11.3358 10.9142 11 10.5 11C10.0858 11 9.75 11.3358 9.75 11.75C9.75 12.1642 10.0858 12.5 10.5 12.5Z M5 3.5C4.8674 3.50001 4.74023 3.5527 4.64646 3.64646C4.5527 3.74023 4.50001 3.8674 4.5 4V7C4.50001 7.1326 4.5527 7.25977 4.64646 7.35354C4.74023 7.4473 4.8674 7.49999 5 7.5H11C11.1326 7.49999 11.2598 7.4473 11.3535 7.35354C11.4473 7.25977 11.5 7.1326 11.5 7V4C11.5 3.8674 11.4473 3.74023 11.3535 3.64646C11.2598 3.5527 11.1326 3.50001 11 3.5H5ZM5.5 4.5H10.5V6.5H5.5V4.5Z M3.5 1.5C2.95364 1.5 2.5 1.95364 2.5 2.5V13.5C2.5 14.0464 2.95364 14.5 3.5 14.5H12.5C13.0464 14.5 13.5 14.0464 13.5 13.5V2.5C13.5 1.95364 13.0464 1.5 12.5 1.5H3.5ZM3.5 2.5H12.5V13.5H3.5V2.5Z', - Calendar: - 'M4.5 8C4.22386 8 4 8.22386 4 8.5C4 8.77614 4.22386 9 4.5 9H11C11.2761 9 11.5 8.77614 11.5 8.5C11.5 8.22386 11.2761 8 11 8H4.5Z M4 10.5C4 10.2239 4.22386 10 4.5 10H7.5C7.77614 10 8 10.2239 8 10.5C8 10.7761 7.77614 11 7.5 11H4.5C4.22386 11 4 10.7761 4 10.5Z M11.5 0.5C11.5 0.223858 11.2761 0 11 0C10.7239 0 10.5 0.223858 10.5 0.5V2H5.5V0.5C5.5 0.223858 5.27614 0 5 0C4.72386 0 4.5 0.223858 4.5 0.5V2H2.5C1.67157 2 1 2.67157 1 3.5V12.5C1 13.3284 1.67157 14 2.5 14H13.5C14.3284 14 15 13.3284 15 12.5V3.5C15 2.67157 14.3284 2 13.5 2H11.5V0.5ZM5 3.00082C4.99029 3.00082 4.98064 3.00055 4.97107 3H2.5C2.22386 3 2 3.22386 2 3.5V5H14V3.5C14 3.22386 13.7761 3 13.5 3H11.0289C11.0194 3.00055 11.0097 3.00082 11 3.00082C10.9903 3.00082 10.9806 3.00055 10.9711 3H5.02893C5.01936 3.00055 5.00971 3.00082 5 3.00082ZM2 12.5V6H14V12.5C14 12.7761 13.7761 13 13.5 13H2.5C2.22386 13 2 12.7761 2 12.5Z', - CalendarBlank: - 'M5 1C4.86739 1 4.74021 1.05268 4.64645 1.14645C4.55268 1.24021 4.5 1.36739 4.5 1.5V2H3C2.45364 2 2 2.45364 2 3V13C2 13.5464 2.45364 14 3 14H13C13.5464 14 14 13.5464 14 13V3C14 2.45364 13.5464 2 13 2H11.5V1.5C11.5 1.36739 11.4473 1.24021 11.3536 1.14645C11.2598 1.05268 11.1326 1 11 1C10.8674 1 10.7402 1.05268 10.6464 1.14645C10.5527 1.24021 10.5 1.36739 10.5 1.5V2H5.5V1.5C5.5 1.36739 5.44732 1.24021 5.35355 1.14645C5.25979 1.05268 5.13261 1 5 1ZM3 3H4.5V3.5C4.5 3.63261 4.55268 3.75979 4.64645 3.85355C4.74021 3.94732 4.86739 4 5 4C5.13261 4 5.25979 3.94732 5.35355 3.85355C5.44732 3.75979 5.5 3.63261 5.5 3.5V3H10.5V3.5C10.5 3.63261 10.5527 3.75979 10.6464 3.85355C10.7402 3.94732 10.8674 4 11 4C11.1326 4 11.2598 3.94732 11.3536 3.85355C11.4473 3.75979 11.5 3.63261 11.5 3.5V3H13V5H3V3ZM3 6H13V13H3V6Z', - CalendarBolt: - 'M11.5 1.5C11.5 1.22386 11.2761 1 11 1C10.7239 1 10.5 1.22386 10.5 1.5V2H5.5V1.5C5.5 1.22386 5.27614 1 5 1C4.72386 1 4.5 1.22386 4.5 1.5V2H3C2.44772 2 2 2.44772 2 3V13C2 13.5523 2.44772 14 3 14H9.5C9.77614 14 10 13.7761 10 13.5C10 13.2239 9.77614 13 9.5 13H3V6H13V7.5C13 7.77614 13.2239 8 13.5 8C13.7761 8 14 7.77614 14 7.5V3C14 2.44772 13.5523 2 13 2H11.5V1.5ZM13 5V3H11.5V3.5C11.5 3.77614 11.2761 4 11 4C10.7239 4 10.5 3.77614 10.5 3.5V3H5.5V3.5C5.5 3.77614 5.27614 4 5 4C4.72386 4 4.5 3.77614 4.5 3.5V3H3V5H13Z M14.5177 9.0499C14.7208 9.14811 14.8329 9.37006 14.7915 9.59175L14.4667 11.3317L15.6864 11.8218C15.8366 11.8821 15.9483 12.0114 15.9861 12.1688C16.024 12.3261 15.9833 12.4921 15.877 12.6141L13.077 15.8284C12.9289 15.9985 12.6853 16.0483 12.4823 15.9501C12.2792 15.8519 12.1671 15.6299 12.2085 15.4083L12.5333 13.6683L11.3136 13.1782C11.1634 13.1179 11.0517 12.9886 11.0139 12.8312C10.976 12.6739 11.0167 12.5079 11.123 12.3859L13.923 9.17158C14.0711 9.00153 14.3147 8.95169 14.5177 9.0499ZM12.3389 12.5125L13.2864 12.8932C13.5075 12.982 13.6352 13.2146 13.5915 13.4489L13.5282 13.788L14.6611 12.4875L13.7136 12.1068C13.4925 12.018 13.3648 11.7854 13.4085 11.5511L13.4718 11.212L12.3389 12.5125Z', - CalendarCheck: - 'M10.2649 7.50024C10.1993 7.49829 10.1339 7.50928 10.0725 7.53259C10.0111 7.5559 9.9549 7.59108 9.9071 7.63611L7.3324 10.0619L6.09412 8.88721C6.04648 8.84201 5.9904 8.80665 5.92909 8.78312C5.86778 8.7596 5.80244 8.74839 5.73679 8.75012C5.67115 8.75186 5.60649 8.76651 5.54651 8.79323C5.48652 8.81996 5.43239 8.85824 5.38721 8.90588C5.34201 8.95352 5.30665 9.0096 5.28312 9.07091C5.2596 9.13222 5.24839 9.19756 5.25013 9.26321C5.25186 9.32885 5.26651 9.39351 5.29323 9.45349C5.31996 9.51347 5.35824 9.56761 5.40588 9.61279L6.98718 11.1128C7.07988 11.2007 7.20268 11.2497 7.33041 11.2499C7.45813 11.2501 7.5811 11.2015 7.67407 11.1139L10.5929 8.36389C10.6894 8.27295 10.7458 8.1474 10.7498 8.01485C10.7537 7.8823 10.7048 7.75362 10.6139 7.65711C10.523 7.56061 10.3974 7.50418 10.2649 7.50024Z M5 1C4.86739 1 4.74021 1.05268 4.64645 1.14645C4.55268 1.24021 4.5 1.36739 4.5 1.5V2H3C2.45364 2 2 2.45364 2 3V13C2 13.5464 2.45364 14 3 14H13C13.5464 14 14 13.5464 14 13V3C14 2.45364 13.5464 2 13 2H11.5V1.5C11.5 1.36739 11.4473 1.24021 11.3536 1.14645C11.2598 1.05268 11.1326 1 11 1C10.8674 1 10.7402 1.05268 10.6464 1.14645C10.5527 1.24021 10.5 1.36739 10.5 1.5V2H5.5V1.5C5.5 1.36739 5.44732 1.24021 5.35355 1.14645C5.25979 1.05268 5.13261 1 5 1ZM3 3H4.5V3.5C4.5 3.63261 4.55268 3.75979 4.64645 3.85355C4.74021 3.94732 4.86739 4 5 4C5.13261 4 5.25979 3.94732 5.35355 3.85355C5.44732 3.75979 5.5 3.63261 5.5 3.5V3H10.5V3.5C10.5 3.63261 10.5527 3.75979 10.6464 3.85355C10.7402 3.94732 10.8674 4 11 4C11.1326 4 11.2598 3.94732 11.3536 3.85355C11.4473 3.75979 11.5 3.63261 11.5 3.5V3H13V5H3V3ZM3 6H13V13H3V6Z', - CaretCircleDown: - 'M5.77625 6.75073C5.64385 6.74375 5.5141 6.78963 5.41553 6.8783C5.36671 6.92222 5.32702 6.97532 5.29873 7.03458C5.27044 7.09384 5.2541 7.1581 5.25064 7.22367C5.24719 7.28925 5.25668 7.35486 5.27858 7.41677C5.30048 7.47868 5.33437 7.53566 5.3783 7.58447L7.6283 10.0845C7.67519 10.1366 7.73251 10.1782 7.79655 10.2068C7.86058 10.2353 7.9299 10.25 8 10.25C8.0701 10.25 8.13942 10.2353 8.20345 10.2068C8.26749 10.1782 8.32481 10.1366 8.3717 10.0845L10.6217 7.58447C10.6656 7.53566 10.6995 7.47868 10.7214 7.41677C10.7433 7.35486 10.7528 7.28925 10.7494 7.22367C10.7459 7.1581 10.7296 7.09384 10.7013 7.03458C10.673 6.97532 10.6333 6.92222 10.5845 6.8783C10.5357 6.83437 10.4787 6.80048 10.4168 6.77858C10.3549 6.75668 10.2892 6.74719 10.2237 6.75064C10.1581 6.7541 10.0938 6.77044 10.0346 6.79873C9.97532 6.82702 9.92222 6.86671 9.8783 6.91553L8 9.00256L6.1217 6.91553C6.07777 6.86672 6.02464 6.82704 5.96537 6.79877C5.90609 6.77049 5.84183 6.75417 5.77625 6.75073Z M8 1.5C4.41604 1.5 1.5 4.41604 1.5 8C1.5 11.5839 4.41603 14.5 8 14.5C11.5839 14.5 14.5 11.5839 14.5 8C14.5 4.41603 11.5839 1.5 8 1.5ZM8 2.5C11.0435 2.5 13.5 4.95647 13.5 8C13.5 11.0435 11.0435 13.5 8 13.5C4.95647 13.5 2.5 11.0435 2.5 8C2.5 4.95647 4.95647 2.5 8 2.5Z', - CarouselElementFeature: - 'M5.5 6.5C5.5 5.67157 6.17157 5 7 5H9C9.82843 5 10.5 5.67157 10.5 6.5V9.5C10.5 10.3284 9.82843 11 9 11H7C6.17157 11 5.5 10.3284 5.5 9.5V6.5ZM7 6C6.72386 6 6.5 6.22386 6.5 6.5V9.5C6.5 9.77614 6.72386 10 7 10H9C9.27614 10 9.5 9.77614 9.5 9.5V6.5C9.5 6.22386 9.27614 6 9 6H7Z M1 3.5C1 2.67157 1.67157 2 2.5 2H13.5C14.3284 2 15 2.67157 15 3.5V12.5C15 13.3284 14.3284 14 13.5 14H2.5C1.67157 14 1 13.3284 1 12.5V3.5ZM14 3.5V5H13C12.1716 5 11.5 5.67157 11.5 6.5V9.5C11.5 10.3284 12.1716 11 13 11H14V12.5C14 12.7761 13.7761 13 13.5 13H2.5C2.22386 13 2 12.7761 2 12.5V11H3C3.82843 11 4.5 10.3284 4.5 9.5V6.5C4.5 5.67157 3.82843 5 3 5H2V3.5C2 3.22386 2.22386 3 2.5 3H13.5C13.7761 3 14 3.22386 14 3.5ZM14 6V10H13C12.7239 10 12.5 9.77614 12.5 9.5V6.5C12.5 6.22386 12.7239 6 13 6H14ZM3 10H2V6H3C3.27614 6 3.5 6.22386 3.5 6.5V9.5C3.5 9.77614 3.27614 10 3 10Z', - ChartBar: - 'M9.75 2C9.6174 2.00001 9.49023 2.0527 9.39646 2.14646C9.3027 2.24023 9.25001 2.3674 9.25 2.5V5H6.25C6.1174 5.00001 5.99023 5.0527 5.89646 5.14646C5.8027 5.24023 5.75001 5.3674 5.75 5.5V8H2.75C2.6174 8.00001 2.49023 8.0527 2.39646 8.14646C2.3027 8.24023 2.25001 8.3674 2.25 8.5V12.5H1.75C1.61739 12.5 1.49021 12.5527 1.39645 12.6464C1.30268 12.7402 1.25 12.8674 1.25 13C1.25 13.1326 1.30268 13.2598 1.39645 13.3536C1.49021 13.4473 1.61739 13.5 1.75 13.5H14.25C14.3826 13.5 14.5098 13.4473 14.6036 13.3536C14.6973 13.2598 14.75 13.1326 14.75 13C14.75 12.8674 14.6973 12.7402 14.6036 12.6464C14.5098 12.5527 14.3826 12.5 14.25 12.5H13.75V2.5C13.75 2.3674 13.6973 2.24023 13.6035 2.14646C13.5098 2.0527 13.3826 2.00001 13.25 2H9.75ZM10.25 3H12.75V12.5H10.25V3ZM6.75 6H9.25V12.5H6.75V6ZM3.25 9H5.75V12.5H3.25V9Z', - Chat: - 'M2.5 3C1.95364 3 1.5 3.45364 1.5 4V13.925C1.5 13.9264 1.5 13.9278 1.5 13.9292C1.50321 14.3138 1.72911 14.665 2.07776 14.8275C2.42665 14.9901 2.84111 14.9369 3.13758 14.6914C3.13819 14.6909 3.1388 14.6903 3.13941 14.6898L5.15821 13.0023C5.15911 13.0015 5.16 13.0008 5.16089 13H13.5C14.0464 13 14.5 12.5464 14.5 12V4C14.5 3.45364 14.0464 3 13.5 3H2.5ZM2.5 4H13.5V12H5.15625C5.15629 12 5.15621 12 5.15625 12C4.91921 12 4.68966 12.0855 4.51038 12.2406L2.5 13.9209V4Z', - ChatTeardropText: - 'M6.24996 6.5C6.11736 6.5 5.99018 6.55268 5.89641 6.64645C5.80264 6.74022 5.74996 6.86739 5.74996 7C5.74996 7.13261 5.80264 7.25979 5.89641 7.35356C5.99018 7.44732 6.11736 7.5 6.24996 7.5H9.99996C10.1326 7.5 10.2598 7.44732 10.3535 7.35356C10.4473 7.25979 10.5 7.13261 10.5 7C10.5 6.86739 10.4473 6.74022 10.3535 6.64645C10.2598 6.55268 10.1326 6.5 9.99996 6.5H6.24996Z M6.24996 8.5C6.11736 8.5 5.99018 8.55268 5.89641 8.64645C5.80264 8.74022 5.74996 8.86739 5.74996 9C5.74996 9.13261 5.80264 9.25979 5.89641 9.35356C5.99018 9.44732 6.11736 9.5 6.24996 9.5H9.99996C10.1326 9.5 10.2598 9.44732 10.3535 9.35356C10.4473 9.25979 10.5 9.13261 10.5 9C10.5 8.86739 10.4473 8.74022 10.3535 8.64645C10.2598 8.55268 10.1326 8.5 9.99996 8.5H6.24996Z M8.11239 1.50183C7.35185 1.51823 6.58765 1.67371 5.85824 1.97583C3.52413 2.94261 2.00009 5.22355 2.00009 7.75V13.0121C1.99658 13.2749 2.09962 13.5286 2.28549 13.7145C2.28541 13.7144 2.28557 13.7146 2.28549 13.7145C2.47132 13.9001 2.72504 14.0033 2.98763 14H8.25009C10.7765 14 13.0575 12.476 14.0243 10.1419C14.991 7.80775 14.4557 5.11726 12.6693 3.33081C11.4411 2.10263 9.78559 1.46576 8.11239 1.50183ZM8.51766 2.50635C9.78982 2.57138 11.0231 3.09877 11.9621 4.03784C13.4646 5.54036 13.9134 7.7961 13.1003 9.75916C12.2872 11.7222 10.3749 13 8.25009 13H3.00009V7.75C3.00009 5.62515 4.27787 3.71276 6.24093 2.89966C6.97708 2.59475 7.75437 2.46733 8.51766 2.50635Z', - ChatCircleText: - 'M5.99999 6.5C5.86738 6.5 5.74021 6.55268 5.64644 6.64645C5.55267 6.74022 5.49999 6.86739 5.49999 7C5.49999 7.13261 5.55267 7.25979 5.64644 7.35356C5.74021 7.44732 5.86738 7.5 5.99999 7.5H9.99999C10.1326 7.5 10.2598 7.44732 10.3535 7.35356C10.4473 7.25979 10.5 7.13261 10.5 7C10.5 6.86739 10.4473 6.74022 10.3535 6.64645C10.2598 6.55268 10.1326 6.5 9.99999 6.5H5.99999Z M5.99999 8.5C5.86738 8.5 5.74021 8.55268 5.64644 8.64645C5.55267 8.74022 5.49999 8.86739 5.49999 9C5.49999 9.13261 5.55267 9.25979 5.64644 9.35356C5.74021 9.44732 5.86738 9.5 5.99999 9.5H9.99999C10.1326 9.5 10.2598 9.44732 10.3535 9.35356C10.4473 9.25979 10.5 9.13261 10.5 9C10.5 8.86739 10.4473 8.74022 10.3535 8.64645C10.2598 8.55268 10.1326 8.5 9.99999 8.5H5.99999Z M7.82299 1.51807C6.47974 1.55197 5.14019 2.00081 4.01073 2.87769C1.48876 4.83568 0.779769 8.33664 2.30187 11.1191L1.77013 12.9972C1.5572 13.7252 2.27478 14.4429 3.0028 14.23L4.88109 13.6982C7.66357 15.2202 11.1645 14.5113 13.1224 11.9894C15.1267 9.40776 14.8944 5.72815 12.5831 3.41687C11.283 2.11678 9.55002 1.47449 7.82299 1.51807ZM7.8496 2.51392C9.30915 2.47663 10.7732 3.02111 11.8761 4.12403C13.8368 6.08476 14.0325 9.18649 12.3325 11.3761C10.6325 13.5658 7.57771 14.1463 5.19237 12.7324C5.13404 12.6979 5.06928 12.6755 5.00205 12.6668C4.93481 12.658 4.8665 12.663 4.80126 12.6815L2.73302 13.2671L3.31859 11.1987C3.33705 11.1335 3.34204 11.0652 3.33327 10.9979C3.3245 10.9307 3.30215 10.8659 3.26757 10.8076C1.85374 8.42228 2.43422 5.36761 4.62389 3.66761C5.58185 2.92387 6.71439 2.54292 7.8496 2.51392Z', - Check: - 'M13.5 4C13.3674 4.00002 13.2402 4.05271 13.1465 4.14648L6.49999 10.793L3.3535 7.64648C3.25974 7.55274 3.13258 7.50008 2.99999 7.50008C2.8674 7.50008 2.74023 7.55274 2.64647 7.64648C2.55272 7.74025 2.50006 7.86741 2.50006 8C2.50006 8.13259 2.55272 8.25975 2.64647 8.35352L6.14647 11.8535C6.24024 11.9472 6.3674 11.9999 6.49999 11.9999C6.63257 11.9999 6.75973 11.9472 6.8535 11.8535L13.8535 4.85352C13.9472 4.75975 13.9999 4.63259 13.9999 4.5C13.9999 4.36741 13.9472 4.24025 13.8535 4.14648C13.7597 4.05271 13.6326 4.00002 13.5 4Z', - CheckBold: - 'M14.2071 3.79289C14.5976 4.18342 14.5976 4.81658 14.2071 5.20711L7.20711 12.2071C6.81658 12.5976 6.18342 12.5976 5.79289 12.2071L2.29289 8.70711C1.90237 8.31658 1.90237 7.68342 2.29289 7.29289C2.68342 6.90237 3.31658 6.90237 3.70711 7.29289L6.5 10.0858L12.7929 3.79289C13.1834 3.40237 13.8166 3.40237 14.2071 3.79289Z', - CheckCircle: - 'M10.7617 6.00012C10.6292 5.99702 10.5008 6.04668 10.4049 6.13818L7.08154 9.30872L5.59546 7.88855C5.49959 7.79693 5.37126 7.74715 5.23869 7.75014C5.10612 7.75314 4.98017 7.80868 4.88855 7.90454C4.79693 8.00041 4.74715 8.12874 4.75014 8.26131C4.75314 8.39388 4.80868 8.51983 4.90454 8.61145L6.73584 10.3615C6.82881 10.4503 6.95244 10.4999 7.08104 10.5C7.20964 10.5 7.33332 10.4506 7.42639 10.3618L11.0951 6.86182C11.1426 6.81651 11.1807 6.76227 11.2073 6.70222C11.2338 6.64217 11.2483 6.57748 11.2499 6.51183C11.2514 6.44619 11.24 6.38089 11.2163 6.31965C11.1927 6.25841 11.1571 6.20243 11.1118 6.15492C11.0665 6.10739 11.0122 6.06925 10.9522 6.04269C10.8921 6.01614 10.8274 6.00166 10.7617 6.00012Z M8 1.5C4.41604 1.5 1.5 4.41604 1.5 8C1.5 11.5839 4.41603 14.5 8 14.5C11.5839 14.5 14.5 11.5839 14.5 8C14.5 4.41603 11.5839 1.5 8 1.5ZM8 2.5C11.0435 2.5 13.5 4.95647 13.5 8C13.5 11.0435 11.0435 13.5 8 13.5C4.95647 13.5 2.5 11.0435 2.5 8C2.5 4.95647 4.95647 2.5 8 2.5Z', - CheckSquare: - 'M10.7617 6.00012C10.6292 5.99702 10.5008 6.04668 10.4049 6.13818L7.08154 9.30872L5.59546 7.88855C5.49959 7.79693 5.37126 7.74715 5.23869 7.75014C5.10612 7.75314 4.98017 7.80868 4.88855 7.90454C4.79693 8.00041 4.74715 8.12874 4.75014 8.26131C4.75314 8.39388 4.80868 8.51983 4.90454 8.61145L6.73584 10.3615C6.82881 10.4503 6.95244 10.4999 7.08104 10.5C7.20964 10.5 7.33332 10.4506 7.42639 10.3618L11.0951 6.86182C11.1426 6.81651 11.1807 6.76227 11.2073 6.70222C11.2338 6.64217 11.2483 6.57748 11.2499 6.51183C11.2514 6.44619 11.24 6.38089 11.2163 6.31965C11.1927 6.25841 11.1571 6.20243 11.1118 6.15492C11.0665 6.10739 11.0122 6.06925 10.9522 6.04269C10.8921 6.01614 10.8274 6.00166 10.7617 6.00012Z M3 2C2.45364 2 2 2.45364 2 3V13C2 13.5464 2.45364 14 3 14H13C13.5464 14 14 13.5464 14 13V3C14 2.45364 13.5464 2 13 2H3ZM3 3H13V13H3V3Z', - ChevronCircleDownFill: - 'M8 1.5C6.71442 1.5 5.45772 1.88122 4.3888 2.59545C3.31988 3.30968 2.48676 4.32484 1.99479 5.51256C1.50282 6.70028 1.37409 8.00721 1.6249 9.26809C1.8757 10.529 2.49477 11.6872 3.40381 12.5962C4.31285 13.5052 5.47104 14.1243 6.73192 14.3751C7.99279 14.6259 9.29973 14.4972 10.4874 14.0052C11.6752 13.5132 12.6903 12.6801 13.4046 11.6112C14.1188 10.5423 14.5 9.28558 14.5 8C14.4967 6.27711 13.8108 4.62573 12.5925 3.40746C11.3743 2.18918 9.7229 1.5033 8 1.5ZM10.6188 7.5875L8.36875 10.0875C8.32191 10.1387 8.26493 10.1795 8.20145 10.2074C8.13797 10.2354 8.06936 10.2498 8 10.2498C7.93064 10.2498 7.86204 10.2354 7.79855 10.2074C7.73507 10.1795 7.6781 10.1387 7.63125 10.0875L5.38125 7.5875C5.2951 7.48917 5.25088 7.36101 5.25807 7.23048C5.26526 7.09994 5.32329 6.97741 5.41973 6.88915C5.51617 6.80088 5.64334 6.7539 5.774 6.75826C5.90467 6.76263 6.02842 6.81799 6.11875 6.9125L8 9L9.88125 6.9125C9.92513 6.86243 9.97856 6.82163 10.0384 6.79248C10.0983 6.76333 10.1633 6.74643 10.2298 6.74277C10.2963 6.73911 10.3628 6.74876 10.4255 6.77115C10.4882 6.79354 10.5458 6.82823 10.5949 6.87318C10.644 6.91812 10.6836 6.97243 10.7115 7.0329C10.7393 7.09336 10.7548 7.15879 10.757 7.22532C10.7593 7.29186 10.7482 7.35817 10.7244 7.42036C10.7007 7.48255 10.6648 7.53938 10.6188 7.5875Z', - ChevronCircleLeftFill: - 'M8 1.5C6.71442 1.5 5.45772 1.88122 4.3888 2.59545C3.31988 3.30968 2.48676 4.32484 1.99479 5.51256C1.50282 6.70028 1.37409 8.00721 1.6249 9.26809C1.8757 10.529 2.49477 11.6872 3.40381 12.5962C4.31285 13.5052 5.47104 14.1243 6.73192 14.3751C7.99279 14.6259 9.29973 14.4972 10.4874 14.0052C11.6752 13.5132 12.6903 12.6801 13.4046 11.6112C14.1188 10.5423 14.5 9.28558 14.5 8C14.4967 6.27711 13.8108 4.62573 12.5925 3.40746C11.3743 2.18918 9.7229 1.5033 8 1.5V1.5ZM9.3375 9.88125C9.43527 9.97078 9.49349 10.0955 9.49935 10.2279C9.50521 10.3603 9.45823 10.4897 9.36875 10.5875C9.32217 10.639 9.26525 10.6801 9.20171 10.7081C9.13817 10.7361 9.06944 10.7504 9 10.75C8.87528 10.7488 8.75528 10.7021 8.6625 10.6187L6.1625 8.36875C6.11135 8.32191 6.0705 8.26493 6.04255 8.20145C6.01461 8.13797 6.00018 8.06936 6.00018 8C6.00018 7.93064 6.01461 7.86203 6.04255 7.79855C6.0705 7.73507 6.11135 7.67809 6.1625 7.63125L8.6625 5.38125C8.71063 5.33525 8.76745 5.29932 8.82965 5.27558C8.89184 5.25183 8.95815 5.24075 9.02468 5.24297C9.09122 5.24519 9.15664 5.26068 9.21711 5.28852C9.27758 5.31637 9.33188 5.356 9.37683 5.40511C9.42177 5.45422 9.45646 5.51181 9.47885 5.5745C9.50125 5.6372 9.51089 5.70373 9.50723 5.7702C9.50357 5.83667 9.48667 5.90174 9.45752 5.96159C9.42838 6.02145 9.38757 6.07488 9.3375 6.11875L7.25 8L9.3375 9.88125Z', - ChevronCircleRightFill: - 'M8 1.5C6.71442 1.5 5.45772 1.88122 4.3888 2.59545C3.31988 3.30968 2.48676 4.32484 1.99479 5.51256C1.50282 6.70028 1.37409 8.00721 1.6249 9.26809C1.8757 10.529 2.49477 11.6872 3.40381 12.5962C4.31285 13.5052 5.47104 14.1243 6.73192 14.3751C7.99279 14.6259 9.29973 14.4972 10.4874 14.0052C11.6752 13.5132 12.6903 12.6801 13.4046 11.6112C14.1188 10.5423 14.5 9.28558 14.5 8C14.4967 6.27711 13.8108 4.62573 12.5925 3.40746C11.3743 2.18918 9.7229 1.5033 8 1.5V1.5ZM10.0875 8.36875L7.5875 10.6187C7.49472 10.7021 7.37473 10.7488 7.25 10.75C7.18057 10.7504 7.11184 10.7361 7.0483 10.7081C6.98476 10.6801 6.92784 10.639 6.88125 10.5875C6.79177 10.4897 6.7448 10.3603 6.75066 10.2279C6.75652 10.0955 6.81473 9.97078 6.9125 9.88125L9 8L6.9125 6.11875C6.818 6.02842 6.76263 5.90466 6.75827 5.774C6.7539 5.64334 6.80089 5.51617 6.88915 5.41973C6.97742 5.32329 7.09994 5.26526 7.23048 5.25807C7.36102 5.25087 7.48918 5.29509 7.5875 5.38125L10.0875 7.63125C10.1387 7.67809 10.1795 7.73507 10.2075 7.79855C10.2354 7.86203 10.2498 7.93064 10.2498 8C10.2498 8.06936 10.2354 8.13797 10.2075 8.20145C10.1795 8.26493 10.1387 8.32191 10.0875 8.36875V8.36875Z', - ChevronDown: - 'M3.64645 5.64645C3.84171 5.45118 4.15829 5.45118 4.35355 5.64645L8 9.29289L11.6464 5.64645C11.8417 5.45118 12.1583 5.45118 12.3536 5.64645C12.5488 5.84171 12.5488 6.15829 12.3536 6.35355L8.35355 10.3536C8.15829 10.5488 7.84171 10.5488 7.64645 10.3536L3.64645 6.35355C3.45118 6.15829 3.45118 5.84171 3.64645 5.64645Z', - ChevronDownSmall: - 'M4.64645 6.14645C4.84171 5.95118 5.15829 5.95118 5.35355 6.14645L8 8.79289L10.6464 6.14645C10.8417 5.95118 11.1583 5.95118 11.3536 6.14645C11.5488 6.34171 11.5488 6.65829 11.3536 6.85355L8.35355 9.85355C8.15829 10.0488 7.84171 10.0488 7.64645 9.85355L4.64645 6.85355C4.45118 6.65829 4.45118 6.34171 4.64645 6.14645Z', - ChevronLeft: - 'M10.3536 3.64645C10.5488 3.84171 10.5488 4.15829 10.3536 4.35355L6.70711 8L10.3536 11.6464C10.5488 11.8417 10.5488 12.1583 10.3536 12.3536C10.1583 12.5488 9.84171 12.5488 9.64645 12.3536L5.64645 8.35355C5.45118 8.15829 5.45118 7.84171 5.64645 7.64645L9.64645 3.64645C9.84171 3.45118 10.1583 3.45118 10.3536 3.64645Z', - ChevronLeftSmall: - 'M9.35355 4.64645C9.54882 4.84171 9.54882 5.15829 9.35355 5.35355L6.70711 8L9.35355 10.6464C9.54882 10.8417 9.54882 11.1583 9.35355 11.3536C9.15829 11.5488 8.84171 11.5488 8.64645 11.3536L5.64645 8.35355C5.45118 8.15829 5.45118 7.84171 5.64645 7.64645L8.64645 4.64645C8.84171 4.45118 9.15829 4.45118 9.35355 4.64645Z', - ChevronRight: - 'M5.64645 12.3536C5.45118 12.1583 5.45118 11.8417 5.64645 11.6464L9.29289 8L5.64645 4.35355C5.45118 4.15829 5.45118 3.84171 5.64645 3.64645C5.84171 3.45118 6.15829 3.45118 6.35355 3.64645L10.3536 7.64645C10.5488 7.84171 10.5488 8.15829 10.3536 8.35355L6.35355 12.3536C6.15829 12.5488 5.84171 12.5488 5.64645 12.3536Z', - ChevronRightSmall: - 'M6.64645 4.64645C6.45118 4.84171 6.45118 5.15829 6.64645 5.35355L9.29289 8L6.64645 10.6464C6.45118 10.8417 6.45118 11.1583 6.64645 11.3536C6.84171 11.5488 7.15829 11.5488 7.35355 11.3536L10.3536 8.35355C10.5488 8.15829 10.5488 7.84171 10.3536 7.64645L7.35355 4.64645C7.15829 4.45118 6.84171 4.45118 6.64645 4.64645Z', - ChevronUp: - 'M12.3536 10.3536C12.1583 10.5488 11.8417 10.5488 11.6464 10.3536L8 6.70711L4.35355 10.3536C4.15829 10.5488 3.84171 10.5488 3.64645 10.3536C3.45118 10.1583 3.45118 9.84171 3.64645 9.64645L7.64645 5.64645C7.84171 5.45118 8.15829 5.45118 8.35355 5.64645L12.3536 9.64645C12.5488 9.84171 12.5488 10.1583 12.3536 10.3536Z', - ChevronUpSmall: - 'M4.64645 9.85355C4.84171 10.0488 5.15829 10.0488 5.35355 9.85355L8 7.20711L10.6464 9.85355C10.8417 10.0488 11.1583 10.0488 11.3536 9.85355C11.5488 9.65829 11.5488 9.34171 11.3536 9.14645L8.35355 6.14645C8.15829 5.95118 7.84171 5.95118 7.64645 6.14645L4.64645 9.14645C4.45118 9.34171 4.45118 9.65829 4.64645 9.85355Z', - Circle: - 'M8 1.5C4.41604 1.5 1.5 4.41604 1.5 8C1.5 11.5839 4.41603 14.5 8 14.5C11.5839 14.5 14.5 11.5839 14.5 8C14.5 4.41603 11.5839 1.5 8 1.5ZM8 2.5C11.0435 2.5 13.5 4.95647 13.5 8C13.5 11.0435 11.0435 13.5 8 13.5C4.95647 13.5 2.5 11.0435 2.5 8C2.5 4.95647 4.95647 2.5 8 2.5Z', - CirclesThreePlus: - 'M4.75 2C3.23715 2 2 3.23715 2 4.75C2 6.26285 3.23715 7.5 4.75 7.5C6.26285 7.5 7.5 6.26285 7.5 4.75C7.5 3.23715 6.26285 2 4.75 2ZM4.75 3C5.7224 3 6.5 3.7776 6.5 4.75C6.5 5.7224 5.7224 6.5 4.75 6.5C3.7776 6.5 3 5.7224 3 4.75C3 3.7776 3.7776 3 4.75 3Z M11.25 2C9.73715 2 8.5 3.23715 8.5 4.75C8.5 6.26285 9.73715 7.5 11.25 7.5C12.7629 7.5 14 6.26285 14 4.75C14 3.23715 12.7629 2 11.25 2ZM11.25 3C12.2224 3 13 3.7776 13 4.75C13 5.7224 12.2224 6.5 11.25 6.5C10.2776 6.5 9.5 5.7224 9.5 4.75C9.5 3.7776 10.2776 3 11.25 3Z M4.75 8.5C3.23715 8.5 2 9.73715 2 11.25C2 12.7629 3.23715 14 4.75 14C6.26285 14 7.5 12.7629 7.5 11.25C7.5 9.73715 6.26285 8.5 4.75 8.5ZM4.75 9.5C5.7224 9.5 6.5 10.2776 6.5 11.25C6.5 12.2224 5.7224 13 4.75 13C3.7776 13 3 12.2224 3 11.25C3 10.2776 3.7776 9.5 4.75 9.5Z M11.25 9C11.1174 9 10.9902 9.05268 10.8964 9.14645C10.8027 9.24021 10.75 9.36739 10.75 9.5V10.75H9.5C9.36739 10.75 9.24021 10.8027 9.14645 10.8964C9.05268 10.9902 9 11.1174 9 11.25C9 11.3826 9.05268 11.5098 9.14645 11.6036C9.24021 11.6973 9.36739 11.75 9.5 11.75H10.75V13C10.75 13.1326 10.8027 13.2598 10.8964 13.3536C10.9902 13.4473 11.1174 13.5 11.25 13.5C11.3826 13.5 11.5098 13.4473 11.6036 13.3536C11.6973 13.2598 11.75 13.1326 11.75 13V11.75H13C13.1326 11.75 13.2598 11.6973 13.3536 11.6036C13.4473 11.5098 13.5 11.3826 13.5 11.25C13.5 11.1174 13.4473 10.9902 13.3536 10.8964C13.2598 10.8027 13.1326 10.75 13 10.75H11.75V9.5C11.75 9.36739 11.6973 9.24021 11.6036 9.14645C11.5098 9.05268 11.3826 9 11.25 9Z', - ClipboardText: - 'M8 1C6.34907 1 5 2.34907 5 4V4.5C5.00001 4.6326 5.0527 4.75977 5.14646 4.85354C5.24023 4.9473 5.3674 4.99999 5.5 5H10.5C10.6326 4.99999 10.7598 4.9473 10.8535 4.85354C10.9473 4.75977 11 4.6326 11 4.5V4C11 2.34907 9.65093 1 8 1ZM8 2C9.11049 2 10 2.88951 10 4H6C6 2.88951 6.88951 2 8 2Z M3.5 2C2.95364 2 2.5 2.45364 2.5 3V13.5C2.50007 14.0463 2.95357 14.4999 3.49988 14.5C3.49984 14.5 3.49992 14.5 3.49988 14.5H12.5C13.0464 14.5 13.5 14.0464 13.5 13.5V3C13.5 2.45364 13.0464 2 12.5 2H10C9.86739 2 9.74021 2.05268 9.64645 2.14645C9.55268 2.24021 9.5 2.36739 9.5 2.5C9.5 2.63261 9.55268 2.75979 9.64645 2.85355C9.74021 2.94732 9.86739 3 10 3H12.5V13.5H3.50012L3.5 3H6C6.13261 3 6.25978 2.94732 6.35355 2.85355C6.44732 2.75979 6.5 2.63261 6.5 2.5C6.5 2.36739 6.44732 2.24021 6.35355 2.14645C6.25978 2.05268 6.13261 2 6 2H3.5Z M6 7C5.86739 7 5.74021 7.05268 5.64645 7.14645C5.55268 7.24021 5.5 7.36739 5.5 7.5C5.5 7.63261 5.55268 7.75979 5.64645 7.85355C5.74021 7.94732 5.86739 8 6 8H10C10.1326 8 10.2598 7.94732 10.3536 7.85355C10.4473 7.75979 10.5 7.63261 10.5 7.5C10.5 7.36739 10.4473 7.24021 10.3536 7.14645C10.2598 7.05268 10.1326 7 10 7H6Z M6 9C5.86739 9 5.74021 9.05268 5.64645 9.14645C5.55268 9.24021 5.5 9.36739 5.5 9.5C5.5 9.63261 5.55268 9.75979 5.64645 9.85355C5.74021 9.94732 5.86739 10 6 10H10C10.1326 10 10.2598 9.94732 10.3536 9.85355C10.4473 9.75979 10.5 9.63261 10.5 9.5C10.5 9.36739 10.4473 9.24021 10.3536 9.14645C10.2598 9.05268 10.1326 9 10 9H6Z', - Clock: - 'M8 4C7.86739 4 7.74021 4.05268 7.64645 4.14645C7.55268 4.24021 7.5 4.36739 7.5 4.5V8C7.50001 8.1326 7.5527 8.25977 7.64646 8.35354C7.74023 8.4473 7.8674 8.49999 8 8.5H11.5C11.6326 8.5 11.7598 8.44732 11.8536 8.35355C11.9473 8.25979 12 8.13261 12 8C12 7.86739 11.9473 7.74021 11.8536 7.64645C11.7598 7.55268 11.6326 7.5 11.5 7.5H8.5V4.5C8.5 4.36739 8.44732 4.24021 8.35355 4.14645C8.25979 4.05268 8.13261 4 8 4Z M8 1.5C4.41604 1.5 1.5 4.41604 1.5 8C1.5 11.5839 4.41603 14.5 8 14.5C11.5839 14.5 14.5 11.5839 14.5 8C14.5 4.41603 11.5839 1.5 8 1.5ZM8 2.5C11.0435 2.5 13.5 4.95647 13.5 8C13.5 11.0435 11.0435 13.5 8 13.5C4.95647 13.5 2.5 11.0435 2.5 8C2.5 4.95647 4.95647 2.5 8 2.5Z', - ClockCounterClockwise: - 'M8.13367 2.0017C6.52708 1.96745 4.93757 2.57928 3.75879 3.75902L1.63452 5.87719C1.54063 5.97081 1.48777 6.09789 1.48756 6.23048C1.48736 6.36307 1.53982 6.49031 1.63342 6.58422C1.67978 6.63073 1.73485 6.66765 1.79547 6.69287C1.8561 6.7181 1.92111 6.73114 1.98677 6.73124C2.05244 6.73134 2.11748 6.71851 2.17819 6.69347C2.23889 6.66843 2.29407 6.63168 2.34058 6.58532L4.46558 4.46654C4.46537 4.46675 4.46578 4.46634 4.46558 4.46654C5.89626 3.03469 8.04552 2.60602 9.91565 3.38024C11.7858 4.15445 13.0029 5.97586 13.0029 7.99999C13.0029 10.0241 11.7858 11.8455 9.91565 12.6197C8.04552 13.394 5.89687 12.9659 4.46619 11.534C4.41977 11.4876 4.36466 11.4507 4.30401 11.4256C4.24335 11.4004 4.17833 11.3875 4.11266 11.3875C4.047 11.3874 3.98197 11.4003 3.92129 11.4255C3.86062 11.4506 3.80548 11.4874 3.75903 11.5338C3.71258 11.5802 3.67573 11.6353 3.65057 11.696C3.62542 11.7566 3.61246 11.8217 3.61244 11.8873C3.61242 11.953 3.62533 12.018 3.65044 12.0787C3.67555 12.1394 3.71237 12.1945 3.75879 12.241C5.47337 13.9569 8.05683 14.4715 10.2981 13.5437C12.5394 12.6158 14.0029 10.4257 14.0029 7.99998C14.0029 5.57424 12.5394 3.38414 10.2981 2.45628C9.5977 2.16633 8.86394 2.01727 8.13367 2.0017Z M1.98755 3.23119C1.85494 3.23119 1.72776 3.28387 1.634 3.37764C1.54023 3.47141 1.48755 3.59858 1.48755 3.73119L1.48756 6.23048C1.48736 6.36307 1.53982 6.49031 1.63342 6.58422C1.72719 6.67799 1.85416 6.73123 1.98677 6.73124L4.48755 6.73119C4.55321 6.73119 4.61823 6.71826 4.67889 6.69313C4.73955 6.668 4.79467 6.63117 4.8411 6.58474C4.88753 6.53832 4.92436 6.4832 4.94949 6.42253C4.97462 6.36187 4.98755 6.29685 4.98755 6.23119C4.98755 6.09858 4.93487 5.97141 4.8411 5.87764C4.74733 5.78387 4.62016 5.73119 4.48755 5.73119H2.48755V3.73119C2.48755 3.59858 2.43487 3.47141 2.3411 3.37764C2.24733 3.28387 2.12016 3.23119 1.98755 3.23119Z M8 4.49999C7.86739 4.49999 7.74021 4.55267 7.64645 4.64644C7.55268 4.7402 7.5 4.86738 7.5 4.99999V7.99999C7.50721 8.02138 7.51585 8.04226 7.52588 8.06249C7.53865 8.12323 7.56262 8.18106 7.59656 8.23302C7.62459 8.28835 7.66267 8.33799 7.70886 8.37938C7.72139 8.3982 7.73517 8.41614 7.75012 8.4331L10.3501 9.9331C10.407 9.96591 10.4698 9.98721 10.5349 9.99576C10.6 10.0043 10.6661 9.99995 10.7295 9.98294C10.793 9.96593 10.8524 9.93659 10.9045 9.89659C10.9566 9.8566 11.0003 9.80675 11.0331 9.74987C11.0659 9.69299 11.0872 9.63021 11.0957 9.5651C11.1043 9.5 11.0999 9.43385 11.0829 9.37043C11.0659 9.30701 11.0366 9.24756 10.9966 9.19547C10.9566 9.14339 10.9067 9.0997 10.8499 9.06688L8.5 7.71117V4.99999C8.5 4.86738 8.44732 4.7402 8.35355 4.64644C8.25979 4.55267 8.13261 4.49999 8 4.49999Z', - CloudArrowDown: - 'M9.75391 2.50427C7.76509 2.59695 5.97041 3.78688 5.08435 5.54614C4.8908 5.51824 4.69598 5.49992 4.5 5.49999C2.29689 5.50013 0.5 7.29686 0.5 9.49999C0.5 11.7032 2.29678 13.5 4.5 13.5H6C6.13261 13.5 6.25979 13.4473 6.35355 13.3535C6.44732 13.2598 6.5 13.1326 6.5 13C6.5 12.8674 6.44732 12.7402 6.35355 12.6464C6.25979 12.5527 6.13261 12.5 6 12.5H4.5C2.83722 12.5 1.5 11.1628 1.5 9.49999C1.5 7.83722 2.83722 6.49999 4.5 6.49999C4.49992 6.49999 4.50008 6.49999 4.5 6.49999C4.57029 6.49996 4.64029 6.50805 4.71033 6.51281C4.57781 6.98374 4.5 7.4799 4.5 7.99999C4.5 8.1326 4.55268 8.25978 4.64645 8.35355C4.74021 8.44731 4.86739 8.49999 5 8.49999C5.13261 8.49999 5.25979 8.44731 5.35355 8.35355C5.44732 8.25978 5.5 8.1326 5.5 7.99999C5.5 4.91375 8.49513 2.755 11.423 3.73095C14.3508 4.7069 15.4517 8.23095 13.6 10.6999C13.5606 10.7525 13.5319 10.8123 13.5156 10.8759C13.4993 10.9395 13.4957 11.0057 13.505 11.0707C13.5142 11.1357 13.5362 11.1982 13.5697 11.2547C13.6032 11.3113 13.6474 11.3606 13.7 11.4C13.7525 11.4394 13.8123 11.4681 13.8759 11.4844C13.9395 11.5007 14.0057 11.5043 14.0707 11.495C14.1357 11.4858 14.1983 11.4638 14.2548 11.4303C14.3113 11.3968 14.3606 11.3526 14.4 11.3C16.6436 8.30855 15.2867 3.9647 11.7393 2.78222C11.0741 2.5605 10.4039 2.47397 9.75391 2.50427Z M9.5 7.49999C9.36739 7.49999 9.24021 7.55267 9.14645 7.64644C9.05268 7.74021 9 7.86738 9 7.99999V11.7931L7.73486 10.5278C7.68843 10.4814 7.63331 10.4446 7.57264 10.4194C7.51198 10.3943 7.44695 10.3814 7.38129 10.3814C7.31562 10.3814 7.2506 10.3943 7.18993 10.4194C7.12926 10.4446 7.07414 10.4814 7.02771 10.5278C6.98127 10.5743 6.94444 10.6294 6.91931 10.69C6.89417 10.7507 6.88124 10.8157 6.88124 10.8814C6.88124 10.9471 6.89417 11.0121 6.91931 11.0728C6.94444 11.1334 6.98127 11.1885 7.02771 11.235L9.14648 13.3535C9.24026 13.4473 9.36741 13.5 9.5 13.5C9.63259 13.5 9.75974 13.4474 9.85352 13.3536L11.9723 11.235C12.0187 11.1885 12.0556 11.1334 12.0807 11.0728C12.1058 11.0121 12.1188 10.9471 12.1188 10.8814C12.1188 10.8157 12.1058 10.7507 12.0807 10.69C12.0556 10.6294 12.0187 10.5743 11.9723 10.5278C11.9259 10.4814 11.8707 10.4446 11.8101 10.4194C11.7494 10.3943 11.6844 10.3814 11.6187 10.3814C11.553 10.3814 11.488 10.3943 11.4274 10.4194C11.3667 10.4446 11.3116 10.4814 11.2651 10.5278L10 11.7931V7.99999C10 7.86738 9.94732 7.74021 9.85355 7.64644C9.75979 7.55267 9.63261 7.49999 9.5 7.49999Z', - CloudArrowUp: - 'M9.5 7.49999C9.3674 7.50002 9.24024 7.55271 9.14648 7.64648L7.02771 9.76513C6.98127 9.81156 6.94444 9.86668 6.91931 9.92735C6.89418 9.98802 6.88124 10.053 6.88124 10.1187C6.88124 10.1844 6.89418 10.2494 6.91931 10.3101C6.94444 10.3707 6.98127 10.4259 7.02771 10.4723C7.07414 10.5187 7.12926 10.5556 7.18993 10.5807C7.2506 10.6058 7.31562 10.6188 7.38129 10.6188C7.44695 10.6188 7.51198 10.6058 7.57264 10.5807C7.63331 10.5556 7.68843 10.5187 7.73486 10.4723L9 9.20702V13C9 13.1326 9.05268 13.2598 9.14645 13.3535C9.24021 13.4473 9.36739 13.5 9.5 13.5C9.63261 13.5 9.75979 13.4473 9.85355 13.3535C9.94732 13.2598 10 13.1326 10 13V9.20702L11.2651 10.4723C11.3116 10.5187 11.3667 10.5556 11.4274 10.5807C11.488 10.6058 11.553 10.6188 11.6187 10.6188C11.6844 10.6188 11.7494 10.6058 11.8101 10.5807C11.8707 10.5556 11.9259 10.5187 11.9723 10.4723C12.0187 10.4259 12.0556 10.3707 12.0807 10.3101C12.1058 10.2494 12.1188 10.1844 12.1188 10.1187C12.1188 10.053 12.1058 9.98802 12.0807 9.92735C12.0556 9.86668 12.0187 9.81156 11.9723 9.76513L9.85352 7.64648C9.84867 7.64436 9.84378 7.64233 9.83887 7.64037C9.74777 7.55234 9.62666 7.50217 9.5 7.49999Z M9.75391 2.50427C7.76509 2.59695 5.97041 3.78688 5.08435 5.54614C4.8908 5.51824 4.69598 5.49992 4.5 5.49999C2.29689 5.50013 0.5 7.29686 0.5 9.49999C0.5 11.7032 2.29678 13.5 4.5 13.5H6C6.13261 13.5 6.25979 13.4473 6.35355 13.3535C6.44732 13.2598 6.5 13.1326 6.5 13C6.5 12.8674 6.44732 12.7402 6.35355 12.6464C6.25979 12.5527 6.13261 12.5 6 12.5H4.5C2.83722 12.5 1.5 11.1628 1.5 9.49999C1.5 7.83722 2.83722 6.49999 4.5 6.49999C4.49992 6.49999 4.50008 6.49999 4.5 6.49999C4.57029 6.49996 4.64029 6.50805 4.71033 6.51281C4.57781 6.98374 4.5 7.4799 4.5 7.99999C4.5 8.1326 4.55268 8.25978 4.64645 8.35355C4.74021 8.44731 4.86739 8.49999 5 8.49999C5.13261 8.49999 5.25979 8.44731 5.35355 8.35355C5.44732 8.25978 5.5 8.1326 5.5 7.99999C5.5 4.91375 8.49513 2.755 11.423 3.73095C14.3508 4.7069 15.4517 8.23095 13.6 10.6999C13.5606 10.7525 13.5319 10.8123 13.5156 10.8759C13.4993 10.9395 13.4957 11.0057 13.505 11.0707C13.5142 11.1357 13.5362 11.1982 13.5697 11.2547C13.6032 11.3113 13.6474 11.3606 13.7 11.4C13.7525 11.4394 13.8123 11.4681 13.8759 11.4844C13.9395 11.5007 14.0057 11.5043 14.0707 11.495C14.1357 11.4858 14.1983 11.4638 14.2548 11.4303C14.3113 11.3968 14.3606 11.3526 14.4 11.3C16.6436 8.30855 15.2867 3.9647 11.7393 2.78222C11.0741 2.5605 10.4039 2.47397 9.75391 2.50427Z', - CloudCheck: - 'M12 6.75C11.8674 6.75002 11.7402 6.80271 11.6465 6.89648L9 9.54297L7.85352 8.39648C7.75975 8.30274 7.63259 8.25007 7.5 8.25007C7.36741 8.25007 7.24025 8.30274 7.14648 8.39648C7.05274 8.49025 7.00008 8.61741 7.00008 8.75C7.00008 8.88259 7.05274 9.00975 7.14648 9.10351L8.64648 10.6035C8.74026 10.6972 8.86741 10.7499 9 10.7499C9.13259 10.7499 9.25974 10.6972 9.35352 10.6035L12.3535 7.60351C12.4473 7.50975 12.4999 7.38259 12.4999 7.25C12.4999 7.11741 12.4473 6.99025 12.3535 6.89648C12.2598 6.80271 12.1326 6.75002 12 6.75Z M9.98474 2.50036C9.81138 2.50093 9.6366 2.50967 9.46094 2.52698C7.52459 2.71769 5.90229 3.9031 5.07996 5.54541C4.88784 5.51794 4.69449 5.49993 4.5 5.5C2.29689 5.50013 0.5 7.29687 0.5 9.5C0.5 11.7032 2.29678 13.5 4.5 13.5H10C13.4579 13.5 16.0689 10.3184 15.3943 6.927C14.8774 4.3302 12.5852 2.49191 9.98474 2.50036ZM9.98877 3.50024C12.1218 3.49196 13.9893 4.99032 14.4136 7.12219C14.9692 9.91574 12.8483 12.5 10 12.5H4.5C2.83722 12.5 1.5 11.1628 1.5 9.5C1.5 7.83722 2.83722 6.5 4.5 6.5C4.49992 6.5 4.50008 6.5 4.5 6.5C4.5691 6.49997 4.63792 6.50797 4.70679 6.51257C4.57314 6.98697 4.50006 7.48568 4.5 8C4.5 8.13261 4.55268 8.25978 4.64645 8.35355C4.74021 8.44732 4.86739 8.5 5 8.5C5.13261 8.5 5.25979 8.44732 5.35355 8.35355C5.44732 8.25978 5.5 8.13261 5.5 8C5.50027 5.6814 7.25153 3.74947 9.55896 3.52222C9.70317 3.50801 9.84657 3.5008 9.98877 3.50024Z', - CodeSimple: - 'M5.52943 3.50086C5.46388 3.49701 5.39821 3.50611 5.33617 3.52764C5.27413 3.54917 5.21694 3.58271 5.16786 3.62635L0.667862 7.62635C0.615111 7.67326 0.572892 7.73081 0.54398 7.79522C0.515069 7.85962 0.500122 7.92941 0.500122 8.00001C0.500122 8.0706 0.515069 8.14039 0.54398 8.2048C0.572892 8.2692 0.615111 8.32675 0.667862 8.37366L5.16786 12.3737C5.26697 12.4617 5.397 12.5069 5.52936 12.4991C5.66172 12.4913 5.78557 12.4312 5.87367 12.3322C5.96175 12.2331 6.00686 12.103 5.99908 11.9707C5.99129 11.8383 5.93126 11.7144 5.83217 11.6263L1.75258 8.00001L5.83217 4.37366C5.93126 4.28556 5.99129 4.16171 5.99908 4.02935C6.00686 3.89699 5.96175 3.76696 5.87367 3.66785C5.7856 3.56875 5.66178 3.50868 5.52943 3.50086Z M10.4706 3.50086C10.3382 3.50868 10.2144 3.56875 10.1264 3.66785C10.0383 3.76696 9.99317 3.89699 10.001 4.02935C10.0087 4.16171 10.0688 4.28556 10.1679 4.37366L14.2475 8.00001L10.1679 11.6263C10.0688 11.7144 10.0087 11.8383 10.001 11.9707C9.99317 12.103 10.0383 12.2331 10.1264 12.3322C10.2145 12.4312 10.3383 12.4913 10.4707 12.4991C10.603 12.5069 10.7331 12.4617 10.8322 12.3737L15.3322 8.37366C15.3849 8.32675 15.4271 8.2692 15.456 8.2048C15.485 8.14039 15.4999 8.0706 15.4999 8.00001C15.4999 7.92941 15.485 7.85962 15.456 7.79522C15.4271 7.73081 15.3849 7.67326 15.3322 7.62635L10.8322 3.62635C10.7831 3.58271 10.7259 3.54917 10.6639 3.52764C10.6018 3.50611 10.5362 3.49701 10.4706 3.50086Z', - Cog: - 'M5.5 8C5.5 6.61929 6.61929 5.5 8 5.5C9.38071 5.5 10.5 6.61929 10.5 8C10.5 9.38071 9.38071 10.5 8 10.5C6.61929 10.5 5.5 9.38071 5.5 8ZM8 6.5C7.17157 6.5 6.5 7.17157 6.5 8C6.5 8.82843 7.17157 9.5 8 9.5C8.82843 9.5 9.5 8.82843 9.5 8C9.5 7.17157 8.82843 6.5 8 6.5Z M5.85892 1.33042C6.06194 1.26488 6.28419 1.33538 6.41231 1.50597L7.34959 2.75389C7.71236 2.74661 8.28737 2.74661 8.65023 2.75388L9.58127 1.51249C9.70866 1.34264 9.92944 1.27176 10.1319 1.33572C11.087 1.63749 11.9652 2.14323 12.7056 2.81794C12.8627 2.96119 12.9121 3.1882 12.8286 3.3838L12.2176 4.81507C12.3152 4.97505 12.4358 5.18003 12.5489 5.37586C12.6621 5.57178 12.7794 5.77864 12.8691 5.94304L14.4098 6.12858C14.6202 6.15392 14.7918 6.30919 14.838 6.51604C15.056 7.49234 15.0583 8.50445 14.8447 9.48173C14.7993 9.68968 14.6272 9.84607 14.4158 9.87143L12.8691 10.0571C12.7793 10.2214 12.6621 10.4283 12.5489 10.6241C12.4358 10.82 12.3152 11.0249 12.2176 11.1849L12.8286 12.6162C12.9121 12.8118 12.8627 13.0388 12.7055 13.1821C11.9667 13.8553 11.0923 14.3625 10.1411 14.6696C9.93809 14.7351 9.71585 14.6646 9.58772 14.494L8.65045 13.2461C8.28767 13.2534 7.71267 13.2534 7.34981 13.2461L6.41877 14.4875C6.29138 14.6574 6.0706 14.7282 5.86814 14.6643C4.91299 14.3625 4.03484 13.8568 3.29448 13.1821C3.13711 13.0386 3.08782 12.8113 3.17168 12.6156L3.78282 11.1893C3.68604 11.0294 3.56553 10.8237 3.45172 10.626C3.33833 10.4291 3.2209 10.2215 3.13106 10.057L1.59023 9.87141C1.37981 9.84607 1.20822 9.6908 1.16203 9.48395C0.944054 8.50765 0.941754 7.49554 1.15529 6.51826C1.20073 6.31031 1.37285 6.15392 1.58419 6.12856L3.13096 5.94292C3.22072 5.77854 3.33796 5.57174 3.45109 5.37586C3.5642 5.18003 3.68479 4.97505 3.78239 4.81507L3.17141 3.38379C3.08792 3.18819 3.13729 2.96117 3.29449 2.81792C4.0333 2.14469 4.90771 1.63748 5.85892 1.33042ZM4.23485 3.32789L4.80539 4.66443C4.87314 4.82314 4.85426 5.00557 4.75544 5.14704C4.69523 5.23324 4.51149 5.53933 4.31703 5.87601C4.12264 6.21257 3.94921 6.52489 3.90449 6.62027C3.83125 6.77647 3.68265 6.88389 3.51136 6.90444L2.06743 7.07774C1.97181 7.68894 1.97332 8.31145 2.07192 8.92219L3.51156 9.09557C3.68234 9.11614 3.83055 9.22304 3.90396 9.3786C3.94977 9.47567 4.12403 9.78957 4.31835 10.1271C4.51221 10.4638 4.69698 10.7736 4.75839 10.8635C4.85483 11.0046 4.87245 11.1853 4.80512 11.3424L4.23531 12.6723C4.71783 13.0621 5.25871 13.3735 5.83805 13.5951L6.70627 12.4375C6.80827 12.3015 6.97238 12.2266 7.14194 12.2388C7.38035 12.2558 8.61968 12.2558 8.85809 12.2388C9.02754 12.2266 9.19154 12.3014 9.29356 12.4372L10.1666 13.5996C10.7442 13.3749 11.2834 13.062 11.7652 12.6721L11.1947 11.3356C11.1269 11.1768 11.1458 10.9944 11.2446 10.8529C11.3048 10.7668 11.4885 10.4607 11.683 10.124C11.8774 9.78742 12.0508 9.4751 12.0955 9.37972C12.1688 9.22351 12.3174 9.1161 12.4887 9.09554L13.9326 8.92225C14.0282 8.31105 14.0267 7.68854 13.9281 7.0778L12.4885 6.90442C12.3173 6.8838 12.1688 6.77641 12.0955 6.62027C12.0508 6.52489 11.8774 6.21257 11.683 5.87601C11.4885 5.53933 11.3048 5.23324 11.2446 5.14704C11.1458 5.00557 11.1269 4.82314 11.1947 4.66443L11.7651 3.32802C11.2825 2.9381 10.7415 2.62656 10.162 2.40487L9.29377 3.56249C9.19177 3.69849 9.02766 3.77335 8.85809 3.76122C8.61968 3.74417 7.38035 3.74417 7.14194 3.76122C6.9725 3.77334 6.80849 3.6986 6.70647 3.56277L5.83343 2.40038C5.25588 2.62513 4.7166 2.93801 4.23485 3.32789Z', - Confetti: - 'M9 0.5C8.86739 0.5 8.74021 0.552678 8.64644 0.646447C8.55268 0.740215 8.5 0.867392 8.5 1V2.5C8.5 2.63261 8.55268 2.75979 8.64644 2.85355C8.74021 2.94732 8.86739 3 9 3C9.13261 3 9.25978 2.94732 9.35355 2.85355C9.44732 2.75979 9.5 2.63261 9.5 2.5V1C9.5 0.867392 9.44732 0.740215 9.35355 0.646447C9.25978 0.552678 9.13261 0.5 9 0.5Z M13.5 1C13.3674 1 13.2402 1.05268 13.1464 1.14645C13.0527 1.24021 13 1.36739 13 1.5C13 1.5 12.9931 1.77062 12.8652 2.02637C12.7374 2.28211 12.5833 2.5 12 2.5C11.0833 2.5 10.4874 3.03211 10.2402 3.52637C9.9931 4.02062 10 4.5 10 4.5C10 4.63261 10.0527 4.75979 10.1464 4.85355C10.2402 4.94732 10.3674 5 10.5 5C10.6326 5 10.7598 4.94732 10.8536 4.85355C10.9473 4.75979 11 4.63261 11 4.5C11 4.5 11.0069 4.22938 11.1348 3.97363C11.2626 3.71789 11.4167 3.5 12 3.5C12.9167 3.5 13.5126 2.96789 13.7598 2.47363C14.0069 1.97938 14 1.5 14 1.5C14 1.36739 13.9473 1.24021 13.8536 1.14645C13.7598 1.05268 13.6326 1 13.5 1Z M6.23266 2.91919C5.8238 2.91257 5.41372 3.14738 5.26135 3.56665C5.26135 3.5666 5.26135 3.56669 5.26135 3.56665L1.90161 12.8153C1.76222 13.1802 1.85107 13.5963 2.1272 13.8726C2.12712 13.8725 2.12728 13.8727 2.1272 13.8726C2.40346 14.1486 2.81976 14.2375 3.18457 14.0983L3.177 14.1012L12.4332 10.7387C12.4331 10.7388 12.4333 10.7386 12.4332 10.7387C12.9248 10.5598 13.1987 10.018 13.051 9.51612C13.0511 9.51624 13.0509 9.516 13.051 9.51612C13.0033 9.35462 12.9155 9.20733 12.7959 9.08874L6.91138 3.2041C6.723 3.01433 6.47779 2.92316 6.23266 2.91919ZM6.20117 3.9082C6.20166 3.90873 6.20214 3.90926 6.20263 3.90979L12.0902 9.79724C12.0907 9.79769 12.0911 9.79814 12.0916 9.79858C12.0945 9.79752 12.0924 9.79808 12.0916 9.79858L9.82812 10.6211L5.3789 6.17188L6.20117 3.9082ZM5.00207 7.20923L8.79077 10.9979L6.5271 11.8202L4.17981 9.47278L5.00207 7.20923ZM3.80298 10.5103L5.48974 12.197L2.84057 13.1594L3.80298 10.5103Z M14.8419 4.02563L13.3419 4.52563C13.2796 4.54639 13.222 4.57922 13.1724 4.62224C13.1228 4.66525 13.0822 4.71762 13.0528 4.77635C13.0234 4.83508 13.0059 4.89902 13.0012 4.96452C12.9966 5.03001 13.0049 5.09579 13.0256 5.15808C13.0464 5.22038 13.0792 5.27797 13.1222 5.32758C13.1653 5.37719 13.2176 5.41784 13.2763 5.44721C13.3351 5.47658 13.399 5.4941 13.4645 5.49875C13.53 5.50341 13.5958 5.49513 13.6581 5.47437L15.1581 4.97437C15.2204 4.95361 15.278 4.92078 15.3276 4.87776C15.3772 4.83475 15.4178 4.78238 15.4472 4.72365C15.4766 4.66492 15.4941 4.60098 15.4988 4.53548C15.5034 4.46999 15.4951 4.40421 15.4744 4.34192C15.4536 4.27962 15.4208 4.22203 15.3778 4.17242C15.3347 4.12281 15.2824 4.08216 15.2236 4.05279C15.1649 4.02342 15.101 4.0059 15.0355 4.00125C14.97 3.99659 14.9042 4.00487 14.8419 4.02563Z M13.5 6.5C13.3674 6.50002 13.2402 6.55271 13.1465 6.64648C13.0527 6.74025 13.0001 6.86741 13.0001 7C13.0001 7.13259 13.0527 7.25975 13.1465 7.35352L14.1465 8.35352C14.2402 8.44726 14.3674 8.49992 14.5 8.49992C14.6326 8.49992 14.7597 8.44726 14.8535 8.35352C14.9473 8.25975 14.9999 8.13259 14.9999 8C14.9999 7.86741 14.9473 7.74025 14.8535 7.64648L13.8535 6.64648C13.7598 6.55271 13.6326 6.50002 13.5 6.5Z', - Copy: - 'M2.5 5C2.3674 5.00001 2.24023 5.0527 2.14646 5.14646C2.0527 5.24023 2.00001 5.3674 2 5.5V13.5C2.00001 13.6326 2.0527 13.7598 2.14646 13.8535C2.24023 13.9473 2.3674 14 2.5 14H10.5C10.6326 14 10.7598 13.9473 10.8535 13.8535C10.9473 13.7598 11 13.6326 11 13.5V5.5C11 5.3674 10.9473 5.24023 10.8535 5.14646C10.7598 5.0527 10.6326 5.00001 10.5 5H2.5ZM3 6H10V13H3V6Z M5.5 2C5.3674 2.00001 5.24023 2.0527 5.14646 2.14646C5.0527 2.24023 5.00001 2.3674 5 2.5V5.5C5 5.63261 5.05268 5.75979 5.14645 5.85355C5.24021 5.94732 5.36739 6 5.5 6C5.63261 6 5.75979 5.94732 5.85355 5.85355C5.94732 5.75979 6 5.63261 6 5.5V3H13V10H10.5C10.3674 10 10.2402 10.0527 10.1464 10.1464C10.0527 10.2402 10 10.3674 10 10.5C10 10.6326 10.0527 10.7598 10.1464 10.8536C10.2402 10.9473 10.3674 11 10.5 11H13.5C13.6326 11 13.7598 10.9473 13.8535 10.8535C13.9473 10.7598 14 10.6326 14 10.5V2.5C14 2.3674 13.9473 2.24023 13.8535 2.14646C13.7598 2.0527 13.6326 2.00001 13.5 2H5.5Z', - Crown: - 'M7.99995 9.49854C7.31439 9.49854 6.62883 9.53548 5.94624 9.60925C5.81442 9.62351 5.69365 9.68954 5.61051 9.79283C5.52736 9.89612 5.48865 10.0282 5.50288 10.16C5.51714 10.2919 5.58317 10.4126 5.68646 10.4958C5.78975 10.5789 5.92184 10.6176 6.05367 10.6034C7.34741 10.4636 8.6525 10.4636 9.94624 10.6034C10.0781 10.6176 10.2102 10.5789 10.3134 10.4958C10.4167 10.4126 10.4828 10.2919 10.497 10.16C10.5113 10.0282 10.4725 9.89612 10.3894 9.79283C10.3063 9.68954 10.1855 9.62351 10.0537 9.60925C9.37108 9.53548 8.68552 9.49854 7.99995 9.49854Z M7.99995 2.04688C7.65453 2.04688 7.30912 2.21301 7.12508 2.54529L5.2906 5.85229C5.29065 5.85221 5.29055 5.85238 5.2906 5.85229C5.28713 5.85847 5.28938 5.85781 5.28267 5.85486L2.1219 4.44934C2.12133 4.4491 2.12076 4.44885 2.12019 4.44861C1.3919 4.12796 0.558477 4.82501 0.745194 5.59851C0.745184 5.59846 0.745204 5.59855 0.745194 5.59851L2.33186 12.3641C2.33218 12.3655 2.33251 12.3668 2.33284 12.3682C2.4652 12.9124 3.03486 13.2485 3.57527 13.1012C3.57588 13.101 3.57649 13.1008 3.5771 13.1006C6.46919 12.2997 9.52449 12.2997 12.4166 13.1006C12.4173 13.1008 12.418 13.101 12.4188 13.1012C12.6805 13.1723 12.9605 13.1348 13.1942 12.9971C13.1941 12.9972 13.1942 12.997 13.1942 12.9971C13.428 12.8592 13.5966 12.6319 13.6607 12.3682C13.6611 12.367 13.6614 12.3658 13.6617 12.3646L15.2555 5.59583L15.2547 5.59851C15.3436 5.22999 15.215 4.84008 14.9243 4.5968C14.6334 4.3535 14.2268 4.29578 13.8797 4.44861C13.8791 4.44885 13.8786 4.4491 13.878 4.44934L10.7172 5.85486C10.7105 5.85781 10.7129 5.85847 10.7094 5.85229L8.87483 2.54529C8.69079 2.21301 8.34538 2.04688 7.99995 2.04688ZM7.99995 3.01257C7.99684 3.01257 7.99373 3.01833 8.00008 3.02979C8.00004 3.02971 8.00012 3.02987 8.00008 3.02979L9.83773 6.34253C9.8387 6.34428 9.83967 6.34603 9.84066 6.34778C10.0939 6.79183 10.6523 6.97615 11.1202 6.77014C11.1208 6.7699 11.1213 6.76966 11.1219 6.76941L14.2827 5.36389C14.2828 5.36344 14.2826 5.36378 14.2827 5.36389C14.2825 5.36478 14.2823 5.36581 14.2821 5.3667L12.6892 12.1318C12.6887 12.1335 12.6879 12.1346 12.6865 12.1355C12.6847 12.1365 12.6831 12.1368 12.6811 12.1362C9.61578 11.2879 6.37751 11.2879 3.3122 12.1364C3.30262 12.139 3.30686 12.1415 3.30451 12.1318L1.71797 5.36707C1.71773 5.36601 1.71749 5.36495 1.71724 5.36389L4.87801 6.76941C4.87858 6.76965 4.87914 6.7699 4.87971 6.77014C5.34758 6.97615 5.90597 6.79183 6.15926 6.34778C6.16024 6.34603 6.16122 6.34428 6.16219 6.34253L8.00008 3.02979C8.00004 3.02971 8.00012 3.02987 8.00008 3.02979C8.00642 3.01833 8.00306 3.01257 7.99995 3.01257Z', - Cube: - 'M8 0.82251C7.82935 0.82251 7.65864 0.866609 7.50623 0.95459L2.01111 4.04553C2.0105 4.04586 2.00989 4.04618 2.00928 4.04651C1.69686 4.22396 1.50196 4.55685 1.5 4.91614C1.5 4.91703 1.5 4.91793 1.5 4.91882V11.0813C1.5 11.0822 1.5 11.0832 1.5 11.0841C1.50201 11.4433 1.69694 11.776 2.00928 11.9535C2.00989 11.9539 2.0105 11.9542 2.01111 11.9546L7.50611 15.0455C7.81098 15.2216 8.18902 15.2216 8.4939 15.0455L13.9889 11.9546C13.9895 11.9543 13.99 11.9539 13.9906 11.9536C14.3031 11.7762 14.4981 11.4433 14.5 11.084C14.5 11.0831 14.5 11.0822 14.5 11.0813V4.91882C14.5 4.91789 14.5 4.91695 14.5 4.91602C14.498 4.55676 14.3031 4.22396 13.9907 4.04651C13.9901 4.04618 13.9895 4.04586 13.9889 4.04553L8.49378 0.95459C8.34136 0.866609 8.17065 0.82251 8 0.82251ZM14.064 4.18054C13.9361 4.14538 13.7996 4.16241 13.6843 4.22791L8.05396 7.42639L2.31214 4.22583C2.25478 4.19386 2.19169 4.17351 2.12646 4.16592C2.06124 4.15834 1.99516 4.16368 1.932 4.18164C1.86884 4.19959 1.80983 4.22981 1.75835 4.27057C1.70687 4.31133 1.66392 4.36183 1.63196 4.41919C1.59999 4.47655 1.57964 4.53964 1.57205 4.60486C1.56447 4.67009 1.56981 4.73616 1.58777 4.79932C1.60572 4.86249 1.63594 4.92149 1.6767 4.97297C1.71746 5.02445 1.76796 5.0674 1.82532 5.09937L7.55383 8.29248L7.5 14.6708C7.4989 14.8034 7.5505 14.931 7.64347 15.0255C7.73643 15.1201 7.86314 15.1738 7.99573 15.1749C8.06139 15.1755 8.12652 15.1631 8.1874 15.1385C8.24828 15.1139 8.30371 15.0775 8.35054 15.0315C8.39737 14.9855 8.43467 14.9307 8.46031 14.8702C8.48596 14.8098 8.49944 14.7449 8.5 14.6792L8.55383 8.29248L14.1782 5.09741C14.2353 5.06498 14.2855 5.02161 14.3258 4.9698C14.3661 4.91798 14.3959 4.85872 14.4133 4.79542C14.4307 4.73211 14.4355 4.66599 14.4274 4.60082C14.4193 4.53566 14.3984 4.47274 14.366 4.41565C14.3335 4.35855 14.2902 4.3084 14.2384 4.26806C14.1865 4.22772 14.1273 4.19798 14.064 4.18054ZM8 1.8241L13.4968 4.91602C13.4994 4.9175 13.5 4.91847 13.5 4.92151V11.0786C13.5 11.0817 13.4995 11.0825 13.4968 11.084L8 14.176L2.5033 11.0841C2.50059 11.0826 2.50002 11.0816 2.5 11.0785V4.92151C2.50002 4.91844 2.50051 4.91765 2.50318 4.91614L8 1.8241Z', - CurrencyDollarSimple: - 'M8 1C7.86739 1 7.74021 1.05268 7.64645 1.14645C7.55268 1.24021 7.5 1.36739 7.5 1.5V2.5H6.75C5.09907 2.5 3.75 3.84907 3.75 5.5C3.75 7.15093 5.09907 8.5 6.75 8.5H9.5C10.0306 8.49999 10.539 8.7106 10.9142 9.08582C11.6995 9.87098 11.6995 11.129 10.9142 11.9142C10.539 12.2894 10.0306 12.5 9.5 12.5H6.5C5.3895 12.5001 4.49994 11.6105 4.5 10.5C4.5 10.3674 4.44732 10.2402 4.35355 10.1464C4.25979 10.0527 4.13261 10 4 10C3.86739 10 3.74021 10.0527 3.64645 10.1464C3.55268 10.2402 3.5 10.3674 3.5 10.5C3.49992 12.151 4.84902 13.5001 6.5 13.5H7.5V14.5C7.5 14.6326 7.55268 14.7598 7.64645 14.8536C7.74021 14.9473 7.86739 15 8 15C8.13261 15 8.25979 14.9473 8.35355 14.8536C8.44732 14.7598 8.5 14.6326 8.5 14.5V13.5H9.5C10.2954 13.5 11.0588 13.1838 11.6212 12.6213C12.7888 11.454 12.7888 9.54599 11.6212 8.37866C11.0588 7.81619 10.2954 7.49998 9.5 7.5H6.75C5.63951 7.5 4.75 6.61049 4.75 5.5C4.75 4.38951 5.63951 3.5 6.75 3.5H9C10.1105 3.50008 10.9999 4.38953 11 5.5C11 5.63261 11.0527 5.75979 11.1464 5.85355C11.2402 5.94732 11.3674 6 11.5 6C11.6326 6 11.7598 5.94732 11.8536 5.85355C11.9473 5.75979 12 5.63261 12 5.5C11.9999 3.84914 10.6509 2.50012 9 2.5H8.5V1.5C8.5 1.36739 8.44732 1.24021 8.35355 1.14645C8.25979 1.05268 8.13261 1 8 1Z', - Cursor: - 'M2.91503 1.85839C2.62311 1.84273 2.33987 1.95711 2.14843 2.14855C1.89317 2.40381 1.77501 2.8222 1.9193 3.2069L5.55688 12.9069C5.55712 12.9075 5.55736 12.9082 5.55761 12.9088C5.71341 13.3189 6.11874 13.5564 6.49926 13.5538C6.87978 13.5513 7.28193 13.3085 7.43224 12.8964C7.4322 12.8965 7.43228 12.8962 7.43224 12.8964L8.70031 9.40757L12.6465 13.3536C12.7402 13.4474 12.8674 13.5 13 13.5C13.1326 13.5 13.2597 13.4474 13.3535 13.3536C13.4473 13.2598 13.4999 13.1327 13.4999 13.0001C13.4999 12.8675 13.4473 12.7403 13.3535 12.6466L9.40746 8.70042L12.8957 7.43247C13.3084 7.28247 13.5515 6.87997 13.5541 6.49924C13.5566 6.11852 13.3189 5.71297 12.9083 5.55747C12.9078 5.55727 12.9073 5.55706 12.9067 5.55686L3.20678 1.91942C3.1106 1.88335 3.01234 1.86361 2.91503 1.85839ZM2.8557 2.8557L12.5541 6.49267L8.54784 7.94884C8.54723 7.94909 8.54662 7.94933 8.54601 7.94957C8.26964 8.05128 8.05116 8.26976 7.94945 8.54613C7.94921 8.54674 7.94896 8.54735 7.94872 8.54796L6.49279 12.5537C6.4931 12.5527 6.49235 12.5548 6.49279 12.5537C6.49283 12.5536 6.49275 12.5538 6.49279 12.5537C6.49235 12.555 6.49291 12.555 6.49266 12.5559C6.49273 12.5555 6.49254 12.5564 6.49266 12.5559C6.49601 12.5582 6.49135 12.5633 6.49266 12.5559C6.49228 12.5548 6.49336 12.5553 6.49279 12.5537L2.8557 2.8557Z', - DashboardElementFeature: - 'M13 7C13 6.72386 12.7761 6.5 12.5 6.5C12.2239 6.5 12 6.72386 12 7V11.5C12 11.7761 12.2239 12 12.5 12C12.7761 12 13 11.7761 13 11.5V7Z M11 9.5C11 9.22386 10.7761 9 10.5 9C10.2239 9 10 9.22386 10 9.5V11.5C10 11.7761 10.2239 12 10.5 12C10.7761 12 11 11.7761 11 11.5V9.5Z M8.5 10.5C8.77614 10.5 9 10.7239 9 11V11.5C9 11.7761 8.77614 12 8.5 12C8.22386 12 8 11.7761 8 11.5V11C8 10.7239 8.22386 10.5 8.5 10.5Z M1 3.5C1 2.67157 1.67157 2 2.5 2H13.5C14.3284 2 15 2.67157 15 3.5V12.5C15 13.3284 14.3284 14 13.5 14H2.5C1.67157 14 1 13.3284 1 12.5V3.5ZM2 10.2071L4 8.20711L5.64645 9.85355C5.84171 10.0488 6.15829 10.0488 6.35355 9.85355L9.5 6.70711V7C9.5 7.27614 9.72386 7.5 10 7.5C10.2761 7.5 10.5 7.27614 10.5 7V5.5C10.5 5.492 10.4998 5.484 10.4994 5.47601C10.4937 5.35616 10.4451 5.23798 10.3536 5.14645C10.3056 5.09851 10.2504 5.06234 10.1914 5.03794C10.1324 5.01349 10.0678 5 10 5H8.5C8.22386 5 8 5.22386 8 5.5C8 5.77614 8.22386 6 8.5 6H8.79289L6 8.79289L4.35355 7.14645C4.15829 6.95118 3.84171 6.95118 3.64645 7.14645L2 8.79289V3.5C2 3.22386 2.22386 3 2.5 3H13.5C13.7761 3 14 3.22386 14 3.5V12.5C14 12.7761 13.7761 13 13.5 13H2.5C2.22386 13 2 12.7761 2 12.5V10.2071Z', - DeviceMobile: - 'M5 1C4.1775 1 3.5 1.6775 3.5 2.5V13.5C3.5 14.3225 4.1775 15 5 15H11C11.8225 15 12.5 14.3225 12.5 13.5V2.5C12.5 1.6775 11.8225 1 11 1H5ZM5 2H11C11.2821 2 11.5 2.21788 11.5 2.5V3H4.5V2.5C4.5 2.21788 4.71788 2 5 2ZM4.5 4H11.5V12H4.5V4ZM4.5 13H11.5V13.5C11.5 13.7821 11.2821 14 11 14H5C4.71788 14 4.5 13.7821 4.5 13.5V13Z', - DividerElementFeature: - 'M14 3C14 2.72386 13.7761 2.5 13.5 2.5C13.2239 2.5 13 2.72386 13 3V4.5H11.5C11.2239 4.5 11 4.72386 11 5C11 5.27614 11.2239 5.5 11.5 5.5H13C13.2652 5.5 13.5196 5.39464 13.7071 5.20711C13.8946 5.01957 14 4.76522 14 4.5V3Z M6.5 5C6.5 4.72386 6.72386 4.5 7 4.5H9C9.27614 4.5 9.5 4.72386 9.5 5C9.5 5.27614 9.27614 5.5 9 5.5H7C6.72386 5.5 6.5 5.27614 6.5 5Z M2.29289 10.7929C2.48043 10.6054 2.73478 10.5 3 10.5H4.5C4.77614 10.5 5 10.7239 5 11C5 11.2761 4.77614 11.5 4.5 11.5H3V13C3 13.2761 2.77614 13.5 2.5 13.5C2.22386 13.5 2 13.2761 2 13V11.5C2 11.2348 2.10536 10.9804 2.29289 10.7929Z M6.5 11C6.5 10.7239 6.72386 10.5 7 10.5H9C9.27614 10.5 9.5 10.7239 9.5 11C9.5 11.2761 9.27614 11.5 9 11.5H7C6.72386 11.5 6.5 11.2761 6.5 11Z M11.5 10.5C11.2239 10.5 11 10.7239 11 11C11 11.2761 11.2239 11.5 11.5 11.5H13V13C13 13.2761 13.2239 13.5 13.5 13.5C13.7761 13.5 14 13.2761 14 13V11.5C14 11.2348 13.8946 10.9804 13.7071 10.7929C13.5196 10.6054 13.2652 10.5 13 10.5H11.5Z M2.5 2.5C2.77614 2.5 3 2.72386 3 3V4.5H4.5C4.77614 4.5 5 4.72386 5 5C5 5.27614 4.77614 5.5 4.5 5.5H3C2.73478 5.5 2.48043 5.39464 2.29289 5.20711C2.10536 5.01957 2 4.76522 2 4.5V3C2 2.72386 2.22386 2.5 2.5 2.5Z M2 8C2 7.72386 2.22386 7.5 2.5 7.5H13.5C13.7761 7.5 14 7.72386 14 8C14 8.27614 13.7761 8.5 13.5 8.5H2.5C2.22386 8.5 2 8.27614 2 8Z', - DonutChartElementFeature: - 'M1.5 8C1.5 4.41015 4.41015 1.5 8 1.5C11.5899 1.5 14.5 4.41015 14.5 8C14.5 11.5899 11.5899 14.5 8 14.5C4.41015 14.5 1.5 11.5899 1.5 8ZM8 2.5C6.66125 2.5 5.43422 2.97831 4.48047 3.77337L6.6213 5.9142C7.01652 5.65244 7.49046 5.5 8 5.5C9.38071 5.5 10.5 6.61929 10.5 8C10.5 8.50954 10.3476 8.98348 10.0858 9.37869L12.2266 11.5195C13.0217 10.5658 13.5 9.33875 13.5 8C13.5 4.96243 11.0376 2.5 8 2.5ZM9.3787 10.0858C8.98348 10.3476 8.50954 10.5 8 10.5C7.21237 10.5 6.50981 10.1358 6.05157 9.56654L3.55066 11.2338C4.55062 12.6073 6.17106 13.5 8 13.5C9.33875 13.5 10.5658 13.0217 11.5195 12.2266L9.3787 10.0858ZM5.59123 8.67158C5.53178 8.45788 5.5 8.23265 5.5 8C5.5 7.49046 5.65244 7.01652 5.9142 6.62131L3.77336 4.48047C2.97831 5.43422 2.5 6.66125 2.5 8C2.5 8.85024 2.69293 9.65542 3.03739 10.3741L5.59123 8.67158ZM9.5 8C9.5 8.82843 8.82843 9.5 8 9.5C7.17157 9.5 6.5 8.82843 6.5 8C6.5 7.17157 7.17157 6.5 8 6.5C8.82843 6.5 9.5 7.17157 9.5 8Z', - DownloadSimple: - 'M8 2C7.86739 2 7.74021 2.05268 7.64645 2.14645C7.55268 2.24021 7.5 2.36739 7.5 2.5V8.29297L5.72852 6.52148C5.63475 6.42774 5.50759 6.37508 5.375 6.37508C5.24241 6.37508 5.11525 6.42774 5.02148 6.52148C4.92774 6.61525 4.87508 6.74241 4.87508 6.875C4.87508 7.00759 4.92774 7.13475 5.02148 7.22852L7.64648 9.85352C7.74024 9.94729 7.8674 9.99998 8 10C8.1326 9.99998 8.25976 9.94729 8.35352 9.85352L10.9785 7.22852C11.0723 7.13475 11.1249 7.00759 11.1249 6.875C11.1249 6.74241 11.0723 6.61525 10.9785 6.52148C10.8848 6.42774 10.7576 6.37508 10.625 6.37508C10.4924 6.37508 10.3652 6.42774 10.2715 6.52148L8.5 8.29297V2.5C8.5 2.36739 8.44732 2.24021 8.35355 2.14645C8.25979 2.05268 8.13261 2 8 2Z M2.5 9C2.36739 9 2.24021 9.05268 2.14645 9.14645C2.05268 9.24021 2 9.36739 2 9.5V13C2.00007 13.5463 2.45357 13.9999 2.99988 14C2.99984 14 2.99992 14 2.99988 14H13C13.5464 14 14 13.5464 14 13V9.5C14 9.36739 13.9473 9.24021 13.8536 9.14645C13.7598 9.05268 13.6326 9 13.5 9C13.3674 9 13.2402 9.05268 13.1464 9.14645C13.0527 9.24021 13 9.36739 13 9.5V13H3.00012L3 9.5C3 9.36739 2.94732 9.24021 2.85355 9.14645C2.75979 9.05268 2.63261 9 2.5 9Z', - DotsSixVertical: - 'M5.75 4.5C6.16419 4.5 6.5 4.16419 6.5 3.75C6.5 3.33581 6.16419 3 5.75 3C5.33581 3 5 3.33581 5 3.75C5 4.16419 5.33581 4.5 5.75 4.5Z M10.25 4.5C10.6642 4.5 11 4.16419 11 3.75C11 3.33581 10.6642 3 10.25 3C9.83581 3 9.5 3.33581 9.5 3.75C9.5 4.16419 9.83581 4.5 10.25 4.5Z M5.75 8.75C6.16419 8.75 6.5 8.41419 6.5 8C6.5 7.58581 6.16419 7.25 5.75 7.25C5.33581 7.25 5 7.58581 5 8C5 8.41419 5.33581 8.75 5.75 8.75Z M10.25 8.75C10.6642 8.75 11 8.41419 11 8C11 7.58581 10.6642 7.25 10.25 7.25C9.83581 7.25 9.5 7.58581 9.5 8C9.5 8.41419 9.83581 8.75 10.25 8.75Z M5.75 13C6.16419 13 6.5 12.6642 6.5 12.25C6.5 11.8358 6.16419 11.5 5.75 11.5C5.33581 11.5 5 11.8358 5 12.25C5 12.6642 5.33581 13 5.75 13Z M10.25 13C10.6642 13 11 12.6642 11 12.25C11 11.8358 10.6642 11.5 10.25 11.5C9.83581 11.5 9.5 11.8358 9.5 12.25C9.5 12.6642 9.83581 13 10.25 13Z', - Drive: - 'M14.6759265,9.27999973 L10.203687,2 L5.79628759,2 L10.2685271,9.27999973 L14.6759265,9.27999973 Z M6.50924926,10.2105263 L4.30554955,14 L12.7962749,14 L15,10.2105263 L6.50924926,10.2105263 Z M5.21295531,2.94736842 L1,10.2105263 L3.2036997,14 L7.48146971,6.73684211 L5.21295531,2.94736842 Z', - DropdownFilterFeature: - 'M8.85355 7.14645C8.65829 6.95118 8.34171 6.95118 8.14645 7.14645C7.95118 7.34171 7.95118 7.65829 8.14645 7.85355L9.64645 9.35355C9.84171 9.54882 10.1583 9.54882 10.3536 9.35355L11.8536 7.85355C12.0488 7.65829 12.0488 7.34171 11.8536 7.14645C11.6583 6.95118 11.3417 6.95118 11.1464 7.14645L10 8.29289L8.85355 7.14645Z M1 7C1 5.34315 2.34315 4 4 4H12C13.6569 4 15 5.34315 15 7V9C15 10.6569 13.6569 12 12 12H4C2.34315 12 1 10.6569 1 9V7ZM4 5H12C13.1046 5 14 5.89543 14 7V9C14 10.1046 13.1046 11 12 11H4C2.89543 11 2 10.1046 2 9V7C2 5.89543 2.89543 5 4 5Z', - ElementsElementFeature: - 'M1.5 4.5C1.5 3.6775 2.1775 3 3 3H13C13.8225 3 14.5 3.6775 14.5 4.5V11.5C14.5 12.3225 13.8225 13 13 13H3C2.1775 13 1.5 12.3225 1.5 11.5V4.5ZM2.5 6V7H5.54724C6.0208 6.99738 6.43583 7.33673 6.52764 7.80096C6.67771 8.49709 7.2874 8.98956 8 8.9895C8.71261 8.98954 9.32227 8.49709 9.47236 7.80095C9.56418 7.33673 9.9792 6.99738 10.4528 7H13.5V6H2.5ZM13.5 5H2.5V4.5C2.5 4.21788 2.71788 4 3 4H13C13.2821 4 13.5 4.21788 13.5 4.5V5ZM13.5 8H10.4524L10.4517 8.00379C10.2065 9.15871 9.18065 9.98956 8 9.9895C6.81933 9.98959 5.79347 9.15873 5.54834 8.00379L5.54761 8H2.5V11.5C2.5 11.7821 2.71788 12 3 12H13C13.2821 12 13.5 11.7821 13.5 11.5V8Z', - Envelope: - 'M2.5 4H13.5V11.3627L9.43164 7.63147C9.3339 7.54186 9.20456 7.49475 9.07209 7.50049C8.93961 7.50624 8.81484 7.56437 8.72522 7.66211C8.63561 7.75985 8.5885 7.88919 8.59424 8.02166C8.59999 8.15414 8.65812 8.27891 8.75586 8.36853L12.7156 12H3.28442L7.24414 8.36853C7.34188 8.27891 7.40001 8.15414 7.40576 8.02166C7.4115 7.88919 7.36439 7.75985 7.27478 7.66211C7.18516 7.56437 7.06039 7.50624 6.92792 7.50049C6.79544 7.49475 6.6661 7.54186 6.56836 7.63147L2.5 11.3627V4Z M2 3C1.8674 3.00001 1.74023 3.0527 1.64646 3.14646C1.5527 3.24023 1.50001 3.3674 1.5 3.5V12C1.50007 12.5463 1.95357 12.9999 2.49988 13C2.49984 13 2.49992 13 2.49988 13H13.5C14.0464 13 14.5 12.5464 14.5 12V3.5C14.5 3.3674 14.4473 3.24023 14.3535 3.14646C14.2598 3.0527 14.1326 3.00001 14 3H2ZM1.97827 3.00049C1.84581 3.00625 1.72107 3.06439 1.63147 3.16211C1.54186 3.25985 1.49475 3.38919 1.50049 3.52167C1.50624 3.65414 1.56437 3.77891 1.66211 3.86853L7.66211 9.36853C7.75433 9.45307 7.87489 9.49996 8 9.49996C8.12511 9.49996 8.24567 9.45307 8.33789 9.36853L14.3379 3.86853C14.4356 3.77891 14.4938 3.65414 14.4995 3.52167C14.5053 3.38919 14.4581 3.25985 14.3685 3.16211C14.2789 3.06437 14.1541 3.00624 14.0217 3.00049C13.8892 2.99475 13.7599 3.04186 13.6621 3.13147L8 8.32166L2.33789 3.13147C2.28949 3.08709 2.23281 3.05268 2.17111 3.03021C2.10941 3.00773 2.04388 2.99764 1.97827 3.00049Z', - EnvelopeSimple: - 'M2.5 4H13.5V12H2.50012L2.5 4Z M2 3C1.8674 3.00001 1.74023 3.0527 1.64646 3.14646C1.5527 3.24023 1.50001 3.3674 1.5 3.5V12C1.50007 12.5463 1.95357 12.9999 2.49988 13C2.49984 13 2.49992 13 2.49988 13H13.5C14.0464 13 14.5 12.5464 14.5 12V3.5C14.5 3.3674 14.4473 3.24023 14.3535 3.14646C14.2598 3.0527 14.1326 3.00001 14 3H2ZM1.97827 3.00049C1.84581 3.00625 1.72107 3.06439 1.63147 3.16211C1.54186 3.25985 1.49475 3.38919 1.50049 3.52167C1.50624 3.65414 1.56437 3.77891 1.66211 3.86853L7.66211 9.36853C7.75433 9.45307 7.87489 9.49996 8 9.49996C8.12511 9.49996 8.24567 9.45307 8.33789 9.36853L14.3379 3.86853C14.4356 3.77891 14.4938 3.65414 14.4995 3.52167C14.5053 3.38919 14.4581 3.25985 14.3685 3.16211C14.2789 3.06437 14.1541 3.00624 14.0217 3.00049C13.8892 2.99475 13.7599 3.04186 13.6621 3.13147L8 8.32166L2.33789 3.13147C2.28949 3.08709 2.23281 3.05268 2.17111 3.03021C2.10941 3.00773 2.04388 2.99764 1.97827 3.00049Z', - ExtensionsFeature: - 'M11.505 2.00002C11.7868 2.0023 11.996 2.17124 12.1126 2.35521L14.3784 5.93199C14.4947 6.11565 14.5737 6.40994 14.3968 6.68347C14.2338 6.93564 13.9591 7 13.7657 7H9.2341C9.03608 7 8.75765 6.933 8.5969 6.67396C8.42729 6.40062 8.508 6.11098 8.62139 5.93199L10.8872 2.35521C11.0065 2.16676 11.22 1.99772 11.505 2.00002ZM11.4999 3.25667L9.76207 6H13.2377L11.4999 3.25667Z M5 2C3.48158 2 2.25 3.23158 2.25 4.75C2.25 6.26842 3.48158 7.5 5 7.5C6.51916 7.5 7.75 6.26832 7.75 4.75C7.75 3.23168 6.51916 2 5 2ZM3.25 4.75C3.25 3.78387 4.03387 3 5 3C5.96669 3 6.75 3.78377 6.75 4.75C6.75 5.71623 5.96669 6.5 5 6.5C4.03387 6.5 3.25 5.71613 3.25 4.75Z M3.32734 9C2.86512 9 2.5 9.3811 2.5 9.82734V13.1727C2.5 13.6349 2.8811 14 3.32734 14H6.67266C7.13488 14 7.5 13.6189 7.5 13.1727V9.82734C7.5 9.36512 7.1189 9 6.67266 9H3.32734ZM3.5 13V10H6.5V13H3.5Z M9.00001 11.9571C8.60949 11.5666 8.60949 10.9334 9.00001 10.5429L10.7943 8.74861C11.1848 8.35809 11.818 8.35809 12.2085 8.74861L14.0015 10.5416C14.392 10.9321 14.392 11.5653 14.0015 11.9558L12.2072 13.7501C11.8167 14.1406 11.1835 14.1406 10.793 13.7501L9.00001 11.9571ZM11.5014 9.45572L9.70712 11.25L11.5001 13.043L13.2944 11.2487L11.5014 9.45572Z', - Eye: - 'M7.99999 5C6.34908 5 4.99999 6.34908 4.99999 8C4.99999 9.65092 6.34908 11 7.99999 11C9.65091 11 11 9.65092 11 8C11 6.34908 9.65091 5 7.99999 5ZM7.99999 6C9.11045 6 9.99999 6.88954 9.99999 8C9.99999 9.11046 9.11045 10 7.99999 10C6.88954 10 5.99999 9.11046 5.99999 8C5.99999 6.88954 6.88954 6 7.99999 6Z M7.99999 3C2.7 3 0.543084 7.79688 0.543084 7.79688C0.514658 7.86082 0.499969 7.93002 0.499969 8C0.499969 8.06998 0.514658 8.13918 0.543084 8.20312C0.543084 8.20312 2.7 13 7.99999 13C13.3 13 15.4569 8.20312 15.4569 8.20312C15.4853 8.13918 15.5 8.06998 15.5 8C15.5 7.93002 15.4853 7.86082 15.4569 7.79688C15.4569 7.79688 13.3 3 7.99999 3ZM7.99999 4C12.0847 4 14.0073 7.16884 14.4418 8C14.0073 8.83116 12.0847 12 7.99999 12C3.91524 12 1.99269 8.83116 1.55822 8C1.99269 7.16884 3.91524 4 7.99999 4Z', - EyeClosed: - 'M2.05285 6.05895C1.921 6.0449 1.78896 6.08381 1.68579 6.1671C1.58264 6.25044 1.51683 6.37134 1.50282 6.5032C1.48881 6.63506 1.52775 6.76708 1.61108 6.87023C1.92197 7.25515 2.31766 7.66813 2.7843 8.0664L1.56066 10.1877C1.52785 10.2446 1.50656 10.3074 1.498 10.3725C1.48945 10.4376 1.49381 10.5038 1.51082 10.5672C1.52783 10.6306 1.55718 10.69 1.59717 10.7421C1.63716 10.7942 1.68701 10.8379 1.74389 10.8707C1.80078 10.9035 1.86356 10.9248 1.92867 10.9334C1.99378 10.9419 2.05994 10.9375 2.12336 10.9205C2.18678 10.9035 2.24623 10.8741 2.29831 10.8341C2.35039 10.7941 2.39407 10.7443 2.42688 10.6874L3.59057 8.67003C4.21787 9.08782 4.93936 9.45718 5.7832 9.69701L5.42004 11.7634C5.39709 11.894 5.42696 12.0284 5.50308 12.137C5.57919 12.2455 5.69532 12.3194 5.82592 12.3424C5.95652 12.3653 6.09089 12.3355 6.19947 12.2594C6.30805 12.1832 6.38194 12.0671 6.4049 11.9365L6.76184 9.90624C7.15471 9.96531 7.56608 9.99987 8 9.99987C8.43131 9.99987 8.84044 9.96586 9.2312 9.90746L9.58874 11.9368C9.60012 12.0014 9.62413 12.0632 9.6594 12.1186C9.69467 12.174 9.74051 12.2219 9.7943 12.2596C9.84809 12.2973 9.90877 12.324 9.97289 12.3382C10.037 12.3525 10.1033 12.3539 10.168 12.3425C10.2326 12.3311 10.2944 12.3071 10.3498 12.2718C10.4052 12.2366 10.4531 12.1907 10.4908 12.1369C10.5284 12.0831 10.5551 12.0225 10.5693 11.9584C10.5836 11.8942 10.585 11.828 10.5736 11.7633L10.21 9.69909C11.0552 9.45971 11.7777 9.09033 12.406 8.67235L13.5674 10.6758C13.6003 10.7326 13.6441 10.7824 13.6963 10.8222C13.7484 10.8621 13.8079 10.8914 13.8714 10.9083C13.9348 10.9251 14.001 10.9294 14.0661 10.9207C14.1312 10.912 14.1939 10.8906 14.2507 10.8577C14.3075 10.8247 14.3573 10.7809 14.3972 10.7288C14.4371 10.6766 14.4663 10.6171 14.4832 10.5537C14.5001 10.4902 14.5043 10.424 14.4956 10.3589C14.487 10.2939 14.4655 10.2311 14.4326 10.1743L13.2122 8.0692C13.6803 7.67006 14.0773 7.25607 14.3889 6.87023C14.4722 6.76708 14.5112 6.63506 14.4972 6.5032C14.4832 6.37134 14.4173 6.25044 14.3142 6.1671C14.211 6.08379 14.079 6.04487 13.9471 6.0589C13.8153 6.07293 13.6944 6.13877 13.6111 6.24193C12.6193 7.46982 10.8439 8.99987 8 8.99987C7.44536 8.99987 6.93261 8.94019 6.45715 8.83739C6.45237 8.83569 6.44757 8.83407 6.44274 8.83251L6.43969 8.83324C4.4871 8.40565 3.18484 7.22736 2.38891 6.24193C2.30559 6.13879 2.1847 6.07297 2.05285 6.05895Z', - EyeSlash: - 'M8.00013 2.99999C7.53899 2.99917 7.07864 3.03701 6.62379 3.11315C6.55902 3.12398 6.49703 3.14747 6.44134 3.18226C6.38565 3.21706 6.33736 3.26248 6.29923 3.31594C6.2611 3.3694 6.23387 3.42984 6.2191 3.49382C6.20433 3.5578 6.2023 3.62407 6.21314 3.68883C6.23505 3.81961 6.30801 3.93632 6.41597 4.01331C6.52393 4.0903 6.65805 4.12125 6.78883 4.09935C7.18869 4.03242 7.59357 3.99923 7.99915 3.99999C7.99882 3.99999 7.99948 3.99999 7.99915 3.99999C12.083 3.99999 14.0065 7.16727 14.4415 7.99926C14.2327 8.39403 13.6815 9.3219 12.7042 10.196C12.6553 10.2398 12.6154 10.2928 12.587 10.352C12.5585 10.4112 12.542 10.4754 12.5383 10.5409C12.5347 10.6065 12.544 10.6721 12.5657 10.7341C12.5874 10.7961 12.6211 10.8531 12.6649 10.9021C12.7533 11.0009 12.8774 11.0606 13.0097 11.0679C13.1421 11.0753 13.272 11.0298 13.3709 10.9414C14.8157 9.64896 15.4569 8.20311 15.4569 8.20311C15.4853 8.13917 15.5 8.06997 15.5 7.99999C15.5 7.93001 15.4853 7.86081 15.4569 7.79686C15.4569 7.79686 13.2994 3.00052 8.00013 2.99999Z M8.56177 5.05248C8.4315 5.02783 8.29677 5.05593 8.18721 5.1306C8.07765 5.20527 8.00223 5.3204 7.97755 5.45067C7.96532 5.51519 7.96592 5.58148 7.97932 5.64576C7.99271 5.71004 8.01864 5.77106 8.05562 5.82532C8.09259 5.87958 8.13989 5.92603 8.19482 5.96201C8.24975 5.99799 8.31122 6.0228 8.37574 6.03502C9.25118 6.20086 9.90696 6.92166 9.98963 7.80883C9.99573 7.87421 10.0146 7.93775 10.0453 7.99582C10.076 8.05389 10.1177 8.10536 10.1683 8.14727C10.2188 8.18919 10.2771 8.22074 10.3399 8.24013C10.4026 8.25951 10.4686 8.26635 10.5339 8.26024C10.666 8.24793 10.7877 8.18368 10.8723 8.08163C10.957 7.97958 10.9976 7.84808 10.9854 7.71605C10.8617 6.38949 9.8708 5.30045 8.56177 5.05248Z M3.02381 2.0006C2.89137 1.99428 2.76183 2.04082 2.6637 2.12999C2.56557 2.21919 2.5069 2.34371 2.50058 2.47617C2.49426 2.60862 2.54082 2.73816 2.63001 2.83629L5.64234 6.14989C4.99783 6.97182 4.81667 8.0854 5.20167 9.08153C5.64782 10.2359 6.7614 10.9994 7.99891 10.9997C8.57697 11.0018 9.13723 10.8317 9.61744 10.5226L12.63 13.8363C12.7192 13.9344 12.8437 13.9931 12.9762 13.9994C13.1086 14.0057 13.2382 13.9592 13.3363 13.87C13.4344 13.7808 13.4931 13.6563 13.4994 13.5238C13.5058 13.3914 13.4592 13.2618 13.37 13.1637L6.6908 5.81652C6.69016 5.81554 6.68951 5.81456 6.68885 5.81359C6.6884 5.81338 6.68796 5.81318 6.68751 5.81298L3.37 2.16369C3.2808 2.06557 3.15627 2.0069 3.02381 2.0006ZM4.78126 3.81261C4.65529 3.7712 4.51803 3.78151 4.39966 3.8413C1.67465 5.21716 0.542853 7.79748 0.542853 7.79748C0.514536 7.86137 0.499927 7.93049 0.49997 8.00038C0.500012 8.07027 0.514704 8.13938 0.543098 8.20324C0.543098 8.20324 2.69954 12.9988 7.99805 13C9.24842 13.0098 10.4832 12.7217 11.6 12.1592C11.6586 12.1296 11.7109 12.0888 11.7537 12.0391C11.7966 11.9893 11.8293 11.9317 11.8498 11.8693C11.8704 11.8069 11.8785 11.7411 11.8736 11.6757C11.8688 11.6102 11.8511 11.5463 11.8215 11.4877C11.792 11.429 11.7512 11.3768 11.7015 11.3339C11.6517 11.291 11.594 11.2584 11.5317 11.2378C11.4693 11.2172 11.4035 11.2091 11.338 11.214C11.2726 11.2189 11.2087 11.2366 11.15 11.2661C10.1746 11.7574 9.09616 12.009 8.00403 12.0001C8.00269 12.0001 8.00135 12.0001 8.00001 12.0001C3.9214 12.0001 1.99934 8.84205 1.56104 8.00512C1.80002 7.53467 2.78966 5.77445 4.85035 4.734C4.90897 4.7044 4.96118 4.66355 5.00401 4.61377C5.04684 4.56399 5.07944 4.50626 5.09996 4.44389C5.12048 4.38151 5.12851 4.3157 5.12359 4.25021C5.11867 4.18473 5.1009 4.12086 5.0713 4.06225C5.01154 3.94387 4.90721 3.85407 4.78126 3.81261ZM6.3307 6.90709L8.92811 9.76427C8.64391 9.91434 8.32855 10.0011 8.00196 9.99987C8.00131 9.99987 8.00066 9.99987 8.00001 9.99987C7.17209 9.99993 6.43288 9.49318 6.13441 8.72094C5.89804 8.10936 5.98275 7.43768 6.3307 6.90709Z', - FadersHorizontal: - 'M6.5 3.25C6.36739 3.25 6.24021 3.30268 6.14645 3.39645C6.05268 3.49021 6 3.61739 6 3.75V6.75C6 6.88261 6.05268 7.00979 6.14645 7.10355C6.24021 7.19732 6.36739 7.25 6.5 7.25C6.63261 7.25 6.75979 7.19732 6.85355 7.10355C6.94732 7.00979 7 6.88261 7 6.75V5.75H13.5C13.6326 5.75 13.7598 5.69732 13.8536 5.60355C13.9473 5.50979 14 5.38261 14 5.25C14 5.11739 13.9473 4.99021 13.8536 4.89645C13.7598 4.80268 13.6326 4.75 13.5 4.75H7V3.75C7 3.61739 6.94732 3.49021 6.85355 3.39645C6.75979 3.30268 6.63261 3.25 6.5 3.25Z M2.5 4.75C2.36739 4.75 2.24021 4.80268 2.14645 4.89645C2.05268 4.99021 2 5.11739 2 5.25C2 5.38261 2.05268 5.50979 2.14645 5.60355C2.24021 5.69732 2.36739 5.75 2.5 5.75H4.5C4.63261 5.75 4.75979 5.69732 4.85355 5.60355C4.94732 5.50979 5 5.38261 5 5.25C5 5.11739 4.94732 4.99021 4.85355 4.89645C4.75979 4.80268 4.63261 4.75 4.5 4.75H2.5Z M10.5 8.75C10.3674 8.75 10.2402 8.80268 10.1464 8.89645C10.0527 8.99021 10 9.11739 10 9.25V12.25C10 12.3826 10.0527 12.5098 10.1464 12.6036C10.2402 12.6973 10.3674 12.75 10.5 12.75C10.6326 12.75 10.7598 12.6973 10.8536 12.6036C10.9473 12.5098 11 12.3826 11 12.25V11.25H13.5C13.6326 11.25 13.7598 11.1973 13.8536 11.1036C13.9473 11.0098 14 10.8826 14 10.75C14 10.6174 13.9473 10.4902 13.8536 10.3964C13.7598 10.3027 13.6326 10.25 13.5 10.25H11V9.25C11 9.11739 10.9473 8.99021 10.8536 8.89645C10.7598 8.80268 10.6326 8.75 10.5 8.75Z M2.5 10.25C2.36739 10.25 2.24021 10.3027 2.14645 10.3964C2.05268 10.4902 2 10.6174 2 10.75C2 10.8826 2.05268 11.0098 2.14645 11.1036C2.24021 11.1973 2.36739 11.25 2.5 11.25H8.5C8.63261 11.25 8.75979 11.1973 8.85355 11.1036C8.94732 11.0098 9 10.8826 9 10.75C9 10.6174 8.94732 10.4902 8.85355 10.3964C8.75979 10.3027 8.63261 10.25 8.5 10.25H2.5Z', - FieldAttachmentElementFeature: - 'M1.5 2.5C1.5 1.67157 2.17157 1 3 1H9C9.82843 1 10.5 1.67157 10.5 2.5C10.5 3.32843 9.82843 4 9 4H3C2.17157 4 1.5 3.32843 1.5 2.5ZM3 2C2.72386 2 2.5 2.22386 2.5 2.5C2.5 2.77614 2.72386 3 3 3H9C9.27614 3 9.5 2.77614 9.5 2.5C9.5 2.22386 9.27614 2 9 2H3Z M1.5 6.5C1.5 5.67157 2.17157 5 3 5H6.5C7.32843 5 8 5.67157 8 6.5V12.5C8 13.3284 7.32843 14 6.5 14H3C2.17157 14 1.5 13.3284 1.5 12.5V6.5ZM3 6C2.72386 6 2.5 6.22386 2.5 6.5V12.5C2.5 12.7761 2.72386 13 3 13H6.5C6.77614 13 7 12.7761 7 12.5V6.5C7 6.22386 6.77614 6 6.5 6H3Z M9 6.5C9 5.67157 9.67157 5 10.5 5H14C14.8284 5 15.5 5.67157 15.5 6.5V12.5C15.5 13.3284 14.8284 14 14 14H10.5C9.67157 14 9 13.3284 9 12.5V6.5ZM10.5 6C10.2239 6 10 6.22386 10 6.5V12.5C10 12.7761 10.2239 13 10.5 13H14C14.2761 13 14.5 12.7761 14.5 12.5V6.5C14.5 6.22386 14.2761 6 14 6H10.5Z', - FieldElementFeature: - 'M1.5 3.5C1.5 3.22386 1.72386 3 2 3H8.5C8.77614 3 9 3.22386 9 3.5C9 3.77614 8.77614 4 8.5 4H2C1.72386 4 1.5 3.77614 1.5 3.5Z M1 7.5C1 6.39543 1.89543 5.5 3 5.5H13C14.1046 5.5 15 6.39543 15 7.5V9.5C15 10.6046 14.1046 11.5 13 11.5H3C1.89543 11.5 1 10.6046 1 9.5V7.5ZM3 6.5C2.44772 6.5 2 6.94772 2 7.5V9.5C2 10.0523 2.44772 10.5 3 10.5H13C13.5523 10.5 14 10.0523 14 9.5V7.5C14 6.94772 13.5523 6.5 13 6.5H3Z', - File: - 'M9.5 1.5C9.36739 1.5 9.24021 1.55268 9.14645 1.64645C9.05268 1.74021 9 1.86739 9 2V5.5C9.00001 5.6326 9.0527 5.75977 9.14646 5.85354C9.24023 5.9473 9.3674 5.99999 9.5 6H13C13.1326 6 13.2598 5.94732 13.3536 5.85355C13.4473 5.75979 13.5 5.63261 13.5 5.5C13.5 5.36739 13.4473 5.24021 13.3536 5.14645C13.2598 5.05268 13.1326 5 13 5H10V2C10 1.86739 9.94732 1.74021 9.85355 1.64645C9.75979 1.55268 9.63261 1.5 9.5 1.5Z M3.5 1.5C2.95364 1.5 2.5 1.95364 2.5 2.5V13.5C2.50007 14.0463 2.95357 14.4999 3.49988 14.5C3.49984 14.5 3.49992 14.5 3.49988 14.5H12.5C13.0464 14.5 13.5 14.0464 13.5 13.5V5.5C13.5 5.36739 13.4473 5.24021 13.3536 5.14645L9.85355 1.64645C9.75979 1.55268 9.63261 1.5 9.5 1.5H3.5ZM3.5 2.5H9.29285L12.5 5.70715V13.5H3.50012L3.5 2.5Z', - FileMarkdown: - 'M2.79289 1.79289C2.98043 1.60536 3.23478 1.5 3.5 1.5H9.5C9.63261 1.5 9.75979 1.55268 9.85355 1.64645L13.3536 5.14645C13.4438 5.2369 13.5 5.36213 13.5 5.5V8C13.5 8.27614 13.2761 8.5 13 8.5C12.7239 8.5 12.5 8.27614 12.5 8V6H9.5C9.22386 6 9 5.77614 9 5.5V2.5H3.5V8C3.5 8.27614 3.27614 8.5 3 8.5C2.72386 8.5 2.5 8.27614 2.5 8V2.5C2.5 2.23478 2.60536 1.98043 2.79289 1.79289ZM10 5V3.20711L11.7929 5H10Z M11.5 10.25C11.5 9.97386 11.2761 9.75 11 9.75C10.7239 9.75 10.5 9.97386 10.5 10.25V12.5429L9.35355 11.3964C9.15829 11.2012 8.84171 11.2012 8.64645 11.3964C8.45118 11.5917 8.45118 11.9083 8.64645 12.1036L10.6464 14.1036C10.6944 14.1515 10.7496 14.1877 10.8086 14.2121C10.8667 14.2361 10.9303 14.2496 10.997 14.25H11.003C11.13 14.2492 11.2567 14.2004 11.3536 14.1036L13.3536 12.1036C13.5488 11.9083 13.5488 11.5917 13.3536 11.3964C13.1583 11.2012 12.8417 11.2012 12.6464 11.3964L11.5 12.5429V10.25Z M3.38411 10.1799C3.24935 10.0182 3.02772 9.95823 2.82979 10.0299C2.63185 10.1016 2.5 10.2895 2.5 10.5V13.5C2.5 13.7762 2.72386 14 3 14C3.27614 14 3.5 13.7762 3.5 13.5V11.8811L4.36589 12.9201C4.46089 13.0341 4.60161 13.1 4.75 13.1C4.89839 13.1 5.03911 13.0341 5.13411 12.9201L6 11.8811V13.5C6 13.7762 6.22386 14 6.5 14C6.77614 14 7 13.7762 7 13.5V10.5C7 10.2895 6.86815 10.1016 6.67021 10.0299C6.47228 9.95823 6.25065 10.0182 6.11589 10.1799L4.75 11.819L3.38411 10.1799Z', - Fingerprint: - 'M7.9974 3.5C7.8112 3.50097 7.62514 3.51257 7.44027 3.53479C7.37507 3.54262 7.31206 3.56323 7.25482 3.59542C7.19759 3.62761 7.14725 3.67076 7.10669 3.7224C7.06613 3.77404 7.03615 3.83317 7.01844 3.89641C7.00074 3.95964 6.99566 4.02575 7.0035 4.09094C7.01134 4.15614 7.03194 4.21916 7.06413 4.27639C7.09632 4.33363 7.13947 4.38396 7.19111 4.42452C7.24276 4.46508 7.30189 4.49507 7.36512 4.51277C7.42836 4.53048 7.49446 4.53555 7.55966 4.52771C7.70629 4.51008 7.85375 4.50093 8.00143 4.50012C9.93966 4.50092 11.5 6.06158 11.5 8C11.4994 8.64795 11.4589 9.29514 11.3787 9.93811C11.3706 10.0033 11.3754 10.0694 11.3928 10.1327C11.4102 10.196 11.44 10.2553 11.4803 10.3071C11.5206 10.359 11.5708 10.4023 11.6279 10.4348C11.685 10.4672 11.7479 10.4881 11.8131 10.4962C11.8782 10.5044 11.9444 10.4996 12.0077 10.4821C12.071 10.4647 12.1303 10.435 12.1821 10.3947C12.2339 10.3543 12.2773 10.3042 12.3097 10.2471C12.3422 10.19 12.3631 10.1271 12.3712 10.0619C12.4564 9.37805 12.4994 8.6895 12.5 8.00037C12.5 8.00049 12.5 8.00024 12.5 8.00037C12.5 5.52101 10.4793 3.5 7.99996 3.5C7.99911 3.5 7.99826 3.5 7.9974 3.5Z M11.617 11.5154C11.5534 11.4992 11.4872 11.4957 11.4222 11.5051C11.3572 11.5145 11.2947 11.5367 11.2383 11.5702C11.1818 11.6038 11.1326 11.6481 11.0933 11.7007C11.054 11.7534 11.0254 11.8132 11.0092 11.8768C10.9181 12.2353 10.815 12.5927 10.7006 12.9357C10.6799 12.998 10.6716 13.0637 10.6763 13.1292C10.6809 13.1947 10.6984 13.2587 10.7278 13.3174C10.7572 13.3761 10.7978 13.4285 10.8474 13.4715C10.897 13.5145 10.9546 13.5474 11.0169 13.5681C11.0792 13.5889 11.145 13.5972 11.2105 13.5925C11.276 13.5878 11.3399 13.5703 11.3987 13.541C11.4574 13.5116 11.5098 13.4709 11.5528 13.4213C11.5958 13.3717 11.6286 13.3141 11.6494 13.2518C11.7726 12.8823 11.882 12.5022 11.9784 12.1232C12.011 11.9947 11.9913 11.8584 11.9236 11.7444C11.8558 11.6305 11.7455 11.5481 11.617 11.5154Z M7.99996 1.5C4.41604 1.5 1.49996 4.41607 1.49996 8C1.49996 8.00045 1.49996 8.0009 1.49996 8.00134C1.5016 8.62511 1.3952 9.24437 1.18539 9.83179C1.14078 9.95666 1.14761 10.0941 1.20436 10.214C1.26111 10.3338 1.36314 10.4262 1.488 10.4708C1.61287 10.5154 1.75035 10.5086 1.87019 10.4519C1.99003 10.3951 2.08242 10.2931 2.12704 10.1682C2.37555 9.47244 2.50175 8.73881 2.49996 8C2.49996 4.95651 4.95648 2.5 7.99996 2.5C11.0434 2.49994 13.5 4.95643 13.5 7.99988C13.4994 9.1427 13.3894 10.2828 13.1716 11.4047C13.1591 11.4691 13.1594 11.5354 13.1725 11.5998C13.1856 11.6641 13.2112 11.7253 13.248 11.7797C13.2847 11.8341 13.3318 11.8808 13.3866 11.917C13.4414 11.9533 13.5028 11.9783 13.5672 11.9908C13.6974 12.0161 13.8323 11.9886 13.9422 11.9144C14.0521 11.8403 14.128 11.7255 14.1533 11.5953C14.3833 10.4108 14.4994 9.20691 14.5 8.00024C14.5 8.00032 14.5 8.00016 14.5 8.00024C14.5 4.41628 11.5839 1.49993 7.99996 1.5Z M5.90048 9.51025C5.77059 9.48359 5.63543 9.5096 5.52472 9.58258C5.41401 9.65556 5.33683 9.76953 5.31014 9.89941C5.10493 10.8991 4.74198 11.8597 4.23483 12.7452C4.2022 12.8022 4.18111 12.8651 4.17277 12.9302C4.16443 12.9953 4.16899 13.0615 4.18621 13.1248C4.20343 13.1882 4.23296 13.2475 4.27312 13.2995C4.31327 13.3515 4.36327 13.395 4.42025 13.4276C4.47723 13.4602 4.54008 13.4813 4.60521 13.4897C4.67034 13.498 4.73648 13.4934 4.79984 13.4762C4.86321 13.459 4.92256 13.4295 4.97451 13.3893C5.02646 13.3492 5.07 13.2992 5.10263 13.2422C5.6625 12.2646 6.06321 11.2042 6.28976 10.1006C6.30296 10.0363 6.30337 9.96997 6.29095 9.90549C6.27853 9.84101 6.25353 9.77961 6.21739 9.72479C6.18124 9.66998 6.13464 9.62281 6.08027 9.586C6.02589 9.54919 5.9648 9.52345 5.90048 9.51025Z M7.99996 5.5C6.6251 5.49987 5.49984 6.62514 5.49996 8C5.49996 8.13261 5.55264 8.25979 5.64641 8.35355C5.74018 8.44732 5.86736 8.5 5.99996 8.5C6.13257 8.5 6.25975 8.44732 6.35352 8.35355C6.44729 8.25979 6.49996 8.13261 6.49996 8C6.4999 7.16564 7.16561 6.49992 7.99996 6.5C8.83432 6.49994 9.50004 7.16564 9.49996 8C9.49996 7.99968 9.49996 8.00032 9.49996 8C9.50393 9.98748 9.06558 11.9519 8.21664 13.7489C8.16 13.8688 8.15331 14.0063 8.19804 14.1311C8.24278 14.256 8.33527 14.3579 8.45516 14.4145C8.57507 14.4712 8.71255 14.4779 8.83739 14.4331C8.96222 14.3884 9.06417 14.2959 9.12081 14.176C10.033 12.2451 10.5041 10.1353 10.5 7.99976C10.5 6.625 9.37475 5.49987 7.99996 5.5Z M5.55429 4.37793C5.489 4.37079 5.42295 4.37658 5.35991 4.39497C5.29686 4.41336 5.23806 4.44399 5.18685 4.48511C4.12173 5.34051 3.50116 6.63342 3.49996 7.99951C3.49996 7.99996 3.49996 8.00041 3.49996 8.00086C3.50202 9.1572 3.23532 10.2982 2.72091 11.3339C2.66194 11.4526 2.65255 11.5899 2.69481 11.7156C2.73706 11.8413 2.82751 11.945 2.94625 12.004C3.00506 12.0332 3.06905 12.0506 3.13456 12.0551C3.20007 12.0596 3.26583 12.0511 3.32807 12.0302C3.39031 12.0093 3.44783 11.9763 3.49732 11.9331C3.54682 11.89 3.58733 11.8375 3.61654 11.7787C4.19987 10.6043 4.50229 9.31042 4.49996 7.99915V8.00049C4.50089 6.93648 4.98348 5.93114 5.81307 5.26489C5.86428 5.22378 5.90689 5.17298 5.93846 5.1154C5.97003 5.05782 5.98995 4.99458 5.99708 4.9293C6.00421 4.86402 5.99842 4.79797 5.98002 4.73493C5.96162 4.67189 5.93098 4.61309 5.88986 4.56189C5.84874 4.5107 5.79794 4.4681 5.74037 4.43654C5.68279 4.40497 5.61956 4.38506 5.55429 4.37793Z M7.99935 7.5C7.93369 7.50008 7.86869 7.51309 7.80806 7.53829C7.74743 7.5635 7.69235 7.60039 7.64598 7.64688C7.59961 7.69336 7.56284 7.74853 7.53779 7.80922C7.51274 7.86992 7.49988 7.93495 7.49996 8.00061C7.50256 9.94911 7.00786 11.866 6.06271 13.5699C6.03085 13.6274 6.01062 13.6905 6.00316 13.7557C5.99571 13.821 6.00118 13.8871 6.01926 13.9502C6.03734 14.0133 6.06767 14.0723 6.10854 14.1237C6.1494 14.1751 6.19999 14.2179 6.25741 14.2498C6.31483 14.2816 6.37796 14.3018 6.4432 14.3093C6.50844 14.3168 6.57451 14.3113 6.63764 14.2932C6.70077 14.2751 6.75972 14.2448 6.81112 14.2039C6.86252 14.1631 6.90537 14.1125 6.93722 14.0551C7.9649 12.2024 8.50278 10.118 8.49996 7.99939C8.49988 7.93373 8.48687 7.86873 8.46167 7.80809C8.43647 7.74746 8.39957 7.69239 8.35309 7.64601C8.3066 7.59964 8.25144 7.56288 8.19074 7.53783C8.13005 7.51277 8.06501 7.49992 7.99935 7.5Z', - Flag: - 'M5.01282 1.63584C4.11168 1.62098 3.15195 1.88595 2.19995 2.59995C2.1932 2.61045 2.18685 2.6212 2.18091 2.63218C2.13948 2.67922 2.10726 2.73363 2.08594 2.79258C2.05137 2.84521 2.02706 2.9039 2.01428 2.96555C2.0091 2.97685 2.00433 2.98833 2 2.99998V13.5C2 13.6326 2.05268 13.7598 2.14645 13.8535C2.24021 13.9473 2.36739 14 2.5 14C2.63261 14 2.75979 13.9473 2.85355 13.8535C2.94732 13.7598 3 13.6326 3 13.5V10.7621C3.82639 10.2015 4.54885 10.0768 5.29236 10.1537C6.09563 10.2368 6.9281 10.5846 7.8031 10.9596C8.6781 11.3346 9.59563 11.7368 10.6049 11.8412C11.6141 11.9456 12.7121 11.716 13.8 10.9C13.8621 10.8534 13.9125 10.7931 13.9472 10.7236C13.9819 10.6542 14 10.5777 14 10.5001V3.0001C14 2.90724 13.9741 2.81622 13.9253 2.73723C13.8765 2.65824 13.8066 2.59441 13.7236 2.55288C13.6405 2.51135 13.5475 2.49377 13.4551 2.50211C13.3626 2.51044 13.2742 2.54437 13.2 2.60008C12.288 3.28406 11.511 3.42952 10.7078 3.34641C9.90449 3.26331 9.07202 2.91539 8.19702 2.54038C7.32202 2.16538 6.40437 1.76332 5.39514 1.65891C5.26899 1.64586 5.14155 1.63797 5.01282 1.63584ZM4.69226 2.63902C4.89232 2.62637 5.09154 2.63277 5.29236 2.65354C6.09563 2.73664 6.92798 3.08457 7.80298 3.45957C8.67798 3.83458 9.59551 4.23663 10.6047 4.34104C11.3667 4.41988 12.1793 4.30739 13 3.89683V10.238C12.1736 10.7986 11.4512 10.9233 10.7076 10.8464C9.90437 10.7633 9.0719 10.4155 8.1969 10.0405C7.3219 9.66551 6.40437 9.26332 5.39514 9.15891C4.63316 9.08009 3.82067 9.19267 3 9.60325V3.26194C3.60318 2.85281 4.15163 2.67318 4.69226 2.63902Z', - FlagFill: - 'M13.725 2.55C13.6412 2.50997 13.548 2.49348 13.4556 2.50229C13.3631 2.51109 13.2748 2.54487 13.2 2.6C11.4312 3.925 9.93125 3.2875 8.2 2.54375C6.46875 1.8 4.43125 0.924997 2.2 2.6H2.19375L2.16875 2.61875L2.11875 2.66875L2.10625 2.6875L2.0875 2.70625C2.0875 2.7125 2.08125 2.71875 2.075 2.725L2.0625 2.75C2.0625 2.75625 2.05625 2.7625 2.05625 2.76875C2.04979 2.77572 2.04546 2.78439 2.04375 2.79375C2.0375 2.8 2.0375 2.80625 2.03125 2.81875C2.025 2.83125 2.025 2.83125 2.025 2.8375C2.025 2.84375 2.01875 2.85625 2.01875 2.8625C2.01875 2.86875 2.0125 2.875 2.0125 2.88125C2.0125 2.8875 2.00625 2.9 2.00625 2.90625V2.9375C2.00625 2.94375 2 2.95625 2 2.9625V13.5C2 13.6326 2.05268 13.7598 2.14645 13.8535C2.24021 13.9473 2.36739 14 2.5 14C2.63261 14 2.75979 13.9473 2.85355 13.8535C2.94732 13.7598 3 13.6326 3 13.5V10.7562C4.68125 9.625 6.13125 10.2437 7.8 10.9625C8.81875 11.3937 9.9125 11.8625 11.0875 11.8625C11.95 11.8625 12.8563 11.6125 13.8 10.9C13.8617 10.8531 13.9119 10.7926 13.9466 10.7233C13.9812 10.6539 13.9995 10.5775 14 10.5V3C13.9992 2.90711 13.9733 2.81617 13.9248 2.73691C13.8764 2.65765 13.8073 2.59304 13.725 2.55Z', - Form: - 'M4.5 6.5C4.5 6.22386 4.72386 6 5 6H7.5C7.77614 6 8 6.22386 8 6.5C8 6.77614 7.77614 7 7.5 7H5C4.72386 7 4.5 6.77614 4.5 6.5Z M5.5 8C4.67157 8 4 8.67157 4 9.5C4 10.3284 4.67157 11 5.5 11H10.5C11.3284 11 12 10.3284 12 9.5C12 8.67157 11.3284 8 10.5 8H5.5ZM5 9.5C5 9.22386 5.22386 9 5.5 9H10.5C10.7761 9 11 9.22386 11 9.5C11 9.77614 10.7761 10 10.5 10H5.5C5.22386 10 5 9.77614 5 9.5Z M1 3.5C1 2.67157 1.67157 2 2.5 2H13.5C14.3284 2 15 2.67157 15 3.5V12.5C15 13.3284 14.3284 14 13.5 14H2.5C1.67157 14 1 13.3284 1 12.5V3.5ZM2.5 3C2.22386 3 2 3.22386 2 3.5V12.5C2 12.7761 2.22386 13 2.5 13H13.5C13.7761 13 14 12.7761 14 12.5V3.5C14 3.22386 13.7761 3 13.5 3H2.5Z', - Formula: - 'M9.66763 2H11.5C11.7761 2 12 2.22386 12 2.5C12 2.77614 11.7761 3 11.5 3H9.66935C9.31784 3.00091 8.97771 3.12472 8.70787 3.35002C8.43803 3.57531 8.25549 3.88789 8.19186 4.23359L7.59893 7.5H10.5C10.7761 7.5 11 7.72386 11 8C11 8.27614 10.7761 8.5 10.5 8.5H7.41741L6.79196 11.9456L6.79177 11.9466C6.68604 12.5221 6.38224 13.0426 5.93303 13.4176C5.48383 13.7927 4.91755 13.9987 4.33237 14H2.5C2.22386 14 2 13.7761 2 13.5C2 13.2239 2.22386 13 2.5 13H4.33013C4.68169 12.9991 5.02226 12.8753 5.29213 12.65C5.56198 12.4247 5.74452 12.1121 5.80814 11.7664L6.40107 8.5H3.5C3.22386 8.5 3 8.27614 3 8C3 7.72386 3.22386 7.5 3.5 7.5H6.58259L7.20823 4.05341C7.31396 3.47785 7.61776 2.95744 8.06697 2.58239C8.51617 2.20735 9.08245 2.00131 9.66763 2Z M13.3536 10.8536C13.5488 10.6583 13.5488 10.3417 13.3536 10.1464C13.1583 9.95118 12.8417 9.95118 12.6464 10.1464L11.5 11.2929L10.3536 10.1464C10.1583 9.95118 9.84171 9.95118 9.64645 10.1464C9.45118 10.3417 9.45118 10.6583 9.64645 10.8536L10.7929 12L9.64645 13.1464C9.45118 13.3417 9.45118 13.6583 9.64645 13.8536C9.84171 14.0488 10.1583 14.0488 10.3536 13.8536L11.5 12.7071L12.6464 13.8536C12.8417 14.0488 13.1583 14.0488 13.3536 13.8536C13.5488 13.6583 13.5488 13.3417 13.3536 13.1464L12.2071 12L13.3536 10.8536Z', - FullscreenElementFeature: - 'M9.5 5C9.5 4.72386 9.72386 4.5 10 4.5H12C12.2761 4.5 12.5 4.72386 12.5 5V7C12.5 7.27614 12.2761 7.5 12 7.5C11.7239 7.5 11.5 7.27614 11.5 7V5.5H10C9.72386 5.5 9.5 5.27614 9.5 5Z M4.5 9C4.5 8.72386 4.27614 8.5 4 8.5C3.72386 8.5 3.5 8.72386 3.5 9V11C3.5 11.2761 3.72386 11.5 4 11.5H6C6.27614 11.5 6.5 11.2761 6.5 11C6.5 10.7239 6.27614 10.5 6 10.5H4.5V9Z M2.5 2C1.67157 2 1 2.67157 1 3.5V12.5C1 13.3284 1.67157 14 2.5 14H13.5C14.3284 14 15 13.3284 15 12.5V3.5C15 2.67157 14.3284 2 13.5 2H2.5ZM2 3.5C2 3.22386 2.22386 3 2.5 3H13.5C13.7761 3 14 3.22386 14 3.5V12.5C14 12.7761 13.7761 13 13.5 13H2.5C2.22386 13 2 12.7761 2 12.5V3.5Z', - FunnelSimple: - 'M6.5 10.5C6.36739 10.5 6.24021 10.5527 6.14645 10.6464C6.05268 10.7402 6 10.8674 6 11C6 11.1326 6.05268 11.2598 6.14645 11.3536C6.24021 11.4473 6.36739 11.5 6.5 11.5H9.5C9.63261 11.5 9.75979 11.4473 9.85355 11.3536C9.94732 11.2598 10 11.1326 10 11C10 10.8674 9.94732 10.7402 9.85355 10.6464C9.75979 10.5527 9.63261 10.5 9.5 10.5H6.5Z M1.5 4.5C1.36739 4.5 1.24021 4.55268 1.14645 4.64645C1.05268 4.74021 1 4.86739 1 5C1 5.13261 1.05268 5.25979 1.14645 5.35355C1.24021 5.44732 1.36739 5.5 1.5 5.5H14.5C14.6326 5.5 14.7598 5.44732 14.8536 5.35355C14.9473 5.25979 15 5.13261 15 5C15 4.86739 14.9473 4.74021 14.8536 4.64645C14.7598 4.55268 14.6326 4.5 14.5 4.5H1.5Z M4 7.5C3.86739 7.5 3.74021 7.55268 3.64645 7.64645C3.55268 7.74021 3.5 7.86739 3.5 8C3.5 8.13261 3.55268 8.25979 3.64645 8.35355C3.74021 8.44732 3.86739 8.5 4 8.5H12C12.1326 8.5 12.2598 8.44732 12.3536 8.35355C12.4473 8.25979 12.5 8.13261 12.5 8C12.5 7.86739 12.4473 7.74021 12.3536 7.64645C12.2598 7.55268 12.1326 7.5 12 7.5H4Z', - Gallery: - 'M1.5 3.5C1.5 2.67157 2.17157 2 3 2H6C6.82843 2 7.5 2.67157 7.5 3.5V6C7.5 6.82843 6.82843 7.5 6 7.5H3C2.17157 7.5 1.5 6.82843 1.5 6V3.5ZM3 3C2.72386 3 2.5 3.22386 2.5 3.5V6C2.5 6.27614 2.72386 6.5 3 6.5H6C6.27614 6.5 6.5 6.27614 6.5 6V3.5C6.5 3.22386 6.27614 3 6 3H3Z M8.5 3.5C8.5 2.67157 9.17157 2 10 2H13C13.8284 2 14.5 2.67157 14.5 3.5V6C14.5 6.82843 13.8284 7.5 13 7.5H10C9.17157 7.5 8.5 6.82843 8.5 6V3.5ZM10 3C9.72386 3 9.5 3.22386 9.5 3.5V6C9.5 6.27614 9.72386 6.5 10 6.5H13C13.2761 6.5 13.5 6.27614 13.5 6V3.5C13.5 3.22386 13.2761 3 13 3H10Z M1.5 10C1.5 9.17157 2.17157 8.5 3 8.5H6C6.82843 8.5 7.5 9.17157 7.5 10V12.5C7.5 13.3284 6.82843 14 6 14H3C2.17157 14 1.5 13.3284 1.5 12.5V10ZM3 9.5C2.72386 9.5 2.5 9.72386 2.5 10V12.5C2.5 12.7761 2.72386 13 3 13H6C6.27614 13 6.5 12.7761 6.5 12.5V10C6.5 9.72386 6.27614 9.5 6 9.5H3Z M8.5 10C8.5 9.17157 9.17157 8.5 10 8.5H13C13.8284 8.5 14.5 9.17157 14.5 10V12.5C14.5 13.3284 13.8284 14 13 14H10C9.17157 14 8.5 13.3284 8.5 12.5V10ZM10 9.5C9.72386 9.5 9.5 9.72386 9.5 10V12.5C9.5 12.7761 9.72386 13 10 13H13C13.2761 13 13.5 12.7761 13.5 12.5V10C13.5 9.72386 13.2761 9.5 13 9.5H10Z', - Gantt: - 'M0 3.5C0 2.67157 0.671573 2 1.5 2H11.5C12.3284 2 13 2.67157 13 3.5V5.5C13 6.32843 12.3284 7 11.5 7H4.5V10C4.5 10.5523 4.94771 11 5.5 11H7.5V10.5C7.5 9.67157 8.17157 9 9 9H14.5C15.3284 9 16 9.67157 16 10.5V12.5C16 13.3284 15.3284 14 14.5 14H9C8.17157 14 7.5 13.3284 7.5 12.5V12H5.5C4.39543 12 3.5 11.1046 3.5 10V7H1.5C0.671573 7 0 6.32843 0 5.5V3.5ZM8.5 12.5C8.5 12.7761 8.72386 13 9 13H14.5C14.7761 13 15 12.7761 15 12.5V10.5C15 10.2239 14.7761 10 14.5 10H9C8.72386 10 8.5 10.2239 8.5 10.5V12.5ZM1.5 3C1.22386 3 1 3.22386 1 3.5V5.5C1 5.77614 1.22386 6 1.5 6H11.5C11.7761 6 12 5.77614 12 5.5V3.5C12 3.22386 11.7761 3 11.5 3H1.5Z', - Gift: - 'M9.76843 1.23768C9.25663 1.23626 8.7442 1.42807 8.35461 1.81336C8.35347 1.81454 8.35233 1.81572 8.3512 1.8169C8.20492 1.96447 8.09356 2.13274 8 2.30982C7.90644 2.13274 7.79508 1.96447 7.6488 1.8169C7.64707 1.8151 7.64532 1.81331 7.64355 1.81153C6.86321 1.04766 5.59869 1.05452 4.82654 1.82667C4.05439 2.59882 4.04754 3.86334 4.8114 4.64368C4.81318 4.64545 4.81497 4.6472 4.81677 4.64893C5.34901 5.1765 6.12263 5.31295 6.78088 5.40687C7.43913 5.50078 8 5.50013 8 5.50013C8 5.50013 8.56087 5.50079 9.21912 5.40687C9.87737 5.31295 10.651 5.1765 11.1832 4.64893C11.1844 4.6478 11.1856 4.64666 11.1868 4.64552C11.9574 3.86634 11.9538 2.59607 11.179 1.82117C10.7915 1.43373 10.2802 1.2391 9.76843 1.23768ZM9.76562 2.23158C10.0203 2.23228 10.2747 2.33126 10.4718 2.52833C10.8651 2.92164 10.8673 3.54316 10.4778 3.93958C10.2963 4.11809 9.65604 4.33437 9.07776 4.41688C8.86823 4.44677 8.6994 4.45709 8.53076 4.46937C8.54304 4.30073 8.55336 4.1319 8.58325 3.92237C8.66576 3.34409 8.88204 2.70384 9.06055 2.52235C9.25835 2.32801 9.51199 2.23087 9.76562 2.23158ZM6.23718 2.23707C6.48944 2.23572 6.74197 2.33114 6.93982 2.52284C7.11827 2.70477 7.33429 3.34443 7.41675 3.92237C7.44664 4.1319 7.45696 4.30073 7.46924 4.46937C7.3006 4.45709 7.13177 4.44677 6.92224 4.41688C6.3443 4.33442 5.70464 4.1184 5.52271 3.93995C5.13806 3.54295 5.14218 2.92509 5.53357 2.5337C5.72991 2.33736 5.98327 2.23842 6.23718 2.23707Z M2.5 4.50001C1.95364 4.50001 1.5 4.95365 1.5 5.50001V7.50001C1.5 8.04637 1.95364 8.50001 2.5 8.50001V12.5C2.50007 13.0463 2.95357 13.4999 3.49988 13.5C3.49984 13.5 3.49992 13.5 3.49988 13.5H12.5C13.0464 13.5 13.5 13.0464 13.5 12.5V8.50001C14.0464 8.50001 14.5 8.04637 14.5 7.50001V5.50001C14.5 4.95365 14.0464 4.50001 13.5 4.50001H2.5ZM2.5 5.50001H7.5V7.50001H2.5V5.50001ZM8.5 5.50001H13.5V7.50001H8.5V5.50001ZM3.5 8.50001H7.5V12.5H3.50012L3.5 8.50001ZM8.5 8.50001H12.5V12.5H8.5V8.50001Z', - GitFork: - 'M11.75 14C12.9867 14 14 12.9867 14 11.75C14 10.6856 13.2477 9.79069 12.25 9.56091V9.50208C12.2546 8.40054 11.3495 7.49543 10.2479 7.5H8.5V6.43909C9.49768 6.20931 10.25 5.31445 10.25 4.25C10.25 3.01328 9.23672 2 8 2C6.76328 2 5.75 3.01328 5.75 4.25C5.75 5.31445 6.50232 6.20931 7.5 6.43909V7.5H5.75195C5.22056 7.49787 4.7098 7.70817 4.33398 8.08386C3.95813 8.45966 3.74774 8.97058 3.75 9.50207V9.56091C2.75232 9.79069 2 10.6855 2 11.75C2 12.9867 3.01328 14 4.25 14C5.48672 14 6.5 12.9867 6.5 11.75C6.5 10.6855 5.74768 9.79069 4.75 9.56091V9.49792C4.74887 9.23275 4.85349 8.97863 5.04102 8.79114C5.2286 8.60361 5.4828 8.49893 5.74805 8.5H10.2521C10.8102 8.49768 11.2523 8.93976 11.25 9.49792V9.56091C10.2523 9.79069 9.5 10.6856 9.5 11.75C9.5 12.9867 10.5133 14 11.75 14ZM11.75 13C11.0537 13 10.5 12.4463 10.5 11.75C10.5 11.0537 11.0537 10.5 11.75 10.5C12.4463 10.5 13 11.0537 13 11.75C13 12.4463 12.4463 13 11.75 13ZM4.25 13C3.55372 13 3 12.4463 3 11.75C3 11.0537 3.55372 10.5 4.25 10.5C4.94628 10.5 5.5 11.0537 5.5 11.75C5.5 12.4463 4.94628 13 4.25 13ZM8 5.5C7.30372 5.5 6.75 4.94628 6.75 4.25C6.75 3.55372 7.30372 3 8 3C8.69628 3 9.25 3.55372 9.25 4.25C9.25 4.94628 8.69628 5.5 8 5.5Z', - Globe: - 'M8 2.5C11.0435 2.5 13.5 4.95647 13.5 8C13.5 11.0435 11.0435 13.5 8 13.5C4.95647 13.5 2.5 11.0435 2.5 8C2.5 4.95647 4.95647 2.5 8 2.5ZM8 2.6626C8.18743 2.6626 8.38741 2.74565 8.6217 2.97705C8.856 3.20845 9.10002 3.58333 9.30811 4.06921C9.72428 5.04098 10 6.44599 10 8.00012C10 9.55422 9.72429 10.9593 9.30811 11.931C9.10001 12.4169 8.856 12.7918 8.6217 13.0232C8.3874 13.2546 8.18743 13.3376 8 13.3376C7.81257 13.3376 7.61259 13.2546 7.3783 13.0232C7.144 12.7918 6.89999 12.4169 6.69189 11.931C6.27571 10.9593 6 9.55422 6 8.00012C6 6.44599 6.27572 5.04098 6.69189 4.06921C6.89998 3.58333 7.144 3.20845 7.3783 2.97705C7.61259 2.74565 7.81257 2.6626 8 2.6626Z M8 1.5C4.41604 1.5 1.5 4.41604 1.5 8C1.5 11.5839 4.41603 14.5 8 14.5C11.5839 14.5 14.5 11.5839 14.5 8C14.5 4.41603 11.5839 1.5 8 1.5ZM8 1.6626C7.49709 1.6626 7.03943 1.90624 6.67554 2.26562C6.31165 2.62501 6.01691 3.10505 5.77258 3.67554C5.28394 4.81652 5 6.33026 5 8.00012C5 9.66995 5.28394 11.1837 5.77258 12.3247C6.01691 12.8952 6.31165 13.3752 6.67554 13.7346C7.03943 14.094 7.49708 14.3376 8 14.3376C8.50292 14.3376 8.96057 14.094 9.32446 13.7346C9.68835 13.3752 9.98309 12.8952 10.2274 12.3247C10.7161 11.1837 11 9.66995 11 8.00012C11 6.33026 10.7161 4.81652 10.2274 3.67554C9.98309 3.10505 9.68835 2.62501 9.32446 2.26562C8.96057 1.90624 8.50291 1.6626 8 1.6626ZM2.34375 5.5C2.21114 5.5 2.08396 5.55268 1.9902 5.64645C1.89643 5.74021 1.84375 5.86739 1.84375 6C1.84375 6.13261 1.89643 6.25979 1.9902 6.35355C2.08396 6.44732 2.21114 6.5 2.34375 6.5H13.6562C13.7889 6.5 13.916 6.44732 14.0098 6.35355C14.1036 6.25979 14.1562 6.13261 14.1562 6C14.1562 5.86739 14.1036 5.74021 14.0098 5.64645C13.916 5.55268 13.7889 5.5 13.6562 5.5H2.34375ZM2.34375 9.5C2.21114 9.5 2.08396 9.55268 1.9902 9.64645C1.89643 9.74021 1.84375 9.86739 1.84375 10C1.84375 10.1326 1.89643 10.2598 1.9902 10.3536C2.08396 10.4473 2.21114 10.5 2.34375 10.5H13.6562C13.7889 10.5 13.916 10.4473 14.0098 10.3536C14.1036 10.2598 14.1562 10.1326 14.1562 10C14.1562 9.86739 14.1036 9.74021 14.0098 9.64645C13.916 9.55268 13.7889 9.5 13.6562 9.5H2.34375Z', - Grid: - 'M2.5 2C1.67157 2 1 2.67157 1 3.5V12.5C1 13.3284 1.67157 14 2.5 14H13.5C14.3284 14 15 13.3284 15 12.5V3.5C15 2.67157 14.3284 2 13.5 2H2.5ZM2 3.5C2 3.22386 2.22386 3 2.5 3H13.5C13.7761 3 14 3.22386 14 3.5V5H2V3.5ZM8.5 6H14V9H8.5V6ZM7.5 9V6H2V9H7.5ZM2 10V12.5C2 12.7761 2.22386 13 2.5 13H7.5V10H2ZM8.5 10H14V12.5C14 12.7761 13.7761 13 13.5 13H8.5V10Z', - GridFour: - 'M3.25 2.25C2.70364 2.25 2.25 2.70364 2.25 3.25V12.75C2.25 13.2964 2.70364 13.75 3.25 13.75H12.75C13.2964 13.75 13.75 13.2964 13.75 12.75V3.25C13.75 2.70364 13.2964 2.25 12.75 2.25H3.25ZM3.25 3.25H7.5V7.5H3.25V3.25ZM8.5 3.25H12.75V7.5H8.5V3.25ZM3.25 8.5H7.5V12.75H3.25V8.5ZM8.5 8.5H12.75V12.75H8.5V8.5Z', - GridLayout: - 'M1.5 3.5C1.5 2.67157 2.17157 2 3 2H6C6.82843 2 7.5 2.67157 7.5 3.5V6C7.5 6.82843 6.82843 7.5 6 7.5H3C2.17157 7.5 1.5 6.82843 1.5 6V3.5ZM3 3C2.72386 3 2.5 3.22386 2.5 3.5V6C2.5 6.27614 2.72386 6.5 3 6.5H6C6.27614 6.5 6.5 6.27614 6.5 6V3.5C6.5 3.22386 6.27614 3 6 3H3Z M8.5 3.5C8.5 2.67157 9.17157 2 10 2H13C13.8284 2 14.5 2.67157 14.5 3.5V6C14.5 6.82843 13.8284 7.5 13 7.5H10C9.17157 7.5 8.5 6.82843 8.5 6V3.5ZM10 3C9.72386 3 9.5 3.22386 9.5 3.5V6C9.5 6.27614 9.72386 6.5 10 6.5H13C13.2761 6.5 13.5 6.27614 13.5 6V3.5C13.5 3.22386 13.2761 3 13 3H10Z M1.5 10C1.5 9.17157 2.17157 8.5 3 8.5H6C6.82843 8.5 7.5 9.17157 7.5 10V12.5C7.5 13.3284 6.82843 14 6 14H3C2.17157 14 1.5 13.3284 1.5 12.5V10ZM3 9.5C2.72386 9.5 2.5 9.72386 2.5 10V12.5C2.5 12.7761 2.72386 13 3 13H6C6.27614 13 6.5 12.7761 6.5 12.5V10C6.5 9.72386 6.27614 9.5 6 9.5H3Z M8.5 10C8.5 9.17157 9.17157 8.5 10 8.5H13C13.8284 8.5 14.5 9.17157 14.5 10V12.5C14.5 13.3284 13.8284 14 13 14H10C9.17157 14 8.5 13.3284 8.5 12.5V10ZM10 9.5C9.72386 9.5 9.5 9.72386 9.5 10V12.5C9.5 12.7761 9.72386 13 10 13H13C13.2761 13 13.5 12.7761 13.5 12.5V10C13.5 9.72386 13.2761 9.5 13 9.5H10Z', - Group: - 'M6 6.5C6 6.91421 5.66421 7.25 5.25 7.25C4.83579 7.25 4.5 6.91421 4.5 6.5C4.5 6.08579 4.83579 5.75 5.25 5.75C5.66421 5.75 6 6.08579 6 6.5Z M7 6.5C7 6.22386 7.22386 6 7.5 6H11C11.2761 6 11.5 6.22386 11.5 6.5C11.5 6.77614 11.2761 7 11 7H7.5C7.22386 7 7 6.77614 7 6.5Z M7.5 9C7.22386 9 7 9.22386 7 9.5C7 9.77614 7.22386 10 7.5 10H11C11.2761 10 11.5 9.77614 11.5 9.5C11.5 9.22386 11.2761 9 11 9H7.5Z M6 9.5C6 9.91421 5.66421 10.25 5.25 10.25C4.83579 10.25 4.5 9.91421 4.5 9.5C4.5 9.08579 4.83579 8.75 5.25 8.75C5.66421 8.75 6 9.08579 6 9.5Z M2.54545 2.5C2.0573 2.5 1.5 2.84588 1.5 3.45455V12.5455C1.5 13.1541 2.0573 13.5 2.54545 13.5H13.4545C13.9427 13.5 14.5 13.1541 14.5 12.5455V3.45455C14.5 2.84588 13.9427 2.5 13.4545 2.5H2.54545ZM2.5 12.4929V3.50706C2.51085 3.50329 2.52597 3.5 2.54545 3.5H13.4545C13.474 3.5 13.4891 3.50329 13.5 3.50706V12.4929C13.4891 12.4967 13.474 12.5 13.4545 12.5H2.54545C2.52597 12.5 2.51085 12.4967 2.5 12.4929Z', - HashStraight: - 'M6 2C5.86739 2 5.74021 2.05268 5.64645 2.14645C5.55268 2.24021 5.5 2.36739 5.5 2.5V5.5H2.5C2.36739 5.5 2.24021 5.55268 2.14645 5.64645C2.05268 5.74021 2 5.86739 2 6C2 6.13261 2.05268 6.25979 2.14645 6.35355C2.24021 6.44732 2.36739 6.5 2.5 6.5H5.5V9.5H2.5C2.36739 9.5 2.24021 9.55268 2.14645 9.64645C2.05268 9.74021 2 9.86739 2 10C2 10.1326 2.05268 10.2598 2.14645 10.3536C2.24021 10.4473 2.36739 10.5 2.5 10.5H5.5V13.5C5.5 13.6326 5.55268 13.7598 5.64645 13.8536C5.74021 13.9473 5.86739 14 6 14C6.13261 14 6.25979 13.9473 6.35355 13.8536C6.44732 13.7598 6.5 13.6326 6.5 13.5V10.5H9.5V13.5C9.5 13.6326 9.55268 13.7598 9.64645 13.8536C9.74021 13.9473 9.86739 14 10 14C10.1326 14 10.2598 13.9473 10.3536 13.8536C10.4473 13.7598 10.5 13.6326 10.5 13.5V10.5H13.5C13.6326 10.5 13.7598 10.4473 13.8536 10.3536C13.9473 10.2598 14 10.1326 14 10C14 9.86739 13.9473 9.74021 13.8536 9.64645C13.7598 9.55268 13.6326 9.5 13.5 9.5H10.5V6.5H13.5C13.6326 6.5 13.7598 6.44732 13.8536 6.35355C13.9473 6.25979 14 6.13261 14 6C14 5.86739 13.9473 5.74021 13.8536 5.64645C13.7598 5.55268 13.6326 5.5 13.5 5.5H10.5V2.5C10.5 2.36739 10.4473 2.24021 10.3536 2.14645C10.2598 2.05268 10.1326 2 10 2C9.86739 2 9.74021 2.05268 9.64645 2.14645C9.55268 2.24021 9.5 2.36739 9.5 2.5V5.5H6.5V2.5C6.5 2.36739 6.44732 2.24021 6.35355 2.14645C6.25979 2.05268 6.13261 2 6 2ZM6.5 6.5H9.5V9.5H6.5V6.5Z', - Heart: - 'M4.91919 2.00084C4.70224 2.00548 4.48326 2.02911 4.26453 2.07286C2.51469 2.42283 1.25 3.96549 1.25 5.74998C1.25 8.12008 2.93149 10.1423 4.54053 11.5904C6.14957 13.0386 7.75574 13.9363 7.75574 13.9363C7.83037 13.978 7.91447 14 8 14C8.08553 14 8.16963 13.978 8.24426 13.9363C8.24426 13.9363 9.85043 13.0386 11.4595 11.5904C13.0685 10.1423 14.75 8.12008 14.75 5.74998C14.75 3.96547 13.4853 2.42278 11.7355 2.07286C11.7356 2.07286 11.7354 2.07286 11.7355 2.07286C10.2911 1.78407 8.84887 2.3816 8 3.52232C7.27969 2.55429 6.13496 1.97485 4.91919 2.00084ZM4.94214 3.00011C6.05795 2.97493 7.09626 3.63099 7.53845 4.69225C7.57642 4.78336 7.64051 4.86119 7.72263 4.91595C7.80476 4.9707 7.90125 4.99993 7.99996 4.99994C8.09866 4.99996 8.19517 4.97076 8.27731 4.91602C8.35945 4.86129 8.42355 4.78347 8.46155 4.69237C8.96698 3.47955 10.2509 2.79585 11.5393 3.05345C12.8277 3.31109 13.75 4.43605 13.75 5.74998C13.75 7.62988 12.3065 9.48267 10.7905 10.847C9.62928 11.8922 8.48065 12.6279 8 12.9192C7.51935 12.6279 6.37072 11.8922 5.20947 10.847C3.69352 9.48267 2.25 7.62988 2.25 5.74998C2.25 4.43605 3.17228 3.31101 4.46069 3.05333C4.62175 3.02112 4.78274 3.0037 4.94214 3.00011Z', - HeartFill: - 'M11 2C10.4178 2 9.84366 2.13554 9.32295 2.3959C8.80224 2.65625 8.3493 3.03426 8 3.5C7.52776 2.87035 6.86939 2.40525 6.11813 2.17057C5.36687 1.9359 4.56082 1.94355 3.81415 2.19244C3.06748 2.44133 2.41804 2.91884 1.95784 3.55734C1.49764 4.19583 1.25 4.96294 1.25 5.75C1.25 10.2438 7.49375 13.7875 7.75625 13.9375C7.83063 13.9793 7.91468 14.0009 8 14C8.08539 14.0015 8.1696 13.9799 8.24375 13.9375C9.39356 13.2653 10.472 12.4779 11.4625 11.5875C13.6437 9.625 14.75 7.6625 14.75 5.75C14.75 4.75544 14.3549 3.80161 13.6517 3.09835C12.9484 2.39509 11.9946 2 11 2Z', - HeroImageElementFeature: - 'M5.25 8C6.2165 8 7 7.2165 7 6.25C7 5.2835 6.2165 4.5 5.25 4.5C4.2835 4.5 3.5 5.2835 3.5 6.25C3.5 7.2165 4.2835 8 5.25 8ZM5.25 7C5.66421 7 6 6.66421 6 6.25C6 5.83579 5.66421 5.5 5.25 5.5C4.83579 5.5 4.5 5.83579 4.5 6.25C4.5 6.66421 4.83579 7 5.25 7Z M2.5 2C1.67157 2 1 2.67157 1 3.5V12.5C1 13.3284 1.67157 14 2.5 14H13.5C14.3284 14 15 13.3284 15 12.5V3.5C15 2.67157 14.3284 2 13.5 2H2.5ZM14 9.89817V3.5C14 3.22386 13.7761 3 13.5 3H2.5C2.22386 3 2 3.22386 2 3.5V12.5C2 12.7761 2.22386 13 2.5 13H4.2567L7.00073 9.50076L7.00821 9.49145C7.09634 9.38443 7.20753 9.3057 7.3255 9.25434C7.44353 9.20296 7.57045 9.17789 7.69603 9.17789C7.82161 9.17789 7.94853 9.20296 8.06656 9.25434C8.18452 9.3057 8.29571 9.38443 8.38385 9.49145L8.39118 9.50056L9.16099 10.4812L10.9182 8.32078C11.0036 8.21219 11.113 8.13209 11.2291 8.07961C11.3477 8.02605 11.4758 8 11.6023 8C11.7288 8 11.8568 8.02605 11.9754 8.07961C12.0792 8.12647 12.1774 8.19535 12.2581 8.28685L14 9.89817ZM5.5275 13H13.5C13.7761 13 14 12.7761 14 12.5V11.2604L11.6125 9.05193L9.84174 11.2291L9.83986 11.2314C9.75121 11.3391 9.63908 11.4178 9.52037 11.4685C9.40159 11.5193 9.27395 11.5433 9.14792 11.5418C9.02191 11.5402 8.89467 11.5131 8.7769 11.459C8.65992 11.4053 8.55018 11.3238 8.46473 11.2138L7.69608 10.2346L5.5275 13Z', - House: - 'M8 1.67725C7.75721 1.67725 7.51436 1.76397 7.32495 1.93726L2.32617 6.47986C2.32379 6.48203 2.32143 6.48423 2.31909 6.48645C2.11974 6.67482 2.00463 6.93622 2.00012 7.21045C2.00006 7.21318 2.00002 7.2159 2 7.21863V12.9999C2.00007 13.5462 2.45357 13.9998 2.99988 13.9999C2.99984 13.9999 2.99992 13.9999 2.99988 13.9999H6C6.54636 13.9999 7 13.5462 7 12.9999V9.99988H9V12.9999C9 13.5462 9.45364 13.9999 10 13.9999H13C13.5464 13.9999 14 13.5462 14 12.9999V7.21863C14 7.21594 13.9999 7.21326 13.9999 7.21057C13.9954 6.93627 13.8802 6.67471 13.6808 6.48633C13.6785 6.48415 13.6762 6.48199 13.6738 6.47986L8.67505 1.93726C8.48564 1.76397 8.24279 1.67725 8 1.67725ZM8 2.67505C8.00041 2.67542 8.00081 2.67578 8.00122 2.67615L12.9941 7.21338C12.9979 7.21694 12.9998 7.22129 13 7.22644V12.9999H10V9.99988C10 9.45352 9.54636 8.99988 9 8.99988H7C6.45363 8.99988 6 9.45352 6 9.99988V12.9999H3.00012L3 7.22656C3.00015 7.22145 3.00206 7.21707 3.00573 7.2135L7.99878 2.67615C7.99919 2.67578 7.99959 2.67542 8 2.67505Z', - ImageSquare: - 'M6.25 6.5C6.66419 6.5 7 6.16419 7 5.75C7 5.33581 6.66419 5 6.25 5C5.83581 5 5.5 5.33581 5.5 5.75C5.5 6.16419 5.83581 6.5 6.25 6.5Z M10.4038 6.71204C10.3055 6.72136 10.2078 6.74527 10.1139 6.78455C10.114 6.78451 10.1138 6.78459 10.1139 6.78455C9.99166 6.83577 9.88049 6.91092 9.78747 7.00537L7.00268 9.79016C7.00182 9.79102 7.00097 9.79187 7.00012 9.79273C6.99987 9.79292 6.99945 9.7934 7.00012 9.79273C6.99923 9.79183 6.9982 9.79106 6.9973 9.79016L5.71239 8.50525C5.325 8.11218 4.67498 8.11218 4.28759 8.50525L2.14648 10.6464C2.10005 10.6928 2.06321 10.7479 2.03808 10.8086C2.01295 10.8692 2.00001 10.9343 2.00001 10.9999C2.00001 11.0656 2.01295 11.1306 2.03808 11.1913C2.06321 11.252 2.10005 11.3071 2.14648 11.3535C2.24025 11.4473 2.36741 11.4999 2.5 11.4999C2.63259 11.4999 2.75975 11.4473 2.85352 11.3535L4.99731 9.20972C4.99817 9.20887 4.99903 9.20801 4.99988 9.20715C4.99546 9.20267 5.0043 9.20267 4.99988 9.20715C5.00073 9.20801 5.00183 9.20887 5.00269 9.20972L6.28748 10.4945C6.3805 10.589 6.49137 10.664 6.61365 10.7152C6.61356 10.7152 6.61373 10.7153 6.61365 10.7152C6.98915 10.8724 7.42673 10.7846 7.71241 10.4946L10.4973 7.70972C10.4982 7.70883 10.499 7.70793 10.4999 7.70703C10.4988 7.7059 10.5013 7.70641 10.4999 7.70703C10.5008 7.70788 10.5018 7.70887 10.5027 7.70972L13.1465 10.3535C13.2403 10.4473 13.3674 10.4999 13.5 10.4999C13.6326 10.4999 13.7598 10.4473 13.8535 10.3535C13.9 10.3071 13.9368 10.252 13.9619 10.1913C13.9871 10.1306 14 10.0656 14 9.99994C14 9.93428 13.9871 9.86925 13.9619 9.80859C13.9368 9.74792 13.9 9.6928 13.8535 9.64637L11.2124 7.00525C10.9982 6.78778 10.6986 6.68406 10.4038 6.71204Z M3 2C2.45364 2 2 2.45364 2 3V13C2 13.5464 2.45364 14 3 14H13C13.5464 14 14 13.5464 14 13V3C14 2.45364 13.5464 2 13 2H3ZM3 3H13V13H3V3Z', - Info: - 'M7.875 6C8.28919 6 8.625 5.66419 8.625 5.25C8.625 4.83581 8.28919 4.5 7.875 4.5C7.46081 4.5 7.125 4.83581 7.125 5.25C7.125 5.66419 7.46081 6 7.875 6Z M7.5 7C7.36739 7 7.24021 7.05268 7.14645 7.14645C7.05268 7.24021 7 7.36739 7 7.5C7 7.63261 7.05268 7.75979 7.14645 7.85355C7.24021 7.94732 7.36739 8 7.5 8V11C7.50001 11.1326 7.5527 11.2598 7.64646 11.3535C7.74023 11.4473 7.8674 11.5 8 11.5H8.5C8.63261 11.5 8.75979 11.4473 8.85355 11.3536C8.94732 11.2598 9 11.1326 9 11C9 10.8674 8.94732 10.7402 8.85355 10.6464C8.75979 10.5527 8.63261 10.5 8.5 10.5V7.5C8.49999 7.3674 8.4473 7.24023 8.35354 7.14646C8.25977 7.0527 8.1326 7.00001 8 7H7.5Z M8 1.5C4.41604 1.5 1.5 4.41604 1.5 8C1.5 11.5839 4.41603 14.5 8 14.5C11.5839 14.5 14.5 11.5839 14.5 8C14.5 4.41603 11.5839 1.5 8 1.5ZM8 2.5C11.0435 2.5 13.5 4.95647 13.5 8C13.5 11.0435 11.0435 13.5 8 13.5C4.95647 13.5 2.5 11.0435 2.5 8C2.5 4.95647 4.95647 2.5 8 2.5Z', - Kanban: - 'M1 3.5C1 2.67157 1.67157 2 2.5 2H13.5C14.3284 2 15 2.67157 15 3.5V9.5C15 10.3284 14.3284 11 13.5 11H11.5C11.3247 11 11.1564 10.9699 11 10.9146V12.5C11 13.3284 10.3284 14 9.5 14H6.5C5.67157 14 5 13.3284 5 12.5V7.91465C4.84361 7.96992 4.67532 8 4.5 8H2.5C1.67157 8 1 7.32843 1 6.5V3.5ZM6 12.5C6 12.7761 6.22386 13 6.5 13H9.5C9.77614 13 10 12.7761 10 12.5V3H6V12.5ZM5 3H2.5C2.22386 3 2 3.22386 2 3.5V6.5C2 6.77614 2.22386 7 2.5 7H4.5C4.77614 7 5 6.77614 5 6.5V3ZM11 3V9.5C11 9.77614 11.2239 10 11.5 10H13.5C13.7761 10 14 9.77614 14 9.5V3.5C14 3.22386 13.7761 3 13.5 3H11Z', - Key: - 'M11.25 5.5C11.6642 5.5 12 5.16419 12 4.75C12 4.33582 11.6642 4 11.25 4C10.8358 4 10.5 4.33582 10.5 4.75C10.5 5.16419 10.8358 5.5 11.25 5.5Z M10.2544 1.00684C9.52711 0.969468 8.78669 1.09049 8.08655 1.3805C6.21972 2.15375 5.00027 3.97865 5 5.99927C4.99815 6.525 5.08566 7.04542 5.24939 7.54346L1.64648 11.1465C1.55272 11.2402 1.50003 11.3674 1.5 11.5V14C1.50001 14.1326 1.5527 14.2598 1.64646 14.3535C1.74023 14.4473 1.8674 14.5 2 14.5H4.5C4.6326 14.5 4.75977 14.4473 4.85354 14.3535C4.9473 14.2598 4.99999 14.1326 5 14V13H6C6.1326 13 6.25977 12.9473 6.35354 12.8535C6.4473 12.7598 6.49999 12.6326 6.5 12.5V11.5H7.5C7.6326 11.5 7.75976 11.4473 7.85352 11.3535L8.45642 10.7506C8.95447 10.9144 9.475 11.0019 10.0007 11C12.0213 10.9998 13.8463 9.78028 14.6195 7.91346C15.3928 6.04641 14.9645 3.89346 13.5355 2.46448C12.6424 1.57137 11.4665 1.06913 10.2544 1.00684ZM10.204 2.00452C11.1732 2.05402 12.1127 2.45598 12.8284 3.17163C13.9734 4.31668 14.3152 6.03476 13.6956 7.53077C13.0759 9.02677 11.6193 10 10 10H9.99805C9.48919 10.0019 8.98501 9.90413 8.5138 9.71204C8.42266 9.67487 8.32259 9.66543 8.22611 9.68488C8.12963 9.70434 8.04104 9.75183 7.97144 9.82141L7.29285 10.5H6C5.8674 10.5 5.74023 10.5527 5.64646 10.6465C5.5527 10.7402 5.50001 10.8674 5.5 11V12H4.5C4.3674 12 4.24023 12.0527 4.14646 12.1465C4.0527 12.2402 4.00001 12.3674 4 12.5V13.5H2.5V11.7072L6.17859 8.02857C6.24815 7.95897 6.29562 7.8704 6.31508 7.77395C6.33454 7.6775 6.3251 7.57745 6.28796 7.48633C6.09589 7.0151 5.99809 6.51071 6 6.00183V6C5.99998 4.38069 6.97323 2.9241 8.46924 2.30445C9.03024 2.07208 9.62245 1.97482 10.204 2.00452Z', - Laptop: - 'M7 5C6.86739 5 6.74021 5.05268 6.64645 5.14645C6.55268 5.24021 6.5 5.36739 6.5 5.5C6.5 5.63261 6.55268 5.75979 6.64645 5.85355C6.74021 5.94732 6.86739 6 7 6H9C9.13261 6 9.25979 5.94732 9.35355 5.85355C9.44732 5.75979 9.5 5.63261 9.5 5.5C9.5 5.36739 9.44732 5.24021 9.35355 5.14645C9.25979 5.05268 9.13261 5 9 5H7Z M3.5 3C2.6775 3 2 3.6775 2 4.5V10.5H1.5C1.3674 10.5 1.24023 10.5527 1.14646 10.6465C1.0527 10.7402 1.00001 10.8674 1 11V12C1.0001 12.8225 1.67745 13.4999 2.49988 13.5C2.49984 13.5 2.49992 13.5 2.49988 13.5H13.5C13.8976 13.5 14.2795 13.3418 14.5607 13.0607C14.8418 12.7795 15 12.3976 15 12V11C15 10.8674 14.9473 10.7402 14.8535 10.6465C14.7598 10.5527 14.6326 10.5 14.5 10.5H14V4.5C14 4.50004 14 4.49996 14 4.5C13.9999 3.67757 13.3226 3.0001 12.5001 3H3.5ZM3.5 4H12.5C12.782 4.0001 13 4.21808 13 4.50012V10.5H3V4.5C3 4.21794 3.21794 4 3.5 4ZM2 11.5H14V12C14 12.1327 13.9475 12.2596 13.8536 12.3535C13.7597 12.4474 13.6328 12.5 13.5 12.5H2.50012C2.21814 12.4999 2.0001 12.282 2 12V11.5Z', - LeftNavElementFeature: - 'M2.5 2C1.67157 2 1 2.67157 1 3.5V12.5C1 13.3284 1.67157 14 2.5 14H13.5C14.3284 14 15 13.3284 15 12.5V3.5C15 2.67157 14.3284 2 13.5 2H2.5ZM2 3.5C2 3.22386 2.22386 3 2.5 3H5V13H2.5C2.22386 13 2 12.7761 2 12.5V3.5ZM6 13H13.5C13.7761 13 14 12.7761 14 12.5V3.5C14 3.22386 13.7761 3 13.5 3H6V13Z', - Lightbulb: - 'M8.58984 3.05703C8.5251 3.04606 8.45883 3.04794 8.39482 3.06258C8.33081 3.07721 8.27031 3.10431 8.21677 3.14233C8.16324 3.18035 8.11771 3.22854 8.0828 3.28416C8.04789 3.33977 8.02427 3.40171 8.0133 3.46645C8.00233 3.53119 8.00421 3.59746 8.01885 3.66147C8.03348 3.72548 8.06058 3.78598 8.0986 3.83952C8.13662 3.89306 8.18481 3.93858 8.24043 3.97349C8.29604 4.00841 8.35798 4.03202 8.42272 4.04299C9.46774 4.22014 10.2837 5.03408 10.4635 6.07864C10.486 6.20929 10.5595 6.32566 10.6678 6.40214C10.7761 6.47862 10.9103 6.50895 11.041 6.48647C11.1717 6.46398 11.2881 6.39051 11.3646 6.2822C11.4411 6.1739 11.4714 6.03964 11.449 5.90896C11.1978 4.44978 10.0497 3.30449 8.58984 3.05703Z M7.8695 1.00015C4.88639 1.06901 2.48659 3.5514 2.49999 6.53286C2.50559 8.21292 3.28395 9.79853 4.60974 10.8304C4.85481 11.0235 4.99799 11.3164 5.00011 11.6283V11.9999C5.00018 12.5462 5.45369 12.9998 5.99999 12.9999C5.99995 12.9999 6.00003 12.9999 5.99999 12.9999H10.0001C10.5465 12.9999 11.0001 12.5463 11.0001 11.9999V11.6259C11.0007 11.3127 11.1455 11.0184 11.3933 10.8268C11.3943 10.8261 11.3953 10.8253 11.3964 10.8245C13.2581 9.3627 13.9761 6.86799 13.1764 4.64016C12.3767 2.41238 10.2358 0.944114 7.8695 1.00015ZM7.89282 1.99978C7.8927 1.99978 7.89294 1.99978 7.89282 1.99978C9.83244 1.95376 11.5796 3.15198 12.2351 4.97805C12.8906 6.80412 12.3048 8.83981 10.7788 10.038L10.7817 10.0357C10.2901 10.4157 10.0013 11.0027 10.0001 11.624C10.0001 11.6237 10.0001 11.6243 10.0001 11.624V11.9999H6.00012V11.6249C6.00008 11.6238 6.00004 11.6227 5.99999 11.6216C5.99581 11.0063 5.71159 10.4254 5.22827 10.0447C5.14283 9.97921 5.03854 9.94303 4.9309 9.94155L5.22558 10.0427C4.14046 9.19911 3.50442 7.90404 3.49999 6.52956C3.5 6.52977 3.5 6.52936 3.49999 6.52956C3.48874 4.0864 5.45112 2.05604 7.89282 1.99978Z M5.49999 14C5.36739 14 5.24021 14.0527 5.14644 14.1465C5.05267 14.2402 4.99999 14.3674 4.99999 14.5C4.99999 14.6326 5.05267 14.7598 5.14644 14.8536C5.24021 14.9473 5.36739 15 5.49999 15H10.5C10.6326 15 10.7598 14.9473 10.8535 14.8536C10.9473 14.7598 11 14.6326 11 14.5C11 14.3674 10.9473 14.2402 10.8535 14.1465C10.7598 14.0527 10.6326 14 10.5 14H5.49999Z', - Lightning: - 'M9.91395 0.507557C9.80685 0.526254 9.70875 0.579351 9.63453 0.658802L2.63453 8.1588C2.57846 8.2189 2.53806 8.2919 2.51692 8.37133C2.49577 8.45076 2.49454 8.53418 2.51332 8.6142C2.5321 8.69422 2.57032 8.76838 2.62459 8.83012C2.67886 8.89185 2.74752 8.93925 2.82448 8.96813L6.42628 10.3188L5.50978 14.902C5.48852 15.0086 5.50261 15.1193 5.5499 15.2172C5.59719 15.3151 5.67511 15.3949 5.77184 15.4446C5.86858 15.4942 5.97887 15.511 6.08599 15.4923C6.19312 15.4737 6.29124 15.4206 6.36549 15.3412L13.3655 7.84117C13.4216 7.78108 13.462 7.70808 13.4831 7.62865C13.5043 7.54922 13.5055 7.4658 13.4867 7.38578C13.4679 7.30575 13.4297 7.23159 13.3754 7.16986C13.3212 7.10813 13.2525 7.06073 13.1755 7.03185L9.57374 5.68114L10.4902 1.09801C10.5061 1.01833 10.5024 0.935966 10.4793 0.85806C10.4562 0.780155 10.4145 0.70905 10.3577 0.6509C10.301 0.592751 10.2309 0.549308 10.1536 0.524331C10.0762 0.499355 9.994 0.493597 9.91395 0.507557ZM9.16456 2.6278L8.50978 5.90197C8.48653 6.01833 8.50541 6.13917 8.56307 6.24289C8.62072 6.34661 8.71337 6.42644 8.82448 6.46813L12.1243 7.70555L6.83546 13.3722L7.49025 10.098C7.5135 9.98165 7.49461 9.86081 7.43696 9.75709C7.37931 9.65337 7.28665 9.57353 7.17555 9.53185L3.87574 8.29442L9.16456 2.6278Z', - LineChartElementFeature: - 'M2.5 3C2.5 2.72386 2.27614 2.5 2 2.5C1.72386 2.5 1.5 2.72386 1.5 3V13C1.5 13.2761 1.72386 13.5 2 13.5H14C14.2761 13.5 14.5 13.2761 14.5 13C14.5 12.7239 14.2761 12.5 14 12.5H2.5V10.2269L6.02424 7.14318L9.70001 9.9C9.88902 10.0418 10.1515 10.0319 10.3293 9.87629L14.3293 6.37629C14.5371 6.19445 14.5581 5.87857 14.3763 5.67075C14.1945 5.46293 13.8786 5.44187 13.6708 5.62371L9.97577 8.85682L6.30001 6.1C6.111 5.95824 5.84856 5.96813 5.67075 6.12371L2.5 8.89812V3Z', - Link: - 'M6.93749 5.81154C6.07524 5.8115 5.24801 6.15511 4.63952 6.76601L2.87206 8.52712C2.86639 8.53281 2.86085 8.53863 2.85546 8.54458C2.29141 9.16344 1.98729 9.97626 2.0067 10.8134C2.03651 12.0876 2.80961 13.23 3.98144 13.7313C5.15327 14.2327 6.51333 14.003 7.45556 13.1447C7.4613 13.1395 7.46691 13.1341 7.4724 13.1287L8.70983 11.8911C8.75627 11.8447 8.7931 11.7896 8.81823 11.7289C8.84336 11.6683 8.8563 11.6032 8.8563 11.5376C8.8563 11.4719 8.84336 11.4069 8.81823 11.3462C8.7931 11.2855 8.75627 11.2304 8.70983 11.184C8.61606 11.0902 8.4889 11.0376 8.35631 11.0376C8.22372 11.0376 8.09656 11.0902 8.0028 11.184L6.77868 12.4081C6.12438 13.0019 5.18732 13.1596 4.37475 12.8119C3.56087 12.4637 3.02721 11.6752 3.00646 10.7902C2.99307 10.2112 3.20288 9.64994 3.59215 9.22133L5.34667 7.47304C5.34712 7.47259 5.34757 7.47215 5.34801 7.4717C5.76912 7.04892 6.34077 6.81151 6.93749 6.81155C7.53422 6.81155 8.10583 7.04894 8.52697 7.4717C8.57331 7.51823 8.62836 7.55517 8.68898 7.58041C8.7496 7.60566 8.8146 7.61872 8.88027 7.61884C8.94593 7.61897 9.01098 7.60616 9.0717 7.58114C9.13241 7.55612 9.1876 7.51939 9.23412 7.47305C9.32804 7.37946 9.38095 7.2524 9.3812 7.11981C9.38145 6.98722 9.32903 6.85996 9.23547 6.76601C8.62695 6.15516 7.79971 5.81155 6.93749 5.81154Z M10.8133 2.00905C10.0028 1.99035 9.18543 2.27105 8.54442 2.85549C8.53873 2.86069 8.53315 2.86602 8.5277 2.87148L7.29015 4.10891C7.24372 4.15534 7.20688 4.21046 7.18175 4.27113C7.15662 4.33179 7.14368 4.39682 7.14368 4.46248C7.14368 4.52815 7.15662 4.59317 7.18175 4.65384C7.20688 4.71451 7.24372 4.76963 7.29015 4.81606C7.33658 4.8625 7.3917 4.89933 7.45237 4.92446C7.51304 4.9496 7.57806 4.96253 7.64373 4.96253C7.70939 4.96253 7.77442 4.9496 7.83508 4.92446C7.89575 4.89933 7.95087 4.8625 7.9973 4.81606L9.22155 3.59194C10.1163 2.77921 11.4786 2.81129 12.3337 3.6664C13.1889 4.5216 13.2208 5.88409 12.4078 6.77883L10.6533 8.52712C10.6529 8.52757 10.6524 8.52802 10.652 8.52846C10.0089 9.17407 9.04352 9.36659 8.20202 9.01699C8.20206 9.01699 8.20197 9.01699 8.20202 9.01699C7.92924 8.90373 7.68135 8.7377 7.47289 8.52846C7.3793 8.43454 7.25224 8.38164 7.11965 8.38138C6.98706 8.38113 6.8598 8.43355 6.76586 8.52712C6.71933 8.57346 6.68239 8.62851 6.65714 8.68913C6.6319 8.74975 6.61884 8.81475 6.61871 8.88042C6.61859 8.94609 6.6314 9.01113 6.65642 9.07185C6.68143 9.13256 6.71816 9.18776 6.76451 9.23427C7.06587 9.53674 7.42403 9.77671 7.81835 9.94045C9.03169 10.4445 10.4332 10.165 11.3605 9.23415L13.1279 7.47304C13.1336 7.4674 13.1391 7.46162 13.1445 7.45571C14.3134 6.17372 14.2675 4.18601 13.0408 2.95925C12.4274 2.34587 11.6239 2.02776 10.8133 2.00905Z', - LinkBreak: - 'M5.99999 2.5C5.86739 2.5 5.74021 2.55268 5.64644 2.64645C5.55267 2.74021 5.49999 2.86739 5.49999 3V4.5C5.49999 4.63261 5.55267 4.75979 5.64644 4.85355C5.74021 4.94732 5.86739 5 5.99999 5C6.1326 5 6.25978 4.94732 6.35355 4.85355C6.44732 4.75979 6.49999 4.63261 6.49999 4.5V3C6.49999 2.86739 6.44732 2.74021 6.35355 2.64645C6.25978 2.55268 6.1326 2.5 5.99999 2.5Z M10.5 2.50171C9.73083 2.50171 8.96164 2.79372 8.37768 3.37769L7.67138 4.08386C7.62494 4.13029 7.58811 4.18541 7.56298 4.24608C7.53785 4.30675 7.52491 4.37177 7.52491 4.43744C7.52491 4.5031 7.53785 4.56813 7.56298 4.6288C7.58811 4.68946 7.62494 4.74458 7.67138 4.79102C7.71781 4.83745 7.77293 4.87429 7.8336 4.89942C7.89427 4.92455 7.95929 4.93749 8.02496 4.93749C8.09062 4.93749 8.15565 4.92455 8.21631 4.89942C8.27698 4.87429 8.3321 4.83745 8.37853 4.79102L9.08483 4.08472C9.87061 3.29894 11.1294 3.29894 11.9152 4.08472C12.7009 4.8705 12.7009 6.12938 11.9152 6.91516L11.209 7.62134C11.1625 7.66777 11.1257 7.72289 11.1006 7.78356C11.0754 7.84422 11.0625 7.90925 11.0625 7.97491C11.0625 8.04058 11.0754 8.1056 11.1006 8.16627C11.1257 8.22694 11.1625 8.28206 11.209 8.32849C11.3027 8.42224 11.4299 8.4749 11.5625 8.4749C11.6951 8.4749 11.8222 8.42224 11.916 8.32849L12.6223 7.62219C13.7902 6.45426 13.7902 4.54561 12.6223 3.37769C12.0383 2.79372 11.2692 2.50171 10.5 2.50171Z M2.99999 5.5C2.86739 5.5 2.74021 5.55268 2.64644 5.64645C2.55267 5.74021 2.49999 5.86739 2.49999 6C2.49999 6.13261 2.55267 6.25979 2.64644 6.35355C2.74021 6.44732 2.86739 6.5 2.99999 6.5H4.49999C4.6326 6.5 4.75978 6.44732 4.85355 6.35355C4.94732 6.25979 4.99999 6.13261 4.99999 6C4.99999 5.86739 4.94732 5.74021 4.85355 5.64645C4.75978 5.55268 4.6326 5.5 4.49999 5.5H2.99999Z M4.43749 7.5249C4.30491 7.52489 4.17775 7.57754 4.08398 7.67126L3.37768 8.37756C2.5204 9.23497 2.26318 10.5283 2.72717 11.6484C3.19115 12.7686 4.28753 13.5012 5.49999 13.5013C6.29585 13.5013 7.05956 13.185 7.62231 12.6222L8.32861 11.9159C8.42235 11.8221 8.47502 11.695 8.47502 11.5624C8.47502 11.4298 8.42235 11.3026 8.32861 11.2089C8.28218 11.1624 8.22705 11.1256 8.16639 11.1005C8.10572 11.0753 8.0407 11.0624 7.97503 11.0624C7.90936 11.0624 7.84434 11.0753 7.78367 11.1005C7.72301 11.1256 7.66788 11.1624 7.62145 11.2089L6.91528 11.915C6.5398 12.2905 6.03101 12.5013 5.49999 12.5013C4.68909 12.5013 3.96143 12.0149 3.65112 11.2657C3.34081 10.5166 3.51153 9.65816 4.08483 9.08472L4.79101 8.37842C4.83745 8.33199 4.87428 8.27687 4.89941 8.2162C4.92455 8.15553 4.93748 8.09051 4.93748 8.02484C4.93748 7.95917 4.92455 7.89415 4.89941 7.83348C4.87428 7.77282 4.83745 7.71769 4.79101 7.67126C4.69724 7.57754 4.57008 7.52489 4.43749 7.5249Z M11.5 9.5C11.3674 9.5 11.2402 9.55268 11.1464 9.64645C11.0527 9.74021 11 9.86739 11 10C11 10.1326 11.0527 10.2598 11.1464 10.3536C11.2402 10.4473 11.3674 10.5 11.5 10.5H13C13.1326 10.5 13.2598 10.4473 13.3535 10.3536C13.4473 10.2598 13.5 10.1326 13.5 10C13.5 9.86739 13.4473 9.74021 13.3535 9.64645C13.2598 9.55268 13.1326 9.5 13 9.5H11.5Z M9.99999 11C9.86739 11 9.74021 11.0527 9.64644 11.1464C9.55267 11.2402 9.49999 11.3674 9.49999 11.5V13C9.49999 13.1326 9.55267 13.2598 9.64644 13.3536C9.74021 13.4473 9.86739 13.5 9.99999 13.5C10.1326 13.5 10.2598 13.4473 10.3535 13.3536C10.4473 13.2598 10.5 13.1326 10.5 13V11.5C10.5 11.3674 10.4473 11.2402 10.3535 11.1464C10.2598 11.0527 10.1326 11 9.99999 11Z', - List: - 'M2.5 11.5C2.36739 11.5 2.24021 11.5527 2.14645 11.6464C2.05268 11.7402 2 11.8674 2 12C2 12.1326 2.05268 12.2598 2.14645 12.3536C2.24021 12.4473 2.36739 12.5 2.5 12.5H13.5C13.6326 12.5 13.7598 12.4473 13.8536 12.3536C13.9473 12.2598 14 12.1326 14 12C14 11.8674 13.9473 11.7402 13.8536 11.6464C13.7598 11.5527 13.6326 11.5 13.5 11.5H2.5Z M2.5 3.5C2.36739 3.5 2.24021 3.55268 2.14645 3.64645C2.05268 3.74021 2 3.86739 2 4C2 4.13261 2.05268 4.25979 2.14645 4.35355C2.24021 4.44732 2.36739 4.5 2.5 4.5H13.5C13.6326 4.5 13.7598 4.44732 13.8536 4.35355C13.9473 4.25979 14 4.13261 14 4C14 3.86739 13.9473 3.74021 13.8536 3.64645C13.7598 3.55268 13.6326 3.5 13.5 3.5H2.5Z M2.5 7.5C2.36739 7.5 2.24021 7.55268 2.14645 7.64645C2.05268 7.74021 2 7.86739 2 8C2 8.13261 2.05268 8.25979 2.14645 8.35355C2.24021 8.44732 2.36739 8.5 2.5 8.5H13.5C13.6326 8.5 13.7598 8.44732 13.8536 8.35355C13.9473 8.25979 14 8.13261 14 8C14 7.86739 13.9473 7.74021 13.8536 7.64645C13.7598 7.55268 13.6326 7.5 13.5 7.5H2.5Z', - ListChecks: - 'M5.77027 2.50035C5.70465 2.49769 5.63915 2.50799 5.5775 2.53065C5.51586 2.55331 5.45929 2.5879 5.41102 2.63243L3.58179 4.31932L2.83948 3.63292C2.74213 3.54289 2.613 3.49521 2.4805 3.50039C2.348 3.50556 2.22298 3.56316 2.13294 3.6605C2.04291 3.75786 1.99524 3.88699 2.00041 4.01949C2.00559 4.15199 2.06318 4.27701 2.16053 4.36705L3.2417 5.36705C3.33402 5.45241 3.45511 5.49987 3.58084 5.49996C3.70658 5.50005 3.82773 5.45277 3.92017 5.36753L6.08899 3.36754C6.13726 3.32302 6.17629 3.26943 6.20385 3.20983C6.23141 3.15023 6.24696 3.08579 6.24961 3.02018C6.25226 2.95457 6.24197 2.88908 6.21931 2.82745C6.19665 2.76582 6.16208 2.70926 6.11756 2.66099C6.02768 2.56351 5.90275 2.50573 5.77027 2.50035Z M8.00001 3.49998C7.8674 3.49998 7.74022 3.55266 7.64645 3.64643C7.55268 3.7402 7.50001 3.86737 7.50001 3.99998C7.50001 4.13259 7.55268 4.25977 7.64645 4.35354C7.74022 4.4473 7.8674 4.49998 8.00001 4.49998H13.5C13.6326 4.49998 13.7598 4.4473 13.8536 4.35354C13.9473 4.25977 14 4.13259 14 3.99998C14 3.86737 13.9473 3.7402 13.8536 3.64643C13.7598 3.55266 13.6326 3.49998 13.5 3.49998H8.00001Z M5.77027 6.50035C5.70465 6.49769 5.63915 6.50799 5.5775 6.53065C5.51586 6.55331 5.45929 6.5879 5.41102 6.63243L3.58179 8.31932L2.83948 7.63292C2.74213 7.54289 2.613 7.49521 2.4805 7.50039C2.348 7.50556 2.22298 7.56316 2.13294 7.6605C2.04291 7.75786 1.99524 7.88699 2.00041 8.01949C2.00559 8.15199 2.06318 8.27701 2.16053 8.36705L3.2417 9.36705C3.33402 9.45241 3.4551 9.49987 3.58084 9.49996C3.70658 9.50006 3.82773 9.45278 3.92017 9.36755L6.08899 7.36755C6.13726 7.32303 6.17629 7.26944 6.20385 7.20985C6.23141 7.15025 6.24696 7.0858 6.24961 7.02019C6.25226 6.95458 6.24197 6.88909 6.21931 6.82747C6.19665 6.76584 6.16208 6.70927 6.11756 6.66101C6.02768 6.56353 5.90275 6.50573 5.77027 6.50035Z M8.00001 7.49998C7.8674 7.49998 7.74022 7.55266 7.64645 7.64643C7.55268 7.7402 7.50001 7.86737 7.50001 7.99998C7.50001 8.13259 7.55268 8.25977 7.64645 8.35354C7.74022 8.4473 7.8674 8.49998 8.00001 8.49998H13.5C13.6326 8.49998 13.7598 8.4473 13.8536 8.35354C13.9473 8.25977 14 8.13259 14 7.99998C14 7.86737 13.9473 7.7402 13.8536 7.64643C13.7598 7.55266 13.6326 7.49998 13.5 7.49998H8.00001Z M5.77027 10.5003C5.70465 10.4977 5.63915 10.508 5.5775 10.5307C5.51586 10.5533 5.45929 10.5879 5.41102 10.6324L3.58179 12.3193L2.83948 11.6329C2.74213 11.5429 2.613 11.4952 2.4805 11.5004C2.348 11.5056 2.22298 11.5632 2.13294 11.6605C2.04291 11.7579 1.99524 11.887 2.00041 12.0195C2.00559 12.152 2.06318 12.277 2.16053 12.367L3.2417 13.367C3.33402 13.4524 3.4551 13.4999 3.58084 13.5C3.70658 13.5001 3.82773 13.4528 3.92017 13.3675L6.08899 11.3675C6.13726 11.323 6.17629 11.2694 6.20385 11.2098C6.23141 11.1502 6.24696 11.0858 6.24961 11.0202C6.25226 10.9546 6.24197 10.8891 6.21931 10.8275C6.19665 10.7658 6.16208 10.7093 6.11756 10.661C6.02768 10.5635 5.90275 10.5056 5.77027 10.5003Z M8.00001 11.5C7.8674 11.5 7.74022 11.5527 7.64645 11.6464C7.55268 11.7402 7.50001 11.8674 7.50001 12C7.50001 12.1326 7.55268 12.2598 7.64645 12.3535C7.74022 12.4473 7.8674 12.5 8.00001 12.5H13.5C13.6326 12.5 13.7598 12.4473 13.8536 12.3535C13.9473 12.2598 14 12.1326 14 12C14 11.8674 13.9473 11.7402 13.8536 11.6464C13.7598 11.5527 13.6326 11.5 13.5 11.5H8.00001Z', - ListDashes: - 'M2.5 3.5C2.36739 3.5 2.24021 3.55268 2.14645 3.64645C2.05268 3.74021 2 3.86739 2 4C2 4.13261 2.05268 4.25979 2.14645 4.35355C2.24021 4.44732 2.36739 4.5 2.5 4.5H3.5C3.63261 4.5 3.75979 4.44732 3.85355 4.35355C3.94732 4.25979 4 4.13261 4 4C4 3.86739 3.94732 3.74021 3.85355 3.64645C3.75979 3.55268 3.63261 3.5 3.5 3.5H2.5Z M6 3.5C5.86739 3.5 5.74021 3.55268 5.64645 3.64645C5.55268 3.74021 5.5 3.86739 5.5 4C5.5 4.13261 5.55268 4.25979 5.64645 4.35355C5.74021 4.44732 5.86739 4.5 6 4.5H13.5C13.6326 4.5 13.7598 4.44732 13.8536 4.35355C13.9473 4.25979 14 4.13261 14 4C14 3.86739 13.9473 3.74021 13.8536 3.64645C13.7598 3.55268 13.6326 3.5 13.5 3.5H6Z M2.5 7.5C2.36739 7.5 2.24021 7.55268 2.14645 7.64645C2.05268 7.74021 2 7.86739 2 8C2 8.13261 2.05268 8.25979 2.14645 8.35355C2.24021 8.44732 2.36739 8.5 2.5 8.5H3.5C3.63261 8.5 3.75979 8.44732 3.85355 8.35355C3.94732 8.25979 4 8.13261 4 8C4 7.86739 3.94732 7.74021 3.85355 7.64645C3.75979 7.55268 3.63261 7.5 3.5 7.5H2.5Z M6 7.5C5.86739 7.5 5.74021 7.55268 5.64645 7.64645C5.55268 7.74021 5.5 7.86739 5.5 8C5.5 8.13261 5.55268 8.25979 5.64645 8.35355C5.74021 8.44732 5.86739 8.5 6 8.5H13.5C13.6326 8.5 13.7598 8.44732 13.8536 8.35355C13.9473 8.25979 14 8.13261 14 8C14 7.86739 13.9473 7.74021 13.8536 7.64645C13.7598 7.55268 13.6326 7.5 13.5 7.5H6Z M2.5 11.5C2.36739 11.5 2.24021 11.5527 2.14645 11.6464C2.05268 11.7402 2 11.8674 2 12C2 12.1326 2.05268 12.2598 2.14645 12.3536C2.24021 12.4473 2.36739 12.5 2.5 12.5H3.5C3.63261 12.5 3.75979 12.4473 3.85355 12.3536C3.94732 12.2598 4 12.1326 4 12C4 11.8674 3.94732 11.7402 3.85355 11.6464C3.75979 11.5527 3.63261 11.5 3.5 11.5H2.5Z M6 11.5C5.86739 11.5 5.74021 11.5527 5.64645 11.6464C5.55268 11.7402 5.5 11.8674 5.5 12C5.5 12.1326 5.55268 12.2598 5.64645 12.3536C5.74021 12.4473 5.86739 12.5 6 12.5H13.5C13.6326 12.5 13.7598 12.4473 13.8536 12.3536C13.9473 12.2598 14 12.1326 14 12C14 11.8674 13.9473 11.7402 13.8536 11.6464C13.7598 11.5527 13.6326 11.5 13.5 11.5H6Z', - ListNumbers: - 'M3.60643 2.76148C3.49492 2.73717 3.37845 2.75172 3.27635 2.80274L2.27635 3.30274C2.21761 3.33211 2.16523 3.37275 2.1222 3.42236C2.07917 3.47197 2.04633 3.52957 2.02557 3.59187C2.0048 3.65417 1.99651 3.71995 2.00117 3.78546C2.00583 3.85096 2.02335 3.91491 2.05272 3.97364C2.08208 4.03238 2.12273 4.08476 2.17234 4.1278C2.22195 4.17083 2.27955 4.20366 2.34185 4.22443C2.40415 4.2452 2.46993 4.25349 2.53543 4.24883C2.60094 4.24417 2.66489 4.22665 2.72362 4.19727L2.99999 4.05909V6.75001C2.99999 6.88262 3.05267 7.00979 3.14643 7.10356C3.2402 7.19733 3.36738 7.25001 3.49999 7.25001C3.63259 7.25001 3.75977 7.19733 3.85354 7.10356C3.94731 7.00979 3.99999 6.88262 3.99999 6.75001V3.25001C3.99997 3.1359 3.96093 3.02522 3.88934 2.93636C3.81776 2.84749 3.71793 2.78579 3.60643 2.76148Z M3.48155 8.49415C3.2908 8.48002 3.09719 8.50554 2.91344 8.57179C2.91349 8.57175 2.9134 8.57183 2.91344 8.57179C2.54579 8.70442 2.25137 8.98804 2.10509 9.35048C2.05546 9.47344 2.05672 9.61107 2.10857 9.73311C2.16042 9.85515 2.25862 9.95159 2.38158 10.0012C2.50454 10.0509 2.64217 10.0496 2.76421 9.99775C2.88625 9.9459 2.98269 9.8477 3.03233 9.72474C3.07231 9.62568 3.15208 9.54886 3.25255 9.51259C3.35323 9.47629 3.46386 9.48443 3.55809 9.53517C3.76161 9.64477 3.81988 9.88652 3.68931 10.0769L2.09972 12.2004C2.0441 12.2748 2.01026 12.3631 2.00199 12.4555C1.99372 12.548 2.01135 12.6409 2.05289 12.7239C2.09444 12.8069 2.15826 12.8767 2.23723 12.9255C2.31619 12.9743 2.40717 13.0001 2.49999 13.0001H4.24999C4.38259 13.0001 4.50977 12.9475 4.60354 12.8537C4.69731 12.7599 4.74999 12.6327 4.74999 12.5001C4.74999 12.3675 4.69731 12.2403 4.60354 12.1466C4.50977 12.0528 4.38259 12.0001 4.24999 12.0001H3.49877L4.50023 10.6622C4.50403 10.6572 4.50773 10.6521 4.51134 10.6469C4.97257 9.97953 4.74644 9.03929 4.03221 8.65467C4.03225 8.65467 4.03217 8.65467 4.03221 8.65467C3.86023 8.56208 3.6723 8.50828 3.48155 8.49415Z M6.49999 11.5C6.36738 11.5 6.2402 11.5527 6.14643 11.6465C6.05267 11.7402 5.99999 11.8674 5.99999 12C5.99999 12.1326 6.05267 12.2598 6.14643 12.3536C6.2402 12.4473 6.36738 12.5 6.49999 12.5H13.5C13.6326 12.5 13.7598 12.4473 13.8535 12.3536C13.9473 12.2598 14 12.1326 14 12C14 11.8674 13.9473 11.7402 13.8535 11.6465C13.7598 11.5527 13.6326 11.5 13.5 11.5H6.49999Z M6.49999 3.50001C6.36738 3.50001 6.2402 3.55269 6.14643 3.64646C6.05267 3.74022 5.99999 3.8674 5.99999 4.00001C5.99999 4.13262 6.05267 4.25979 6.14643 4.35356C6.2402 4.44733 6.36738 4.50001 6.49999 4.50001H13.5C13.6326 4.50001 13.7598 4.44733 13.8535 4.35356C13.9473 4.25979 14 4.13262 14 4.00001C14 3.8674 13.9473 3.74022 13.8535 3.64646C13.7598 3.55269 13.6326 3.50001 13.5 3.50001H6.49999Z M6.49999 7.50001C6.36738 7.50001 6.2402 7.55269 6.14643 7.64646C6.05267 7.74022 5.99999 7.8674 5.99999 8.00001C5.99999 8.13262 6.05267 8.25979 6.14643 8.35356C6.2402 8.44733 6.36738 8.50001 6.49999 8.50001H13.5C13.6326 8.50001 13.7598 8.44733 13.8535 8.35356C13.9473 8.25979 14 8.13262 14 8.00001C14 7.8674 13.9473 7.74022 13.8535 7.64646C13.7598 7.55269 13.6326 7.50001 13.5 7.50001H6.49999Z', - Lock: - 'M8 10.25C8.41419 10.25 8.75 9.91419 8.75 9.5C8.75 9.08581 8.41419 8.75 8 8.75C7.58581 8.75 7.25 9.08581 7.25 9.5C7.25 9.91419 7.58581 10.25 8 10.25Z M8 0.5C6.48714 0.5 5.25 1.73714 5.25 3.25V5H3C2.45364 5 2 5.45364 2 6V13C2 13.5464 2.45364 14 3 14H13C13.5464 14 14 13.5464 14 13V6C14 5.45364 13.5464 5 13 5H10.75V3.25C10.75 1.73714 9.51286 0.5 8 0.5ZM8 1.5C8.97242 1.5 9.75 2.27758 9.75 3.25V5H6.25V3.25C6.25 2.27758 7.02758 1.5 8 1.5ZM3 6H13V13H3V6Z', - LockSimple: - 'M8 0.5C6.48714 0.5 5.25 1.73714 5.25 3.25V5H3C2.45364 5 2 5.45364 2 6V13C2 13.5464 2.45364 14 3 14H13C13.5464 14 14 13.5464 14 13V6C14 5.45364 13.5464 5 13 5H10.75V3.25C10.75 1.73714 9.51286 0.5 8 0.5ZM8 1.5C8.97242 1.5 9.75 2.27758 9.75 3.25V5H6.25V3.25C6.25 2.27758 7.02758 1.5 8 1.5ZM3 6H13V13H3V6Z', - Lookup: - 'M2.5 3.5C2.22386 3.5 2 3.72386 2 4C2 4.27614 2.22386 4.5 2.5 4.5H13.5C13.7761 4.5 14 4.27614 14 4C14 3.72386 13.7761 3.5 13.5 3.5H2.5Z M2.5 7.5C2.22386 7.5 2 7.72386 2 8C2 8.27614 2.22386 8.5 2.5 8.5H6C6.27614 8.5 6.5 8.27614 6.5 8C6.5 7.72386 6.27614 7.5 6 7.5H2.5Z M2.5 11.5C2.22386 11.5 2 11.7239 2 12C2 12.2761 2.22386 12.5 2.5 12.5H6C6.27614 12.5 6.5 12.2761 6.5 12C6.5 11.7239 6.27614 11.5 6 11.5H2.5Z M11 6C9.067 6 7.5 7.567 7.5 9.5C7.5 11.433 9.067 13 11 13C11.788 13 12.5151 12.7396 13.1001 12.3002C13.1141 12.3188 13.1295 12.3366 13.1464 12.3536L14.6464 13.8536C14.8417 14.0488 15.1583 14.0488 15.3536 13.8536C15.5488 13.6583 15.5488 13.3417 15.3536 13.1464L13.8536 11.6464C13.8366 11.6295 13.8188 11.6141 13.8002 11.6001C14.2396 11.0151 14.5 10.288 14.5 9.5C14.5 7.567 12.933 6 11 6ZM8.5 9.5C8.5 8.11929 9.61929 7 11 7C12.3807 7 13.5 8.11929 13.5 9.5C13.5 10.8807 12.3807 12 11 12C9.61929 12 8.5 10.8807 8.5 9.5Z', - MagnifyingGlass: - 'M7.25 1.5C4.08028 1.5 1.5 4.08028 1.5 7.25C1.5 10.4197 4.08028 13 7.25 13C8.65529 13 9.94315 12.4911 10.9432 11.6503L13.6465 14.3534C13.7402 14.4471 13.8674 14.4998 14 14.4998C14.1326 14.4998 14.2598 14.4471 14.3535 14.3534C14.4473 14.2596 14.4999 14.1325 14.4999 13.9999C14.4999 13.8673 14.4473 13.7401 14.3535 13.6464L11.6504 10.9431C12.4912 9.94305 13 8.65523 13 7.25C13 4.08028 10.4197 1.5 7.25 1.5ZM7.25 2.5C9.87928 2.5 12 4.62072 12 7.25C12 8.56227 11.4715 9.74761 10.6154 10.6061C10.6132 10.607 10.611 10.6079 10.6089 10.6088C10.608 10.611 10.6071 10.6132 10.6062 10.6154C9.74772 11.4715 8.5623 12 7.25 12C4.62072 12 2.5 9.87928 2.5 7.25C2.5 4.62072 4.62072 2.5 7.25 2.5Z', - MapPin: - 'M8 4C6.62522 4 5.5 5.12522 5.5 6.5C5.5 7.87478 6.62522 9 8 9C9.37478 9 10.5 7.87478 10.5 6.5C10.5 5.12522 9.37478 4 8 4ZM8 5C8.83434 5 9.5 5.66566 9.5 6.5C9.5 7.33434 8.83434 8 8 8C7.16566 8 6.5 7.33434 6.5 6.5C6.5 5.66566 7.16566 5 8 5Z M8 1C4.96836 1 2.5 3.46836 2.5 6.5C2.5 11.3704 7.71326 14.9097 7.71326 14.9097C7.7973 14.9685 7.89741 15 8 15C8.10259 15 8.2027 14.9685 8.28674 14.9097C8.28674 14.9097 13.5 11.3704 13.5 6.5C13.5 3.46836 11.0316 1 8 1ZM8 2C10.4912 2 12.5 4.0088 12.5 6.5C12.5 10.2338 8.75014 13.2816 8 13.8583C7.24986 13.2816 3.5 10.2338 3.5 6.5C3.5 4.0088 5.5088 2 8 2Z', - Megaphone: - 'M2.61988 1.65908C2.07327 1.61165 1.50481 2.02729 1.5 2.63344C1.5 2.63474 1.5 2.63605 1.5 2.63735V12.3624C1.5 12.3638 1.5 12.3651 1.5 12.3665C1.50489 12.9575 2.04097 13.4254 2.6272 13.3504C2.62712 13.3504 2.62728 13.3504 2.6272 13.3504C2.81475 13.3263 2.99192 13.2494 3.13758 13.1288C3.13835 13.1282 3.13913 13.1276 3.1399 13.127C6.28407 10.4928 9.5 10.4999 9.5 10.4999H12C13.6509 10.4999 15 9.15083 15 7.4999C15 5.84897 13.6509 4.4999 12 4.4999H9.5C9.5 4.4999 6.28407 4.50697 3.1399 1.87282C3.13913 1.87221 3.13835 1.8716 3.13758 1.87099C2.98191 1.74217 2.80208 1.67488 2.61988 1.65908ZM2.49988 2.64113C2.49983 2.64109 2.49993 2.64117 2.49988 2.64113C5.90501 5.49253 9.50001 5.4999 9.50001 5.4999H12C13.1105 5.4999 14 6.38941 14 7.4999C14 8.61039 13.1105 9.4999 12 9.4999H9.50001C9.50001 9.4999 5.90532 9.50719 2.50025 12.3584C2.49987 12.3574 2.49685 12.3589 2.50025 12.3584C2.5003 12.3584 2.5002 12.3585 2.50025 12.3584C2.50027 12.3601 2.50071 12.3578 2.50025 12.3584L2.49988 2.64113ZM9.5 4.4999C9.3674 4.4999 9.24022 4.5527 9.14645 4.64647C9.05268 4.74023 9 4.86741 9 5.00002V12.7269C8.99708 13.066 9.16832 13.3847 9.45264 13.5694L10.135 14.0222C10.1349 14.0221 10.1351 14.0223 10.135 14.0222C10.4048 14.202 10.7468 14.2401 11.0494 14.1237C11.0493 14.1237 11.0495 14.1236 11.0494 14.1237C11.3522 14.007 11.5807 13.7493 11.6599 13.4346C11.66 13.4342 11.6601 13.4338 11.6602 13.4334L12.4852 10.1209C12.5011 10.0571 12.5043 9.99092 12.4946 9.92598C12.4849 9.86103 12.4624 9.79864 12.4286 9.74235C12.3948 9.68607 12.3502 9.63701 12.2974 9.59796C12.2446 9.55892 12.1846 9.53066 12.1209 9.51479C12.0571 9.49891 11.9909 9.49575 11.926 9.50547C11.861 9.51518 11.7986 9.5376 11.7424 9.57144C11.6861 9.60528 11.637 9.64987 11.598 9.70267C11.5589 9.75547 11.5307 9.81545 11.5148 9.87917L10.6902 13.1903C10.6905 13.1906 10.6902 13.1904 10.6902 13.1903C10.6898 13.1901 10.6893 13.1899 10.689 13.1897L10.0015 12.7334C10.001 12.7331 10.0005 12.7328 10 12.7324C10 12.732 10 12.7316 10 12.7312V5.00002C10 4.86741 9.94734 4.74023 9.85357 4.64647C9.7598 4.5527 9.63261 4.4999 9.5 4.4999Z', - Minus: - 'M2.5 7.5C2.36739 7.5 2.24021 7.55268 2.14645 7.64645C2.05268 7.74021 2 7.86739 2 8C2 8.13261 2.05268 8.25979 2.14645 8.35355C2.24021 8.44732 2.36739 8.5 2.5 8.5H13.5C13.6326 8.5 13.7598 8.44732 13.8536 8.35355C13.9473 8.25979 14 8.13261 14 8C14 7.86739 13.9473 7.74021 13.8536 7.64645C13.7598 7.55268 13.6326 7.5 13.5 7.5H2.5Z', - ModalElementFeature: - 'M4 6.5C4 5.67157 4.67157 5 5.5 5H10.5C11.3284 5 12 5.67157 12 6.5V9.5C12 10.3284 11.3284 11 10.5 11H5.5C4.67157 11 4 10.3284 4 9.5V6.5ZM5.5 6C5.22386 6 5 6.22386 5 6.5V9.5C5 9.77614 5.22386 10 5.5 10H10.5C10.7761 10 11 9.77614 11 9.5V6.5C11 6.22386 10.7761 6 10.5 6H5.5Z M1 3.5C1 2.67157 1.67157 2 2.5 2H13.5C14.3284 2 15 2.67157 15 3.5V12.5C15 13.3284 14.3284 14 13.5 14H2.5C1.67157 14 1 13.3284 1 12.5V3.5ZM2.5 3C2.22386 3 2 3.22386 2 3.5V12.5C2 12.7761 2.22386 13 2.5 13H13.5C13.7761 13 14 12.7761 14 12.5V3.5C14 3.22386 13.7761 3 13.5 3H2.5Z', - Multiselect: - 'M10.6096 3.18767C10.7821 2.97204 11.0967 2.93708 11.3124 3.10958L12.25 3.85971L13.1877 3.10958C13.4033 2.93708 13.7179 2.97204 13.8905 3.18767C14.063 3.4033 14.028 3.71795 13.8124 3.89045L12.5624 4.89045C12.3798 5.03654 12.1203 5.03654 11.9377 4.89045L10.6877 3.89045C10.472 3.71795 10.4371 3.4033 10.6096 3.18767Z M2 4C2 3.72386 2.22386 3.5 2.5 3.5H8C8.27614 3.5 8.5 3.72386 8.5 4C8.5 4.27614 8.27614 4.5 8 4.5H2.5C2.22386 4.5 2 4.27614 2 4Z M2.5 7.5C2.22386 7.5 2 7.72386 2 8C2 8.27614 2.22386 8.5 2.5 8.5H8C8.27614 8.5 8.5 8.27614 8.5 8C8.5 7.72386 8.27614 7.5 8 7.5H2.5Z M2.5 11.5C2.22386 11.5 2 11.7239 2 12C2 12.2761 2.22386 12.5 2.5 12.5H8C8.27614 12.5 8.5 12.2761 8.5 12C8.5 11.7239 8.27614 11.5 8 11.5H2.5Z M10.6096 7.18767C10.7821 6.97204 11.0967 6.93708 11.3124 7.10958L12.25 7.85971L13.1877 7.10958C13.4033 6.93708 13.7179 6.97204 13.8905 7.18767C14.063 7.4033 14.028 7.71795 13.8124 7.89045L12.5624 8.89045C12.3798 9.03654 12.1203 9.03654 11.9377 8.89045L10.6877 7.89045C10.472 7.71795 10.4371 7.4033 10.6096 7.18767Z M10.6096 11.1877C10.7821 10.972 11.0967 10.9371 11.3124 11.1096L12.25 11.8597L13.1877 11.1096C13.4033 10.9371 13.7179 10.972 13.8905 11.1877C14.063 11.4033 14.028 11.7179 13.8124 11.8905L12.5624 12.8905C12.3798 13.0365 12.1203 13.0365 11.9377 12.8905L10.6877 11.8905C10.472 11.7179 10.4371 11.4033 10.6096 11.1877Z', - NoNavElementFeature: - 'M1 3.5C1 2.67157 1.67157 2 2.5 2H13.5C14.3284 2 15 2.67157 15 3.5V12.5C15 13.3284 14.3284 14 13.5 14H2.5C1.67157 14 1 13.3284 1 12.5V3.5ZM2.5 3C2.22386 3 2 3.22386 2 3.5V12.5C2 12.7761 2.22386 13 2.5 13H13.5C13.7761 13 14 12.7761 14 12.5V3.5C14 3.22386 13.7761 3 13.5 3H2.5Z', - NumberElementFeature: - 'M10.4182 5.02914C10.6782 5.12202 10.8138 5.40813 10.7209 5.66818L9.70951 8.50001H11V7.5C11 7.22386 11.2239 7 11.5 7C11.7761 7 12 7.22386 12 7.5V10.5C12 10.7761 11.7761 11 11.5 11C11.2239 11 11 10.7761 11 10.5V9.50001H9C8.8374 9.50001 8.68496 9.42095 8.5913 9.28804C8.49763 9.15513 8.47444 8.98497 8.52913 8.83185L9.77913 5.33185C9.87201 5.07179 10.1581 4.93627 10.4182 5.02914Z M5.40008 6.08673C5.55291 6.00602 5.72889 5.98063 5.89825 6.01482C6.06762 6.04901 6.22005 6.14069 6.32972 6.27445C6.4394 6.40821 6.49957 6.57582 6.49999 6.74893L6.5 6.74996C6.50065 6.89459 6.45836 7.03606 6.37865 7.15647L4.09975 10.2003C3.98629 10.3519 3.96815 10.5545 4.05288 10.7238C4.13762 10.8931 4.31069 11 4.5 11H7.25C7.52614 11 7.75 10.7761 7.75 10.5C7.75 10.2239 7.52614 10 7.25 10H5.49895L7.1867 7.74571C7.19116 7.73976 7.19548 7.73372 7.19966 7.72757C7.39666 7.43843 7.50136 7.09634 7.49999 6.74648C7.499 6.34307 7.35879 5.95234 7.10301 5.64039C6.84722 5.32844 6.49155 5.11441 6.09612 5.03459C5.70069 4.95477 5.28986 5.01407 4.93313 5.20244C4.57641 5.39081 4.29573 5.69665 4.1385 6.06816C4.03087 6.32247 4.14978 6.61587 4.40409 6.72349C4.65839 6.83112 4.9518 6.71221 5.05942 6.45791C5.12687 6.29853 5.24723 6.16744 5.40008 6.08673Z M4.5 2C2.567 2 1 3.567 1 5.5V10.5C1 12.433 2.567 14 4.5 14H11.5C13.433 14 15 12.433 15 10.5V5.5C15 3.567 13.433 2 11.5 2H4.5ZM2 5.5C2 4.11929 3.11929 3 4.5 3H11.5C12.8807 3 14 4.11929 14 5.5V10.5C14 11.8807 12.8807 13 11.5 13H4.5C3.11929 13 2 11.8807 2 10.5V5.5Z', - Overflow: - 'M5 8C5 8.55228 4.55228 9 4 9C3.44772 9 3 8.55228 3 8C3 7.44772 3.44772 7 4 7C4.55228 7 5 7.44772 5 8Z M8 9C8.55228 9 9 8.55228 9 8C9 7.44772 8.55228 7 8 7C7.44772 7 7 7.44772 7 8C7 8.55228 7.44772 9 8 9Z M13 8C13 8.55228 12.5523 9 12 9C11.4477 9 11 8.55228 11 8C11 7.44772 11.4477 7 12 7C12.5523 7 13 7.44772 13 8Z', - PaintBrushHousehold: - 'M12.9994 1.01163C12.4892 1.0089 11.9779 1.19817 11.5877 1.58011C11.58 1.58767 11.5725 1.59549 11.5653 1.60354L8.13485 5.42764L7.55977 4.85269L7.55709 4.85C6.97474 4.2762 6.02517 4.2762 5.44283 4.85L0.646449 9.64639C0.552679 9.74017 0.5 9.86735 0.5 9.99997C0.5 10.1326 0.552679 10.2598 0.646449 10.3535L5.64645 15.3535C5.74022 15.4473 5.86738 15.4999 5.99996 15.4999C6.13255 15.4999 6.25971 15.4473 6.35348 15.3535L11.1473 10.5597L11.1499 10.5572C11.7237 9.97481 11.7237 9.02513 11.1499 8.44278L11.1473 8.44022L10.5725 7.86527L14.3963 4.4346C14.4044 4.4274 14.4122 4.41995 14.4198 4.41226C15.1836 3.63191 15.1769 2.36739 14.4047 1.59524C14.0187 1.20916 13.5095 1.01435 12.9994 1.01163ZM12.994 2.00564C13.2479 2.007 13.5013 2.10594 13.6976 2.30228C14.0888 2.69348 14.093 3.31089 13.709 3.70791L9.90353 7.12149L9.9028 7.12222C9.69915 7.30567 9.57961 7.56537 9.5726 7.83938C9.56564 8.11312 9.67142 8.37846 9.86496 8.57217L10.4376 9.14469C10.6358 9.34579 10.6358 9.65415 10.4376 9.85526L9.49996 10.793L5.207 6.50003L6.14462 5.56241C6.34573 5.36425 6.6542 5.36425 6.85531 5.56241L7.4277 6.13479C7.62143 6.32838 7.88684 6.43431 8.16061 6.42728C8.43448 6.42022 8.69407 6.30078 8.87753 6.09732L8.87839 6.09635L12.2923 2.29068C12.49 2.0996 12.7421 2.00429 12.994 2.00564ZM4.49996 7.20706L8.79293 11.5L5.99996 14.2929L5.11959 13.4124L6.71603 11.8159C6.76247 11.7695 6.7993 11.7144 6.82443 11.6537C6.84956 11.5931 6.8625 11.528 6.8625 11.4624C6.8625 11.3967 6.84956 11.3317 6.82443 11.271C6.7993 11.2103 6.76247 11.1552 6.71603 11.1088C6.6696 11.0624 6.61448 11.0255 6.55381 11.0004C6.49314 10.9753 6.42812 10.9623 6.36245 10.9623C6.29679 10.9623 6.23176 10.9753 6.1711 11.0004C6.11043 11.0255 6.05531 11.0624 6.00888 11.1088L4.41244 12.7054L3.36959 11.6624L4.96603 10.0659C5.01247 10.0195 5.0493 9.96439 5.07443 9.90373C5.09956 9.84306 5.1125 9.77804 5.1125 9.71237C5.1125 9.6467 5.09956 9.58168 5.07443 9.52101C5.0493 9.46035 5.01247 9.40522 4.96603 9.35879C4.9196 9.31236 4.86448 9.27552 4.80381 9.25039C4.74314 9.22526 4.67812 9.21232 4.61245 9.21232C4.54679 9.21232 4.48176 9.22526 4.4211 9.25039C4.36043 9.27552 4.30531 9.31236 4.25888 9.35879L2.66244 10.9554L1.707 10L4.49996 7.20706Z', - PaintBucket: - 'M2.36878 1.36865C2.30311 1.36863 2.23808 1.38154 2.17741 1.40666C2.11673 1.43177 2.06159 1.46859 2.01515 1.51501C1.96871 1.56144 1.93187 1.61657 1.90674 1.67723C1.88161 1.7379 1.86868 1.80292 1.86868 1.86859C1.86868 1.93426 1.88161 1.99928 1.90674 2.05995C1.93187 2.12062 1.96871 2.17574 2.01515 2.22217L6.21803 6.42505C6.08351 6.67237 6.00001 6.95077 6.00001 7.25C6.00001 8.21058 6.78943 9 7.75001 9C8.71059 9 9.50001 8.21058 9.50001 7.25C9.50001 6.28942 8.71059 5.5 7.75001 5.5C7.45086 5.5 7.17258 5.58356 6.9253 5.71802L2.7223 1.51501C2.62853 1.42129 2.50137 1.36864 2.36878 1.36865ZM7.75001 6.5C8.17018 6.5 8.50001 6.82983 8.50001 7.25C8.50001 7.67017 8.17018 8 7.75001 8C7.32984 8 7.00001 7.67017 7.00001 7.25C7.00001 7.04405 7.08091 6.86114 7.21119 6.72681C7.21491 6.72531 7.21862 6.72376 7.2223 6.72217C7.22618 6.71703 7.22997 6.71183 7.23365 6.70654C7.36745 6.57966 7.54709 6.5 7.75001 6.5Z M14.25 9.75C14.1174 9.75003 13.9902 9.80272 13.8965 9.89648C13.8965 9.89648 13.5499 10.2425 13.209 10.7539C12.868 11.2653 12.5 11.9583 12.5 12.75C12.5 13.7106 13.2894 14.5 14.25 14.5C15.2106 14.5 16 13.7106 16 12.75C16 11.9583 15.632 11.2653 15.291 10.7539C14.9501 10.2425 14.6035 9.89648 14.6035 9.89648C14.5098 9.80272 14.3826 9.75003 14.25 9.75ZM14.25 11.0325C14.3204 11.1233 14.3825 11.1938 14.459 11.3086C14.743 11.7347 15 12.2917 15 12.75C15 13.1701 14.6701 13.5 14.25 13.5C13.8299 13.5 13.5 13.1701 13.5 12.75C13.5 12.2917 13.757 11.7347 14.041 11.3086C14.1176 11.1938 14.1796 11.1233 14.25 11.0325Z M7.21876 0.5C7.08616 0.500026 6.959 0.552716 6.86524 0.646484L0.852671 6.65894C0.851813 6.65979 0.850959 6.66064 0.850108 6.6615C0.276242 7.24384 0.276242 8.19366 0.850108 8.776C0.850959 8.77686 0.851813 8.77771 0.852671 8.77856L6.15895 14.0848C6.15984 14.0857 6.16073 14.0865 6.16163 14.0874C6.74398 14.6612 7.69354 14.6612 8.27589 14.0874C8.27679 14.0865 8.27768 14.0857 8.27858 14.0848L14.291 8.07226C14.3848 7.97849 14.4374 7.85133 14.4374 7.71875C14.4374 7.58616 14.3848 7.459 14.291 7.36523L7.57228 0.646483C7.47852 0.552715 7.35136 0.500025 7.21876 0.5ZM7.21876 1.70703L13.2305 7.71875L7.57374 13.3754C7.37274 13.5731 7.06478 13.5731 6.86378 13.3754L1.56239 8.0741C1.36466 7.87311 1.36441 7.56475 1.56214 7.36376C1.56203 7.36388 1.56225 7.36364 1.56214 7.36376L7.21876 1.70703Z', - PaperPlaneRight: - 'M2.8034 1.09789C2.60439 1.122 2.41703 1.20318 2.27264 1.32604C1.98386 1.57175 1.83203 2.0136 1.97931 2.42479L3.96649 7.99913C3.96637 7.99884 3.96661 7.99942 3.96649 7.99913C3.96637 7.99941 3.96661 8.00056 3.96649 8.00084L1.97918 13.5752C1.83191 13.9864 1.98386 14.4282 2.27264 14.6739C2.56142 14.9197 3.02192 14.9988 3.40423 14.7876C3.40509 14.7871 3.40594 14.7867 3.4068 14.7862L13.9631 8.87377L13.9606 8.87499C14.4477 8.60564 14.6207 7.97159 14.3381 7.49217C14.2482 7.33962 14.1189 7.21409 13.9644 7.12767C13.964 7.12718 13.9635 7.1267 13.9631 7.12621L13.9608 7.12499L3.4068 1.21373C3.40594 1.21328 3.40509 1.21283 3.40423 1.21239C3.21308 1.10679 3.00242 1.07378 2.8034 1.09789ZM2.92071 2.08751L13.4744 7.99877L13.4748 7.99742C13.4754 7.99828 13.476 7.99913 13.4766 7.99999C13.4766 7.99995 13.4766 8.00003 13.4766 7.99999C13.4759 8.00036 13.4751 8.00084 13.4743 8.00121L2.92071 13.9123L4.85016 8.49999H8.49994C8.63255 8.49999 8.75972 8.44731 8.85349 8.35354C8.94726 8.25977 8.99994 8.13259 8.99994 7.99999C8.99994 7.86738 8.94726 7.7402 8.85349 7.64643C8.75972 7.55266 8.63255 7.49999 8.49994 7.49999H4.85016L2.92071 2.08751C2.9208 2.08776 2.92063 2.08726 2.92071 2.08751Z', - Paperclip: - 'M10.9919 1.57034C10.3505 1.55456 9.72725 1.78616 9.25179 2.21694C9.24481 2.22326 9.23802 2.22977 9.23141 2.23647L3.02669 8.52859C3.02634 8.52896 3.02607 8.52934 3.02572 8.52969C1.67201 9.8921 1.67491 12.1086 3.03316 13.4668C4.39193 14.8256 6.60912 14.8286 7.9714 13.4733C7.97123 13.4735 7.97156 13.4731 7.9714 13.4733L13.1031 8.35403C13.1496 8.30766 13.1865 8.25258 13.2117 8.19194C13.2369 8.13131 13.2499 8.0663 13.25 8.00063C13.2501 7.93496 13.2372 7.86993 13.2122 7.80923C13.1871 7.74853 13.1504 7.69336 13.104 7.64688C13.0576 7.60038 13.0025 7.56348 12.9419 7.53828C12.8813 7.51307 12.8162 7.50006 12.7506 7.49998C12.6849 7.4999 12.6199 7.51276 12.5592 7.53781C12.4985 7.56287 12.4433 7.59964 12.3968 7.64602L7.26559 12.7648C6.28576 13.739 4.7173 13.7369 3.7402 12.7598C2.76293 11.7825 2.76094 10.2137 3.73568 9.23391C3.73621 9.23338 3.73674 9.23285 3.73727 9.23232L9.92745 2.95486C10.2124 2.69851 10.584 2.56066 10.9674 2.57009C11.555 2.58453 12.0776 2.93827 12.3092 3.47854C12.5402 4.01757 12.4369 4.6382 12.0444 5.07363L5.86739 11.3378C5.6662 11.5184 5.37188 11.5111 5.18038 11.3196C4.98853 11.1278 4.98126 10.8326 5.16292 10.6314L10.3564 5.35061C10.4494 5.25607 10.501 5.12847 10.4999 4.99588C10.4988 4.86329 10.4451 4.73657 10.3505 4.64358C10.256 4.55061 10.1284 4.49899 9.99582 4.50009C9.86323 4.50119 9.7365 4.55491 9.64352 4.64944L4.43722 9.94314C4.43261 9.94785 4.42809 9.95265 4.42367 9.95754C3.88887 10.5463 3.91083 11.4644 4.47323 12.0268C5.03563 12.5892 5.95373 12.6111 6.54244 12.0763C6.5492 12.0702 6.5558 12.0639 6.56222 12.0574L12.7685 5.76357C12.7734 5.75859 12.7782 5.7535 12.7829 5.74832C13.4386 5.02477 13.613 3.98196 13.2282 3.0845C12.8435 2.18703 11.9681 1.59432 10.9919 1.57034Z', - Paragraph: - 'M4.24999 3C4.43937 3 4.6125 3.107 4.6972 3.27639L6.4472 6.77639C6.5707 7.02338 6.47058 7.32372 6.22359 7.44721C5.9766 7.57071 5.67627 7.4706 5.55277 7.22361L5.17327 6.4646H3.3267L2.9472 7.22361C2.82371 7.4706 2.52337 7.57071 2.27638 7.44721C2.02939 7.32372 1.92928 7.02338 2.05277 6.77639L3.80277 3.27639C3.88747 3.107 4.0606 3 4.24999 3ZM3.8267 5.4646H4.67327L4.24999 4.61803L3.8267 5.4646Z M7.5 3.75C7.22386 3.75 7 3.97386 7 4.25C7 4.52614 7.22386 4.75 7.5 4.75H13.5C13.7761 4.75 14 4.52614 14 4.25C14 3.97386 13.7761 3.75 13.5 3.75H7.5Z M8 6.75C8 6.47386 8.22386 6.25 8.5 6.25H11.5C11.7761 6.25 12 6.47386 12 6.75C12 7.02614 11.7761 7.25 11.5 7.25H8.5C8.22386 7.25 8 7.02614 8 6.75Z M2 9.25C2 8.97386 2.22386 8.75 2.5 8.75H13.5C13.7761 8.75 14 8.97386 14 9.25C14 9.52614 13.7761 9.75 13.5 9.75H2.5C2.22386 9.75 2 9.52614 2 9.25Z M2 11.75C2 11.4739 2.22386 11.25 2.5 11.25H11.5C11.7761 11.25 12 11.4739 12 11.75C12 12.0261 11.7761 12.25 11.5 12.25H2.5C2.22386 12.25 2 12.0261 2 11.75Z', - Pause: - 'M3.5 2C2.95364 2 2.5 2.45364 2.5 3V13C2.5 13.5464 2.95364 14 3.5 14H5.75C6.29636 14 6.75 13.5464 6.75 13V3C6.75 2.45364 6.29636 2 5.75 2H3.5ZM3.5 3H5.75V13H3.5V3Z M10.25 2C9.70364 2 9.25 2.45364 9.25 3V13C9.25 13.5464 9.70364 14 10.25 14H12.5C13.0464 14 13.5 13.5464 13.5 13V3C13.5 2.45364 13.0464 2 12.5 2H10.25ZM10.25 3H12.5V13H10.25V3Z', - PencilSimple: - 'M10.5 1.71045C10.2406 1.71045 9.9813 1.80867 9.7876 2.00525L2.29017 9.50269C2.28988 9.50297 2.2896 9.50326 2.28931 9.50354C2.10332 9.69048 1.9991 9.94419 2.00001 10.2079V12.9999C2.00007 13.5462 2.45358 13.9998 2.99988 13.9999C2.99984 13.9999 2.99993 13.9999 2.99988 13.9999H5.79212C6.05578 14.0008 6.30942 13.8966 6.49635 13.7107C6.49667 13.7104 6.497 13.71 6.49732 13.7097L13.9948 6.21228C14.3878 5.82489 14.3878 5.17499 13.9948 4.7876L11.2124 2.00525C11.0187 1.80867 10.7594 1.71045 10.5 1.71045ZM10.4999 2.70715C10.4955 2.70269 10.5043 2.70269 10.4999 2.70715C10.5008 2.70801 10.5018 2.70887 10.5027 2.70972L13.2902 5.49719C13.291 5.49805 13.2919 5.4989 13.2927 5.49976C13.2972 5.49534 13.2972 5.50418 13.2927 5.49976C13.2919 5.50062 13.291 5.50183 13.2902 5.50269L12 6.79297L9.20704 4L10.4973 2.70972C10.4982 2.70887 10.499 2.70801 10.4999 2.70715ZM8.50001 4.70703L11.293 7.5L5.79297 12.9999H3.00013L3.00001 10.207L8.50001 4.70703Z', - Percent: - 'M4.75 2.5C3.51328 2.5 2.5 3.51328 2.5 4.75C2.5 5.98672 3.51328 7 4.75 7C5.98672 7 7 5.98672 7 4.75C7 3.51328 5.98672 2.5 4.75 2.5ZM4.75 3.5C5.44628 3.5 6 4.05372 6 4.75C6 5.44628 5.44628 6 4.75 6C4.05372 6 3.5 5.44628 3.5 4.75C3.5 4.05372 4.05372 3.5 4.75 3.5Z M11.25 9C10.0133 9 9 10.0133 9 11.25C9 12.4867 10.0133 13.5 11.25 13.5C12.4867 13.5 13.5 12.4867 13.5 11.25C13.5 10.0133 12.4867 9 11.25 9ZM11.25 10C11.9463 10 12.5 10.5537 12.5 11.25C12.5 11.9463 11.9463 12.5 11.25 12.5C10.5537 12.5 10 11.9463 10 11.25C10 10.5537 10.5537 10 11.25 10Z M12.5 3C12.3674 3.00002 12.2402 3.05271 12.1465 3.14648L3.14648 12.1465C3.05274 12.2402 3.00008 12.3674 3.00008 12.5C3.00008 12.6326 3.05274 12.7598 3.14648 12.8535C3.24025 12.9473 3.36741 12.9999 3.5 12.9999C3.63259 12.9999 3.75975 12.9473 3.85352 12.8535L12.8535 3.85352C12.9473 3.75975 12.9999 3.63259 12.9999 3.5C12.9999 3.36741 12.9473 3.24025 12.8535 3.14648C12.7598 3.05271 12.6326 3.00002 12.5 3Z', - PersonalCloseup: - 'M13.393 12.366C11.821 14.316 9.414 15.563 6.714 15.563 4.032 15.563 1.638 14.331.066 12.402 1.511 10.385 4.079 9.39 6.814 9.39 9.533 9.39 12.002 10.373 13.393 12.366ZM2.902 3.913C2.902 6.074 4.654 7.825 6.814 7.825 8.975 7.825 10.727 6.074 10.727 3.913 10.727 1.752 8.975 0 6.814 0 4.654 0 2.902 1.752 2.902 3.913Z', - PersonBolt: - 'M6.75 2C4.40279 2 2.5 3.90279 2.5 6.25C2.5 7.78227 3.31088 9.12515 4.52694 9.87293C4.24321 9.96591 3.96461 10.076 3.69272 10.2028C2.70011 10.6656 1.82093 11.3403 1.11696 12.1793C0.939472 12.3909 0.967077 12.7063 1.17862 12.8837C1.39017 13.0612 1.70554 13.0336 1.88304 12.8221C2.49314 12.0949 3.2551 11.5102 4.11536 11.1091C4.97562 10.7079 5.9133 10.5 6.8625 10.5C7.8117 10.5 8.74938 10.7079 9.60964 11.1091C9.66548 11.1351 9.7209 11.1619 9.77589 11.1895C10.0227 11.3133 10.3232 11.2135 10.447 10.9666C10.5707 10.7198 10.471 10.4193 10.2241 10.2956C10.1607 10.2637 10.0967 10.2328 10.0323 10.2028C9.7127 10.0537 9.38385 9.92781 9.04829 9.82558C10.2224 9.0693 11 7.75045 11 6.25C11 3.90279 9.09721 2 6.75 2ZM3.5 6.25C3.5 4.45507 4.95507 3 6.75 3C8.54493 3 10 4.45507 10 6.25C10 8.04493 8.54493 9.5 6.75 9.5C4.95507 9.5 3.5 8.04493 3.5 6.25Z M14.5177 9.0499C14.7208 9.14811 14.8329 9.37006 14.7915 9.59175L14.4667 11.3317L15.6864 11.8218C15.8366 11.8821 15.9483 12.0114 15.9861 12.1688C16.024 12.3261 15.9833 12.4921 15.877 12.6141L13.077 15.8284C12.9289 15.9985 12.6853 16.0483 12.4823 15.9501C12.2792 15.8519 12.1671 15.6299 12.2085 15.4083L12.5333 13.6683L11.3136 13.1782C11.1634 13.1179 11.0517 12.9886 11.0139 12.8312C10.976 12.6739 11.0167 12.5079 11.123 12.3859L13.923 9.17158C14.0711 9.00153 14.3147 8.95169 14.5177 9.0499ZM12.3389 12.5125L13.2864 12.8932C13.5075 12.982 13.6352 13.2146 13.5915 13.4489L13.5282 13.788L14.6611 12.4875L13.7136 12.1068C13.4925 12.018 13.3648 11.7854 13.4085 11.5511L13.4718 11.212L12.3389 12.5125Z', - Phone: - 'M5.05505 1.529C3.31253 1.75232 2.0004 3.24304 2 4.99983C2 4.99979 2 4.99987 2 4.99983C2 9.96446 6.03536 13.9999 11 13.9999C12.7569 13.9997 14.2478 12.6874 14.471 10.9448C14.5276 10.503 14.2784 10.0721 13.8672 9.90082L10.9504 8.64814C10.9505 8.64815 10.9504 8.64812 10.9504 8.64814C10.6386 8.51176 10.2765 8.54499 9.99475 8.73591C9.99494 8.73578 9.99463 8.73603 9.99475 8.73591L8.4348 9.77791C8.43415 9.77831 8.4335 9.77872 8.43285 9.77913C7.4727 9.31646 6.69738 8.54299 6.23241 7.58393L7.27404 5.99958C7.27519 5.99788 7.27633 5.99617 7.27746 5.99446C7.45646 5.71466 7.48468 5.36245 7.35265 5.05769L6.09912 2.13276C5.92786 1.72149 5.49693 1.4723 5.05505 1.529ZM5.17798 2.52192L6.4342 5.4532C6.43449 5.45389 6.43477 5.45458 6.43506 5.45527C6.43544 5.45472 6.43531 5.45587 6.43506 5.45527L5.39465 7.03779C5.394 7.0388 5.39335 7.03982 5.3927 7.04084C5.2041 7.33226 5.18035 7.7027 5.3302 8.01582C5.33052 8.01651 5.33085 8.0172 5.33118 8.01789C5.89488 9.182 6.8364 10.121 8.00195 10.6817C8.00285 10.6822 8.00374 10.6826 8.00464 10.6831C8.3233 10.834 8.70011 10.8053 8.99231 10.608L10.5527 9.56574C10.5526 9.56578 10.5528 9.5657 10.5527 9.56574L13.4777 10.8218C13.4776 10.8217 13.4778 10.8219 13.4777 10.8218C13.3156 12.0709 12.26 12.9997 11 12.9999C6.5758 12.9999 3 9.42414 3 4.99995C3.00034 3.73992 3.92891 2.68417 5.17798 2.52192Z', - PictureInPicture: - 'M9.50635 7.5C8.95999 7.5 8.50635 7.95364 8.50635 8.5V12.5C8.50635 12.6326 8.55903 12.7598 8.65279 12.8536C8.74656 12.9473 8.87374 13 9.00635 13C9.13896 13 9.26613 12.9473 9.3599 12.8536C9.45367 12.7598 9.50635 12.6326 9.50635 12.5V8.5H14.5063C14.639 8.5 14.7661 8.44732 14.8599 8.35355C14.9537 8.25979 15.0063 8.13261 15.0063 8C15.0063 7.86739 14.9537 7.74021 14.8599 7.64645C14.7661 7.55268 14.639 7.5 14.5063 7.5H9.50635Z M3.00635 3C2.45999 3 2.00635 3.45364 2.00635 4V12C2.00635 12.5464 2.45999 13 3.00635 13H14.0063C14.5527 13 15.0063 12.5464 15.0063 12V4C15.0063 3.45364 14.5527 3 14.0063 3H3.00635ZM3.00635 4H14.0063V12H3.00635V4Z', - PieChartElementFeature: - 'M1.5 8C1.5 4.41015 4.41015 1.5 8 1.5C11.5899 1.5 14.5 4.41015 14.5 8C14.5 11.5899 11.5899 14.5 8 14.5C4.41015 14.5 1.5 11.5899 1.5 8ZM8.5 7.13363V2.52242C10.1486 2.67099 11.5875 3.54706 12.4931 4.82714L8.5 7.13363ZM8.23699 8.44038C8.24556 8.43575 8.25399 8.43089 8.26225 8.42579L12.9941 5.69262C13.3188 6.3943 13.5 7.17599 13.5 8C13.5 11.0376 11.0376 13.5 8 13.5C6.14404 13.5 4.50279 12.5807 3.50675 11.1726L8.23699 8.44038ZM7.5 7.71124V2.52242C4.69675 2.77504 2.5 5.13098 2.5 8C2.5 8.82392 2.68117 9.60552 3.00584 10.3071L7.5 7.71124Z', - Pilcrow: - 'M5.25 2.5C4.38805 2.5 3.5614 2.84241 2.9519 3.4519C2.34241 4.0614 2 4.88804 2 5.75C2 6.61195 2.34241 7.4386 2.9519 8.0481C3.5614 8.65759 4.38805 9 5.25 9H8V12H7.5C7.22386 12 7 12.2239 7 12.5C7 12.7761 7.22386 13 7.5 13H11.5C11.7761 13 12 12.7761 12 12.5C12 12.2239 11.7761 12 11.5 12H11V3.5H12.7222C12.9984 3.5 13.2222 3.27614 13.2222 3C13.2222 2.72386 12.9984 2.5 12.7222 2.5H5.25ZM10 3.5H9V12H10V3.5ZM8 3.5H5.25C4.65326 3.5 4.08097 3.73705 3.65901 4.15901C3.23705 4.58097 3 5.15326 3 5.75C3 6.34674 3.23705 6.91903 3.65901 7.34099C4.08097 7.76295 4.65326 8 5.25 8H8V3.5Z', - Pivot: - 'M2 13.5C2 13.7761 2.22386 14 2.5 14C2.77614 14 3 13.7761 3 13.5V3.00068L13.5 3C13.7761 3 14 2.77614 14 2.5C14 2.22386 13.7761 2 13.5 2H3C2.44772 2 2 2.44839 2 3.00068V13.5Z M9 11.5C9.66304 11.5 10.2989 11.2366 10.7678 10.7678C11.2366 10.2989 11.5 9.66304 11.5 9V6.70711L10.8536 7.35355C10.6583 7.54882 10.3417 7.54882 10.1464 7.35355C9.95119 7.15829 9.95119 6.84171 10.1464 6.64645L11.6464 5.14645C11.7402 5.05268 11.8674 5 12 5C12.1381 5 12.2631 5.05596 12.3536 5.14645L13.8536 6.64645C14.0488 6.84171 14.0488 7.15829 13.8536 7.35355C13.6583 7.54882 13.3417 7.54882 13.1464 7.35355L12.5 6.70711V9C12.5 9.92826 12.1313 10.8185 11.4749 11.4749C10.8185 12.1313 9.92826 12.5 9 12.5H6.70711L7.35355 13.1464C7.54882 13.3417 7.54882 13.6583 7.35355 13.8536C7.15829 14.0488 6.84171 14.0488 6.64645 13.8536L5.14645 12.3536C4.95118 12.1583 4.95118 11.8417 5.14645 11.6464L6.64645 10.1464C6.84171 9.95118 7.15829 9.95118 7.35355 10.1464C7.54882 10.3417 7.54882 10.6583 7.35355 10.8536L6.70711 11.5H9Z', - Play: - 'M5.01514 1.50812C4.49587 1.51398 4.00063 1.93185 4 2.49933C4 2.49917 4 2.4995 4 2.49933V13.4998C4 13.4995 4 13.5001 4 13.4998C4.001 14.256 4.88038 14.7472 5.52429 14.3508L14.5105 8.85541C14.8125 8.67704 14.9988 8.35052 14.9987 7.99982C14.9987 7.64931 14.8125 7.32305 14.5109 7.1446L5.52502 1.64936C5.36411 1.5502 5.18811 1.50617 5.01514 1.50812ZM5 2.50043C5.00053 2.50076 5.00106 2.50108 5.00159 2.50141L13.9929 7.99994L5.00159 13.4984C5.0011 13.4986 5.00061 13.4989 5.00012 13.4992C5.00029 13.499 4.99995 13.4993 5.00012 13.4992C5.00015 13.5247 4.97913 13.5125 5.00012 13.4992L5 2.50043Z', - Plus: - 'M8 2C7.86739 2 7.74021 2.05268 7.64645 2.14645C7.55268 2.24021 7.5 2.36739 7.5 2.5V7.5H2.5C2.36739 7.5 2.24021 7.55268 2.14645 7.64645C2.05268 7.74021 2 7.86739 2 8C2 8.13261 2.05268 8.25979 2.14645 8.35355C2.24021 8.44732 2.36739 8.5 2.5 8.5H7.5V13.5C7.5 13.6326 7.55268 13.7598 7.64645 13.8536C7.74021 13.9473 7.86739 14 8 14C8.13261 14 8.25979 13.9473 8.35355 13.8536C8.44732 13.7598 8.5 13.6326 8.5 13.5V8.5H13.5C13.6326 8.5 13.7598 8.44732 13.8536 8.35355C13.9473 8.25979 14 8.13261 14 8C14 7.86739 13.9473 7.74021 13.8536 7.64645C13.7598 7.55268 13.6326 7.5 13.5 7.5H8.5V2.5C8.5 2.36739 8.44732 2.24021 8.35355 2.14645C8.25979 2.05268 8.13261 2 8 2Z', - PlusCircle: - 'M8 5C7.86739 5 7.74021 5.05268 7.64645 5.14645C7.55268 5.24021 7.5 5.36739 7.5 5.5V7.5H5.5C5.36739 7.5 5.24021 7.55268 5.14645 7.64645C5.05268 7.74021 5 7.86739 5 8C5 8.13261 5.05268 8.25979 5.14645 8.35355C5.24021 8.44732 5.36739 8.5 5.5 8.5H7.5V10.5C7.5 10.6326 7.55268 10.7598 7.64645 10.8536C7.74021 10.9473 7.86739 11 8 11C8.13261 11 8.25979 10.9473 8.35355 10.8536C8.44732 10.7598 8.5 10.6326 8.5 10.5V8.5H10.5C10.6326 8.5 10.7598 8.44732 10.8536 8.35355C10.9473 8.25979 11 8.13261 11 8C11 7.86739 10.9473 7.74021 10.8536 7.64645C10.7598 7.55268 10.6326 7.5 10.5 7.5H8.5V5.5C8.5 5.36739 8.44732 5.24021 8.35355 5.14645C8.25979 5.05268 8.13261 5 8 5Z M8 1.5C4.41604 1.5 1.5 4.41604 1.5 8C1.5 11.5839 4.41603 14.5 8 14.5C11.5839 14.5 14.5 11.5839 14.5 8C14.5 4.41603 11.5839 1.5 8 1.5ZM8 2.5C11.0435 2.5 13.5 4.95647 13.5 8C13.5 11.0435 11.0435 13.5 8 13.5C4.95647 13.5 2.5 11.0435 2.5 8C2.5 4.95647 4.95647 2.5 8 2.5Z', - PopoverElementFeature: - 'M8.5 8C8.5 7.17157 9.17157 6.5 10 6.5H11C11.8284 6.5 12.5 7.17157 12.5 8V10C12.5 10.8284 11.8284 11.5 11 11.5H10C9.17157 11.5 8.5 10.8284 8.5 10V8ZM10 7.5C9.72386 7.5 9.5 7.72386 9.5 8V10C9.5 10.2761 9.72386 10.5 10 10.5H11C11.2761 10.5 11.5 10.2761 11.5 10V8C11.5 7.72386 11.2761 7.5 11 7.5H10Z M1 3.5C1 2.67157 1.67157 2 2.5 2H13.5C14.3284 2 15 2.67157 15 3.5V12.5C15 13.3284 14.3284 14 13.5 14H2.5C1.67157 14 1 13.3284 1 12.5V3.5ZM2.5 3C2.22386 3 2 3.22386 2 3.5V12.5C2 12.7761 2.22386 13 2.5 13H13.5C13.7761 13 14 12.7761 14 12.5V3.5C14 3.22386 13.7761 3 13.5 3H2.5Z', - Printer: - 'M11.75 8C12.1642 8 12.5 7.66419 12.5 7.25C12.5 6.83581 12.1642 6.5 11.75 6.5C11.3358 6.5 11 6.83581 11 7.25C11 7.66419 11.3358 8 11.75 8Z M2.8313 4.5C1.98492 4.5 1.25 5.15455 1.25 6V11C1.25001 11.1326 1.3027 11.2598 1.39646 11.3535C1.49023 11.4473 1.6174 11.5 1.75 11.5H4C4.13261 11.5 4.25979 11.4473 4.35355 11.3536C4.44732 11.2598 4.5 11.1326 4.5 11C4.5 10.8674 4.44732 10.7402 4.35355 10.6464C4.25979 10.5527 4.13261 10.5 4 10.5H2.25V6C2.25 5.74545 2.49018 5.5 2.8313 5.5H13.1687C13.5098 5.5 13.75 5.74545 13.75 6V10.5H12C11.8674 10.5 11.7402 10.5527 11.6464 10.6464C11.5527 10.7402 11.5 10.8674 11.5 11C11.5 11.1326 11.5527 11.2598 11.6464 11.3536C11.7402 11.4473 11.8674 11.5 12 11.5H14.25C14.3826 11.5 14.5098 11.4473 14.6035 11.3535C14.6973 11.2598 14.75 11.1326 14.75 11V6C14.75 5.15455 14.0151 4.5 13.1687 4.5H2.8313Z M4 2C3.8674 2.00001 3.74023 2.0527 3.64646 2.14646C3.5527 2.24023 3.50001 2.3674 3.5 2.5V5C3.5 5.13261 3.55268 5.25979 3.64645 5.35355C3.74021 5.44732 3.86739 5.5 4 5.5C4.13261 5.5 4.25979 5.44732 4.35355 5.35355C4.44732 5.25979 4.5 5.13261 4.5 5V3H11.5V5C11.5 5.13261 11.5527 5.25979 11.6464 5.35355C11.7402 5.44732 11.8674 5.5 12 5.5C12.1326 5.5 12.2598 5.44732 12.3536 5.35355C12.4473 5.25979 12.5 5.13261 12.5 5V2.5C12.5 2.3674 12.4473 2.24023 12.3535 2.14646C12.2598 2.0527 12.1326 2.00001 12 2H4Z M4 9C3.8674 9.00001 3.74023 9.0527 3.64646 9.14646C3.5527 9.24023 3.50001 9.3674 3.5 9.5V13.75C3.50001 13.8826 3.5527 14.0098 3.64646 14.1035C3.74023 14.1973 3.8674 14.25 4 14.25H12C12.1326 14.25 12.2598 14.1973 12.3535 14.1035C12.4473 14.0098 12.5 13.8826 12.5 13.75V9.5C12.5 9.3674 12.4473 9.24023 12.3535 9.14646C12.2598 9.0527 12.1326 9.00001 12 9H4ZM4.5 10H11.5V13.25H4.5V10Z', - PushPinSimple: - 'M4 2C3.86739 2 3.74021 2.05268 3.64645 2.14645C3.55268 2.24021 3.5 2.36739 3.5 2.5C3.5 2.63261 3.55268 2.75979 3.64645 2.85355C3.74021 2.94732 3.86739 3 4 3H4.40405L3.08044 10.5H2.5C2.36739 10.5 2.24021 10.5527 2.14645 10.6464C2.05268 10.7402 2 10.8674 2 11C2 11.1326 2.05268 11.2598 2.14645 11.3536C2.24021 11.4473 2.36739 11.5 2.5 11.5H7.5V15C7.5 15.1326 7.55268 15.2598 7.64645 15.3536C7.74021 15.4473 7.86739 15.5 8 15.5C8.13261 15.5 8.25979 15.4473 8.35355 15.3536C8.44732 15.2598 8.5 15.1326 8.5 15V11.5H13.5C13.6326 11.5 13.7598 11.4473 13.8536 11.3536C13.9473 11.2598 14 11.1326 14 11C14 10.8674 13.9473 10.7402 13.8536 10.6464C13.7598 10.5527 13.6326 10.5 13.5 10.5H12.9196L11.5959 3H12C12.1326 3 12.2598 2.94732 12.3536 2.85355C12.4473 2.75979 12.5 2.63261 12.5 2.5C12.5 2.36739 12.4473 2.24021 12.3536 2.14645C12.2598 2.05268 12.1326 2 12 2H4ZM5.41956 3H10.5804L11.9041 10.5H4.09595L5.41956 3Z', - Question: - 'M8.07349 4.50134C7.75062 4.49155 7.43049 4.55078 7.13904 4.67151C6.36183 4.99344 5.75017 5.76895 5.75 6.74988C5.74997 6.88249 5.80262 7.00968 5.89636 7.10347C5.99011 7.19726 6.11727 7.24997 6.24988 7.25C6.31554 7.25002 6.38056 7.2371 6.44123 7.21199C6.5019 7.18687 6.55703 7.15006 6.60347 7.10364C6.64991 7.05722 6.68675 7.00211 6.71189 6.94145C6.73704 6.8808 6.74998 6.81578 6.75 6.75012C6.75013 6.17215 7.08092 5.77793 7.52173 5.59534C7.96254 5.41275 8.47515 5.45759 8.88391 5.86621C9.24251 6.22468 9.34907 6.75995 9.15503 7.22839C8.96099 7.69684 8.50716 8.00009 8.00012 8C7.93445 7.99999 7.86942 8.01292 7.80875 8.03804C7.74808 8.06316 7.69295 8.09999 7.6465 8.14642C7.60006 8.19285 7.56322 8.24797 7.53809 8.30864C7.51295 8.36931 7.50001 8.43433 7.5 8.5V9C7.5 9.13261 7.55268 9.25979 7.64645 9.35355C7.74021 9.44732 7.86739 9.5 8 9.5C8.13261 9.5 8.25979 9.44732 8.35355 9.35355C8.44732 9.25979 8.5 9.13261 8.5 9V8.93738C9.1999 8.77686 9.79665 8.2924 10.0789 7.61108C10.4266 6.77156 10.2336 5.80137 9.59094 5.15894C9.15735 4.7255 8.61159 4.51766 8.07349 4.50134Z M8 12C8.41419 12 8.75 11.6642 8.75 11.25C8.75 10.8358 8.41419 10.5 8 10.5C7.58581 10.5 7.25 10.8358 7.25 11.25C7.25 11.6642 7.58581 12 8 12Z M8 1.5C4.41604 1.5 1.5 4.41604 1.5 8C1.5 11.5839 4.41603 14.5 8 14.5C11.5839 14.5 14.5 11.5839 14.5 8C14.5 4.41603 11.5839 1.5 8 1.5ZM8 2.5C11.0435 2.5 13.5 4.95647 13.5 8C13.5 11.0435 11.0435 13.5 8 13.5C4.95647 13.5 2.5 11.0435 2.5 8C2.5 4.95647 4.95647 2.5 8 2.5Z', - Quotes: - 'M2.5 3.5C1.95364 3.5 1.5 3.95364 1.5 4.5V8.5C1.5 8.49996 1.5 8.50004 1.5 8.5C1.50007 9.04631 1.95357 9.49993 2.49988 9.5C2.49984 9.5 2.49992 9.5 2.49988 9.5H6.25V10C6.25006 11.1105 5.36051 12.0001 4.25 12C4.11739 12 3.99021 12.0527 3.89645 12.1464C3.80268 12.2402 3.75 12.3674 3.75 12.5C3.75 12.6326 3.80268 12.7598 3.89645 12.8536C3.99021 12.9473 4.11739 13 4.25 13C5.901 13.0001 7.25012 11.651 7.25 10V4.5C7.25 3.95364 6.79636 3.5 6.25 3.5H2.5ZM2.5 4.5H6.25V8.5H2.50012L2.5 4.5Z M9.74988 3.5C9.20357 3.50007 8.75007 3.95357 8.75 4.49988C8.75 4.49984 8.75 4.49992 8.75 4.49988V8.5C8.75 9.04636 9.20364 9.5 9.75 9.5H13.5V10C13.5 10.5306 13.2894 11.039 12.9142 11.4142C12.539 11.7894 12.0306 12 11.5 12C11.3674 12 11.2402 12.0527 11.1464 12.1464C11.0527 12.2402 11 12.3674 11 12.5C11 12.6326 11.0527 12.7598 11.1464 12.8536C11.2402 12.9473 11.3674 13 11.5 13C12.2955 13 13.0589 12.6839 13.6213 12.1213C14.1839 11.5589 14.5 10.7955 14.5 10V4.5C14.5 3.95364 14.0464 3.5 13.5 3.5H9.74988C9.74992 3.5 9.74984 3.5 9.74988 3.5ZM9.75 4.5H13.5V8.5H9.75V4.5Z', - Radio: - 'M1.5 8C1.5 4.41015 4.41015 1.5 8 1.5C11.5899 1.5 14.5 4.41015 14.5 8C14.5 11.5899 11.5899 14.5 8 14.5C4.41015 14.5 1.5 11.5899 1.5 8ZM8 2.5C4.96243 2.5 2.5 4.96243 2.5 8C2.5 11.0376 4.96243 13.5 8 13.5C11.0376 13.5 13.5 11.0376 13.5 8C13.5 4.96243 11.0376 2.5 8 2.5Z', - RadioButton: - 'M8 4C5.79678 4 4 5.79678 4 8C4 10.2032 5.79678 12 8 12C10.2032 12 12 10.2032 12 8C12 5.79678 10.2032 4 8 4ZM8 5C9.66278 5 11 6.33722 11 8C11 9.66278 9.66278 11 8 11C6.33722 11 5 9.66278 5 8C5 6.33722 6.33722 5 8 5Z M8 1.5C4.41604 1.5 1.5 4.41604 1.5 8C1.5 11.5839 4.41603 14.5 8 14.5C11.5839 14.5 14.5 11.5839 14.5 8C14.5 4.41603 11.5839 1.5 8 1.5ZM8 2.5C11.0435 2.5 13.5 4.95647 13.5 8C13.5 11.0435 11.0435 13.5 8 13.5C4.95647 13.5 2.5 11.0435 2.5 8C2.5 4.95647 4.95647 2.5 8 2.5Z', - RecordDetailElementFeature: - 'M9.00003 5.50012C8.99998 5.22398 9.2238 5.0001 9.49994 5.00007L11.9999 4.99979C12.2761 4.99976 12.5 5.22359 12.5 5.49973C12.5001 5.77588 12.2763 5.99976 12.0001 5.99979L9.50011 6.00007C9.22397 6.0001 9.00007 5.77626 9.00003 5.50012Z M9 8C9 7.72386 9.22386 7.5 9.5 7.5H12C12.2761 7.5 12.5 7.72386 12.5 8C12.5 8.27614 12.2761 8.5 12 8.5H9.5C9.22386 8.5 9 8.27614 9 8Z M3.5 10.5C3.5 10.2239 3.72386 10 4 10H12C12.2761 10 12.5 10.2239 12.5 10.5C12.5 10.7761 12.2761 11 12 11H4C3.72386 11 3.5 10.7761 3.5 10.5Z M3.5 5.5C3.5 5.22386 3.72386 5 4 5H7C7.27614 5 7.5 5.22386 7.5 5.5V8C7.5 8.27614 7.27614 8.5 7 8.5H4C3.72386 8.5 3.5 8.27614 3.5 8V5.5ZM4.5 6V7.5H6.5V6H4.5Z M1 3.5C1 2.67157 1.67157 2 2.5 2H13.5C14.3284 2 15 2.67157 15 3.5V12.5C15 13.3284 14.3284 14 13.5 14H2.5C1.67157 14 1 13.3284 1 12.5V3.5ZM2.5 3C2.22386 3 2 3.22386 2 3.5V12.5C2 12.7761 2.22386 13 2.5 13H13.5C13.7761 13 14 12.7761 14 12.5V3.5C14 3.22386 13.7761 3 13.5 3H2.5Z', - RichText: - 'M11.6159 1.76375C11.8846 1.8278 12.0504 2.09747 11.9863 2.36608L11.6008 3.98316C11.9532 3.82101 12.3472 3.7652 12.7579 3.85855L12.7601 3.85905C13.357 3.99757 13.788 4.43143 14.0299 4.95253C14.2714 5.47296 14.3413 6.10784 14.2164 6.73938L14.2154 6.7439L14.2134 6.75319C13.9345 7.98014 12.8651 9.01048 11.6496 8.73418C11.219 8.63629 10.8758 8.39843 10.6245 8.07751L10.5705 8.30417C10.5064 8.57278 10.2367 8.73862 9.96813 8.67457C9.69952 8.61052 9.53369 8.34085 9.59773 8.07223L11.0136 2.13414C11.0777 1.86553 11.3473 1.6997 11.6159 1.76375ZM11.1692 6.0612C11.3798 5.13469 12.0694 4.72844 12.5352 4.83343C12.7648 4.88714 12.979 5.06374 13.1228 5.3735C13.2663 5.68282 13.3225 6.09738 13.2367 6.53842C13.0241 7.46078 12.3359 7.86466 11.8713 7.75905C11.4055 7.65318 10.9585 6.98844 11.1692 6.0612Z M5.95321 2.26618C5.86656 2.1025 5.69652 2.00012 5.51132 2.00012C5.32611 2.00012 5.15608 2.1025 5.06943 2.26618L2.05808 7.95427C1.92888 8.19832 2.02198 8.5009 2.26603 8.63011C2.51009 8.75931 2.81267 8.66621 2.94187 8.42216L3.68676 7.01514H7.33587L8.08077 8.42216C8.20997 8.66621 8.51255 8.75931 8.7566 8.63011C9.00066 8.5009 9.09376 8.19832 8.96455 7.95427L5.95321 2.26618ZM5.51132 3.56875L6.80646 6.01514H4.21618L5.51132 3.56875Z M2.49998 10.5001C2.22384 10.5001 1.99998 10.724 1.99998 11.0001C1.99998 11.2763 2.22384 11.5001 2.49998 11.5001H13.5C13.7761 11.5001 14 11.2763 14 11.0001C14 10.724 13.7761 10.5001 13.5 10.5001H2.49998Z M1.99998 13.5001C1.99998 13.224 2.22384 13.0001 2.49998 13.0001H11.5C11.7761 13.0001 12 13.224 12 13.5001C12 13.7763 11.7761 14.0001 11.5 14.0001H2.49998C2.22384 14.0001 1.99998 13.7763 1.99998 13.5001Z', - RowHeightExtraLarge: - 'M12.1464 3.64645L13.1464 2.64645C13.3417 2.45118 13.6583 2.45118 13.8536 2.64645L14.8536 3.64645C15.0488 3.84171 15.0488 4.15829 14.8536 4.35355C14.6583 4.54882 14.3417 4.54882 14.1464 4.35355L14 4.20711V11.7929L14.1464 11.6464C14.3417 11.4512 14.6583 11.4512 14.8536 11.6464C15.0488 11.8417 15.0488 12.1583 14.8536 12.3536L13.8536 13.3536C13.7598 13.4473 13.6326 13.5 13.5 13.5C13.3674 13.5 13.2402 13.4473 13.1464 13.3536L12.1464 12.3536C11.9512 12.1583 11.9512 11.8417 12.1464 11.6464C12.3417 11.4512 12.6583 11.4512 12.8536 11.6464L13 11.7929V4.20711L12.8536 4.35355C12.6583 4.54882 12.3417 4.54882 12.1464 4.35355C11.9512 4.15829 11.9512 3.84171 12.1464 3.64645Z M1 4.5C1 3.67157 1.67157 3 2.5 3H8.5C9.32843 3 10 3.67157 10 4.5V11.5C10 12.3284 9.32843 13 8.5 13H2.5C1.67157 13 1 12.3284 1 11.5V4.5ZM2.5 4C2.22386 4 2 4.22386 2 4.5V11.5C2 11.7761 2.22386 12 2.5 12H8.5C8.77614 12 9 11.7761 9 11.5V4.5C9 4.22386 8.77614 4 8.5 4H2.5Z', - RowHeightLarge: - 'M13.1464 2.64645L12.1464 3.64645C11.9512 3.84171 11.9512 4.15829 12.1464 4.35355C12.3417 4.54882 12.6583 4.54882 12.8536 4.35355L13 4.20711V11.7929L12.8536 11.6464C12.6583 11.4512 12.3417 11.4512 12.1464 11.6464C11.9512 11.8417 11.9512 12.1583 12.1464 12.3536L13.1464 13.3536C13.2402 13.4473 13.3674 13.5 13.5 13.5C13.6326 13.5 13.7598 13.4473 13.8536 13.3536L14.8536 12.3536C15.0488 12.1583 15.0488 11.8417 14.8536 11.6464C14.6583 11.4512 14.3417 11.4512 14.1464 11.6464L14 11.7929V4.20711L14.1464 4.35355C14.3417 4.54882 14.6583 4.54882 14.8536 4.35355C15.0488 4.15829 15.0488 3.84171 14.8536 3.64645L13.8536 2.64645C13.6583 2.45118 13.3417 2.45118 13.1464 2.64645Z M1 4.5C1 3.67157 1.67157 3 2.5 3H8.5C9.32843 3 10 3.67157 10 4.5V8.5C10 9.32843 9.32843 10 8.5 10H2.5C1.67157 10 1 9.32843 1 8.5V4.5ZM2.5 4C2.22386 4 2 4.22386 2 4.5V8.5C2 8.77614 2.22386 9 2.5 9H8.5C8.77614 9 9 8.77614 9 8.5V4.5C9 4.22386 8.77614 4 8.5 4H2.5Z M1.5 12C1.22386 12 1 12.2239 1 12.5C1 12.7761 1.22386 13 1.5 13H9.5C9.77614 13 10 12.7761 10 12.5C10 12.2239 9.77614 12 9.5 12H1.5Z', - RowHeightMedium: - 'M12.1464 3.64645L13.1464 2.64645C13.3417 2.45118 13.6583 2.45118 13.8536 2.64645L14.8536 3.64645C15.0488 3.84171 15.0488 4.15829 14.8536 4.35355C14.6583 4.54882 14.3417 4.54882 14.1464 4.35355L14 4.20711V11.7929L14.1464 11.6464C14.3417 11.4512 14.6583 11.4512 14.8536 11.6464C15.0488 11.8417 15.0488 12.1583 14.8536 12.3536L13.8536 13.3536C13.7598 13.4473 13.6326 13.5 13.5 13.5C13.3674 13.5 13.2402 13.4473 13.1464 13.3536L12.1464 12.3536C11.9512 12.1583 11.9512 11.8417 12.1464 11.6464C12.3417 11.4512 12.6583 11.4512 12.8536 11.6464L13 11.7929V4.20711L12.8536 4.35355C12.6583 4.54882 12.3417 4.54882 12.1464 4.35355C11.9512 4.15829 11.9512 3.84171 12.1464 3.64645Z M2.5 3C1.67157 3 1 3.67157 1 4.5V5.5C1 6.32843 1.67157 7 2.5 7H8.5C9.32843 7 10 6.32843 10 5.5V4.5C10 3.67157 9.32843 3 8.5 3H2.5ZM2 4.5C2 4.22386 2.22386 4 2.5 4H8.5C8.77614 4 9 4.22386 9 4.5V5.5C9 5.77614 8.77614 6 8.5 6H2.5C2.22386 6 2 5.77614 2 5.5V4.5Z M1.5 9C1.22386 9 1 9.22386 1 9.5C1 9.77614 1.22386 10 1.5 10H9.5C9.77614 10 10 9.77614 10 9.5C10 9.22386 9.77614 9 9.5 9H1.5Z M1 12.5C1 12.2239 1.22386 12 1.5 12H9.5C9.77614 12 10 12.2239 10 12.5C10 12.7761 9.77614 13 9.5 13H1.5C1.22386 13 1 12.7761 1 12.5Z', - RowHeightSmall: - 'M13.1464 2.64645L12.1464 3.64645C11.9512 3.84171 11.9512 4.15829 12.1464 4.35355C12.3417 4.54882 12.6583 4.54882 12.8536 4.35355L13 4.20711V11.7929L12.8536 11.6464C12.6583 11.4512 12.3417 11.4512 12.1464 11.6464C11.9512 11.8417 11.9512 12.1583 12.1464 12.3536L13.1464 13.3536C13.2402 13.4473 13.3674 13.5 13.5 13.5C13.6326 13.5 13.7598 13.4473 13.8536 13.3536L14.8536 12.3536C15.0488 12.1583 15.0488 11.8417 14.8536 11.6464C14.6583 11.4512 14.3417 11.4512 14.1464 11.6464L14 11.7929V4.20711L14.1464 4.35355C14.3417 4.54882 14.6583 4.54882 14.8536 4.35355C15.0488 4.15829 15.0488 3.84171 14.8536 3.64645L13.8536 2.64645C13.6583 2.45118 13.3417 2.45118 13.1464 2.64645Z M1.5 3C1.22386 3 1 3.22386 1 3.5C1 3.77614 1.22386 4 1.5 4H9.5C9.77614 4 10 3.77614 10 3.5C10 3.22386 9.77614 3 9.5 3H1.5Z M1.5 6C1.22386 6 1 6.22386 1 6.5C1 6.77614 1.22386 7 1.5 7H9.5C9.77614 7 10 6.77614 10 6.5C10 6.22386 9.77614 6 9.5 6H1.5Z M1 9.5C1 9.22386 1.22386 9 1.5 9H9.5C9.77614 9 10 9.22386 10 9.5C10 9.77614 9.77614 10 9.5 10H1.5C1.22386 10 1 9.77614 1 9.5Z M1.5 12C1.22386 12 1 12.2239 1 12.5C1 12.7761 1.22386 13 1.5 13H9.5C9.77614 13 10 12.7761 10 12.5C10 12.2239 9.77614 12 9.5 12H1.5Z', - Rss: - 'M3.25 13.5C3.66419 13.5 4 13.1642 4 12.75C4 12.3358 3.66419 12 3.25 12C2.83582 12 2.5 12.3358 2.5 12.75C2.5 13.1642 2.83582 13.5 3.25 13.5Z M2.99903 2.50002C2.93337 2.50015 2.86837 2.51321 2.80776 2.53845C2.74714 2.5637 2.6921 2.60064 2.64576 2.64716C2.59942 2.69368 2.5627 2.74887 2.53769 2.80958C2.51268 2.87029 2.49987 2.93533 2.5 3.001C2.50013 3.06666 2.51319 3.13165 2.53844 3.19226C2.56368 3.25288 2.60062 3.30792 2.64714 3.35426C2.69366 3.4006 2.74885 3.43732 2.80956 3.46233C2.87027 3.48734 2.93532 3.50015 3.00098 3.50002C5.52071 3.49526 7.9373 4.49734 9.71424 6.28383C9.71488 6.28448 9.71553 6.28513 9.71619 6.28578C10.6006 7.16547 11.3019 8.21163 11.7797 9.36391C12.2575 10.5162 12.5024 11.7516 12.5 12.999C12.4999 13.0647 12.5127 13.1297 12.5377 13.1905C12.5627 13.2512 12.5994 13.3064 12.6458 13.3529C12.6921 13.3994 12.7471 13.4363 12.8078 13.4616C12.8684 13.4868 12.9334 13.4999 12.999 13.5C13.0647 13.5001 13.1297 13.4873 13.1904 13.4623C13.2512 13.4373 13.3063 13.4006 13.3529 13.3543C13.3994 13.3079 13.4363 13.2529 13.4616 13.1923C13.4868 13.1316 13.4999 13.0667 13.5 13.001C13.5026 11.6215 13.2318 10.2551 12.7034 8.98085C12.1752 7.70697 11.3999 6.55042 10.4222 5.57778C8.45752 3.60299 5.78472 2.49476 2.99903 2.50002Z M3 5.50002C2.86739 5.50002 2.74022 5.5527 2.64645 5.64647C2.55268 5.74023 2.5 5.86741 2.5 6.00002C2.5 6.13263 2.55268 6.2598 2.64645 6.35357C2.74022 6.44734 2.86739 6.50002 3 6.50002C6.59577 6.50002 9.5 9.40425 9.5 13C9.5 13.1326 9.55268 13.2598 9.64645 13.3536C9.74022 13.4473 9.86739 13.5 10 13.5C10.1326 13.5 10.2598 13.4473 10.3536 13.3536C10.4473 13.2598 10.5 13.1326 10.5 13C10.5 8.86381 7.13621 5.50002 3 5.50002Z M2.99842 8.50002C2.93275 8.50023 2.86778 8.51337 2.80719 8.53869C2.74661 8.56401 2.69161 8.60101 2.64533 8.64759C2.59904 8.69416 2.56239 8.7494 2.53745 8.81014C2.51252 8.87089 2.49979 8.93594 2.5 9.00161C2.50021 9.06727 2.51335 9.13224 2.53867 9.19283C2.56399 9.25341 2.60099 9.30841 2.64757 9.35469C2.69415 9.40098 2.74938 9.43763 2.81013 9.46257C2.87087 9.4875 2.93593 9.50023 3.00159 9.50002C3.9305 9.49715 4.82164 9.8647 5.47852 10.5215C6.13535 11.1784 6.50289 12.0695 6.5 12.9984C6.49979 13.0641 6.51252 13.1292 6.53745 13.1899C6.56239 13.2506 6.59904 13.3059 6.64533 13.3525C6.69161 13.399 6.74661 13.436 6.80719 13.4614C6.86778 13.4867 6.93275 13.4998 6.99842 13.5C7.06408 13.5002 7.12914 13.4875 7.18988 13.4626C7.25062 13.4376 7.30586 13.401 7.35243 13.3547C7.39901 13.3084 7.43602 13.2534 7.46134 13.1928C7.48666 13.1322 7.49979 13.0673 7.5 13.0016C7.50372 11.8068 7.03051 10.6594 6.18567 9.81447C6.18567 9.81451 6.18567 9.81443 6.18567 9.81447C5.34077 8.96962 4.19324 8.49633 2.99842 8.50002Z', - ScatterPlotElementFeature: - 'M2 2.5C2.27614 2.5 2.5 2.72386 2.5 3V12.5H14C14.2761 12.5 14.5 12.7239 14.5 13C14.5 13.2761 14.2761 13.5 14 13.5H2C1.72386 13.5 1.5 13.2761 1.5 13V3C1.5 2.72386 1.72386 2.5 2 2.5Z M6 8.5C5.44772 8.5 5 8.94772 5 9.5C5 10.0523 5.44772 10.5 6 10.5C6.55228 10.5 7 10.0523 7 9.5C7 8.94772 6.55228 8.5 6 8.5Z M4 6C4 5.44772 4.44772 5 5 5C5.55228 5 6 5.44772 6 6C6 6.55228 5.55228 7 5 7C4.44772 7 4 6.55228 4 6Z M9 6.5C8.44772 6.5 8 6.94772 8 7.5C8 8.05228 8.44772 8.5 9 8.5C9.55228 8.5 10 8.05228 10 7.5C10 6.94772 9.55228 6.5 9 6.5Z M7.5 4C7.5 3.44772 7.94772 3 8.5 3C9.05228 3 9.5 3.44772 9.5 4C9.5 4.55228 9.05228 5 8.5 5C7.94772 5 7.5 4.55228 7.5 4Z M12 4.5C11.4477 4.5 11 4.94772 11 5.5C11 6.05228 11.4477 6.5 12 6.5C12.5523 6.5 13 6.05228 13 5.5C13 4.94772 12.5523 4.5 12 4.5Z M11 9.5C11 8.94772 11.4477 8.5 12 8.5C12.5523 8.5 13 8.94772 13 9.5C13 10.0523 12.5523 10.5 12 10.5C11.4477 10.5 11 10.0523 11 9.5Z', - Scissors: - 'M3.75 2.5C2.51328 2.5 1.5 3.51328 1.5 4.75C1.5 5.98672 2.51328 7 3.75 7C4.33649 7 4.86827 6.76704 5.27002 6.39551L7.61462 8.00012L5.27002 9.60449C4.86827 9.23297 4.33649 9 3.75 9C2.51328 9 1.5 10.0133 1.5 11.25C1.5 12.4867 2.51328 13.5 3.75 13.5C4.98672 13.5 6 12.4867 6 11.25C6 10.9594 5.9399 10.683 5.83826 10.4274L8.49988 8.60596L14.2177 12.5189C14.3271 12.5938 14.4618 12.6221 14.5921 12.5977C14.7224 12.5733 14.8377 12.4981 14.9126 12.3887C14.9875 12.2792 15.0158 12.1446 14.9914 12.0142C14.967 11.8839 14.8918 11.7686 14.7823 11.6937L5.83813 5.57275C5.93981 5.31704 6 5.04065 6 4.75C6 3.51328 4.98672 2.5 3.75 2.5ZM3.75 3.5C4.44628 3.5 5 4.05372 5 4.75C5 5.01055 4.92235 5.25111 4.78882 5.4502C4.78624 5.45184 4.78368 5.45351 4.78113 5.4552C4.78067 5.45735 4.78022 5.45951 4.77979 5.46167C4.55544 5.78772 4.18041 6 3.75 6C3.05372 6 2.5 5.44628 2.5 4.75C2.5 4.05372 3.05372 3.5 3.75 3.5ZM3.75 10C4.18041 10 4.55544 10.2123 4.77979 10.5383C4.78022 10.5405 4.78067 10.5427 4.78113 10.5449C4.78372 10.5467 4.78632 10.5484 4.78894 10.55C4.92238 10.7491 5 10.9895 5 11.25C5 11.9463 4.44628 12.5 3.75 12.5C3.05372 12.5 2.5 11.9463 2.5 11.25C2.5 10.5537 3.05372 10 3.75 10Z M14.5923 3.40234C14.5277 3.39023 14.4615 3.39095 14.3972 3.40446C14.3329 3.41797 14.272 3.44401 14.2178 3.48108L9.98657 6.37488C9.87711 6.44973 9.80187 6.565 9.7774 6.69533C9.75293 6.82565 9.78123 6.96036 9.85608 7.06982C9.93093 7.17928 10.0462 7.25452 10.1765 7.27899C10.3069 7.30347 10.4416 7.27517 10.551 7.20032L14.7823 4.30652C14.8366 4.26945 14.8829 4.22206 14.9188 4.16706C14.9547 4.11206 14.9794 4.05053 14.9915 3.98599C15.0036 3.92144 15.0029 3.85515 14.9894 3.79088C14.9759 3.72662 14.9498 3.66565 14.9127 3.61145C14.8379 3.50202 14.7226 3.4268 14.5923 3.40234Z', - SelectCaret: - 'M7.18866 2.93509C7.58748 2.3542 8.41323 2.35586 8.81193 2.93472L10.8113 5.84444C11.0439 6.18316 11.0462 6.59001 10.9008 6.90704C10.7549 7.2253 10.4341 7.5 9.99908 7.5H6.00095C5.56589 7.5 5.24509 7.22532 5.09917 6.90706C4.95382 6.59004 4.95602 6.18328 5.18862 5.84456L7.18866 2.93509ZM8.00042 3.51949L9.987 6.41065C9.99532 6.42278 9.99901 6.43461 9.99982 6.44737C10.0007 6.46174 9.9978 6.4772 9.99179 6.49031C9.9899 6.49443 9.98805 6.49761 9.98645 6.5H6.01354C6.01194 6.49761 6.01008 6.49442 6.00818 6.49028C6.00217 6.47718 5.99924 6.46171 6.00015 6.44734C6.00096 6.43459 6.00464 6.42277 6.01297 6.41064L8.00042 3.51949Z M5.0992 9.09293C5.24513 8.77468 5.56592 8.5 6.00098 8.5H9.99911C10.4342 8.5 10.7549 8.7747 10.9008 9.09296C11.0462 9.40998 11.044 9.81674 10.8114 10.1555L8.81166 13.0645C8.41285 13.6453 7.58677 13.6441 7.18808 13.0653L5.18872 10.1556C4.95613 9.81683 4.95385 9.40995 5.0992 9.09293ZM6.01299 9.58936L7.99959 12.4805L9.98703 9.58934C9.9953 9.57725 9.99904 9.56535 9.99985 9.55263C10.0008 9.53826 9.99783 9.5228 9.99182 9.50969C9.98993 9.50557 9.98808 9.50239 9.98648 9.5H6.01357C6.01197 9.50239 6.01011 9.50558 6.00821 9.50972C6.0022 9.52282 5.99927 9.53829 6.00018 9.55266C6.00099 9.5654 6.00468 9.57725 6.01299 9.58936Z', - Shapes: - 'M7.00499 2.00002C7.28683 2.0023 7.49605 2.17124 7.61259 2.35521L9.87837 5.93199C9.99471 6.11565 10.0737 6.40994 9.89683 6.68347C9.73379 6.93564 9.45909 7 9.26566 7H4.7341C4.53608 7 4.25765 6.933 4.0969 6.67396C3.92729 6.40062 4.008 6.11098 4.12139 5.93199L6.38717 2.35521C6.50655 2.16676 6.72 1.99772 7.00499 2.00002ZM6.99988 3.25667L5.26207 6H8.73769L6.99988 3.25667Z M9 8.82734C9 8.3811 9.36512 8 9.82734 8H13.1727C13.6189 8 14 8.36512 14 8.82734V12.1727C14 12.6189 13.6349 13 13.1727 13H9.82734C9.3811 13 9 12.6349 9 12.1727V8.82734ZM10 9V12H13V9H10Z M2 11.25C2 9.73158 3.23158 8.5 4.75 8.5C6.26916 8.5 7.5 9.73168 7.5 11.25C7.5 12.7683 6.26916 14 4.75 14C3.23158 14 2 12.7684 2 11.25ZM4.75 9.5C3.78387 9.5 3 10.2839 3 11.25C3 12.2161 3.78387 13 4.75 13C5.71669 13 6.5 12.2162 6.5 11.25C6.5 10.2838 5.71669 9.5 4.75 9.5Z', - Share: - 'M10.4999 6C7.53819 6.00085 4.94618 8.00758 4.20349 10.8746C4.17025 11.003 4.18936 11.1393 4.25661 11.2536C4.32387 11.3679 4.43377 11.4508 4.56213 11.484C4.6905 11.5173 4.82682 11.4981 4.94109 11.4309C5.05537 11.3636 5.13826 11.2537 5.17151 11.1254C5.80119 8.69457 7.98899 7.00077 10.5 7H14C14.1326 7 14.2598 6.94732 14.3536 6.85355C14.4473 6.75979 14.5 6.63261 14.5 6.5C14.5 6.36739 14.4473 6.24021 14.3536 6.14645C14.2598 6.05268 14.1326 6 14 6H10.5C10.5 6 10.4999 6 10.4999 6H10.4999ZM11 3C10.8674 3.00002 10.7402 3.05271 10.6465 3.14648C10.5527 3.24025 10.5001 3.36741 10.5001 3.5C10.5001 3.63259 10.5527 3.75975 10.6465 3.85352L13.293 6.5L10.6465 9.14648C10.5527 9.24025 10.5001 9.36741 10.5001 9.5C10.5001 9.63259 10.5527 9.75975 10.6465 9.85352C10.7402 9.94726 10.8674 9.99992 11 9.99992C11.1326 9.99992 11.2598 9.94726 11.3535 9.85352L14.3535 6.85352C14.4472 6.75974 14.4999 6.63259 14.4999 6.5C14.4999 6.36741 14.4472 6.24026 14.3535 6.14648L11.3535 3.14648C11.2598 3.05271 11.1326 3.00002 11 3ZM2 5C1.86739 5 1.74021 5.05268 1.64645 5.14645C1.55268 5.24021 1.5 5.36739 1.5 5.5V13C1.5 13 1.5 13.0001 1.5 13.0001C1.50007 13.5464 1.95357 13.9999 2.49988 14C2.49992 14 2.49996 14 2.5 14H12C12.1326 14 12.2598 13.9473 12.3536 13.8536C12.4473 13.7598 12.5 13.6326 12.5 13.5C12.5 13.3674 12.4473 13.2402 12.3536 13.1464C12.2598 13.0527 12.1326 13 12 13H2.50012H2.5V5.5C2.5 5.36739 2.44732 5.24021 2.35355 5.14645C2.25979 5.05268 2.13261 5 2 5Z', - ShareWithBolt: - 'M13.9621 2.30861C13.9377 2.24964 13.9015 2.19439 13.8536 2.14645C13.8056 2.09851 13.7504 2.06234 13.6914 2.03794C13.6324 2.01349 13.5678 2 13.5 2H9.75C9.47386 2 9.25 2.22386 9.25 2.5C9.25 2.77614 9.47386 3 9.75 3H12.2929L8.64645 6.64645C8.45118 6.84171 8.45118 7.15829 8.64645 7.35355C8.84171 7.54882 9.15829 7.54882 9.35355 7.35355L13 3.70711V6.25C13 6.52614 13.2239 6.75 13.5 6.75C13.7761 6.75 14 6.52614 14 6.25V2.497C13.9996 2.4303 13.9861 2.36669 13.9621 2.30861Z M2.29289 4.29289C2.48043 4.10536 2.73478 4 3 4H7C7.27614 4 7.5 4.22386 7.5 4.5C7.5 4.77614 7.27614 5 7 5H3V13H9.5C9.77614 13 10 13.2239 10 13.5C10 13.7761 9.77614 14 9.5 14H3C2.73478 14 2.48043 13.8946 2.29289 13.7071C2.10536 13.5196 2 13.2652 2 13V5C2 4.73478 2.10536 4.48043 2.29289 4.29289Z M14.5178 9.04993C14.7208 9.14814 14.8329 9.37009 14.7915 9.59177L14.4667 11.3317L15.6864 11.8218C15.8366 11.8821 15.9483 12.0114 15.9861 12.1688C16.024 12.3262 15.9833 12.4921 15.877 12.6142L13.077 15.8284C12.9289 15.9985 12.6853 16.0483 12.4823 15.9501C12.2793 15.8519 12.1671 15.63 12.2085 15.4083L12.5333 13.6683L11.3136 13.1783C11.1634 13.1179 11.0517 12.9886 11.0139 12.8312C10.976 12.6739 11.0167 12.5079 11.123 12.3859L13.923 9.1716C14.0711 9.00156 14.3147 8.95172 14.5178 9.04993ZM12.3389 12.5125L13.2864 12.8932C13.5076 12.9821 13.6352 13.2146 13.5915 13.4489L13.5282 13.7881L14.6611 12.4875L13.7136 12.1068C13.4925 12.018 13.3648 11.7854 13.4085 11.5511L13.4718 11.212L12.3389 12.5125Z', - ShieldSlash: - 'M2.11767 1.00057C2.05207 0.997427 1.98649 1.00723 1.92468 1.02944C1.86288 1.05164 1.80605 1.0858 1.75744 1.12997C1.65931 1.21916 1.60064 1.34368 1.59432 1.47614C1.588 1.6086 1.63456 1.73814 1.72375 1.83627L2.47021 2.65768C2.18983 2.83613 2 3.14773 2 3.49996V7.16867C2 9.93513 3.19725 11.8365 4.50048 13.0359C5.79475 14.2269 7.17737 14.7545 7.67236 14.9193C7.88359 14.9969 8.11576 14.9969 8.32702 14.9194C8.84887 14.7466 10.3617 14.1625 11.7079 12.8241L13.5363 14.8363C13.6254 14.9344 13.75 14.9931 13.8824 14.9994C14.0149 15.0057 14.1444 14.9592 14.2425 14.87C14.3407 14.7808 14.3994 14.6562 14.4057 14.5238C14.412 14.3913 14.3654 14.2618 14.2762 14.1637L2.46374 1.16366C2.37457 1.06557 2.25009 1.00691 2.11767 1.00057ZM3 3.49996H3.23547L11.0343 12.0828C9.83622 13.2848 8.4266 13.8345 8 13.9749C7.59778 13.8417 6.32382 13.3548 5.17761 12.3C4.03084 11.2447 3 9.65219 3 7.16867V3.49996Z M6.15625 2.49996C6.02364 2.49996 5.89646 2.55264 5.80269 2.64641C5.70892 2.74018 5.65625 2.86736 5.65625 2.99996C5.65625 3.13257 5.70892 3.25975 5.80269 3.35352C5.89646 3.44729 6.02364 3.49996 6.15625 3.49996H13V7.16867C13 8.35692 12.7599 9.33118 12.3884 10.1495C12.3613 10.2093 12.3462 10.2738 12.344 10.3395C12.3418 10.4051 12.3525 10.4705 12.3756 10.532C12.3987 10.5935 12.4337 10.6498 12.4786 10.6977C12.5234 10.7457 12.5773 10.7844 12.6371 10.8115C12.6969 10.8386 12.7614 10.8537 12.8271 10.8559C12.8927 10.8581 12.9581 10.8474 13.0196 10.8243C13.081 10.8012 13.1374 10.7662 13.1853 10.7213C13.2333 10.6765 13.2719 10.6226 13.2991 10.5628C13.7276 9.61865 14 8.49291 14 7.16867V3.49996C14 2.9536 13.5464 2.49996 13 2.49996H6.15625Z', - ShoppingBagOpen: - 'M5.5 6.5C5.36739 6.5 5.24021 6.55268 5.14645 6.64645C5.05268 6.74021 5 6.86739 5 7C4.99999 7.79543 5.31621 8.55876 5.87866 9.12122C7.04599 10.2888 8.95401 10.2888 10.1213 9.12122C10.6838 8.55877 11 7.79544 11 7C11 6.86739 10.9473 6.74021 10.8536 6.64645C10.7598 6.55268 10.6326 6.5 10.5 6.5C10.3674 6.5 10.2402 6.55268 10.1464 6.64645C10.0527 6.74021 10 6.86739 10 7C10 7.53061 9.7894 8.039 9.41418 8.41418C8.62902 9.1995 7.37098 9.1995 6.58582 8.41418C6.2106 8.03899 6 7.53062 6 7C6 6.86739 5.94732 6.74021 5.85355 6.64645C5.75979 6.55268 5.63261 6.5 5.5 6.5Z M2.5 2.5C1.95364 2.5 1.5 2.95364 1.5 3.5V12.5C1.5 13.0464 1.95364 13.5 2.5 13.5H13.5C14.0464 13.5 14.5 13.0464 14.5 12.5V3.5C14.5 2.95364 14.0464 2.5 13.5 2.5H2.5ZM2.5 3.5H13.5V4.5H2.5V3.5ZM2.5 5.5H13.5V12.5H2.5V5.5Z', - SidebarElementFeature: - 'M2.5 2C1.67157 2 1 2.67157 1 3.5V12.5C1 13.3284 1.67157 14 2.5 14H13.5C14.3284 14 15 13.3284 15 12.5V3.5C15 2.67157 14.3284 2 13.5 2H2.5ZM2 3.5C2 3.22386 2.22386 3 2.5 3H9V13H2.5C2.22386 13 2 12.7761 2 12.5V3.5ZM10 13H13.5C13.7761 13 14 12.7761 14 12.5V3.5C14 3.22386 13.7761 3 13.5 3H10V13Z', - SignOut: - 'M3 2C2.45364 2 2 2.45364 2 3V13C2.00007 13.5463 2.45357 13.9999 2.99988 14C2.99984 14 2.99992 14 2.99988 14H6.5C6.63261 14 6.75979 13.9473 6.85355 13.8536C6.94732 13.7598 7 13.6326 7 13.5C7 13.3674 6.94732 13.2402 6.85355 13.1464C6.75979 13.0527 6.63261 13 6.5 13H3.00012L3 3H6.5C6.6326 3 6.75978 2.94732 6.85355 2.85355C6.94732 2.75979 7 2.63261 7 2.5C7 2.36739 6.94732 2.24021 6.85355 2.14645C6.75978 2.05268 6.6326 2 6.5 2H3Z M10.875 4.875C10.7424 4.87502 10.6152 4.92771 10.5215 5.02148C10.4277 5.11525 10.3751 5.24241 10.3751 5.375C10.3751 5.50759 10.4277 5.63475 10.5215 5.72852L12.293 7.5H6.5C6.36739 7.5 6.24021 7.55268 6.14645 7.64645C6.05268 7.74021 6 7.86739 6 8C6 8.13261 6.05268 8.25979 6.14645 8.35355C6.24021 8.44732 6.36739 8.5 6.5 8.5H12.293L10.5215 10.2715C10.4277 10.3652 10.3751 10.4924 10.3751 10.625C10.3751 10.7576 10.4277 10.8848 10.5215 10.9785C10.6152 11.0723 10.7424 11.1249 10.875 11.1249C11.0076 11.1249 11.1348 11.0723 11.2285 10.9785L13.8535 8.35352C13.9414 8.26249 13.9915 8.14153 13.9938 8.01501C13.9959 8.01004 13.998 8.00504 14 8C13.9985 7.98678 13.9964 7.97363 13.9938 7.96057C13.9917 7.93512 13.9877 7.90985 13.9818 7.88501C13.9757 7.85934 13.9675 7.8342 13.9574 7.80981C13.9476 7.78622 13.936 7.7634 13.9227 7.74158C13.9089 7.7191 13.8934 7.69776 13.8762 7.67773C13.8691 7.66703 13.8615 7.6566 13.8535 7.64648L11.2285 5.02148C11.1348 4.92771 11.0076 4.87502 10.875 4.875Z', - SlackLogo: - 'M12.5 4.50024C11.9693 4.49857 11.4591 4.70864 11.0839 5.08398C10.7082 5.45981 10.4979 5.97058 10.5 6.50195V8C10.5 8.1326 10.5527 8.25977 10.6465 8.35354C10.7402 8.4473 10.8674 8.49999 11 8.5H12.4978C13.0293 8.5023 13.5402 8.29197 13.916 7.91614C14.2913 7.54089 14.5015 7.03067 14.4998 6.5C14.5032 5.40015 13.5998 4.49678 12.5 4.50024ZM12.4978 5.5C12.4993 5.50001 12.5007 5.50001 12.5022 5.5C13.0604 5.49761 13.5024 5.93964 13.5 6.4978C13.5 6.49927 13.5 6.50073 13.5 6.5022C13.5011 6.76736 13.3965 7.02139 13.209 7.20886C13.209 7.20881 13.2089 7.2089 13.209 7.20886C13.0215 7.39638 12.7674 7.50115 12.5022 7.5C12.5015 7.5 12.5007 7.5 12.5 7.5H11.5V6.5C11.5 6.49935 11.5 6.4987 11.5 6.49805C11.499 6.23279 11.6036 5.97863 11.7911 5.79102C11.9786 5.60349 12.2326 5.49887 12.4978 5.5Z M9.49988 1.5C8.40132 1.50014 7.50014 2.40132 7.5 3.49988C7.5 3.49984 7.5 3.49992 7.5 3.49988V8C7.50001 8.1326 7.5527 8.25977 7.64646 8.35354C7.74023 8.4473 7.8674 8.49999 8 8.5H9.5C10.0302 8.49997 10.5392 8.28918 10.9142 7.91431C10.9142 7.91435 10.9142 7.91426 10.9142 7.91431C11.2891 7.53937 11.5 7.03026 11.5 6.5V3.5C11.4999 2.40141 10.5985 1.50009 9.49988 1.5C9.49992 1.5 9.49984 1.5 9.49988 1.5ZM8 7.5C7.8674 7.50001 7.74023 7.5527 7.64646 7.64646C7.5527 7.74023 7.50001 7.8674 7.5 8V9.5C7.50003 10.0302 7.71082 10.5392 8.08569 10.9142C8.08565 10.9142 8.08574 10.9142 8.08569 10.9142C8.46064 11.2891 8.96979 11.5 9.5 11.5H12.5C13.0302 11.5 13.5392 11.2892 13.9142 10.9143C14.6909 10.1374 14.691 8.86274 13.9143 8.08581C13.9143 8.08585 13.9143 8.08577 13.9143 8.08581C13.5393 7.71096 13.0303 7.5 12.5 7.5H8ZM9.5 2.5C10.0582 2.50005 10.5 2.94179 10.5 3.5V6.5C10.5 6.76537 10.3948 7.01947 10.2072 7.20715C10.0194 7.39482 9.76544 7.49999 9.5 7.5H8.5V3.50012C8.50013 2.94201 8.94189 2.50014 9.5 2.5Z M6.5 1.50024C5.40011 1.4968 4.4968 2.40011 4.50024 3.5C4.49685 4.60057 5.40125 5.50457 6.50208 5.5H8C8.1326 5.49999 8.25977 5.4473 8.35354 5.35354C8.4473 5.25977 8.49999 5.1326 8.5 5V3.5022C8.50471 2.40133 7.6006 1.49678 6.5 1.50024ZM6.49792 2.5C6.49935 2.50001 6.50078 2.50001 6.5022 2.5C7.06036 2.49761 7.50239 2.93964 7.5 3.4978C7.5 3.49854 7.5 3.49915 7.5 3.49988V4.5H6.5C6.49931 4.5 6.49862 4.5 6.49792 4.5C5.93976 4.50232 5.49768 4.06024 5.5 3.50208C5.50001 3.50069 5.50001 3.49931 5.5 3.49792C5.49766 2.93976 5.93976 2.49766 6.49792 2.5Z M3.50208 7.5C2.40125 7.49543 1.49685 8.39943 1.50024 9.5C1.49857 10.0306 1.7086 10.5408 2.08386 10.916C2.45912 11.2913 2.96935 11.5014 3.5 11.4998C4.6006 11.5032 5.50462 10.5988 5.5 9.49793V8C5.49999 7.8674 5.44731 7.74023 5.35354 7.64646C5.25978 7.5527 5.13261 7.50001 5 7.5H3.50208ZM3.49792 8.5C3.49862 8.5 3.49931 8.5 3.5 8.5H4.5V9.5C4.5 9.50069 4.5 9.50138 4.5 9.50208C4.50234 10.0602 4.06024 10.5023 3.50208 10.5C3.50065 10.5 3.49923 10.5 3.4978 10.5C3.23263 10.5011 2.97862 10.3965 2.79114 10.209C2.79118 10.209 2.7911 10.2089 2.79114 10.209C2.60362 10.0215 2.49887 9.76737 2.5 9.5022C2.50001 9.50077 2.50001 9.49935 2.5 9.49792C2.49768 8.93976 2.93976 8.49768 3.49792 8.5Z M8.5 11.5H9.5C9.50065 11.5 9.5013 11.5 9.50195 11.5C9.76719 11.4989 10.0214 11.6036 10.209 11.7911C10.3965 11.9786 10.5011 12.2327 10.5 12.4979C10.5 12.4993 10.5 12.5008 10.5 12.5022C10.5011 12.7674 10.3965 13.0214 10.209 13.2089C10.209 13.2088 10.2089 13.2089 10.209 13.2089C10.0215 13.3964 9.76735 13.5012 9.50219 13.5C9.50073 13.5 9.49926 13.5 9.4978 13.5C9.09214 13.5017 8.72924 13.2598 8.57482 12.8846C8.52487 12.7633 8.49946 12.6333 8.5 12.5021C8.5 12.5014 8.5 12.5007 8.5 12.5V11.5Z M6.5 7.5C5.40141 7.50006 4.50009 8.40141 4.5 9.5V12.5C4.5 13.0302 4.71078 13.5392 5.08569 13.9142C5.08565 13.9142 5.08574 13.9142 5.08569 13.9142C5.86262 14.6909 7.13726 14.691 7.91419 13.9143C7.91415 13.9143 7.91423 13.9143 7.91419 13.9143C8.28904 13.5393 8.5 13.0303 8.5 12.5V8C8.49999 7.8674 8.4473 7.74023 8.35354 7.64646C8.25977 7.5527 8.1326 7.50001 8 7.5H6.5ZM6.5 8.5H7.5V12.5C7.5 12.7653 7.39475 13.0194 7.20715 13.207C6.81235 13.6017 6.18765 13.6017 5.79285 13.207C5.60522 13.0193 5.5 12.7654 5.5 12.5V9.5C5.50005 8.94179 5.94179 8.50005 6.5 8.5ZM8 10.5C7.8674 10.5 7.74023 10.5527 7.64646 10.6465C7.5527 10.7402 7.50001 10.8674 7.5 11V12.4979C7.49892 12.7611 7.54996 13.022 7.65015 13.2654C7.95796 14.0132 8.69112 14.5023 9.49963 14.4999C10.0304 14.5017 10.5407 14.2915 10.916 13.9161C11.2913 13.5409 11.5015 13.0307 11.4998 12.5C11.5015 11.9693 11.2914 11.4592 10.916 11.0839C10.5402 10.7082 10.0294 10.4979 9.49805 10.5H8ZM8.5 8.5H12.5C12.7653 8.5 13.0194 8.60525 13.207 8.79285C13.6017 9.18765 13.6017 9.81235 13.207 10.2072C13.0193 10.3948 12.7654 10.5 12.5 10.5H9.5C9.23456 10.5 8.98056 10.3948 8.79285 10.2072C8.60518 10.0194 8.50001 9.76544 8.5 9.5V8.5Z M3.5 4.5C2.40135 4.5 1.5 5.40135 1.5 6.5C1.5 6.49996 1.5 6.50004 1.5 6.5C1.50014 7.59856 2.40144 8.49986 3.5 8.5C3.49996 8.5 3.50004 8.5 3.5 8.5H8C8.1326 8.49999 8.25977 8.4473 8.35354 8.35354C8.4473 8.25977 8.49999 8.1326 8.5 8V6.5C8.49994 5.40141 7.59859 4.50009 6.5 4.5H3.5ZM3.5 5.5H6.5C7.05821 5.50005 7.49995 5.94179 7.5 6.5V7.5H3.50012C2.94201 7.49987 2.50014 7.05811 2.5 6.5C2.50007 5.94185 2.94183 5.5 3.5 5.5Z', - Smiley: - 'M5.52832 9.01672C5.40015 8.98273 5.26372 9.00104 5.14905 9.06763C5.03437 9.13418 4.95082 9.24356 4.91678 9.37171C4.88274 9.49986 4.90099 9.63629 4.96753 9.75098C5.27581 10.2821 5.71831 10.723 6.25049 11.0294C7.91938 11.9904 10.0658 11.4166 11.0325 9.75098C11.0654 9.69418 11.0869 9.63145 11.0956 9.56636C11.1043 9.50128 11.1001 9.43511 11.0833 9.37164C11.0664 9.30817 11.0372 9.24865 10.9974 9.19646C10.9575 9.14428 10.9078 9.10046 10.851 9.0675C10.7363 9.00097 10.5998 8.98271 10.4717 9.01676C10.3435 9.0508 10.2342 9.13434 10.1676 9.24902C9.47157 10.4483 7.95113 10.8547 6.74951 10.1627C6.74955 10.1627 6.74947 10.1627 6.74951 10.1627C6.36907 9.94368 6.05278 9.62868 5.8324 9.24902C5.76585 9.13433 5.65647 9.05077 5.52832 9.01672Z M5.75 7.5C6.16419 7.5 6.5 7.16419 6.5 6.75C6.5 6.33581 6.16419 6 5.75 6C5.33581 6 5 6.33581 5 6.75C5 7.16419 5.33581 7.5 5.75 7.5Z M10.25 7.5C10.6642 7.5 11 7.16419 11 6.75C11 6.33581 10.6642 6 10.25 6C9.83581 6 9.5 6.33581 9.5 6.75C9.5 7.16419 9.83581 7.5 10.25 7.5Z M8 1.5C4.41604 1.5 1.5 4.41604 1.5 8C1.5 11.5839 4.41603 14.5 8 14.5C11.5839 14.5 14.5 11.5839 14.5 8C14.5 4.41603 11.5839 1.5 8 1.5ZM8 2.5C11.0435 2.5 13.5 4.95647 13.5 8C13.5 11.0435 11.0435 13.5 8 13.5C4.95647 13.5 2.5 11.0435 2.5 8C2.5 4.95647 4.95647 2.5 8 2.5Z', - SortAscending: - 'M3 11.5C2.86739 11.5 2.74021 11.5527 2.64645 11.6464C2.55268 11.7402 2.5 11.8674 2.5 12C2.5 12.1326 2.55268 12.2598 2.64645 12.3536C2.74021 12.4473 2.86739 12.5 3 12.5H6.5C6.63261 12.5 6.75979 12.4473 6.85355 12.3536C6.94732 12.2598 7 12.1326 7 12C7 11.8674 6.94732 11.7402 6.85355 11.6464C6.75979 11.5527 6.63261 11.5 6.5 11.5H3Z M3 3.5C2.86739 3.5 2.74021 3.55268 2.64645 3.64645C2.55268 3.74021 2.5 3.86739 2.5 4C2.5 4.13261 2.55268 4.25979 2.64645 4.35355C2.74021 4.44732 2.86739 4.5 3 4.5H11.5C11.6326 4.5 11.7598 4.44732 11.8536 4.35355C11.9473 4.25979 12 4.13261 12 4C12 3.86739 11.9473 3.74021 11.8536 3.64645C11.7598 3.55268 11.6326 3.5 11.5 3.5H3Z M3 7.5C2.86739 7.5 2.74021 7.55268 2.64645 7.64645C2.55268 7.74021 2.5 7.86739 2.5 8C2.5 8.13261 2.55268 8.25979 2.64645 8.35355C2.74021 8.44732 2.86739 8.5 3 8.5H7.5C7.63261 8.5 7.75979 8.44732 7.85355 8.35355C7.94732 8.25979 8 8.13261 8 8C8 7.86739 7.94732 7.74021 7.85355 7.64645C7.75979 7.55268 7.63261 7.5 7.5 7.5H3Z M11.5 6.5C11.3674 6.5 11.2402 6.55268 11.1464 6.64645C11.0527 6.74021 11 6.86739 11 7V11.793L9.35352 10.1465C9.25975 10.0527 9.13259 10.0001 9 10.0001C8.86741 10.0001 8.74025 10.0527 8.64648 10.1465C8.55274 10.2402 8.50008 10.3674 8.50008 10.5C8.50008 10.6326 8.55274 10.7598 8.64648 10.8535L11.1465 13.3535C11.2403 13.4472 11.3674 13.4999 11.5 13.4999C11.6326 13.4999 11.7597 13.4472 11.8535 13.3535L14.3535 10.8535C14.4473 10.7598 14.4999 10.6326 14.4999 10.5C14.4999 10.3674 14.4473 10.2402 14.3535 10.1465C14.2598 10.0527 14.1326 10.0001 14 10.0001C13.8674 10.0001 13.7402 10.0527 13.6465 10.1465L12 11.793V7C12 6.86739 11.9473 6.74021 11.8536 6.64645C11.7598 6.55268 11.6326 6.5 11.5 6.5Z', - SortDescending: - 'M3 11.5C2.86739 11.5 2.74021 11.5527 2.64645 11.6464C2.55268 11.7402 2.5 11.8674 2.5 12C2.5 12.1326 2.55268 12.2598 2.64645 12.3536C2.74021 12.4473 2.86739 12.5 3 12.5H11.5C11.6326 12.5 11.7598 12.4473 11.8536 12.3536C11.9473 12.2598 12 12.1326 12 12C12 11.8674 11.9473 11.7402 11.8536 11.6464C11.7598 11.5527 11.6326 11.5 11.5 11.5H3Z M3 3.5C2.86739 3.5 2.74021 3.55268 2.64645 3.64645C2.55268 3.74021 2.5 3.86739 2.5 4C2.5 4.13261 2.55268 4.25979 2.64645 4.35355C2.74021 4.44732 2.86739 4.5 3 4.5H6.5C6.63261 4.5 6.75979 4.44732 6.85355 4.35355C6.94732 4.25979 7 4.13261 7 4C7 3.86739 6.94732 3.74021 6.85355 3.64645C6.75979 3.55268 6.63261 3.5 6.5 3.5H3Z M3 7.5C2.86739 7.5 2.74021 7.55268 2.64645 7.64645C2.55268 7.74021 2.5 7.86739 2.5 8C2.5 8.13261 2.55268 8.25979 2.64645 8.35355C2.74021 8.44732 2.86739 8.5 3 8.5H7.5C7.63261 8.5 7.75979 8.44732 7.85355 8.35355C7.94732 8.25979 8 8.13261 8 8C8 7.86739 7.94732 7.74021 7.85355 7.64645C7.75979 7.55268 7.63261 7.5 7.5 7.5H3Z M11.5 2.5C11.3674 2.50003 11.2402 2.55272 11.1465 2.64648L8.64648 5.14648C8.55274 5.24025 8.50008 5.36741 8.50008 5.5C8.50008 5.63259 8.55274 5.75975 8.64648 5.85352C8.74025 5.94726 8.86741 5.99992 9 5.99992C9.13259 5.99992 9.25975 5.94726 9.35352 5.85352L11 4.20703V9C11 9.13261 11.0527 9.25979 11.1464 9.35355C11.2402 9.44732 11.3674 9.5 11.5 9.5C11.6326 9.5 11.7598 9.44732 11.8536 9.35355C11.9473 9.25979 12 9.13261 12 9V4.20703L13.6465 5.85352C13.7402 5.94726 13.8674 5.99992 14 5.99992C14.1326 5.99992 14.2598 5.94726 14.3535 5.85352C14.4473 5.75975 14.4999 5.63259 14.4999 5.5C14.4999 5.36741 14.4473 5.24025 14.3535 5.14648L11.8535 2.64648C11.8487 2.64437 11.8438 2.64234 11.8389 2.64038C11.7478 2.55235 11.6267 2.50218 11.5 2.5Z', - Sparkle: - 'M11 0.5C10.8674 0.5 10.7402 0.552678 10.6464 0.646447C10.5527 0.740215 10.5 0.867392 10.5 1V2H9.5C9.36739 2 9.24021 2.05268 9.14645 2.14645C9.05268 2.24021 9 2.36739 9 2.5C9 2.63261 9.05268 2.75979 9.14645 2.85355C9.24021 2.94732 9.36739 3 9.5 3H10.5V4C10.5 4.13261 10.5527 4.25979 10.6464 4.35355C10.7402 4.44732 10.8674 4.5 11 4.5C11.1326 4.5 11.2598 4.44732 11.3536 4.35355C11.4473 4.25979 11.5 4.13261 11.5 4V3H12.5C12.6326 3 12.7598 2.94732 12.8536 2.85355C12.9473 2.75979 13 2.63261 13 2.5C13 2.36739 12.9473 2.24021 12.8536 2.14645C12.7598 2.05268 12.6326 2 12.5 2H11.5V1C11.5 0.867392 11.4473 0.740215 11.3536 0.646447C11.2598 0.552678 11.1326 0.5 11 0.5Z M6.99988 2.94897C6.58344 2.94893 6.20729 3.21072 6.0625 3.6012C6.06262 3.60092 6.06238 3.60148 6.0625 3.6012L4.86206 6.8584C4.86206 6.85836 4.86206 6.85844 4.86206 6.8584C4.86136 6.86031 4.86043 6.86136 4.85852 6.86206C4.85856 6.86206 4.85848 6.86206 4.85852 6.86206L1.60205 8.06213C1.60168 8.06226 1.60132 8.06238 1.60095 8.0625C1.21068 8.20745 0.949045 8.58356 0.948975 8.99988C0.948975 8.99976 0.948975 9 0.948975 8.99988C0.949156 9.41609 1.2107 9.79238 1.60083 9.93739C1.60124 9.93755 1.60164 9.93772 1.60205 9.93789L4.8584 11.138C4.85832 11.1379 4.85848 11.138 4.8584 11.138C4.86026 11.1387 4.86125 11.1395 4.86194 11.1414C4.8619 11.1413 4.86198 11.1415 4.86194 11.1414L6.06214 14.398C6.0623 14.3984 6.06246 14.3988 6.06262 14.3992C6.20763 14.7893 6.58355 15.0509 6.99976 15.0511C6.99959 15.0511 6.99992 15.0511 6.99976 15.0511C7.41589 15.0508 7.79234 14.7894 7.93739 14.3993C7.93756 14.3989 7.93772 14.3984 7.93789 14.398L9.13796 11.1416C9.13821 11.1409 9.13863 11.1401 9.13918 11.1396C9.13905 11.1397 9.13931 11.1394 9.13918 11.1396C9.13974 11.139 9.14078 11.1382 9.14151 11.138C9.14147 11.138 9.14155 11.138 9.14151 11.138L12.398 9.93788C12.3978 9.938 12.3983 9.93776 12.398 9.93788C12.8098 9.78521 13.0512 9.38079 13.0512 9.00001C13.0512 8.61922 12.8106 8.21518 12.3988 8.06251C12.3991 8.06264 12.3986 8.06238 12.3988 8.06251L9.14176 6.86206C9.13982 6.86132 9.1387 6.86034 9.13797 6.8584L7.93787 3.60205C7.93799 3.60233 7.93774 3.60177 7.93787 3.60205C7.79308 3.21157 7.41632 2.94893 6.99988 2.94897ZM6.99988 3.94897L8.19959 7.2041C8.19959 7.20406 8.19959 7.20414 8.19959 7.2041C8.30137 7.48024 8.51964 7.69864 8.79578 7.80042C8.79574 7.80042 8.79582 7.80042 8.79578 7.80042L12.0511 9L8.79602 10.1995C8.65937 10.2498 8.53532 10.3292 8.43237 10.4321C8.43244 10.432 8.43233 10.4321 8.43237 10.4321C8.32941 10.535 8.24975 10.6594 8.19946 10.796L7.00012 14.0507C7.00005 14.0507 7.00018 14.0507 7.00012 14.0507L5.80053 10.7961C5.69883 10.5199 5.48031 10.3013 5.2041 10.1996L1.94922 9.00012L5.20398 7.80041C5.4803 7.69871 5.69869 7.4804 5.80042 7.2041L6.99988 3.94897Z M14 4C13.8674 4 13.7402 4.05268 13.6464 4.14645C13.5527 4.24021 13.5 4.36739 13.5 4.5V5H13C12.8674 5 12.7402 5.05268 12.6464 5.14645C12.5527 5.24021 12.5 5.36739 12.5 5.5C12.5 5.63261 12.5527 5.75979 12.6464 5.85355C12.7402 5.94732 12.8674 6 13 6H13.5V6.5C13.5 6.63261 13.5527 6.75979 13.6464 6.85355C13.7402 6.94732 13.8674 7 14 7C14.1326 7 14.2598 6.94732 14.3536 6.85355C14.4473 6.75979 14.5 6.63261 14.5 6.5V6H15C15.1326 6 15.2598 5.94732 15.3536 5.85355C15.4473 5.75979 15.5 5.63261 15.5 5.5C15.5 5.36739 15.4473 5.24021 15.3536 5.14645C15.2598 5.05268 15.1326 5 15 5H14.5V4.5C14.5 4.36739 14.4473 4.24021 14.3536 4.14645C14.2598 4.05268 14.1326 4 14 4Z', - Spiral: - 'M8.5 2C4.91607 2 2 4.91607 2 8.5C1.99989 11.8078 4.69216 14.5001 8 14.5C9.45848 14.5 10.8578 13.9204 11.8892 12.8892C12.9204 11.8578 13.5 10.4585 13.5 9C13.5 6.2445 11.2555 4 8.5 4C8.50024 4 8.49976 4 8.5 4C6.02262 4.00386 4.00386 6.02189 4 8.49927C4 8.49903 4 8.49951 4 8.49927C4 10.7025 5.79679 12.5 8 12.5C9.92707 12.5 11.5 10.9271 11.5 9C11.5 8.20453 11.1839 7.44112 10.6213 6.87866C10.0589 6.31615 9.29547 5.99998 8.5 6C7.83719 5.99998 7.20097 6.26351 6.7323 6.73218C6.26381 7.2006 6.0004 7.83644 6.00012 8.4989C5.99812 9.03003 6.20834 9.5406 6.58398 9.91614C6.95958 10.2915 7.46997 10.5017 8.00098 10.4999C8.39822 10.4996 8.77972 10.3415 9.06067 10.0607C9.06063 10.0607 9.06071 10.0607 9.06067 10.0607C9.34175 9.77949 9.5 9.39764 9.5 9C9.5 8.45364 9.04636 8 8.5 8C8.36739 8 8.24021 8.05268 8.14645 8.14645C8.05268 8.24021 8 8.36739 8 8.5C8 8.63261 8.05268 8.75979 8.14645 8.85355C8.24021 8.94732 8.36739 9 8.5 9C8.5 9.13268 8.44748 9.25958 8.35364 9.35352C8.25972 9.44736 8.13277 9.49999 8 9.5C7.99935 9.5 7.9987 9.5 7.99805 9.5C7.73278 9.50105 7.47863 9.39639 7.29102 9.20887C7.10349 9.02139 6.99885 8.76736 7 8.5022C7 8.50147 7 8.50073 7 8.5C6.99999 8.102 7.15788 7.72074 7.43933 7.43933C7.72074 7.15788 8.102 6.99999 8.5 7C9.03061 6.99999 9.539 7.2106 9.91418 7.58582C10.2894 7.961 10.5 8.46939 10.5 9C10.5 10.3866 9.38663 11.5 8 11.5C6.33731 11.5 5.00013 10.1629 5 8.50024C5 8.50031 5 8.50018 5 8.50024C5.00342 6.56279 6.56255 5.00341 8.5 4.99999C10.7151 4.99999 12.5 6.78493 12.5 8.99999C12.5 10.1937 12.026 11.3379 11.182 12.182C10.3379 13.026 9.19367 13.5 8 13.5C5.23263 13.5001 2.99991 11.2674 3 8.5C3 5.45651 5.45651 3 8.5 3C11.8196 3.00006 14.4999 5.68039 14.5 9C14.5 9.13261 14.5527 9.25979 14.6464 9.35355C14.7402 9.44732 14.8674 9.5 15 9.5C15.1326 9.5 15.2598 9.44732 15.3536 9.35355C15.4473 9.25979 15.5 9.13261 15.5 9C15.4999 5.13997 12.36 2.00007 8.5 2Z', - Square: - 'M3 2C2.45364 2 2 2.45364 2 3V13C2 13.5464 2.45364 14 3 14H13C13.5464 14 14 13.5464 14 13V3C14 2.45364 13.5464 2 13 2H3ZM3 3H13V13H3V3Z', - Star: - 'M7.99975 0.996094C7.57318 0.996128 7.18836 1.26571 7.04259 1.66663L7.04784 1.65295L5.67284 5.11548C5.67142 5.119 5.67004 5.12254 5.66869 5.1261C5.66565 5.13431 5.66096 5.1378 5.65221 5.13831C5.6512 5.13838 5.65018 5.13846 5.64916 5.13855L1.96166 5.37598C1.96179 5.37594 1.96154 5.37602 1.96166 5.37598C1.51229 5.40516 1.16169 5.73277 1.04735 6.10218C0.933105 6.47128 1.03046 6.92969 1.37353 7.21521L4.19848 9.57142C4.19901 9.57187 4.19954 9.57232 4.20007 9.57276C4.21392 9.58422 4.21899 9.59964 4.21471 9.61707C4.21472 9.61701 4.2147 9.61712 4.21471 9.61707L3.37182 12.9387C3.2506 13.4131 3.44889 13.8848 3.78808 14.1426C4.12727 14.4004 4.64722 14.4608 5.06213 14.1968L7.99255 12.3412C7.9923 12.3414 7.99279 12.341 7.99255 12.3412C7.99764 12.338 8.00161 12.3375 8.00671 12.3407C8.00662 12.3407 8.00679 12.3407 8.00671 12.3407L11.157 14.3408C11.1574 14.3411 11.1578 14.3414 11.1582 14.3417C11.5342 14.5789 12.0093 14.5257 12.3175 14.2924C12.6257 14.059 12.8055 13.6299 12.6971 13.2023C12.6972 13.2024 12.6971 13.2021 12.6971 13.2023L11.7853 9.61706C11.781 9.59967 11.786 9.58428 11.7998 9.57287C11.8004 9.57238 11.8009 9.5719 11.8015 9.57141L14.6261 7.21545C14.9694 6.92994 15.0669 6.4714 14.9526 6.10217C14.8383 5.73276 14.4881 5.40527 14.0387 5.37609C14.0388 5.37613 14.0386 5.37605 14.0387 5.37609L10.3508 5.13854C10.3498 5.13846 10.3487 5.13838 10.3476 5.1383C10.3389 5.13779 10.3343 5.13439 10.3313 5.12621C10.3299 5.12262 10.3286 5.11904 10.3271 5.11547L8.95213 1.65295L8.95738 1.66674C8.81167 1.26574 8.4264 0.996056 7.99975 0.996094ZM7.99987 1.99609C7.99974 1.99609 8 1.99609 7.99987 1.99609C8.00935 1.99609 8.01434 1.99939 8.01758 2.0083C8.01926 2.01292 8.02101 2.01752 8.02283 2.02209L9.39783 5.4845L9.39368 5.47375C9.53379 5.85173 9.88715 6.11327 10.2896 6.13672L13.9741 6.37402C14.006 6.37609 13.9898 6.37346 13.9973 6.39782C14.0048 6.42217 14.0118 6.42588 13.9868 6.44665C13.9865 6.44686 13.9862 6.44707 13.986 6.44728L11.1627 8.80214C10.8543 9.05717 10.7183 9.46962 10.8147 9.85805C10.8149 9.85894 10.8152 9.85984 10.8154 9.86073L11.7278 13.4478C11.7382 13.4889 11.7274 13.4848 11.7137 13.4951C11.7001 13.5055 11.722 13.5149 11.6918 13.4959L8.54296 11.4967C8.21256 11.2868 7.78728 11.2867 7.4569 11.4967C7.45696 11.4967 7.45684 11.4968 7.4569 11.4967L4.52623 13.3525C4.52591 13.3527 4.52558 13.353 4.52526 13.3532C4.45892 13.3954 4.43836 13.3808 4.39318 13.3465C4.34799 13.3121 4.31816 13.2744 4.34068 13.1863C4.3406 13.1866 4.34077 13.186 4.34068 13.1863L5.18468 9.86049C5.18489 9.85959 5.18509 9.8587 5.18529 9.8578C5.28156 9.46947 5.14573 9.05742 4.83752 8.80237L2.01403 6.44727C2.01375 6.44706 2.01346 6.44685 2.01318 6.44664C1.98816 6.42587 1.99514 6.42216 2.00268 6.39781C2.01021 6.37347 1.99424 6.37596 2.02612 6.37389L5.71337 6.13646L5.71032 6.13659C6.11276 6.11317 6.46615 5.85184 6.60632 5.47387L7.97717 2.02209C7.97898 2.01751 7.98073 2.01292 7.98242 2.00829C7.98567 1.99933 7.99034 1.99609 7.99987 1.99609Z', - StarFill: - 'M14.9498 6.0875C14.8872 5.89061 14.7666 5.71723 14.6038 5.59009C14.4409 5.46294 14.2435 5.38797 14.0373 5.375L10.3248 5.11875L8.94978 1.65C8.8747 1.459 8.74401 1.29494 8.57464 1.17905C8.40526 1.06317 8.205 1.00079 7.99978 1C7.79455 1.00079 7.59429 1.06317 7.42492 1.17905C7.25555 1.29494 7.12486 1.459 7.04978 1.65L5.64978 5.1375L1.96228 5.375C1.75634 5.38881 1.55929 5.4641 1.39661 5.59112C1.23393 5.71814 1.11311 5.89106 1.04978 6.0875C0.984741 6.28695 0.980941 6.50131 1.03887 6.70294C1.0968 6.90458 1.21379 7.08423 1.37478 7.21875L4.21228 9.61875L3.36853 12.9375C3.31015 13.162 3.32066 13.3989 3.39868 13.6174C3.4767 13.8358 3.61863 14.0258 3.80603 14.1625C3.98792 14.293 4.20463 14.3664 4.42843 14.373C4.65222 14.3797 4.87292 14.3195 5.06228 14.2L7.99353 12.3438H8.00603L11.1623 14.3375C11.3242 14.4427 11.5129 14.4991 11.706 14.5C11.8637 14.4988 12.019 14.4614 12.1599 14.3908C12.3009 14.3201 12.4237 14.2181 12.5191 14.0926C12.6145 13.967 12.6798 13.8213 12.71 13.6666C12.7403 13.5118 12.7346 13.3522 12.6935 13.2L11.7998 9.56875L14.6248 7.21875C14.7858 7.08423 14.9028 6.90458 14.9607 6.70294C15.0186 6.50131 15.0148 6.28695 14.9498 6.0875Z', - Switcher: - 'M3 2.5C2.72386 2.5 2.5 2.72386 2.5 3V7C2.5 7.27614 2.72386 7.5 3 7.5H7C7.27614 7.5 7.5 7.27614 7.5 7V3C7.5 2.72386 7.27614 2.5 7 2.5H3ZM3.5 6.5V3.5H6.5V6.5H3.5Z M9 2.5C8.72386 2.5 8.5 2.72386 8.5 3V7C8.5 7.27614 8.72386 7.5 9 7.5H13C13.2761 7.5 13.5 7.27614 13.5 7V3C13.5 2.72386 13.2761 2.5 13 2.5H9ZM9.5 6.5V3.5H12.5V6.5H9.5Z M3 8.5C2.72386 8.5 2.5 8.72386 2.5 9V13C2.5 13.2761 2.72386 13.5 3 13.5H7C7.27614 13.5 7.5 13.2761 7.5 13V9C7.5 8.72386 7.27614 8.5 7 8.5H3ZM3.5 12.5V9.5H6.5V12.5H3.5Z M11.6469 8.58567C10.3135 8.22838 8.94295 9.01968 8.58567 10.3531C8.22838 11.6865 9.01968 13.0571 10.3531 13.4143C11.0725 13.6071 11.8027 13.4655 12.3787 13.0849C12.3844 13.0912 12.3903 13.0975 12.3964 13.1036L13.6465 14.3536C13.8417 14.5488 14.1583 14.5488 14.3536 14.3536C14.5488 14.1583 14.5488 13.8417 14.3536 13.6464L13.1036 12.3964C13.0976 12.3905 13.0916 12.3847 13.0854 12.3792C13.2302 12.1602 13.3427 11.9145 13.4143 11.6469C13.7716 10.3135 12.9803 8.94295 11.6469 8.58567ZM9.55159 10.6119C9.76593 9.81197 10.5882 9.33725 11.3881 9.55159C12.188 9.76593 12.6628 10.5882 12.4484 11.3881C12.2341 12.188 11.4118 12.6628 10.6119 12.4484C9.81197 12.2341 9.33725 11.4118 9.55159 10.6119Z', - Table: - 'M2 3C1.8674 3.00001 1.74023 3.0527 1.64646 3.14646C1.5527 3.24023 1.50001 3.3674 1.5 3.5V12C1.50007 12.5463 1.95357 12.9999 2.49988 13C2.49984 13 2.49992 13 2.49988 13H13.5C14.0464 13 14.5 12.5464 14.5 12V3.5C14.5 3.3674 14.4473 3.24023 14.3535 3.14646C14.2598 3.0527 14.1326 3.00001 14 3H2ZM2.5 4H13.5V6H2.5V4ZM2.5 7H5V9H2.5V7ZM6 7H13.5V9H6V7ZM2.5 10H5V12H2.50012L2.5 10ZM6 10H13.5V12H6V10Z', - Tabs: - 'M2.37732 5.00001C1.93622 4.99793 1.54173 5.29005 1.41492 5.71254C1.41496 5.71254 1.41488 5.71254 1.41492 5.71254L0.0211182 10.3563C0.021777 10.3813 0.0243069 10.4062 0.0286865 10.4308C0.0174068 10.4531 0.00781772 10.4763 0 10.5C0.0109784 10.5398 0.0268463 10.5781 0.0472412 10.614C0.0600036 10.6747 0.0839289 10.7324 0.117798 10.7843C0.157779 10.8316 0.206209 10.8711 0.26062 10.9008C0.289169 10.9307 0.321315 10.9569 0.356323 10.9789C0.381298 10.9782 0.406189 10.9757 0.430786 10.9713C0.453108 10.9826 0.476244 10.9922 0.5 11H15.5C15.5238 10.9922 15.5469 10.9826 15.5692 10.9713C15.5938 10.9757 15.6187 10.9782 15.6437 10.9789C15.7168 10.9547 15.7834 10.914 15.8383 10.8599C15.8454 10.8534 15.8524 10.8467 15.8591 10.8399C15.9464 10.7491 15.9963 10.6288 15.9988 10.5029C15.9992 10.502 15.9996 10.501 16 10.5C15.998 10.4753 15.9941 10.4509 15.9884 10.4268C15.9869 10.4031 15.9837 10.3796 15.9789 10.3563L15.9756 10.3455L14.5852 5.71254C14.4584 5.29005 14.0638 4.99793 13.6227 5.00001H13C12.8674 5.00001 12.7402 5.05269 12.6464 5.14646C12.5527 5.24023 12.5 5.3674 12.5 5.50001C12.5 5.63262 12.5527 5.7598 12.6464 5.85356C12.7402 5.94733 12.8674 6.00001 13 6.00001H13.625C13.6258 6.00001 13.6265 6.00001 13.6273 6.00001L14.8279 10H12.8719L11.5852 5.71254C11.4584 5.29005 11.0638 4.99793 10.6227 5.00001H10C9.86739 5.00001 9.74021 5.05269 9.64645 5.14646C9.55268 5.24023 9.5 5.3674 9.5 5.50001C9.5 5.63262 9.55268 5.7598 9.64645 5.85356C9.74021 5.94733 9.86739 6.00001 10 6.00001H10.625C10.6258 6.00001 10.6265 6.00001 10.6273 6.00001L11.8279 10H9.87195L8.58521 5.71254C8.58525 5.71254 8.58516 5.71254 8.58521 5.71254C8.4584 5.29005 8.06378 4.99793 7.62268 5.00001H2.37732ZM2.37268 6.00001C2.37345 6.00001 2.37423 6.00001 2.375 6.00001H7.625C7.62577 6.00001 7.62655 6.00001 7.62732 6.00001L8.82788 10H1.17212L2.37268 6.00001Z', - Target: - 'M7.72273 4.51998C7.12722 4.56759 6.53883 4.76765 6.01985 5.12594C6.01985 5.12589 6.01985 5.12598 6.01985 5.12594C3.84116 6.63025 4.09172 9.97634 6.47004 11.1395C8.84836 12.3026 11.6433 10.4461 11.4931 7.80282C11.4894 7.73727 11.4728 7.67309 11.4443 7.61394C11.4157 7.5548 11.3758 7.50186 11.3268 7.45814C11.2778 7.41442 11.2207 7.38078 11.1587 7.35913C11.0967 7.33749 11.0311 7.32827 10.9655 7.332C10.8331 7.33952 10.7092 7.39932 10.6209 7.49823C10.5326 7.59715 10.4872 7.72709 10.4947 7.85946C10.6042 9.78624 8.64299 11.089 6.90937 10.2412C5.17579 9.39334 4.99994 7.0455 6.58796 5.94894C7.58036 5.26382 8.91187 5.38383 9.76582 6.23532C9.81232 6.28168 9.8675 6.31843 9.92821 6.34347C9.98891 6.36851 10.054 6.38134 10.1196 6.38124C10.1853 6.38114 10.2503 6.3681 10.3109 6.34288C10.3715 6.31765 10.4266 6.28073 10.473 6.23422C10.5666 6.14031 10.619 6.01307 10.6188 5.88048C10.6186 5.74789 10.5658 5.62081 10.4719 5.52719C9.72758 4.78503 8.71524 4.44064 7.72273 4.51998Z M8.27131 1.49728C6.69195 1.42794 5.08767 1.92948 3.79512 3.01657C1.21008 5.19068 0.720799 8.99335 2.66853 11.7535C4.61626 14.5137 8.36303 15.3272 11.2778 13.6203C14.1926 11.9134 15.3186 8.24657 13.8638 5.19711C13.8067 5.07744 13.7044 4.98536 13.5795 4.94111C13.4545 4.89686 13.317 4.90407 13.1973 4.96115C13.0777 5.01823 12.9855 5.12051 12.9413 5.24551C12.897 5.3705 12.9042 5.50796 12.9613 5.62765C14.1962 8.21635 13.2467 11.3085 10.7725 12.7574C8.29845 14.2062 5.13925 13.5205 3.48555 11.177C1.83184 8.83348 2.24458 5.62724 4.43879 3.78183C6.50435 2.04461 9.48832 2.06943 11.5206 3.77243L7.64656 7.64645C7.55281 7.74021 7.50015 7.86737 7.50015 7.99996C7.50015 8.13255 7.55281 8.25972 7.64656 8.35348C7.74032 8.44723 7.86748 8.49989 8.00007 8.49989C8.13266 8.49989 8.25982 8.44723 8.35359 8.35348L14.3536 2.35348C14.4473 2.25972 14.5 2.13255 14.5 1.99996C14.5 1.86738 14.4473 1.74021 14.3536 1.64645C14.2598 1.5527 14.1327 1.50004 14.0001 1.50004C13.8675 1.50004 13.7403 1.5527 13.6466 1.64645L12.2275 3.06552C11.0919 2.08879 9.69294 1.55969 8.27131 1.49728Z', - TeamLocked: - 'M2.3334 8.09179C2.78458 7.8662 3.28223 7.74916 3.78667 7.75C3.80393 7.75003 3.82098 7.74919 3.8378 7.74751C3.85539 7.74575 3.87271 7.74308 3.88972 7.73955C4.11707 7.69235 4.28781 7.4908 4.28756 7.24949C4.28728 6.97335 4.0632 6.74972 3.78705 6.75C3.50235 6.75029 3.22345 6.66955 2.98294 6.51722C2.74242 6.36489 2.55022 6.14725 2.4288 5.88974C2.30737 5.63223 2.26174 5.34549 2.29723 5.06301C2.33272 4.78053 2.44787 4.51399 2.62922 4.29452C2.81057 4.07505 3.05063 3.91173 3.32136 3.82363C3.59208 3.73553 3.88229 3.72629 4.15807 3.797C4.43385 3.8677 4.68381 4.01543 4.87875 4.22292C5.07368 4.43042 5.20555 4.6891 5.25893 4.96875C5.31071 5.23999 5.57257 5.41791 5.84381 5.36613C6.11506 5.31435 6.29297 5.05249 6.24119 4.78125C6.15222 4.31517 5.93245 3.88403 5.60755 3.53821C5.28266 3.19239 4.86605 2.94617 4.40642 2.82833C3.94679 2.71048 3.46311 2.72588 3.0119 2.87271C2.5607 3.01955 2.16059 3.29176 1.85834 3.65753C1.5561 4.02331 1.36418 4.46756 1.30503 4.93835C1.24588 5.40915 1.32194 5.88706 1.52431 6.31624C1.66427 6.61306 1.86065 6.8781 2.10108 7.09733C2.02863 7.12862 1.95697 7.16197 1.88619 7.19736C1.29564 7.49264 0.782268 7.92182 0.387004 8.45068C0.221689 8.67187 0.266984 8.98519 0.488175 9.15051C0.709366 9.31582 1.02269 9.27053 1.18801 9.04934C1.48999 8.64528 1.88222 8.31738 2.3334 8.09179Z M11.8795 3.797C12.1553 3.72629 12.4455 3.73553 12.7162 3.82363C12.987 3.91173 13.227 4.07505 13.4084 4.29452C13.5897 4.51399 13.7049 4.78053 13.7404 5.06301C13.7759 5.34549 13.7302 5.63223 13.6088 5.88974C13.4874 6.14725 13.2952 6.36489 13.0547 6.51722C12.8141 6.66955 12.5352 6.75029 12.2505 6.75C12.1815 6.74993 12.1157 6.76385 12.0559 6.78909C11.9259 6.84391 11.824 6.95209 11.7775 7.0862C11.7596 7.13778 11.7499 7.19318 11.75 7.25084C11.7505 7.52698 11.9747 7.75047 12.2509 7.75C12.7553 7.74916 13.253 7.8662 13.7041 8.09179C14.1553 8.31738 14.5475 8.64528 14.8495 9.04934C15.0148 9.27053 15.3282 9.31582 15.5494 9.15051C15.7706 8.98519 15.8158 8.67187 15.6505 8.45068C15.2553 7.92182 14.7419 7.49264 14.1514 7.19736C14.0806 7.16198 14.0089 7.12863 13.9365 7.09735C14.1769 6.87811 14.3733 6.61307 14.5133 6.31624C14.7157 5.88706 14.7917 5.40915 14.7326 4.93835C14.6734 4.46756 14.4815 4.02331 14.1793 3.65753C13.877 3.29176 13.4769 3.01955 13.0257 2.87271C12.5745 2.72588 12.0908 2.71048 11.6312 2.82833C11.1715 2.94617 10.7549 3.19239 10.43 3.53821C10.1051 3.88403 9.88537 4.31517 9.7964 4.78125C9.74463 5.05249 9.92254 5.31435 10.1938 5.36613C10.465 5.41791 10.7269 5.23999 10.7787 4.96875C10.832 4.6891 10.9639 4.43042 11.1589 4.22292C11.3538 4.01543 11.6038 3.8677 11.8795 3.797Z M5.50003 8.16667V9H5.00003C4.44775 9 4.00003 9.44772 4.00003 10V13.5C4.00003 14.0523 4.44775 14.5 5.00003 14.5H11C11.5523 14.5 12 14.0523 12 13.5V10C12 9.44772 11.5523 9 11 9H10.5V8.16667C10.5 7.55955 10.2099 7.00037 9.73434 6.60404C9.26141 6.20994 8.63658 6 8.00003 6C7.36348 6 6.73865 6.20994 6.26572 6.60404C5.79014 7.00037 5.50003 7.55955 5.50003 8.16667ZM8.00003 7C7.57571 7 7.18313 7.14125 6.90591 7.37227C6.63135 7.60106 6.50003 7.88973 6.50003 8.16667V9H9.50003V8.16667C9.50003 7.88973 9.36871 7.60106 9.09415 7.37227C8.81694 7.14125 8.42435 7 8.00003 7ZM5.00003 13.5V10H11V13.5H5.00003Z', - TextAlt: - 'M8.44187 3.26606C8.35522 3.10237 8.18518 3 7.99998 3C7.81477 3 7.64474 3.10237 7.55808 3.26606L3.05808 11.7661C2.92888 12.0101 3.02198 12.3127 3.26603 12.4419C3.51009 12.5711 3.81267 12.478 3.94187 12.2339L5.12455 10H10.8754L12.0581 12.2339C12.1873 12.478 12.4899 12.5711 12.7339 12.4419C12.978 12.3127 13.0711 12.0101 12.9419 11.7661L8.44187 3.26606ZM10.346 9L7.99998 4.56863L5.65396 9H10.346Z', - TextBolder: - 'M4 2.5C3.8674 2.50001 3.74023 2.5527 3.64646 2.64646C3.5527 2.74023 3.50001 2.8674 3.5 3V12.5C3.50001 12.6326 3.5527 12.7598 3.64646 12.8535C3.74023 12.9473 3.8674 13 4 13H9.5C10.2954 13 11.0588 12.6838 11.6212 12.1213C12.7888 10.954 12.7888 9.04599 11.6212 7.87866C11.3401 7.59754 11.0088 7.37799 10.6484 7.22864C11.1706 6.72737 11.5 6.02682 11.5 5.25C11.5 3.73714 10.2629 2.5 8.75 2.5H4ZM4.5 3.5H8.75C9.72242 3.5 10.5 4.27758 10.5 5.25C10.5 6.22242 9.72242 7 8.75 7H4.5V3.5ZM4.5 8H9.5C10.0306 7.99999 10.539 8.2106 10.9142 8.58582C11.6995 9.37098 11.6995 10.629 10.9142 11.4142C10.539 11.7894 10.0306 12 9.5 12H4.5V8Z', - TextIndent: - 'M2.5 3C2.3674 3.00002 2.24024 3.05271 2.14648 3.14648C2.05274 3.24025 2.00008 3.36741 2.00008 3.5C2.00008 3.63259 2.05274 3.75975 2.14648 3.85352L4.29297 6L2.14648 8.14648C2.05274 8.24025 2.00008 8.36741 2.00008 8.5C2.00008 8.63259 2.05274 8.75975 2.14648 8.85352C2.24025 8.94726 2.36741 8.99992 2.5 8.99992C2.63259 8.99992 2.75975 8.94726 2.85352 8.85352L5.35352 6.35352C5.44725 6.25974 5.4999 6.13259 5.4999 6C5.4999 5.86741 5.44725 5.74026 5.35352 5.64648L2.85352 3.14648C2.75976 3.05271 2.6326 3.00002 2.5 3Z M7 3.5C6.86739 3.5 6.74021 3.55268 6.64645 3.64645C6.55268 3.74021 6.5 3.86739 6.5 4C6.5 4.13261 6.55268 4.25979 6.64645 4.35355C6.74021 4.44732 6.86739 4.5 7 4.5H13.5C13.6326 4.5 13.7598 4.44732 13.8536 4.35355C13.9473 4.25979 14 4.13261 14 4C14 3.86739 13.9473 3.74021 13.8536 3.64645C13.7598 3.55268 13.6326 3.5 13.5 3.5H7Z M7 7.5C6.86739 7.5 6.74021 7.55268 6.64645 7.64645C6.55268 7.74021 6.5 7.86739 6.5 8C6.5 8.13261 6.55268 8.25979 6.64645 8.35355C6.74021 8.44732 6.86739 8.5 7 8.5H13.5C13.6326 8.5 13.7598 8.44732 13.8536 8.35355C13.9473 8.25979 14 8.13261 14 8C14 7.86739 13.9473 7.74021 13.8536 7.64645C13.7598 7.55268 13.6326 7.5 13.5 7.5H7Z M2.5 11.5C2.36739 11.5 2.24021 11.5527 2.14645 11.6464C2.05268 11.7402 2 11.8674 2 12C2 12.1326 2.05268 12.2598 2.14645 12.3536C2.24021 12.4473 2.36739 12.5 2.5 12.5H13.5C13.6326 12.5 13.7598 12.4473 13.8536 12.3536C13.9473 12.2598 14 12.1326 14 12C14 11.8674 13.9473 11.7402 13.8536 11.6464C13.7598 11.5527 13.6326 11.5 13.5 11.5H2.5Z', - TextItalic: - 'M7 3C6.86739 3 6.74021 3.05268 6.64645 3.14645C6.55268 3.24021 6.5 3.36739 6.5 3.5C6.5 3.63261 6.55268 3.75979 6.64645 3.85355C6.74021 3.94732 6.86739 4 7 4H8.80627L6.13965 12H4C3.86739 12 3.74021 12.0527 3.64645 12.1464C3.55268 12.2402 3.5 12.3674 3.5 12.5C3.5 12.6326 3.55268 12.7598 3.64645 12.8536C3.74021 12.9473 3.86739 13 4 13H9C9.13261 13 9.25979 12.9473 9.35355 12.8536C9.44732 12.7598 9.5 12.6326 9.5 12.5C9.5 12.3674 9.44732 12.2402 9.35355 12.1464C9.25979 12.0527 9.13261 12 9 12H7.19373L9.86035 4H12C12.1326 4 12.2598 3.94732 12.3536 3.85355C12.4473 3.75979 12.5 3.63261 12.5 3.5C12.5 3.36739 12.4473 3.24021 12.3536 3.14645C12.2598 3.05268 12.1326 3 12 3H7Z', - TextOutdent: - 'M4.5 3C4.36739 3.00002 4.24023 3.05271 4.14648 3.14648L1.64648 5.64648C1.55275 5.74026 1.50009 5.86741 1.50009 6C1.50009 6.13259 1.55275 6.25974 1.64648 6.35352L4.14648 8.85352C4.24024 8.94726 4.36741 8.99992 4.5 8.99992C4.63258 8.99992 4.75975 8.94726 4.85351 8.85352C4.94726 8.75975 4.99992 8.63259 4.99992 8.5C4.99992 8.36741 4.94726 8.24025 4.85351 8.14648L2.70703 6L4.85351 3.85352C4.94726 3.75975 4.99992 3.63259 4.99992 3.5C4.99992 3.36741 4.94726 3.24025 4.85351 3.14648C4.75976 3.05271 4.6326 3.00002 4.5 3Z M7 3.5C6.86739 3.5 6.74021 3.55268 6.64644 3.64645C6.55267 3.74021 6.5 3.86739 6.5 4C6.5 4.13261 6.55267 4.25979 6.64644 4.35355C6.74021 4.44732 6.86739 4.5 7 4.5H13.5C13.6326 4.5 13.7598 4.44732 13.8535 4.35355C13.9473 4.25979 14 4.13261 14 4C14 3.86739 13.9473 3.74021 13.8535 3.64645C13.7598 3.55268 13.6326 3.5 13.5 3.5H7Z M7 7.5C6.86739 7.5 6.74021 7.55268 6.64644 7.64645C6.55267 7.74021 6.5 7.86739 6.5 8C6.5 8.13261 6.55267 8.25979 6.64644 8.35355C6.74021 8.44732 6.86739 8.5 7 8.5H13.5C13.6326 8.5 13.7598 8.44732 13.8535 8.35355C13.9473 8.25979 14 8.13261 14 8C14 7.86739 13.9473 7.74021 13.8535 7.64645C13.7598 7.55268 13.6326 7.5 13.5 7.5H7Z M2.5 11.5C2.36739 11.5 2.24021 11.5527 2.14644 11.6464C2.05267 11.7402 2 11.8674 2 12C2 12.1326 2.05267 12.2598 2.14644 12.3536C2.24021 12.4473 2.36739 12.5 2.5 12.5H13.5C13.6326 12.5 13.7598 12.4473 13.8535 12.3536C13.9473 12.2598 14 12.1326 14 12C14 11.8674 13.9473 11.7402 13.8535 11.6464C13.7598 11.5527 13.6326 11.5 13.5 11.5H2.5Z', - TextStrikethrough: - 'M7.99988 2.5C6.93773 2.5 6.00339 2.80573 5.31689 3.33643C4.63122 3.86648 4.19502 4.64164 4.19397 5.49695C4.19168 5.71924 4.22478 5.94047 4.29248 6.15222C4.31248 6.21477 4.3446 6.27276 4.38701 6.32289C4.42942 6.37302 4.48129 6.4143 4.53965 6.44439C4.59802 6.47447 4.66174 6.49277 4.72717 6.49822C4.79261 6.50368 4.85848 6.4962 4.92102 6.4762C5.04731 6.43581 5.15238 6.34692 5.21313 6.22907C5.27389 6.11123 5.28535 5.97407 5.245 5.84778C5.20967 5.7373 5.19237 5.62184 5.19373 5.50586V5.5C5.19373 4.97529 5.44467 4.50156 5.92847 4.12756C6.41226 3.75357 7.13084 3.5 7.99988 3.5C9.33892 3.5 10.299 4.09849 10.6434 4.81128C10.672 4.87041 10.7119 4.92334 10.761 4.96704C10.81 5.01074 10.8671 5.04435 10.9292 5.06596C10.9912 5.08756 11.0568 5.09674 11.1224 5.09296C11.188 5.08919 11.2522 5.07253 11.3113 5.04395C11.4307 4.98623 11.5222 4.88346 11.5658 4.75824C11.6094 4.63302 11.6015 4.49561 11.5438 4.37622C10.9883 3.22651 9.63584 2.5 7.99988 2.5Z M2.5 7.5C2.36739 7.5 2.24021 7.55268 2.14645 7.64645C2.05268 7.74021 2 7.86739 2 8C2 8.13261 2.05268 8.25979 2.14645 8.35355C2.24021 8.44732 2.36739 8.5 2.5 8.5H8.56873C9.2761 8.70935 9.91916 8.94995 10.3422 9.25964C10.7776 9.57845 11 9.90751 11 10.5C11 11.0013 10.7195 11.4786 10.1837 11.8611C9.64797 12.2436 8.86908 12.5 8 12.5C7.13092 12.5 6.35203 12.2436 5.81628 11.8611C5.28054 11.4786 5 11.0013 5 10.5C5 10.3674 4.94732 10.2402 4.85355 10.1464C4.75979 10.0527 4.63261 10 4.5 10C4.36739 10 4.24021 10.0527 4.14645 10.1464C4.05268 10.2402 4 10.3674 4 10.5C4 11.3799 4.50379 12.1527 5.23523 12.6749C5.96667 13.1971 6.93783 13.5 8 13.5C9.06217 13.5 10.0333 13.1971 10.7648 12.6749C11.4962 12.1527 12 11.3799 12 10.5C12 9.63251 11.571 8.95109 10.9852 8.5H13.5C13.6326 8.5 13.7598 8.44732 13.8536 8.35355C13.9473 8.25979 14 8.13261 14 8C14 7.86739 13.9473 7.74021 13.8536 7.64645C13.7598 7.55268 13.6326 7.5 13.5 7.5H2.5Z', - TextT: - 'M3 3C2.8674 3.00001 2.74023 3.0527 2.64646 3.14646C2.5527 3.24023 2.50001 3.3674 2.5 3.5V5.5C2.5 5.63261 2.55268 5.75979 2.64645 5.85355C2.74021 5.94732 2.86739 6 3 6C3.13261 6 3.25979 5.94732 3.35355 5.85355C3.44732 5.75979 3.5 5.63261 3.5 5.5V4H7.5V12H6C5.86739 12 5.74021 12.0527 5.64645 12.1464C5.55268 12.2402 5.5 12.3674 5.5 12.5C5.5 12.6326 5.55268 12.7598 5.64645 12.8536C5.74021 12.9473 5.86739 13 6 13H10C10.1326 13 10.2598 12.9473 10.3536 12.8536C10.4473 12.7598 10.5 12.6326 10.5 12.5C10.5 12.3674 10.4473 12.2402 10.3536 12.1464C10.2598 12.0527 10.1326 12 10 12H8.5V4H12.5V5.5C12.5 5.63261 12.5527 5.75979 12.6464 5.85355C12.7402 5.94732 12.8674 6 13 6C13.1326 6 13.2598 5.94732 13.3536 5.85355C13.4473 5.75979 13.5 5.63261 13.5 5.5V3.5C13.5 3.3674 13.4473 3.24023 13.3535 3.14646C13.2598 3.0527 13.1326 3.00001 13 3H3Z', - TextUnderline: - 'M4 3C3.86739 3 3.74021 3.05268 3.64645 3.14645C3.55268 3.24021 3.5 3.36739 3.5 3.5V7.5C3.5 9.97936 5.52064 12 8 12C10.4794 12 12.5 9.97936 12.5 7.5V3.5C12.5 3.36739 12.4473 3.24021 12.3536 3.14645C12.2598 3.05268 12.1326 3 12 3C11.8674 3 11.7402 3.05268 11.6464 3.14645C11.5527 3.24021 11.5 3.36739 11.5 3.5V7.5C11.5 9.43892 9.93892 11 8 11C6.06108 11 4.5 9.43892 4.5 7.5V3.5C4.5 3.36739 4.44732 3.24021 4.35355 3.14645C4.25979 3.05268 4.13261 3 4 3Z M2.5 13C2.36739 13 2.24021 13.0527 2.14645 13.1464C2.05268 13.2402 2 13.3674 2 13.5C2 13.6326 2.05268 13.7598 2.14645 13.8536C2.24021 13.9473 2.36739 14 2.5 14H13.5C13.6326 14 13.7598 13.9473 13.8536 13.8536C13.9473 13.7598 14 13.6326 14 13.5C14 13.3674 13.9473 13.2402 13.8536 13.1464C13.7598 13.0527 13.6326 13 13.5 13H2.5Z', - ThumbsUp: - 'M7.5 1C7.40714 0.999994 7.31611 1.02584 7.23711 1.07466C7.15812 1.12347 7.09427 1.19331 7.05273 1.27637L4.69092 6H2C1.45364 6 1 6.45364 1 7V12.5C1.00007 13.0463 1.45357 13.4999 1.99988 13.5C1.99984 13.5 1.99992 13.5 1.99988 13.5H12.6188C12.6195 13.5 12.6203 13.5 12.6211 13.5C13.3705 13.4966 14.0087 12.9311 14.1023 12.1875C14.1023 12.1877 14.1023 12.1873 14.1023 12.1875L14.8519 6.19103C14.9702 5.30298 14.2635 4.49779 13.3676 4.5H10V3.5C10.0001 2.12516 8.87484 0.999914 7.5 1ZM7.79443 2.0293C8.48551 2.1645 9.00005 2.76663 9 3.5V5C9.00001 5.1326 9.0527 5.25977 9.14646 5.35354C9.24023 5.4473 9.3674 5.49999 9.5 5.5H13.3688C13.3692 5.5 13.3696 5.5 13.37 5.5C13.6779 5.49924 13.9013 5.7537 13.8606 6.05896C13.8604 6.0603 13.8603 6.06164 13.8601 6.06299L13.1101 12.0625C13.0782 12.3155 12.8714 12.4988 12.6164 12.5H5.5V6.61816L7.79443 2.0293ZM2 7H4.5V12.5H2.00012L2 7Z', - ThumbsUpFill: - 'M14.4937 5.00625C14.3518 4.84816 14.1785 4.72147 13.9847 4.63429C13.791 4.54711 13.5812 4.50137 13.3687 4.5H10V3.5C9.99835 2.83747 9.73443 2.20254 9.26594 1.73406C8.79746 1.26557 8.16253 1.00165 7.5 1C7.40711 1.00075 7.31618 1.02674 7.23691 1.07518C7.15765 1.12361 7.09304 1.19268 7.05 1.275L4.6875 6H2C1.73478 6 1.48043 6.10536 1.29289 6.29289C1.10536 6.48043 1 6.73478 1 7V12.5C1 12.7652 1.10536 13.0196 1.29289 13.2071C1.48043 13.3946 1.73478 13.5 2 13.5H12.6187C12.9836 13.4985 13.3355 13.3646 13.6091 13.1232C13.8827 12.8818 14.0593 12.5493 14.1062 12.1875L14.8562 6.1875C14.8812 5.97623 14.8619 5.76207 14.7994 5.55869C14.737 5.35531 14.6329 5.16716 14.4937 5.00625ZM2 7H4.5V12.5H2V7Z', - Timeline: - 'M9 0.5C9 0.223858 8.77614 0 8.5 0C8.22386 0 8 0.223858 8 0.5V15.5C8 15.7761 8.22386 16 8.5 16C8.77614 16 9 15.7761 9 15.5V14H11.5C12.3284 14 13 13.3284 13 12.5V10.5C13 9.67157 12.3284 9 11.5 9H9V7H14.5C15.3284 7 16 6.32843 16 5.5V3.5C16 2.67157 15.3284 2 14.5 2H9V0.5ZM9 3V6H14.5C14.7761 6 15 5.77614 15 5.5V3.5C15 3.22386 14.7761 3 14.5 3H9ZM9 10V13H11.5C11.7761 13 12 12.7761 12 12.5V10.5C12 10.2239 11.7761 10 11.5 10H9Z M4.5 2H7V3H4.5C4.22386 3 4 3.22386 4 3.5V5.5C4 5.77614 4.22386 6 4.5 6H7V7H4.5C3.67157 7 3 6.32843 3 5.5V3.5C3 2.67157 3.67157 2 4.5 2Z M7 9H1.5C0.671573 9 0 9.67157 0 10.5V12.5C0 13.3284 0.671573 14 1.5 14H7V13H1.5C1.22386 13 1 12.7761 1 12.5V10.5C1 10.2239 1.22386 10 1.5 10H7V9Z', - ToggleRight: - 'M11 5.5C9.62522 5.5 8.5 6.62522 8.5 8C8.5 9.37478 9.62522 10.5 11 10.5C12.3748 10.5 13.5 9.37478 13.5 8C13.5 6.62522 12.3748 5.5 11 5.5ZM11 6.5C11.8343 6.5 12.5 7.16566 12.5 8C12.5 8.83434 11.8343 9.5 11 9.5C10.1657 9.5 9.5 8.83434 9.5 8C9.5 7.16566 10.1657 6.5 11 6.5Z M5 3.5C2.52065 3.5 0.5 5.52065 0.5 8C0.5 10.4793 2.52065 12.5 5 12.5H11C13.4793 12.5 15.5 10.4793 15.5 8C15.5 5.52065 13.4793 3.5 11 3.5H5ZM5 4.5H11C12.9389 4.5 14.5 6.0611 14.5 8C14.5 9.9389 12.9389 11.5 11 11.5H5C3.0611 11.5 1.5 9.9389 1.5 8C1.5 6.0611 3.0611 4.5 5 4.5Z', - TopLeftNavElementFeature: - 'M1 3.5C1 2.67157 1.67157 2 2.5 2H13.5C14.3284 2 15 2.67157 15 3.5V12.5C15 13.3284 14.3284 14 13.5 14H2.5C1.67157 14 1 13.3284 1 12.5V3.5ZM2.5 3C2.22386 3 2 3.22386 2 3.5V5H14V3.5C14 3.22386 13.7761 3 13.5 3H2.5ZM2 12.5V6H5V13H2.5C2.22386 13 2 12.7761 2 12.5ZM6 13H13.5C13.7761 13 14 12.7761 14 12.5V6H6V13Z', - TopNavElementFeature: - 'M2.5 2C1.67157 2 1 2.67157 1 3.5V12.5C1 13.3284 1.67157 14 2.5 14H13.5C14.3284 14 15 13.3284 15 12.5V3.5C15 2.67157 14.3284 2 13.5 2H2.5ZM2 3.5C2 3.22386 2.22386 3 2.5 3H13.5C13.7761 3 14 3.22386 14 3.5V5H2V3.5ZM2 6V12.5C2 12.7761 2.22386 13 2.5 13H13.5C13.7761 13 14 12.7761 14 12.5V6H2Z', - Trash: - 'M6.5 6C6.36739 6 6.24021 6.05268 6.14645 6.14645C6.05268 6.24021 6 6.36739 6 6.5V10.5C6 10.6326 6.05268 10.7598 6.14645 10.8536C6.24021 10.9473 6.36739 11 6.5 11C6.63261 11 6.75979 10.9473 6.85355 10.8536C6.94732 10.7598 7 10.6326 7 10.5V6.5C7 6.36739 6.94732 6.24021 6.85355 6.14645C6.75979 6.05268 6.63261 6 6.5 6Z M9.5 6C9.36739 6 9.24021 6.05268 9.14645 6.14645C9.05268 6.24021 9 6.36739 9 6.5V10.5C9 10.6326 9.05268 10.7598 9.14645 10.8536C9.24021 10.9473 9.36739 11 9.5 11C9.63261 11 9.75979 10.9473 9.85355 10.8536C9.94732 10.7598 10 10.6326 10 10.5V6.5C10 6.36739 9.94732 6.24021 9.85355 6.14645C9.75979 6.05268 9.63261 6 9.5 6Z M6.5 1C5.6775 1 5 1.6775 5 2.5V3H2.5C2.36739 3 2.24021 3.05268 2.14645 3.14645C2.05268 3.24021 2 3.36739 2 3.5C2 3.63261 2.05268 3.75979 2.14645 3.85355C2.24021 3.94732 2.36739 4 2.5 4H3V13C3.00007 13.5463 3.45357 13.9999 3.99988 14C3.99984 14 3.99992 14 3.99988 14H12C12.5464 14 13 13.5464 13 13V4H13.5C13.6326 4 13.7598 3.94732 13.8536 3.85355C13.9473 3.75979 14 3.63261 14 3.5C14 3.36739 13.9473 3.24021 13.8536 3.14645C13.7598 3.05268 13.6326 3 13.5 3H11V2.5C11 2.50004 11 2.49996 11 2.5C10.9999 1.67757 10.3226 1.0001 9.50012 1C9.50016 1 9.50008 1 9.50012 1H6.5ZM6.5 2H9.5C9.78202 2.0001 9.99996 2.21808 10 2.50012V3H6V2.5C6 2.21794 6.21794 2 6.5 2ZM4 4H12V13H4.00012L4 4Z', - TrashSimple: - 'M5.5 1C5.36739 1 5.24021 1.05268 5.14645 1.14645C5.05268 1.24021 5 1.36739 5 1.5C5 1.63261 5.05268 1.75979 5.14645 1.85355C5.24021 1.94732 5.36739 2 5.5 2H10.5C10.6326 2 10.7598 1.94732 10.8536 1.85355C10.9473 1.75979 11 1.63261 11 1.5C11 1.36739 10.9473 1.24021 10.8536 1.14645C10.7598 1.05268 10.6326 1 10.5 1H5.5Z M2.5 3C2.36739 3 2.24021 3.05268 2.14645 3.14645C2.05268 3.24021 2 3.36739 2 3.5C2 3.63261 2.05268 3.75979 2.14645 3.85355C2.24021 3.94732 2.36739 4 2.5 4H3V13C3.00007 13.5463 3.45357 13.9999 3.99988 14C3.99984 14 3.99992 14 3.99988 14H12C12.5464 14 13 13.5464 13 13V4H13.5C13.6326 4 13.7598 3.94732 13.8536 3.85355C13.9473 3.75979 14 3.63261 14 3.5C14 3.36739 13.9473 3.24021 13.8536 3.14645C13.7598 3.05268 13.6326 3 13.5 3H2.5ZM4 4H12V13H4.00012L4 4Z', - TwitterLogo: - 'M10.537 2.50002C8.88146 2.47806 7.5 3.84509 7.5 5.50002V5.87929C4.97109 5.21518 2.85352 3.1465 2.85352 3.1465C2.78899 3.08201 2.70806 3.03639 2.61947 3.0146C2.53088 2.99281 2.43802 2.99566 2.35094 3.02286C2.26386 3.05005 2.18588 3.10055 2.12544 3.16889C2.065 3.23722 2.02441 3.32079 2.00806 3.41054C1.48516 6.28647 2.27925 8.32915 3.22241 9.60915C3.77392 10.3576 4.35705 10.8331 4.76758 11.1206C3.82445 12.1798 2.32446 12.7819 2.32446 12.7819C2.25395 12.8083 2.19032 12.8504 2.13833 12.9049C2.08634 12.9593 2.04733 13.0249 2.0242 13.0965C2.00108 13.1682 1.99445 13.2442 2.0048 13.3188C2.01515 13.3934 2.04222 13.4647 2.08398 13.5274C2.08398 13.5274 2.16392 13.639 2.2749 13.7389C2.38588 13.8388 2.54912 13.9587 2.77637 14.0723C3.23087 14.2995 3.9375 14.5 5 14.5C9.40889 14.502 13.074 11.1196 13.4578 6.74941L15.3535 4.85353C15.4234 4.7836 15.471 4.69451 15.4903 4.59754C15.5095 4.50057 15.4996 4.40006 15.4618 4.30872C15.424 4.21737 15.3599 4.13929 15.2777 4.08435C15.1955 4.0294 15.0989 4.00006 15 4.00002H13.0941C12.5686 3.09247 11.6016 2.51246 10.537 2.50002ZM10.5244 3.50002C10.5248 3.50002 10.5251 3.50002 10.5255 3.50002C11.3136 3.50913 12.0202 3.97757 12.3355 4.69997C12.3744 4.78914 12.4384 4.86502 12.5198 4.91832C12.6012 4.97161 12.6964 5 12.7937 5.00002H13.793L12.6277 6.16518C12.5418 6.25113 12.4901 6.36543 12.4823 6.48671C12.2285 10.437 8.95864 13.5019 5.00024 13.5C5.00032 13.5 5.00016 13.5 5.00024 13.5C4.33566 13.5 3.8766 13.4112 3.55603 13.3075C4.26955 12.9241 5.25685 12.2661 5.91602 11.2774C5.95534 11.2184 5.98168 11.1517 5.99327 11.0817C6.00486 11.0118 6.00145 10.9402 5.98326 10.8717C5.96506 10.8031 5.93251 10.7393 5.88774 10.6843C5.84298 10.6293 5.78704 10.5845 5.72363 10.5528C5.72363 10.5528 4.83443 10.1109 4.02759 9.01589C3.31793 8.05278 2.68817 6.61451 2.87097 4.49684C3.84397 5.30283 5.71174 6.6255 7.91785 6.99318C7.98947 7.0051 8.06283 7.00128 8.13282 6.98199C8.20282 6.9627 8.26777 6.92839 8.32317 6.88146C8.37857 6.83453 8.42309 6.7761 8.45362 6.71023C8.48416 6.64436 8.49999 6.57262 8.5 6.50002V5.50002C8.5 4.40532 9.43043 3.4851 10.5244 3.50002Z', - UploadSimple: - 'M8 2C7.8674 2.00002 7.74024 2.05271 7.64648 2.14648L5.02148 4.77148C4.92774 4.86525 4.87508 4.99241 4.87508 5.125C4.87508 5.25759 4.92774 5.38475 5.02148 5.47852C5.11525 5.57226 5.24241 5.62492 5.375 5.62492C5.50759 5.62492 5.63475 5.57226 5.72852 5.47852L7.5 3.70703V9.5C7.5 9.63261 7.55268 9.75979 7.64645 9.85355C7.74021 9.94732 7.86739 10 8 10C8.13261 10 8.25979 9.94732 8.35355 9.85355C8.44732 9.75979 8.5 9.63261 8.5 9.5V3.70703L10.2715 5.47852C10.3652 5.57226 10.4924 5.62492 10.625 5.62492C10.7576 5.62492 10.8848 5.57226 10.9785 5.47852C11.0723 5.38475 11.1249 5.25759 11.1249 5.125C11.1249 4.99241 11.0723 4.86525 10.9785 4.77148L8.35352 2.14648C8.34867 2.14437 8.34378 2.14234 8.33887 2.14038C8.24777 2.05235 8.12666 2.00218 8 2Z M2.5 9C2.36739 9 2.24021 9.05268 2.14645 9.14645C2.05268 9.24021 2 9.36739 2 9.5V13C2.00007 13.5463 2.45357 13.9999 2.99988 14C2.99984 14 2.99992 14 2.99988 14H13C13.5464 14 14 13.5464 14 13V9.5C14 9.36739 13.9473 9.24021 13.8536 9.14645C13.7598 9.05268 13.6326 9 13.5 9C13.3674 9 13.2402 9.05268 13.1464 9.14645C13.0527 9.24021 13 9.36739 13 9.5V13H3.00012L3 9.5C3 9.36739 2.94732 9.24021 2.85355 9.14645C2.75978 9.05268 2.63261 9 2.5 9Z', - User: - 'M8 9.49951C5.32109 9.49957 2.84382 10.93 1.50451 13.2501C1.43822 13.365 1.42025 13.5014 1.45457 13.6295C1.48888 13.7576 1.57267 13.8668 1.6875 13.9331C1.80235 13.9994 1.93883 14.0173 2.06691 13.983C2.195 13.9487 2.30419 13.8648 2.37048 13.75C3.53197 11.738 5.67677 10.4996 8 10.4995C10.3232 10.4995 12.4681 11.7379 13.6295 13.75C13.6958 13.8648 13.805 13.9487 13.9331 13.983C14.0612 14.0173 14.1976 13.9994 14.3125 13.9331C14.4273 13.8668 14.5111 13.7576 14.5454 13.6295C14.5797 13.5014 14.5618 13.365 14.4955 13.2501C13.1563 10.9299 10.679 9.49944 8 9.49951Z M8 1.5C5.52065 1.5 3.5 3.52065 3.5 6C3.5 8.47935 5.52065 10.4995 8 10.4995C10.4793 10.4995 12.5 8.47935 12.5 6C12.5 3.52065 10.4793 1.5 8 1.5ZM8 2.5C9.9389 2.5 11.5 4.0611 11.5 6C11.5 7.9389 9.9389 9.49951 8 9.49951C6.0611 9.49951 4.5 7.9389 4.5 6C4.5 4.0611 6.0611 2.5 8 2.5Z', - Users: - 'M10.5922 3C10.2508 3.00105 9.91112 3.04688 9.58166 3.13623C9.45368 3.17094 9.34474 3.25507 9.27878 3.3701C9.21283 3.48514 9.19527 3.62166 9.22997 3.74963C9.26468 3.87761 9.34881 3.98655 9.46384 4.05251C9.57888 4.11846 9.7154 4.13602 9.84338 4.10132C10.0882 4.03493 10.3407 4.00099 10.5944 4.00012C12.1188 4.00046 13.3437 5.22551 13.3437 6.75C13.3437 8.27471 12.1184 9.5 10.5937 9.5C10.5938 9.5 10.5936 9.5 10.5937 9.5C10.5286 9.51286 10.4664 9.53855 10.4114 9.57556C10.3503 9.58836 10.2923 9.61245 10.2401 9.64661C10.2059 9.69888 10.1818 9.75709 10.1691 9.81824C10.1322 9.87324 10.1066 9.93502 10.0937 10C10.1066 10.0651 10.1323 10.1273 10.1693 10.1824C10.1821 10.2434 10.2062 10.3015 10.2403 10.3536C10.2926 10.3878 10.3508 10.4119 10.412 10.4247C10.467 10.4616 10.5288 10.4872 10.5937 10.5C10.5936 10.5 10.5939 10.5 10.5937 10.5C12.2226 10.499 13.7486 11.2915 14.6844 12.6248C14.7222 12.6785 14.7701 12.7243 14.8255 12.7595C14.881 12.7947 14.9428 12.8187 15.0075 12.83C15.0721 12.8413 15.1384 12.8398 15.2025 12.8255C15.2666 12.8112 15.3272 12.7844 15.381 12.7467C15.4895 12.6705 15.5633 12.5544 15.5862 12.4238C15.6091 12.2932 15.5792 12.1588 15.5031 12.0503C14.7925 11.0381 13.7964 10.2873 12.6661 9.87122C13.6757 9.19798 14.3437 8.04978 14.3437 6.75C14.3437 4.68486 12.6589 3 10.5937 3C10.5932 3 10.5927 3 10.5922 3Z M5.49999 9.49976C3.54646 9.49969 1.71405 10.4516 0.590934 12.05C0.514698 12.1585 0.484679 12.2929 0.50748 12.4235C0.530281 12.5541 0.604033 12.6703 0.712516 12.7466C0.821007 12.8228 0.955338 12.8528 1.08596 12.83C1.21658 12.8072 1.3328 12.7335 1.40905 12.625C2.34551 11.2922 3.87113 10.4997 5.49999 10.4998C7.12885 10.4998 8.65439 11.2923 9.59093 12.625C9.66718 12.7335 9.7834 12.8072 9.91402 12.83C10.0446 12.8528 10.179 12.8228 10.2875 12.7466C10.3959 12.6703 10.4697 12.5541 10.4925 12.4235C10.5153 12.2929 10.4853 12.1585 10.409 12.05C9.28586 10.4518 7.45347 9.4998 5.49999 9.49976Z M5.49999 3C3.43484 3 1.74999 4.68485 1.74999 6.75C1.74999 8.81515 3.43484 10.4998 5.49999 10.4998C7.56514 10.4998 9.24999 8.81515 9.24999 6.75C9.24999 4.68485 7.56514 3 5.49999 3ZM5.49999 4C7.02471 4 8.24999 5.22528 8.24999 6.75C8.24999 8.27472 7.02471 9.49976 5.49999 9.49976C3.97527 9.49976 2.74999 8.27472 2.74999 6.75C2.74999 5.22528 3.97527 4 5.49999 4Z', - UsersFour: - 'M11.0001 1.5C9.62528 1.5 8.50007 2.62522 8.50007 4C8.50007 4.73367 8.82648 5.38947 9.3343 5.8479C8.83907 6.05937 8.38547 6.36163 7.99995 6.74658C7.67829 6.42533 7.30963 6.15322 6.9007 5.94873C6.9007 5.94877 6.9007 5.94869 6.9007 5.94873C5.06649 5.03169 2.83049 5.55943 1.60004 7.19995C1.56063 7.25248 1.53196 7.31226 1.51566 7.37587C1.49935 7.43949 1.49574 7.50569 1.50502 7.5707C1.51431 7.63571 1.5363 7.69825 1.56976 7.75476C1.60322 7.81126 1.64748 7.86063 1.70002 7.90002C1.75255 7.93943 1.81233 7.96811 1.87594 7.98441C1.93955 8.00072 2.00576 8.00433 2.07076 7.99505C2.13577 7.98576 2.19832 7.96376 2.25482 7.93031C2.31133 7.89685 2.36069 7.85259 2.40009 7.80005C3.34448 6.54092 5.04577 6.13929 6.45356 6.84314C6.90485 7.06881 7.2973 7.3964 7.60004 7.80005C7.62002 7.81397 7.64099 7.82641 7.66279 7.83728C7.67365 7.85907 7.6861 7.88004 7.70002 7.90002C7.73904 7.91546 7.77985 7.92591 7.82148 7.93115C7.87766 7.95735 7.93822 7.97287 8.00007 7.97693C8.06192 7.97287 8.12248 7.95735 8.17866 7.93115C8.22029 7.92591 8.2611 7.91546 8.30012 7.90002C8.33522 7.87242 8.36646 7.84023 8.39301 7.80432C8.39538 7.80292 8.39774 7.80149 8.40009 7.80005C9.01418 6.98136 9.97667 6.50005 11.0001 6.5C12.0235 6.50005 12.986 6.98136 13.6 7.80005C13.6394 7.85259 13.6888 7.89685 13.7453 7.93031C13.8018 7.96376 13.8644 7.98576 13.9294 7.99505C13.9944 8.00433 14.0606 8.00072 14.1242 7.98441C14.1878 7.96811 14.2476 7.93943 14.3001 7.90002C14.3527 7.86063 14.3969 7.81126 14.4304 7.75476C14.4638 7.69825 14.4858 7.63571 14.4951 7.5707C14.5044 7.50569 14.5008 7.43949 14.4845 7.37587C14.4682 7.31226 14.4395 7.25248 14.4001 7.19995C13.9464 6.5951 13.3434 6.13725 12.6658 5.8479C13.1737 5.38947 13.5001 4.73367 13.5001 4C13.5001 2.62522 12.3749 1.5 11.0001 1.5ZM11.0001 2.5C11.8344 2.5 12.5001 3.16566 12.5001 4C12.5001 4.83434 11.8344 5.5 11.0001 5.5C10.1657 5.5 9.50007 4.83434 9.50007 4C9.50007 3.16566 10.1657 2.5 11.0001 2.5Z M11.0001 8C9.62528 8 8.50007 9.12522 8.50007 10.5C8.50007 11.2337 8.82648 11.8895 9.3343 12.3479C8.65671 12.6372 8.05373 13.0951 7.60004 13.7C7.56063 13.7525 7.53196 13.8123 7.51566 13.8759C7.49935 13.9395 7.49574 14.0057 7.50502 14.0707C7.51431 14.1357 7.5363 14.1983 7.56976 14.2548C7.60322 14.3113 7.64748 14.3606 7.70002 14.4C7.75255 14.4394 7.81233 14.4681 7.87594 14.4844C7.93955 14.5007 8.00575 14.5043 8.07076 14.495C8.13577 14.4858 8.19832 14.4638 8.25483 14.4303C8.31133 14.3968 8.36069 14.3526 8.40009 14.3C9.01418 13.4814 9.97667 13.0001 11.0001 13C12.0235 13.0001 12.986 13.4814 13.6 14.3C13.6394 14.3526 13.6888 14.3968 13.7453 14.4303C13.8018 14.4638 13.8644 14.4858 13.9294 14.495C13.9944 14.5043 14.0606 14.5007 14.1242 14.4844C14.1878 14.4681 14.2476 14.4394 14.3001 14.4C14.3527 14.3606 14.3969 14.3113 14.4304 14.2548C14.4638 14.1983 14.4858 14.1357 14.4951 14.0707C14.5044 14.0057 14.5008 13.9395 14.4845 13.8759C14.4682 13.8123 14.4395 13.7525 14.4001 13.7C13.9464 13.0951 13.3434 12.6372 12.6658 12.3479C13.1737 11.8895 13.5001 11.2337 13.5001 10.5C13.5001 9.12522 12.3749 8 11.0001 8ZM11.0001 9C11.8344 9 12.5001 9.66566 12.5001 10.5C12.5001 11.3343 11.8344 12 11.0001 12C10.1657 12 9.50007 11.3343 9.50007 10.5C9.50007 9.66566 10.1657 9 11.0001 9Z M5.00007 1.5C3.62528 1.5 2.50007 2.62522 2.50007 4C2.50007 5.37478 3.62528 6.5 5.00007 6.5C6.37485 6.5 7.50007 5.37478 7.50007 4C7.50007 2.62522 6.37485 1.5 5.00007 1.5ZM5.00007 2.5C5.83441 2.5 6.50007 3.16566 6.50007 4C6.50007 4.83434 5.83441 5.5 5.00007 5.5C4.16573 5.5 3.50007 4.83434 3.50007 4C3.50007 3.16566 4.16573 2.5 5.00007 2.5Z M5.00007 8C3.62528 8 2.50007 9.12522 2.50007 10.5C2.50007 11.8748 3.62528 13 5.00007 13C6.37485 13 7.50007 11.8748 7.50007 10.5C7.50007 9.12522 6.37485 8 5.00007 8ZM5.10639 12.0021C3.76722 11.9702 2.44598 12.5721 1.60004 13.7C1.56063 13.7525 1.53196 13.8123 1.51566 13.8759C1.49935 13.9395 1.49574 14.0057 1.50502 14.0707C1.51431 14.1357 1.5363 14.1983 1.56976 14.2548C1.60322 14.3113 1.64748 14.3606 1.70002 14.4C1.75255 14.4394 1.81233 14.4681 1.87594 14.4844C1.93955 14.5007 2.00576 14.5043 2.07076 14.495C2.13577 14.4858 2.19832 14.4638 2.25482 14.4303C2.31133 14.3968 2.36069 14.3526 2.40009 14.3C3.34448 13.0409 5.04577 12.6393 6.45356 13.3431C6.90485 13.5688 7.2973 13.8964 7.60004 14.3C7.63944 14.3526 7.68881 14.3968 7.74531 14.4303C7.80182 14.4638 7.86436 14.4858 7.92937 14.495C7.99438 14.5043 8.06058 14.5007 8.1242 14.4844C8.18781 14.4681 8.24759 14.4394 8.30012 14.4C8.35265 14.3606 8.39692 14.3113 8.43037 14.2548C8.46383 14.1983 8.48583 14.1357 8.49511 14.0707C8.5044 14.0057 8.50079 13.9395 8.48448 13.8759C8.46818 13.8123 8.4395 13.7525 8.40009 13.7C8.00423 13.1721 7.49081 12.7438 6.9007 12.4487C6.32751 12.1621 5.71511 12.0166 5.10639 12.0021ZM5.00007 9C5.83441 9 6.50007 9.66566 6.50007 10.5C6.50007 11.3343 5.83441 12 5.00007 12C4.16573 12 3.50007 11.3343 3.50007 10.5C3.50007 9.66566 4.16573 9 5.00007 9Z', - UsersThree: - 'M3.68726 2.76918C3.00369 2.77619 2.31788 3.05605 1.8208 3.65761C0.919321 4.74857 1.17576 6.24775 2.08557 7.09572C1.40673 7.38504 0.802933 7.84404 0.349488 8.4507C0.310181 8.50329 0.281619 8.56312 0.265432 8.62675C0.249245 8.69038 0.24575 8.75658 0.255147 8.82157C0.264544 8.88656 0.286648 8.94905 0.320199 9.00549C0.353749 9.06194 0.398088 9.11122 0.450684 9.15053C0.503281 9.18983 0.563104 9.21839 0.626738 9.23458C0.690373 9.25077 0.756572 9.25426 0.821558 9.24487C0.886543 9.23547 0.949041 9.21337 1.00548 9.17981C1.06193 9.14626 1.11121 9.10193 1.15051 9.04933C1.76315 8.2297 2.72586 7.74834 3.74915 7.75001C3.74907 7.75005 3.74923 7.74997 3.74915 7.75001C3.74953 7.75001 3.75011 7.75001 3.75049 7.75001C3.87664 7.74769 3.99725 7.69777 4.08814 7.61024C4.09539 7.60337 4.10243 7.59629 4.10925 7.589C4.19691 7.49831 4.24706 7.37783 4.24963 7.25172C4.24951 7.252 4.24976 7.25144 4.24963 7.25172C4.24959 7.25147 4.24992 7.25038 4.24988 7.25013C4.24984 7.25034 4.24992 7.24993 4.24988 7.25013C4.24976 7.24984 4.24976 7.24894 4.24963 7.24865C4.24718 7.12237 4.19703 7.0017 4.10925 6.91088C4.10254 6.90377 4.09562 6.89685 4.0885 6.89013C3.99767 6.80248 3.87706 6.75243 3.75086 6.75001C3.75044 6.75001 3.75005 6.75014 3.74963 6.75014C3.74967 6.75018 3.74959 6.7501 3.74963 6.75014C2.44509 6.75147 1.76078 5.30012 2.59168 4.29457C3.42258 3.28902 4.97671 3.68735 5.22131 4.96876C5.23363 5.03326 5.25853 5.09471 5.29459 5.14958C5.33066 5.20446 5.37718 5.25169 5.4315 5.28859C5.48582 5.32549 5.54687 5.35132 5.61118 5.36462C5.67548 5.37792 5.74178 5.37843 5.80628 5.3661C5.93651 5.34123 6.05154 5.26564 6.12605 5.15596C6.20057 5.04629 6.22847 4.91151 6.20361 4.78126C5.95974 3.50367 4.82653 2.7575 3.68726 2.76918Z M12.3127 2.76918C11.1735 2.7575 10.0403 3.50367 9.79639 4.78126C9.77154 4.91151 9.79943 5.04629 9.87395 5.15596C9.94846 5.26564 10.0635 5.34123 10.1937 5.3661C10.2582 5.37843 10.3245 5.37792 10.3888 5.36462C10.4531 5.35132 10.5142 5.32549 10.5685 5.28859C10.6228 5.25169 10.6693 5.20446 10.7054 5.14958C10.7415 5.09471 10.7664 5.03326 10.7787 4.96876C11.0233 3.68735 12.5774 3.28902 13.4083 4.29457C14.2392 5.30012 13.555 6.75134 12.2505 6.75001C12.2505 6.74997 12.2504 6.75005 12.2505 6.75001C12.25 6.75001 12.2496 6.75001 12.2491 6.75001C12.1871 6.76292 12.1282 6.78748 12.0753 6.8224C12.0115 6.83534 11.9508 6.86064 11.8966 6.89686C11.8603 6.95112 11.835 7.01196 11.8221 7.07594C11.7873 7.12872 11.7629 7.18762 11.75 7.24952C11.75 7.24931 11.7501 7.24973 11.75 7.24952C11.75 7.24976 11.7501 7.25064 11.75 7.25088C11.7629 7.31289 11.7875 7.37187 11.8224 7.42471C11.8353 7.48856 11.8606 7.54927 11.8969 7.60342C11.9511 7.63969 12.0119 7.66499 12.0759 7.67788C12.1287 7.71269 12.1876 7.73717 12.2495 7.75003C12.2499 7.75003 12.2502 7.7499 12.2506 7.7499C12.2505 7.74986 12.2507 7.74994 12.2506 7.7499C13.2738 7.7481 14.237 8.22964 14.8495 9.04934C14.8888 9.10194 14.9381 9.14628 14.9945 9.17983C15.051 9.21338 15.1135 9.23548 15.1785 9.24488C15.2434 9.25428 15.3096 9.25078 15.3733 9.2346C15.4369 9.21841 15.4967 9.18985 15.5493 9.15054C15.6019 9.11123 15.6463 9.06195 15.6798 9.00551C15.7134 8.94907 15.7355 8.88657 15.7449 8.82158C15.7543 8.7566 15.7508 8.6904 15.7346 8.62676C15.7184 8.56313 15.6898 8.50331 15.6505 8.45071C15.1971 7.844 14.5934 7.38493 13.9146 7.09561C14.8243 6.24762 15.0806 4.74853 14.1792 3.65762C13.6821 3.05606 12.9962 2.77619 12.3127 2.76918Z M8 5.75001C6.34908 5.75001 5 7.0991 5 8.75001C5 9.72266 5.47549 10.5819 6.19788 11.1309C5.23485 11.5518 4.42849 12.3022 3.95068 13.2808C3.92187 13.3398 3.90497 13.4039 3.90093 13.4694C3.8969 13.535 3.90582 13.6007 3.92717 13.6628C3.94853 13.7249 3.98191 13.7821 4.0254 13.8313C4.0689 13.8805 4.12165 13.9207 4.18067 13.9495C4.29982 14.0076 4.4372 14.0161 4.56258 13.9729C4.68796 13.9298 4.79107 13.8386 4.84924 13.7195C5.43767 12.5144 6.65894 11.7517 8 11.7517C9.34106 11.7517 10.5623 12.5144 11.1508 13.7195C11.2089 13.8386 11.312 13.9298 11.4374 13.9729C11.5628 14.0161 11.7002 14.0076 11.8193 13.9495C11.8783 13.9207 11.9311 13.8805 11.9746 13.8313C12.0181 13.7821 12.0515 13.7249 12.0728 13.6628C12.0942 13.6007 12.1031 13.535 12.0991 13.4694C12.095 13.4039 12.0781 13.3398 12.0493 13.2808C11.5715 12.3022 10.7652 11.5518 9.80212 11.1309C10.5245 10.5819 11 9.72266 11 8.75001C11 7.0991 9.65092 5.75001 8 5.75001ZM8 6.75001C9.11046 6.75001 10 7.63956 10 8.75001C10 9.86047 9.11046 10.75 8 10.75C6.88955 10.75 6 9.86047 6 8.75001C6 7.63956 6.88955 6.75001 8 6.75001Z', - VideoCamera: - 'M15.0883 4.50793C14.9728 4.48723 14.8538 4.50775 14.752 4.56592L11.252 6.56592C11.1368 6.63171 11.0526 6.74054 11.0177 6.86846C10.9828 6.99638 11.0001 7.13292 11.0659 7.24805C11.1317 7.36316 11.2405 7.44743 11.3685 7.48232C11.4964 7.51721 11.6329 7.49986 11.748 7.43408L14.5 5.86157V10.1384L11.748 8.56592C11.6329 8.50014 11.4964 8.48279 11.3685 8.51768C11.2405 8.55257 11.1317 8.63684 11.0659 8.75195C11.0001 8.86708 10.9828 9.00362 11.0177 9.13154C11.0526 9.25946 11.1368 9.36829 11.252 9.43408L14.752 11.4341C14.828 11.4775 14.9141 11.5002 15.0017 11.4999C15.0892 11.4996 15.1752 11.4763 15.2509 11.4324C15.3267 11.3884 15.3895 11.3253 15.4332 11.2495C15.477 11.1736 15.5 11.0876 15.5 11V5C15.5 4.88272 15.4587 4.76917 15.3835 4.67923C15.3082 4.58928 15.2037 4.52865 15.0883 4.50793Z M1.5 3.25C0.953638 3.25 0.5 3.70364 0.5 4.25V10.25C0.499871 11.6249 1.62514 12.7501 3 12.75H11C11.5464 12.75 12 12.2964 12 11.75V5.75C12.0001 4.37516 10.8748 3.24991 9.5 3.25H1.5ZM1.5 4.25H9.5C10.3344 4.24995 11.0001 4.91564 11 5.75V11.75H3C2.16564 11.7501 1.49992 11.0844 1.5 10.25V4.25Z', - Warning: - 'M8.00001 11.9999C8.4142 11.9999 8.75001 11.6641 8.75001 11.2499C8.75001 10.8358 8.4142 10.4999 8.00001 10.4999C7.58583 10.4999 7.25001 10.8358 7.25001 11.2499C7.25001 11.6641 7.58583 11.9999 8.00001 11.9999Z M8.00001 5.99995C7.86741 5.99995 7.74023 6.05263 7.64646 6.1464C7.55269 6.24016 7.50001 6.36734 7.50001 6.49995V8.99995C7.50001 9.13256 7.55269 9.25974 7.64646 9.3535C7.74023 9.44727 7.86741 9.49995 8.00001 9.49995C8.13262 9.49995 8.2598 9.44727 8.35357 9.3535C8.44734 9.25974 8.50001 9.13256 8.50001 8.99995V6.49995C8.50001 6.36734 8.44734 6.24016 8.35357 6.1464C8.2598 6.05263 8.13262 5.99995 8.00001 5.99995Z M8.10292 1.50581C7.81437 1.48481 7.51739 1.5465 7.25063 1.70124C7.02407 1.83268 6.83632 2.02175 6.7057 2.24873C6.70542 2.24893 6.70513 2.24914 6.70485 2.24934L6.7035 2.25166L1.20485 11.7493C1.20497 11.7492 1.20472 11.7495 1.20485 11.7493C0.640593 12.7266 1.36981 13.9959 2.4983 13.9998C2.49887 13.9998 2.49944 13.9998 2.50001 13.9998H13.5C13.5005 13.9998 13.5011 13.9998 13.5016 13.9998C14.0359 13.9981 14.5307 13.7107 14.7969 13.2475C15.0631 12.7845 15.0626 12.2125 14.7955 11.75C14.7956 11.7502 14.7954 11.7498 14.7955 11.75L9.29652 2.25166C9.04133 1.80549 8.58384 1.5408 8.10292 1.50581ZM7.94057 2.50129C8.13219 2.47578 8.32429 2.56596 8.42848 2.74812C8.42893 2.74889 8.42938 2.74967 8.42983 2.75044L13.9295 12.2498C14.019 12.4049 14.0192 12.5938 13.93 12.7491C13.9301 12.7491 13.9299 12.7491 13.93 12.7491C13.841 12.9041 13.678 12.9989 13.4993 12.9998H2.50075C2.09342 12.9977 1.86675 12.603 2.07057 12.2499L7.57021 2.75044L7.56886 2.75007C7.56976 2.74946 7.57066 2.74885 7.57155 2.74824C7.61479 2.67267 7.67715 2.60992 7.75246 2.56623C7.81296 2.53113 7.87669 2.50979 7.94057 2.50129Z', - WarningCircle: - 'M8 11.5C8.41419 11.5 8.75 11.1642 8.75 10.75C8.75 10.3358 8.41419 10 8 10C7.58581 10 7.25 10.3358 7.25 10.75C7.25 11.1642 7.58581 11.5 8 11.5Z M8 4.5C7.86739 4.5 7.74021 4.55268 7.64645 4.64645C7.55268 4.74021 7.5 4.86739 7.5 5V8.5C7.5 8.63261 7.55268 8.75979 7.64645 8.85355C7.74021 8.94732 7.86739 9 8 9C8.13261 9 8.25979 8.94732 8.35355 8.85355C8.44732 8.75979 8.5 8.63261 8.5 8.5V5C8.5 4.86739 8.44732 4.74021 8.35355 4.64645C8.25979 4.55268 8.13261 4.5 8 4.5Z M8 1.5C4.41604 1.5 1.5 4.41604 1.5 8C1.5 11.5839 4.41603 14.5 8 14.5C11.5839 14.5 14.5 11.5839 14.5 8C14.5 4.41603 11.5839 1.5 8 1.5ZM8 2.5C11.0435 2.5 13.5 4.95647 13.5 8C13.5 11.0435 11.0435 13.5 8 13.5C4.95647 13.5 2.5 11.0435 2.5 8C2.5 4.95647 4.95647 2.5 8 2.5Z', - WindowsLogo: - 'M13.4109 2.00812L8.41089 2.91437C8.29563 2.93525 8.19135 2.99594 8.11626 3.08585C8.04117 3.17575 8.00002 3.28917 8 3.40631V7.00006C8.00001 7.13266 8.0527 7.25983 8.14646 7.3536C8.24023 7.44736 8.3674 7.50005 8.5 7.50006H13.5C13.6326 7.50005 13.7598 7.44736 13.8535 7.3536C13.9473 7.25983 14 7.13266 14 7.00006V2.50006C14 2.42686 13.9839 2.35456 13.9529 2.28825C13.9219 2.22195 13.8767 2.16327 13.8205 2.11635C13.7643 2.06943 13.6985 2.03542 13.6277 2.01672C13.5569 1.99802 13.4829 1.99508 13.4109 2.00812ZM13 3.09882V6.50006H9V3.82379L13 3.09882Z M6.41089 3.2829L2.41089 4.008C2.29563 4.02888 2.19135 4.08957 2.11626 4.17948C2.04117 4.26938 2.00002 4.3828 2 4.49994V6.99994C2.00001 7.13254 2.0527 7.25971 2.14646 7.35348C2.24023 7.44724 2.3674 7.49993 2.5 7.49994H6.5C6.6326 7.49993 6.75977 7.44724 6.85354 7.35348C6.9473 7.25971 6.99999 7.13254 7 6.99994V3.77496C7 3.70175 6.98393 3.62943 6.95291 3.56312C6.9219 3.4968 6.8767 3.4381 6.82051 3.39117C6.76432 3.34424 6.69851 3.31021 6.62773 3.29151C6.55695 3.2728 6.48293 3.26986 6.41089 3.2829ZM6 4.37372V6.49994H3V4.91742L6 4.37372Z M2.5 8.50006C2.3674 8.50007 2.24023 8.55276 2.14646 8.64652C2.0527 8.74029 2.00001 8.86746 2 9.00006V11.5001C2.00002 11.6172 2.04117 11.7306 2.11626 11.8205C2.19135 11.9104 2.29563 11.9711 2.41089 11.992L6.41089 12.7171C6.48293 12.7301 6.55695 12.7272 6.62773 12.7085C6.69851 12.6898 6.76432 12.6558 6.82051 12.6088C6.8767 12.5619 6.9219 12.5032 6.95291 12.4369C6.98393 12.3706 7 12.2982 7 12.225V9.00006C6.99999 8.86746 6.9473 8.74029 6.85354 8.64652C6.75977 8.55276 6.6326 8.50007 6.5 8.50006H2.5ZM3 9.50006H6V11.6263L3 11.0826V9.50006Z M8.5 8.50006C8.3674 8.50007 8.24023 8.55276 8.14646 8.64652C8.0527 8.74029 8.00001 8.86746 8 9.00006V12.5938C8.00002 12.711 8.04117 12.8244 8.11626 12.9143C8.19135 13.0042 8.29563 13.0649 8.41089 13.0858L13.4109 13.992C13.4829 14.005 13.5569 14.0021 13.6277 13.9834C13.6985 13.9647 13.7643 13.9307 13.8205 13.8838C13.8767 13.8369 13.9219 13.7782 13.9529 13.7119C13.9839 13.6456 14 13.5733 14 13.5001V9.00006C14 8.86746 13.9473 8.74029 13.8535 8.64652C13.7598 8.55276 13.6326 8.50007 13.5 8.50006H8.5ZM9 9.50006H13V12.9013L9 12.1763V9.50006Z', - X: - 'M3.49999 3C3.36738 3.00002 3.24022 3.05271 3.14647 3.14648C3.05272 3.24025 3.00006 3.36741 3.00006 3.5C3.00006 3.63259 3.05272 3.75975 3.14647 3.85352L12.1465 12.8535C12.2402 12.9473 12.3674 12.9999 12.5 12.9999C12.6326 12.9999 12.7597 12.9473 12.8535 12.8535C12.9472 12.7598 12.9999 12.6326 12.9999 12.5C12.9999 12.3674 12.9472 12.2402 12.8535 12.1465L3.8535 3.14648C3.75975 3.05271 3.63259 3.00002 3.49999 3Z M12.5 3C12.3674 3.00002 12.2402 3.05271 12.1465 3.14648L3.14647 12.1465C3.05272 12.2402 3.00006 12.3674 3.00006 12.5C3.00006 12.6326 3.05272 12.7598 3.14647 12.8535C3.24023 12.9473 3.3674 12.9999 3.49999 12.9999C3.63258 12.9999 3.75974 12.9473 3.8535 12.8535L12.8535 3.85352C12.9472 3.75975 12.9999 3.63259 12.9999 3.5C12.9999 3.36741 12.9472 3.24025 12.8535 3.14648C12.7597 3.05271 12.6326 3.00002 12.5 3Z', - XBold: - 'M13.2071 4.20711C13.5976 3.81658 13.5976 3.18342 13.2071 2.79289C12.8166 2.40237 12.1834 2.40237 11.7929 2.79289L8 6.58579L4.20711 2.79289C3.81658 2.40237 3.18342 2.40237 2.79289 2.79289C2.40237 3.18342 2.40237 3.81658 2.79289 4.20711L6.58579 8L2.79289 11.7929C2.40237 12.1834 2.40237 12.8166 2.79289 13.2071C3.18342 13.5976 3.81658 13.5976 4.20711 13.2071L8 9.41421L11.7929 13.2071C12.1834 13.5976 12.8166 13.5976 13.2071 13.2071C13.5976 12.8166 13.5976 12.1834 13.2071 11.7929L9.41421 8L13.2071 4.20711Z', - XCircle: - 'M6 5.5C5.8674 5.50002 5.74024 5.55271 5.64648 5.64648C5.55274 5.74025 5.50008 5.86741 5.50008 6C5.50008 6.13259 5.55274 6.25975 5.64648 6.35352L7.29297 8L5.64648 9.64648C5.55274 9.74025 5.50008 9.86741 5.50008 10C5.50008 10.1326 5.55274 10.2598 5.64648 10.3535C5.74025 10.4473 5.86741 10.4999 6 10.4999C6.13259 10.4999 6.25975 10.4473 6.35352 10.3535L8 8.70703L9.64648 10.3535C9.74025 10.4473 9.86741 10.4999 10 10.4999C10.1326 10.4999 10.2598 10.4473 10.3535 10.3535C10.4473 10.2598 10.4999 10.1326 10.4999 10C10.4999 9.86741 10.4473 9.74025 10.3535 9.64648L8.70703 8L10.3535 6.35352C10.4473 6.25975 10.4999 6.13259 10.4999 6C10.4999 5.86741 10.4473 5.74025 10.3535 5.64648C10.2598 5.55274 10.1326 5.50008 10 5.50008C9.86741 5.50008 9.74025 5.55274 9.64648 5.64648L8 7.29297L6.35352 5.64648C6.25976 5.55271 6.1326 5.50002 6 5.5Z M8 1.5C4.41604 1.5 1.5 4.41604 1.5 8C1.5 11.5839 4.41603 14.5 8 14.5C11.5839 14.5 14.5 11.5839 14.5 8C14.5 4.41603 11.5839 1.5 8 1.5ZM8 2.5C11.0435 2.5 13.5 4.95647 13.5 8C13.5 11.0435 11.0435 13.5 8 13.5C4.95647 13.5 2.5 11.0435 2.5 8C2.5 4.95647 4.95647 2.5 8 2.5Z', -}; - -/** @hidden */ -type PhosphorIconName = keyof typeof phosphorIconConfig; - -export const legacyIconNameToPhosphorIconName: Partial< - { - [iconName in IconName]: PhosphorIconName; - } -> = { - aiAssistant: 'AiAssistant', - apple: 'Apple', - apps: 'ExtensionsFeature', - applyRowTemplate: 'PaintBucket', - ascending: 'SortAscending', - attachment: 'Paperclip', - automations: 'GitFork', - autonumber: 'Autonumber', - barcode: 'Barcode', - bell: 'Bell', - bold: 'TextBolder', - bolt: 'Lightning', - boltList: 'BoltList', - book: 'Book', - calendar: 'Calendar', - caret: 'ChevronDown', - chart: 'ChartBar', - chat: 'Chat', - check: 'Check', - checkbox: 'CheckSquare', - checkboxChecked: 'CheckSquare', - checkboxUnchecked: 'Square', - checklist: 'ListChecks', - chevronDown: 'ChevronDown', - chevronLeft: 'ChevronLeft', - chevronRight: 'ChevronRight', - chevronUp: 'ChevronUp', - clipboard: 'ClipboardText', - code: 'CodeSimple', - cog: 'Cog', - collapse: 'ArrowsInSimple', - collapseSidebar: 'ArrowLineLeft', - contacts: 'AddressBook', - count: 'Calculator', - count1: 'Calculator', - cube: 'Cube', - cursor: 'Cursor', - day: 'Calendar', - dayAuto: 'CalendarBolt', - dedent: 'TextOutdent', - descending: 'SortDescending', - dollar: 'CurrencyDollarSimple', - down: 'ArrowDown', - download: 'ArrowCircleDown', - dragHandle: 'DotsSixVertical', - drive: 'Drive', - duplicate: 'Copy', - edit: 'PencilSimple', - envelope: 'EnvelopeSimple', - envelope1: 'EnvelopeSimple', - expand: 'ArrowsOutSimple', - expand1: 'ArrowsOutSimple', - expandSidebar: 'ArrowLineRight', - feed: 'Rss', - file: 'File', - filter: 'FunnelSimple', - flag: 'FlagFill', - form: 'Form', - formula: 'Formula', - fullscreen: 'ArrowsOut', - gallery: 'Gallery', - gantt: 'Gantt', - gift: 'Gift', - grid: 'Grid', - grid1: 'GridLayout', - group: 'Group', - heart: 'HeartFill', - help: 'Question', - hide: 'EyeClosed', - hide1: 'EyeSlash', - history: 'ClockCounterClockwise', - home: 'House', - hyperlink: 'Link', - hyperlinkCancel: 'LinkBreak', - indent: 'TextIndent', - info: 'Info', - italic: 'TextItalic', - kanban: 'Kanban', - laptop: 'Laptop', - left: 'ArrowLeft', - lightbulb: 'Lightbulb', - link: 'Link', - link1: 'ArrowRightList', - lock: 'Lock', - logout: 'SignOut', - lookup: 'Lookup', - mapPin: 'MapPin', - markdown: 'FileMarkdown', - megaphone: 'Megaphone', - menu: 'List', - minus: 'Minus', - mobile: 'DeviceMobile', - multicollaborator: 'Users', - multiselect: 'Multiselect', - number: 'HashStraight', - ol: 'ListNumbers', - overflow: 'Overflow', - overlay: 'PictureInPicture', - paint: 'PaintBucket', - paragraph: 'Paragraph', - paragraph1: 'Pilcrow', - pause: 'Pause', - percent: 'Percent', - personal: 'User', - personalAuto: 'PersonBolt', - personalCloseup: 'PersonalCloseup', - phone: 'Phone', - pivot: 'Pivot', - play: 'Play', - plus: 'Plus', - plusFilled: 'PlusCircle', - premium: 'Sparkle', - print: 'Printer', - public: 'Globe', - publish: 'CloudArrowUp', - quote: 'Quotes', - quote1: 'Quotes', - radio: 'Radio', - radioSelected: 'RadioButton', - redo: 'ArrowCounterClockwise', - redo1: 'ArrowArcRight', - richText: 'RichText', - right: 'ArrowRight', - rollup: 'Spiral', - rollup1: 'Spiral', - rowHeightExtraLarge: 'RowHeightExtraLarge', - rowHeightLarge: 'RowHeightLarge', - rowHeightMedium: 'RowHeightMedium', - rowHeightSmall: 'RowHeightSmall', - search: 'MagnifyingGlass', - select: 'CaretCircleDown', - selectCaret: 'SelectCaret', - settings: 'FadersHorizontal', - shapes: 'Shapes', - share: 'ArrowSquareOut', - share1: 'ArrowSquareOut', - shareWithBolt: 'ShareWithBolt', - show: 'Eye', - show1: 'Eye', - slack: 'SlackLogo', - smiley: 'Smiley', - sort: 'ArrowsDownUp', - stack: 'Kanban', - star: 'Star', - strikethrough: 'TextStrikethrough', - switcher: 'Switcher', - tabs: 'ArrowSquareDown', - team: 'UsersThree', - teamLocked: 'TeamLocked', - text: 'TextAlt', - thumbsUp: 'ThumbsUpFill', - time: 'Clock', - timeline: 'Timeline', - toggle: 'ToggleRight', - trash: 'Trash', - twitter: 'TwitterLogo', - ul: 'ListDashes', - underline: 'TextUnderline', - undo: 'ArrowArcLeft', - up: 'ArrowUp', - upload: 'ArrowCircleUp', - video: 'VideoCamera', - view: 'ImageSquare', - warning: 'Warning', - windows: 'WindowsLogo', - x: 'X', - xCheckbox: 'XBold', -}; diff --git a/packages/sdk/src/ui/input.tsx b/packages/sdk/src/ui/input.tsx deleted file mode 100644 index b0d951070..000000000 --- a/packages/sdk/src/ui/input.tsx +++ /dev/null @@ -1,308 +0,0 @@ -/** @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'; -import {createEnum, EnumType, createPropTypeFromEnum} from '../private_utils'; -import useTheme from './theme/use_theme'; -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'; - -/** @internal */ -type InputVariant = EnumType; -const InputVariant = createEnum('default'); - -/** @internal */ -function useInputVariant(variant: InputVariant = InputVariant.default) { - const {inputVariants} = useTheme(); - return inputVariants[variant]; -} - -export const ValidInputType = createEnum( - 'date', - 'datetime-local', - 'email', - 'month', - 'number', - 'password', - 'search', - 'tel', - 'text', - 'time', - 'url', - 'week', -); -/** */ -type ValidInputType = EnumType; - -/** - * Style props shared between the {@link Input} and {@link InputSynced} components. Accepts: - * * {@link FlexItemSetProps} - * * {@link MarginProps} - * * {@link MaxWidthProps} - * * {@link MinWidthProps} - * * {@link PositionSetProps} - * * {@link WidthProps} - * - * @noInheritDoc - */ -export interface InputStyleProps - extends MaxWidthProps, - MinWidthProps, - WidthProps, - FlexItemSetProps, - PositionSetProps, - MarginProps {} - -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. - * - * @noInheritDoc - */ -export interface SharedInputProps extends InputStyleProps, TooltipAnchorProps { - /** The size of the input. Defaults to `default`. */ - size?: ControlSizeProp; - /** The `type` for the input. Defaults to `text`. */ - type?: EnumType; - /** The `disabled` attribute. */ - disabled?: boolean; - /** The `required` attribute. */ - required?: boolean; - /** The `spellcheck` attribute. */ - spellCheck?: boolean; - /** The `tabindex` attribute. */ - tabIndex?: number; - /** The `name` attribute. */ - name?: string; - /** The `id` attribute. */ - id?: string; - /** The `autoFocus` attribute. */ - autoFocus?: boolean; - /** The `max` attribute. */ - max?: number | string; - /** The `maxLength` attribute. */ - maxLength?: number; - /** The placeholder for the input. */ - placeholder?: string; - /** The `minLength` attribute. */ - minLength?: number; - /** The `step` attribute. */ - step?: number | string; - /** The `pattern` attribute. */ - pattern?: string; - /** The `readOnly` attribute. */ - readOnly?: boolean; - /** The `autoComplete` attribute. */ - autoComplete?: string; - /** Additional styles to apply to the input. */ - style?: React.CSSProperties; - /** Additional class names to apply to the input, separated by spaces. */ - className?: string; - /** A function to be called when the input changes. */ - onChange?(e: React.ChangeEvent): unknown; - /** A function to be called when the input loses focus. */ - onBlur?(e: React.FocusEvent): unknown; - /** A function to be called when the input gains focus. */ - onFocus?(e: React.FocusEvent): unknown; - /** A space separated list of label element IDs. */ - ['aria-labelledby']?: string; - /** A space separated list of description element IDs. */ - ['aria-describedby']?: string; - /** The `min` attribute. */ - min?: number | string; -} - -export const SupportedInputType = createEnum( - 'date', - 'datetime-local', - 'email', - 'month', - 'number', - 'password', - 'search', - 'tel', - 'text', - 'time', - 'url', - 'week', -); -/** - * Supported types for the {@link Input} component. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#%3Cinput%3E_types) for more information. - */ -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} - * - * @docsPath UI/components/Input - */ -interface InputProps extends SharedInputProps { - /** The input's current value. */ - value: string; -} - -/** - * An input component. A wrapper around `` that fits in with Airtable's user interface. - * - * [[ Story id="input--example" title="Input example" ]] - * - * @docsPath UI/components/Input - * @component - */ -const Input = (props: InputProps, ref: React.Ref) => { - const { - size = ControlSize.default, - type = SupportedInputType.text, - value, - placeholder, - onMouseEnter, - onMouseLeave, - onClick, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - hasOnClick, - onChange, - onBlur, - onFocus, - style, - className, - disabled, - required, - spellCheck, - tabIndex, - id, - name, - autoFocus, - max, - maxLength, - min, - minLength, - step, - pattern, - readOnly, - autoComplete, - 'aria-labelledby': ariaLabelledBy, - 'aria-describedby': ariaDescribedByProp, - ...styleProps - } = props; - - const formFieldContextValue = useFormField(); - const controlId = formFieldContextValue ? formFieldContextValue.controlId : undefined; - const descriptionId = formFieldContextValue ? formFieldContextValue.descriptionId : undefined; - const ariaDescribedBy = - [ariaDescribedByProp, descriptionId].filter(Boolean).join(' ') || undefined; - const classNameForInputVariant = useInputVariant(); - const classNameForInputSize = useInputSize(size); - const classNameForStyleProps = useStyledSystem({width: '100%', ...styleProps}, styleParser); - - return ( - - ); -}; - -const ForwardedRefInput = React.forwardRef(Input); - -ForwardedRefInput.propTypes = { - value: PropTypes.string.isRequired, - ...sharedInputPropTypes, -}; - -ForwardedRefInput.displayName = 'Input'; - -export default ForwardedRefInput; diff --git a/packages/sdk/src/ui/input_synced.tsx b/packages/sdk/src/ui/input_synced.tsx deleted file mode 100644 index b271169b9..000000000 --- a/packages/sdk/src/ui/input_synced.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/** @module @airtable/blocks/ui: Input */ /** */ -import * as React from 'react'; -import {spawnError} from '../error_utils'; -import {GlobalConfigKey} from '../types/global_config'; -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: - * * {@link InputStyleProps} - * - * @docsPath UI/components/InputSynced - * @groupPath UI/components/Input - */ -interface InputSyncedProps extends SharedInputProps { - /** A string key or array key path in {@link GlobalConfig}. The input value will always reflect the value stored in {@link GlobalConfig} for this key. Changing the input value will update {@link GlobalConfig}. */ - globalConfigKey: GlobalConfigKey; -} - -/** - * A wrapper around the {@link Input} component that syncs with {@link GlobalConfig}. - * - * [[ Story id="input--example-synced" title="Synced input example" ]] - * - * @docsPath UI/components/InputSynced - * @groupPath UI/components/Input - * @component - */ -const InputSynced = (props: InputSyncedProps, ref: React.Ref) => { - const { - globalConfigKey, - type = SupportedInputType.text, - disabled, - onChange, - ...restOfProps - } = props; - const [value, setValue, canSetValue] = useSynced(globalConfigKey); - - let inputValue; - if (value === null || value === undefined) { - inputValue = ''; - } else if (typeof value === 'string') { - inputValue = value; - } else { - throw spawnError( - 'InputSynced only works with a global config value that is a string, null or undefined.', - ); - } - return ( - ) => { - setValue(e.target.value); - if (onChange) { - onChange(e); - } - }} - value={inputValue} - type={type} - /> - ); -}; - -const ForwardedRefInputSynced = React.forwardRef(InputSynced); - -ForwardedRefInputSynced.propTypes = { - globalConfigKey: globalConfigSyncedComponentHelpers.globalConfigKeyPropType, - ...sharedInputPropTypes, -}; - -ForwardedRefInputSynced.displayName = 'InputSynced'; - -export default ForwardedRefInputSynced; diff --git a/packages/sdk/src/ui/key_codes.ts b/packages/sdk/src/ui/key_codes.ts deleted file mode 100644 index d8cf08e0f..000000000 --- a/packages/sdk/src/ui/key_codes.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * @internal - */ -export const KeyCodes = Object.freeze({ - BACKSPACE: 8 as const, - TAB: 9 as const, - ENTER: 13 as const, - ALT: 18 as const, - ESCAPE: 27 as const, - SPACE: 32 as const, - PAGE_UP: 33 as const, - PAGE_DOWN: 34 as const, - END: 35 as const, - HOME: 36 as const, - LEFT: 37 as const, - UP: 38 as const, - RIGHT: 39 as const, - DOWN: 40 as const, - DELETE: 46 as const, - A: 65 as const, - B: 66 as const, - C: 67 as const, - D: 68 as const, - E: 69 as const, - F: 70 as const, - G: 71 as const, - H: 72 as const, - I: 73 as const, - J: 74 as const, - K: 75 as const, - L: 76 as const, - M: 77 as const, - N: 78 as const, - O: 79 as const, - P: 80 as const, - Q: 81 as const, - R: 82 as const, - S: 83 as const, - T: 84 as const, - U: 85 as const, - V: 86 as const, - W: 87 as const, - X: 88 as const, - Y: 89 as const, - Z: 90 as const, - F2: 113 as const, - F3: 114 as const, - COMMA: 188 as const, - PERIOD: 190 as const, - FORWARD_SLASH: 191 as const, - BACKTICK: 192 as const, - BACK_SLASH: 220 as const, -}); - -/** - * @internal - * OS-aware check for command/ctrl key. - */ -export function isCommandModifierKeyEvent( - e: KeyboardEvent | React.KeyboardEvent | React.MouseEvent, -): boolean { - const isMac = window.navigator.platform.toLowerCase().indexOf('mac') !== -1; - if (isMac) { - return e.metaKey; - } else { - return e.ctrlKey; - } -} diff --git a/packages/sdk/src/ui/label.tsx b/packages/sdk/src/ui/label.tsx deleted file mode 100644 index 33690ae4d..000000000 --- a/packages/sdk/src/ui/label.tsx +++ /dev/null @@ -1,115 +0,0 @@ -/** @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'; - -/** - * Props for the {@link Label} component. Also accepts: - * * {@link AllStylesProps} - * * {@link AriaProps} - * - * @noInheritDoc - * @docsPath UI/components/Label - */ -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. */ - htmlFor?: string; - /** The `id` attribute. */ - id?: string; - /** The contents of the label. */ - children?: React.ReactNode | string; - /** Additional class names to apply, separated by spaces. */ - className?: string; - /** Additional styles. */ - style?: React.CSSProperties; - /** Data attributes that are spread onto the element, e.g. `dataAttributes={{'data-*': '...'}}`. */ - dataAttributes?: DataAttributesProp; - /** The `role` attribute. */ - role?: string; -} - -/** - * A label component. - * - * [[ Story id="label--example" title="Label example" ]] - * - * @docsPath UI/components/Label - * @component - */ -const Label = (props: LabelProps, ref: React.Ref) => { - const { - size = TextSize.default, - htmlFor, - id, - children, - className, - style, - dataAttributes, - role, - 'aria-label': ariaLabel, - 'aria-labelledby': ariaLabelledBy, - 'aria-describedby': ariaDescribedBy, - 'aria-controls': ariaControls, - 'aria-expanded': ariaExpanded, - 'aria-haspopup': ariaHasPopup, - 'aria-hidden': ariaHidden, - 'aria-live': ariaLive, - ...styleProps - } = props; - - const classNameForTextStyle = useTextStyle(size); - const classNameForStyleProps = useStyledSystem({ - display: 'inline-block', - textColor: 'light', - fontWeight: 'strong', - marginBottom: '6px', - ...styleProps, - }); - - return ( - - ); -}; - -const ForwardedRefLabel = React.forwardRef(Label); - -ForwardedRefLabel.propTypes = { - size: textSizePropType, - htmlFor: PropTypes.string, - id: PropTypes.string, - dataAttributes: dataAttributesPropType, - children: PropTypes.node, - className: PropTypes.string, - style: PropTypes.object, - ...allStylesPropTypes, - ...ariaPropTypes, -}; - -ForwardedRefLabel.displayName = 'Label'; - -export default ForwardedRefLabel; diff --git a/packages/sdk/src/ui/link.tsx b/packages/sdk/src/ui/link.tsx deleted file mode 100644 index 4990417c6..000000000 --- a/packages/sdk/src/ui/link.tsx +++ /dev/null @@ -1,294 +0,0 @@ -/** @module @airtable/blocks/ui: Link */ /** */ -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 useStyledSystem from './use_styled_system'; -import useTheme from './theme/use_theme'; -import {ariaPropTypes, AriaProps} from './types/aria_props'; -import {dataAttributesPropType, DataAttributesProp} from './types/data_attributes_prop'; -import createResponsivePropType from './system/utils/create_responsive_prop_type'; -import {OptionalResponsiveProp} from './system/utils/types'; -import { - maxWidth, - maxWidthPropTypes, - MaxWidthProps, - minWidth, - minWidthPropTypes, - MinWidthProps, - width, - widthPropTypes, - WidthProps, - flexItemSet, - flexItemSetPropTypes, - FlexItemSetProps, - positionSet, - positionSetPropTypes, - PositionSetProps, - fontWeight, - fontWeightPropTypes, - FontWeightProps, - spacingSet, - spacingSetPropTypes, - SpacingSetProps, - display, -} from './system'; -import {tooltipAnchorPropTypes, TooltipAnchorProps} from './types/tooltip_anchor_props'; -import {useTextStyle, TextSize, TextSizeProp, textSizePropType} from './text'; -import {IconName, iconNamePropType} from './icon_config'; -import Icon from './icon'; - -/** - * Style props for the {@link Link} component. Also accepts: - * * {@link FlexItemSetProps} - * * {@link FontWeightProps} - * * {@link MaxWidthProps} - * * {@link MinWidthProps} - * * {@link PositionSetProps} - * * {@link SpacingSetProps} - * * {@link WidthProps} - * - * @noInheritDoc - */ -export interface LinkStyleProps - extends MaxWidthProps, - MinWidthProps, - WidthProps, - FlexItemSetProps, - PositionSetProps, - FontWeightProps, - SpacingSetProps { - /** Defines the display type of an element, which consists of the two basic qualities of how an element generates boxes — the outer display type defining how the box participates in flow layout, and the inner display type defining how the children of the box are laid out. */ - display?: OptionalResponsiveProp<'inline-flex' | 'flex' | 'none'>; -} - -const styleParser = compose( - display, - maxWidth, - minWidth, - width, - flexItemSet, - positionSet, - fontWeight, - spacingSet, -); - -export const linkStylePropTypes = { - display: createResponsivePropType(PropTypes.oneOf(['inline-flex', 'flex', 'none'] as const)), - ...maxWidthPropTypes, - ...minWidthPropTypes, - ...widthPropTypes, - ...flexItemSetPropTypes, - ...positionSetPropTypes, - ...fontWeightPropTypes, - ...spacingSetPropTypes, -}; - -/** - * Variants for the {@link Link} component: - * - * • **default** - * - * Blue text. - * - * • **dark** - * - * Dark gray text. - * - * • **light** - * - * Light gray text. - */ -type LinkVariant = EnumType; -const LinkVariant = createEnum('default', 'dark', 'light'); -const linkVariantPropType = createPropTypeFromEnum(LinkVariant); - -/** @internal */ -function useLinkVariant(variant: LinkVariant = LinkVariant.default): string { - const {linkVariants} = useTheme(); - return linkVariants[variant]; -} - -/** - * Props for the {@link Link} component. Also supports: - * * {@link AriaProps} - * * {@link LinkStyleProps} - * - * @docsPath UI/components/Link - * @noInheritDoc - */ -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`. */ - variant?: LinkVariant; - /** The name of the icon or a react node. For more details, see the {@link IconName|list of supported icons}. */ - icon?: IconName | React.ReactElement; - /** Adds an underline to the link when true. */ - underline?: boolean; - /** The target URL or URL fragment for the link. */ - href: string; - /** Specifies where to display the linked URL. */ - target?: string; - /** The `id` attribute. */ - id?: string; - /** Indicates if the link can be focused and if/where it participates in sequential keyboard navigation. */ - tabIndex?: number; - /** Additional class names to apply to the link. */ - className?: string; - /** Additional styles to apply to the link. */ - style?: React.CSSProperties; - /** Data attributes that are spread onto the element, e.g. `dataAttributes={{'data-*': '...'}}`. */ - dataAttributes?: DataAttributesProp; - /** The contents of the link. */ - children?: React.ReactNode | string; -} - -const reasonableUrlSchemeRegex = /^[a-z0-9]+:\/\//i; - -/** @internal */ -function _getSanitizedHref(href: string): string | undefined { - if (!href) { - return undefined; - } - const hasScheme = href.indexOf('://') !== -1; - if (!hasScheme) { - return href; - } else if ( - reasonableUrlSchemeRegex.test(href) && - !/^javascript:/i.test(href) && - !/^data:/i.test(href) - ) { - return href; - } else { - return 'http://' + href; - } -} - -/** - * A styled link component with security benefits. - * - * [[ Story id="link--example" title="Link example" ]] - * - * ## Security benefits - * - * This component is a styled wrapper around the `` tag that offers a few security benefits: - * - * - Limited XSS protection. If the `href` starts with `javascript:` or `data:`, `http://` will be prepended. - * - There is [reverse tabnabbing prevention](https://www.owasp.org/index.php/Reverse_Tabnabbing). If `target` is set, the `rel` attribute will be set to `noopener noreferrer`. - * - * Developers should use `Link` instead of `a` when possible. - * - * @docsPath UI/components/Link - * @component - */ -const Link = (props: LinkProps, ref: React.Ref) => { - const { - size = TextSize.default, - variant = LinkVariant.default, - underline = false, - icon, - href, - id, - target, - onMouseEnter, - onMouseLeave, - onClick, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - hasOnClick, - tabIndex, - className, - style, - children, - dataAttributes, - 'aria-label': ariaLabel, - 'aria-labelledby': ariaLabelledBy, - 'aria-describedby': ariaDescribedBy, - 'aria-controls': ariaControls, - 'aria-expanded': ariaExpanded, - 'aria-haspopup': ariaHasPopup, - 'aria-hidden': ariaHidden, - 'aria-live': ariaLive, - ...styleProps - } = props; - - const classNameForTextStyle = useTextStyle(size); - const classNameForLinkVariant = useLinkVariant(variant); - const classNameForUnderline = useStyledSystem({ - textDecoration: underline ? 'underline' : 'none', - }); - const classNameForStyleProps = useStyledSystem( - { - display: 'inline-flex', - padding: '0 0.1em', - margin: '0 -0.1em', - maxWidth: '100%', - - ...styleProps, - }, - styleParser, - ); - - const rel = target ? 'noopener noreferrer' : undefined; - - return ( - - {typeof icon === 'string' ? ( - - ) : ( - icon - )} - {children} - - ); -}; - -const ForwardedRefLink = React.forwardRef(Link); - -ForwardedRefLink.propTypes = { - size: textSizePropType, - variant: linkVariantPropType, - icon: PropTypes.oneOfType([iconNamePropType, PropTypes.element]), - href: PropTypes.string.isRequired, - target: PropTypes.string, - tabIndex: PropTypes.number, - className: PropTypes.string, - style: PropTypes.object, - children: PropTypes.node, - dataAttributes: dataAttributesPropType, - ...tooltipAnchorPropTypes, - ...linkStylePropTypes, - ...ariaPropTypes, -}; - -ForwardedRefLink.displayName = 'Link'; - -export default ForwardedRefLink; diff --git a/packages/sdk/src/ui/loader.tsx b/packages/sdk/src/ui/loader.tsx deleted file mode 100644 index 623b01c6c..000000000 --- a/packages/sdk/src/ui/loader.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/** @module @airtable/blocks/ui: Loader */ /** */ -import PropTypes from 'prop-types'; -import {cx} from 'emotion'; -import * as React from 'react'; -import {compose} from '@styled-system/core'; -import {baymax} from './baymax_utils'; -import useStyledSystem from './use_styled_system'; -import { - flexItemSet, - flexItemSetPropTypes, - FlexItemSetProps, - positionSet, - positionSetPropTypes, - PositionSetProps, - margin, - marginPropTypes, - MarginProps, -} from './system'; - -const ORIGINAL_SIZE = 54; - -/** - * Style props for the {@link Loader} component. Accepts: - * * {@link FlexItemSetProps} - * * {@link MarginProps} - * * {@link PositionSetProps} - */ -interface LoaderStyleProps extends FlexItemSetProps, PositionSetProps, MarginProps {} - -const styleParser = compose(flexItemSet, positionSet, margin); - -export const loaderStylePropTypes = { - ...flexItemSetPropTypes, - ...positionSetPropTypes, - ...marginPropTypes, -}; - -/** - * Props for the {@link Loader} component. Also accepts: - * * {@link LoaderStyleProps} - * - * @docsPath UI/components/Loader - * @noInheritDoc - */ -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`. */ - scale: number; - /** Additional class names to apply to the loading spinner. */ - className?: string; - /** Additional styles to apply to the loading spinner. */ - style?: React.CSSProperties; -} - -/** - * A loading spinner component. - * - * [[ Story id="loader--example" title="Loader example" ]] - * - * @docsPath UI/components/Loader - * @component - */ -const Loader = (props: LoaderProps) => { - const {fillColor, scale, className, style, ...styleProps} = props; - const classNameForStyleProps = useStyledSystem(styleProps, styleParser); - - return ( - - - - - - - - ); -}; - -Loader.propTypes = { - fillColor: PropTypes.string.isRequired, - scale: PropTypes.number.isRequired, - className: PropTypes.string, - style: PropTypes.object, - ...loaderStylePropTypes, -}; - -Loader.defaultProps = { - fillColor: '#888', - scale: 0.3, -}; - -export default Loader; diff --git a/packages/sdk/src/ui/modal.tsx b/packages/sdk/src/ui/modal.tsx deleted file mode 100644 index e425f001b..000000000 --- a/packages/sdk/src/ui/modal.tsx +++ /dev/null @@ -1,196 +0,0 @@ -/** @hidden */ /** */ -import PropTypes from 'prop-types'; -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 {baymax} from './baymax_utils'; -import withStyledSystem from './with_styled_system'; -import { - dimensionsSet, - dimensionsSetPropTypes, - DimensionsSetProps, - display, - displayPropTypes, - flexContainerSet, - flexContainerSetPropTypes, - FlexContainerSetProps, - spacingSet, - spacingSetPropTypes, - SpacingSetProps, -} from './system'; -import {OptionalResponsiveProp} from './system/utils/types'; - -/** - * Props for the {@link Modal} component. Also accepts: - * * {@link ModalStyleProps} - * - * @hidden - */ - -interface ModalProps { - /** Callback function to fire when the modal is closed. */ - onClose?: () => unknown; - /** Extra `className`s to apply to the modal element, separated by spaces. */ - className?: string; - /** Extra styles to apply to the modal element. */ - style?: React.CSSProperties; - /** Extra `className`s to apply to the background element, separated by spaces. */ - backgroundClassName?: string; - /** Extra styles to apply to the background element. */ - backgroundStyle?: React.CSSProperties; - /** */ - children: React.ReactNode; -} - -/** - * Style props shared between the {@link Modal}, {@link Dialog}, and {@link ConfirmationDialog} components. Also accepts: - * * {@link DimensionsSetProps} - * * {@link FlexContainerSetProps} - * * {@link SpacingSetProps} - * - * @hidden - * @noInheritDoc - */ -export interface ModalStyleProps - extends DimensionsSetProps, - FlexContainerSetProps, - SpacingSetProps { - /** Defines the display type of an element, which consists of the two basic qualities of how an element generates boxes — the outer display type defining how the box participates in flow layout, and the inner display type defining how the children of the box are laid out. */ - display?: OptionalResponsiveProp<'block' | 'flex'>; -} - -const styleParser = compose(dimensionsSet, display, flexContainerSet, spacingSet); - -export const modalStylePropTypes = { - ...dimensionsSetPropTypes, - ...displayPropTypes, - ...flexContainerSetPropTypes, - ...spacingSetPropTypes, -}; - -/** - * Generic modal component with minimal styling. - * - * @hidden - */ - -export class Modal extends React.Component { - /** @hidden */ - static propTypes = { - onClose: PropTypes.func, - className: PropTypes.string, - style: PropTypes.object, - backgroundClassName: PropTypes.string, - backgroundStyle: PropTypes.object, - }; - /** @internal */ - _background: HTMLDivElement | null; - /** @internal */ - _container: HTMLDivElement; - /** @internal */ - _originalActiveElement: Element | null; - /** @internal */ - _mouseDownOutsideModal: boolean; - /** @hidden */ - constructor(props: ModalProps) { - super(props); - - this._background = null; - this._container = document.createElement('div'); - this._originalActiveElement = null; - this._mouseDownOutsideModal = false; - - this._onMouseDown = this._onMouseDown.bind(this); - this._onMouseUp = this._onMouseUp.bind(this); - } - /** @hidden */ - componentDidMount() { - const container = this._container; - - container.setAttribute('tabIndex', '0'); - container.style.zIndex = '99999'; - container.style.position = 'fixed'; - invariant(document.body, 'no document body'); - document.body.appendChild(container); - - if (document.hasFocus()) { - this._originalActiveElement = document.activeElement; - container.focus(); - } - } - /** @hidden */ - componentWillUnmount() { - invariant(document.body, 'no document body'); - document.body.removeChild(this._container); - if (this._originalActiveElement !== null) { - (this._originalActiveElement as HTMLElement).focus(); - } - } - /** @internal */ - _onMouseDown(e: React.MouseEvent) { - if (this._shouldClickingOnElementCloseModal(e.target)) { - this._mouseDownOutsideModal = true; - } - } - /** @internal */ - _onMouseUp(e: React.MouseEvent) { - if ( - this._mouseDownOutsideModal && - this.props.onClose && - this._shouldClickingOnElementCloseModal(e.target) - ) { - this.props.onClose(); - } - this._mouseDownOutsideModal = false; - } - /** @internal */ - _shouldClickingOnElementCloseModal(el: EventTarget | null) { - return el === this._background; - } - /** @hidden */ - render() { - const {className, style, backgroundClassName, backgroundStyle, children} = this.props; - - return ReactDOM.createPortal( -
    (this._background = el)} - className={cx( - baymax('fixed all-0 darken3 flex items-center justify-center'), - backgroundClassName, - )} - style={backgroundStyle} - onMouseDown={this._onMouseDown} - onMouseUp={this._onMouseUp} - > -
    - {children} -
    -
    , - this._container, - ); - } -} - -export default withStyledSystem( - Modal, - styleParser, - modalStylePropTypes, - { - display: 'block', - width: '100%', - maxWidth: '100vw', - maxHeight: '100vh', - margin: 3, - padding: 3, - }, -); diff --git a/packages/sdk/src/ui/model_picker_select.tsx b/packages/sdk/src/ui/model_picker_select.tsx deleted file mode 100644 index d9fbeb418..000000000 --- a/packages/sdk/src/ui/model_picker_select.tsx +++ /dev/null @@ -1,92 +0,0 @@ -/** @hidden */ /** */ -import * as React from 'react'; -import PropTypes from 'prop-types'; -import {invariant} from '../error_utils'; -import Table from '../models/table'; -import View from '../models/view'; -import Field from '../models/field'; -import Select, {sharedSelectBasePropTypes, SharedSelectBaseProps} from './select'; -import {SelectOptionValue} from './select_and_select_buttons_helpers'; -import useWatchable from './use_watchable'; - -type AnyModel = Table | View | Field; - -interface ModelPickerSelectProps extends SharedSelectBaseProps { - models: Array; - selectedModelId: string | null; - modelKeysToWatch: Array; - shouldAllowPickingNone?: boolean; - shouldAllowPickingModelFn?: (arg1: Model) => boolean; - placeholder: string; - onChange: (newValue: string | null) => unknown; -} - -function ModelPickerSelect( - props: ModelPickerSelectProps, - ref: React.Ref, -) { - const { - models, - modelKeysToWatch, - selectedModelId, - shouldAllowPickingNone, - shouldAllowPickingModelFn, - placeholder, - onChange, - ...restOfProps - } = props; - - useWatchable(models as any, modelKeysToWatch); - - function _onChange(value: SelectOptionValue) { - invariant(value === null || typeof value === 'string', 'value must be null or model id'); - onChange(value); - } - - const options = [ - {value: null, label: placeholder, disabled: !shouldAllowPickingNone}, - ...models.map(model => { - return { - value: model.id, - label: model.name, - disabled: shouldAllowPickingModelFn && !shouldAllowPickingModelFn(model), - }; - }), - ]; - - return ( - ` that fits in with Airtable's user interface. - * - * [[ Story id="select--example" title="Select example" ]] - * - * @component - * @docsPath UI/components/Select - */ -const Select = (props: SelectProps, ref: React.Ref) => { - const { - size = ControlSize.default, - value, - options: originalOptions = [], - autoFocus, - disabled, - id, - name, - tabIndex, - onChange, - onMouseEnter, - onMouseLeave, - onClick, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - hasOnClick, - className, - style, - 'aria-label': ariaLabel, - 'aria-describedby': ariaDescribedByProp, - 'aria-labelledby': ariaLabelledBy, - ...styleProps - } = props; - const formFieldContextValue = useFormField(); - const controlId = formFieldContextValue ? formFieldContextValue.controlId : undefined; - const descriptionId = formFieldContextValue ? formFieldContextValue.descriptionId : undefined; - const ariaDescribedBy = - [ariaDescribedByProp, descriptionId].filter(Boolean).join(' ') || undefined; - const classNameForSelectVariant = useSelectVariant(); - const classNameForSelectSize = useSelectSize(size); - const classNameForStyleProps = useStyledSystem( - { - width: '100%', - ...styleProps, - }, - styleParser, - ); - - function _onChange(e: React.ChangeEvent) { - if (onChange) { - const newValue = stringToOptionValue(e.currentTarget.value); - onChange(newValue); - } - } - - const validationResult = validateOptions(originalOptions); - if (!validationResult.isValid) { - throw spawnError(': ${String(value)}`.substr(0, 100)); - } - options.push(...originalOptions); - - return ( - - ); -}; - -const ForwardedRefSelect = React.forwardRef(Select); - -ForwardedRefSelect.displayName = 'Select'; - -ForwardedRefSelect.propTypes = { - value: selectOptionValuePropType, - ...sharedSelectPropTypes, -}; - -export default ForwardedRefSelect; diff --git a/packages/sdk/src/ui/select_and_select_buttons_helpers.ts b/packages/sdk/src/ui/select_and_select_buttons_helpers.ts deleted file mode 100644 index dd29a0d37..000000000 --- a/packages/sdk/src/ui/select_and_select_buttons_helpers.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** @module @airtable/blocks/ui: Select */ /** */ -import PropTypes from 'prop-types'; -import * as React from 'react'; - -/** - * Supported value types for {@link SelectOption}. - */ -export type SelectOptionValue = string | number | boolean | null | undefined; - -/** @internal */ -export function isSelectOptionValue(value: unknown): value is SelectOptionValue { - return ( - value === null || - value === undefined || - typeof value === 'string' || - typeof value === 'number' || - typeof value === 'boolean' - ); -} - -/** - * A select option for {@link Select}, {@link TablePicker}, {@link ViewPicker}, {@link FieldPicker}, and their `Synced` counterparts. - */ -export interface SelectOption { - /** The value for the select option. */ - value: SelectOptionValue; - /** The label for the select option. */ - label: React.ReactNode; - /** If set to `true`, this option will not be selectable. */ - disabled?: boolean | null; -} - -export const selectOptionValuePropType = PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - PropTypes.bool, -]) as PropTypes.Validator>; - -export const validateOptions = (options: Array) => { - if (options) { - for (const option of options) { - if (typeof option.value === 'object' && option.value !== null) { - return { - isValid: false, - reason: - 'option value must be a string, number, boolean, null, or undefined. Got an object.', - }; - } - } - } - return {isValid: true}; -}; - -/** @internal */ -export function optionValueToString(value: SelectOptionValue): string { - const valueJson = JSON.stringify( - value === undefined ? {isUndefined: true} : {notUndefinedValue: value}, - ); - return valueJson; -} -/** @internal */ -export function stringToOptionValue(valueJson: string): SelectOptionValue { - const parsed = JSON.parse(valueJson); - const value = parsed.isUndefined ? null : parsed.notUndefinedValue; - return value; -} diff --git a/packages/sdk/src/ui/select_buttons.tsx b/packages/sdk/src/ui/select_buttons.tsx deleted file mode 100644 index 31b430539..000000000 --- a/packages/sdk/src/ui/select_buttons.tsx +++ /dev/null @@ -1,250 +0,0 @@ -/** @module @airtable/blocks/ui: SelectButtons */ /** */ -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 { - validateOptions, - optionValueToString, - selectOptionValuePropType, - SelectOption, - SelectOptionValue, -} from './select_and_select_buttons_helpers'; -import useStyledSystem from './use_styled_system'; -import useTheme from './theme/use_theme'; -import cssHelpers from './css_helpers'; -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, - useSelectButtonsSize, -} from './control_sizes'; - -/** @internal */ -type SelectButtonsVariant = EnumType; -const SelectButtonsVariant = createEnum('default'); - -/** @internal */ -function useSelectButtonsVariant(variant: SelectButtonsVariant = SelectButtonsVariant.default) { - const {selectButtonsVariants} = useTheme(); - return selectButtonsVariants[variant]; -} - -/** - * Style props shared between the {@link SelectButtons} and {@link SelectButtonsSynced} components. Accepts: - * * {@link FlexItemSetProps} - * * {@link MarginProps} - * * {@link MaxWidthProps} - * * {@link MinWidthProps} - * * {@link PositionSetProps} - * * {@link WidthProps} - * - * @noInheritDoc - */ -export interface SelectButtonsStyleProps - extends MaxWidthProps, - MinWidthProps, - WidthProps, - FlexItemSetProps, - PositionSetProps, - MarginProps {} - -const styleParser = compose(maxWidth, minWidth, width, flexItemSet, positionSet, margin); - -export const selectButtonsStylePropTypes = { - ...maxWidthPropTypes, - ...minWidthPropTypes, - ...widthPropTypes, - ...flexItemSetPropTypes, - ...positionSetPropTypes, - ...marginPropTypes, -}; - -/** - * Props shared between the {@link SelectButtons} and {@link SelectButtonsSynced} components. - * - * @noInheritDoc - */ -export interface SharedSelectButtonsProps extends SelectButtonsStyleProps, TooltipAnchorProps { - /** The list of select options. */ - options: Array; - /** A function to be called when the selected option changes. */ - onChange?: (value: SelectOptionValue) => void; - /** If set to `true`, the user cannot interact with the select. */ - disabled?: boolean; - /** Additional class names to apply to the select. */ - className?: string; - /** The size of the select buttons. */ - size?: ControlSizeProp; - /** Additional styles to apply to the select. */ - style?: React.CSSProperties; - /** The `aria-label` attribute. Use this if the select is not referenced by a label element. */ - ['aria-label']?: string; - /** A space separated list of label element IDs. */ - ['aria-labelledby']?: string; - /** A space separated list of description element IDs. */ - ['aria-describedby']?: string; -} - -export const sharedSelectButtonsPropTypes = { - options: PropTypes.arrayOf( - PropTypes.shape({ - value: selectOptionValuePropType, - label: PropTypes.node.isRequired, - disabled: PropTypes.bool, - }).isRequired, - ).isRequired, - onChange: PropTypes.func, - disabled: PropTypes.bool, - size: controlSizePropType, - className: PropTypes.string, - style: PropTypes.object, - 'aria-labelledby': PropTypes.string, - 'aria-describedby': PropTypes.string, - ...selectButtonsStylePropTypes, - ...tooltipAnchorPropTypes, - ...selectButtonsStylePropTypes, -}; - -/** - * Props for the {@link SelectButtons} component. Also accepts: - * * {@link SelectButtonsStyleProps} - * - * @docsPath UI/components/SelectButtons - */ -interface SelectButtonsProps extends SharedSelectButtonsProps { - /** The value of the selected option. */ - value: SelectOptionValue; -} - -/** - * A segmented control for selecting one value from a set of options. - * - * [[ Story id="selectbuttons--example" title="Select buttons example" ]] - * - * @docsPath UI/components/SelectButtons - * @component - */ -const SelectButtons = (props: SelectButtonsProps, ref: React.Ref) => { - const { - className, - style, - options, - disabled, - value, - size = ControlSize.default, - onChange, - onMouseEnter, - onMouseLeave, - onClick, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - hasOnClick, - 'aria-describedby': ariaDescribedBy, - 'aria-labelledby': ariaLabelledBy, - ...styleProps - } = props; - const {containerClassNameForVariant, optionClassNameForVariant} = useSelectButtonsVariant(); - const containerClassNameForSize = useSelectButtonsSize(size); - const classNameForStyleProps = useStyledSystem({width: '100%', ...styleProps}, styleParser); - const [generatedId] = useState(getLocallyUniqueId('sb-')); - const [isFocused, setIsFocused] = useState(false); - - useEffect(() => { - const validationResult = validateOptions(options); - if (!validationResult.isValid) { - throw spawnError(' %s', validationResult.reason); - } - }, [options]); - - function _onChange(newValue: SelectOptionValue) { - if (onChange) { - if (newValue !== value) { - onChange(newValue); - } - } - } - - return ( -
    setIsFocused(true)} - onBlur={() => setIsFocused(false)} - className={cx( - containerClassNameForSize, - containerClassNameForVariant, - classNameForStyleProps, - className, - )} - style={style} - data-disabled={disabled} - data-focused={isFocused} - aria-labelledby={ariaLabelledBy} - aria-describedby={ariaDescribedBy} - > - {options.map((option, index) => { - const isSelected = option.value === value; - const isOptionDisabled = disabled || option.disabled; - const radioId = `${generatedId}-${index}`; - const valueJson = optionValueToString(option.value); - return ( -
    - _onChange(option.value)} - disabled={!!isOptionDisabled} - checked={isSelected} - className={cssHelpers.VISUALLY_HIDDEN} - id={radioId} - name={generatedId} - /> - -
    - ); - })} -
    - ); -}; - -const ForwardedRefSelectButtons = React.forwardRef( - SelectButtons, -); - -ForwardedRefSelectButtons.propTypes = { - value: selectOptionValuePropType, - ...sharedSelectButtonsPropTypes, -}; - -ForwardedRefSelectButtons.displayName = 'SelectButtons'; - -export default ForwardedRefSelectButtons; diff --git a/packages/sdk/src/ui/select_buttons_synced.tsx b/packages/sdk/src/ui/select_buttons_synced.tsx deleted file mode 100644 index 45c3bb22a..000000000 --- a/packages/sdk/src/ui/select_buttons_synced.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/** @module @airtable/blocks/ui: SelectButtons */ /** */ -import * as React from 'react'; -import {spawnError} from '../error_utils'; -import {GlobalConfigKey} from '../types/global_config'; -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: - * * {@link SelectButtonsStyleProps} - * - * @docsPath UI/components/SelectButtonsSynced - * @groupPath UI/components/SelectButtons - */ -interface SelectButtonsSyncedProps extends SharedSelectButtonsProps { - /** A string key or array key path in {@link GlobalConfig}. The selected option will always reflect the value stored in {@link GlobalConfig} for this key. Selecting a new option will update {@link GlobalConfig}. */ - globalConfigKey: GlobalConfigKey; -} - -/** - * A wrapper around the {@link SelectButtons} component that syncs with {@link GlobalConfig}. - * - * [[ Story id="selectbuttons--synced-example" title="SelectButtonsSynced example" ]] - * - * @docsPath UI/components/SelectButtonsSynced - * @groupPath UI/components/SelectButtons - * @component - */ -const SelectButtonsSynced = (props: SelectButtonsSyncedProps, ref: React.Ref) => { - const {globalConfigKey, onChange, disabled, ...restOfProps} = props; - const [value, setValue, canSetValue] = useSynced(globalConfigKey); - - let selectValue; - if (value === null || value === undefined) { - selectValue = null; - } else if ( - typeof value === 'string' || - typeof value === 'number' || - typeof value === 'boolean' - ) { - selectValue = value; - } else { - throw spawnError( - 'SelectButtonsSynced only works with a global config value that is a string, number, boolean, null or undefined.', - ); - } - - return ( - { - setValue(newValue); - if (onChange) { - onChange(newValue); - } - }} - disabled={disabled || !canSetValue} - /> - ); -}; - -const ForwardedRefSelectButtonsSynced = React.forwardRef( - SelectButtonsSynced, -); - -ForwardedRefSelectButtonsSynced.propTypes = { - globalConfigKey: globalConfigSyncedComponentHelpers.globalConfigKeyPropType, - ...sharedSelectButtonsPropTypes, -}; - -ForwardedRefSelectButtonsSynced.displayName = 'SelectButtonsSynced'; - -export default ForwardedRefSelectButtonsSynced; diff --git a/packages/sdk/src/ui/select_synced.tsx b/packages/sdk/src/ui/select_synced.tsx deleted file mode 100644 index 6c0b3da4b..000000000 --- a/packages/sdk/src/ui/select_synced.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/** @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 Select, {sharedSelectPropTypes, SharedSelectProps} from './select'; -import useSynced from './use_synced'; - -/** - * Props for the {@link SelectSynced} component. Also accepts: - * * {@link SelectStyleProps} - * - * @docsPath UI/components/SelectSynced - * @groupPath UI/components/Select - */ -interface SelectSyncedProps extends SharedSelectProps { - /** A string key or array key path in {@link GlobalConfig}. The selected option will always reflect the value stored in {@link GlobalConfig} for this key. Selecting a new option will update {@link GlobalConfig}. */ - globalConfigKey: GlobalConfigKey; -} - -/** - * A wrapper around the {@link Select} component that syncs with {@link GlobalConfig}. - * - * [[ Story id="select--synced-example" title="Synced select example" ]] - * - * @docsPath UI/components/SelectSynced - * @groupPath UI/components/Select - * @component - */ -const SelectSynced = (props: SelectSyncedProps, ref: React.Ref) => { - const {globalConfigKey, disabled, onChange, ...restOfProps} = props; - const [value, setValue, canSetValue] = useSynced(globalConfigKey); - - let selectValue; - if (value === null || value === undefined) { - selectValue = null; - } else if ( - typeof value === 'string' || - typeof value === 'number' || - typeof value === 'boolean' - ) { - selectValue = value; - } else { - throw spawnError( - 'SelectSynced only works with a global config value that is a string, number, boolean, null or undefined.', - ); - } - - return ( - setValue(e.target.value)} - placeholder={`${capitalize(size)} input`} - /> - setValue(e.target.value)} /> - - ); - }; - `; - }} - > - {values => ( - - setValue(e.target.value)} /> - - )} - - ); -} - -stories.add('example', () => ); - -stories.add('text input field', () => - React.createElement(() => { - const [value, setValue] = useState(''); - 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" - /> - ); - }), -); - -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'); - - 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 deleted file mode 100644 index 80a2f60eb..000000000 --- a/packages/sdk/stories/heading.size.stories.tsx +++ /dev/null @@ -1,66 +0,0 @@ -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'; - -const stories = storiesOf('Heading/size', module); - -stories.add('default sizes', () => ( - - {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. - - - ))} - -)); - -stories.add('caps sizes', () => ( - - {keys(theme.headingStyles.caps).map(size => ( - - 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 - -
    -)); diff --git a/packages/sdk/stories/heading.stories.tsx b/packages/sdk/stories/heading.stories.tsx deleted file mode 100644 index 692fce117..000000000 --- a/packages/sdk/stories/heading.stories.tsx +++ /dev/null @@ -1,148 +0,0 @@ -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 Example from './helpers/example'; -import {createJsxPropsStringFromValuesMap} from './helpers/code_utils'; - -const stories = storiesOf('Heading', module); - -function HeadingExample() { - return ( - { - const props = createJsxPropsStringFromValuesMap(values); - let sizeOutOfBoundsComment; - if (values.variant === 'caps' && !has(theme.headingStyles.caps, values.size)) { - sizeOutOfBoundsComment = `// The caps variant only supports ${keys( - theme.headingStyles.caps, - ).join(', ')}. It will use the default size otherwise.`; - } else { - sizeOutOfBoundsComment = ''; - } - return ` - import {Heading} from '@airtable/blocks/ui'; - - ${sizeOutOfBoundsComment} - const headingExample = ( - The brown fox jumped over the lazy dog - ); - `; - }} - > - {values => The brown fox jumped over the lazy dog} - - ); -} - -stories.add('example', () => ); - -stories.add('as', () => ( - - {[ - '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 - - -)); - -stories.add('custom className', () => ( - - Inspect element to see class name - -)); - -stories.add('id attribute', () => ( - - Inspect element to see id - -)); - -stories.add('style attribute', () => ( - - - Inspect element to see style attribute - - -)); - -stories.add('data attributes', () => ( - - - Inspect element to see data attributes - - -)); - -stories.add('role attribute', () => ( - - Inspect element to see role attribute - -)); - -stories.add('aria attributes', () => ( - - - Inspect element to see aria attributes - - -)); diff --git a/packages/sdk/stories/helpers/attachments.ts b/packages/sdk/stories/helpers/attachments.ts deleted file mode 100644 index 07ef0c943..000000000 --- a/packages/sdk/stories/helpers/attachments.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default [ - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAIAAABvFaqvAAAD3klEQVR4AWJMKStX09QENElVibLbMFS2kww/5jJ/lr/6vsrtDrq3rqBbKH2VmZn5vQvDE8eW3CNlCrqKxxEcnaObc+NByqkZT5fL1bU//twu58zsR5PxyZNXLl+enDoZzp4r3lEhmNiPXZ1eLOIfu/nk7++//uu1A3L+8Luv6y9fvS999sT0t6emvz3CX9z065vfv/HqDz/9QqVU3gfnvHfBe3ilp4PhCDheeuF56XavvPHeYr547uptZ86dcYUkRek6KShpYmw//On46MFn77jrLuZcChVjUsxxCBHcx92m8u6hW84+HD47P/YpphRbzsyloK3rIjXjB++4fCuvhRxYGKP/W+gJ+ir4Gki5K7nk9VHgXXAlBMSrqqqRdcLC6cIQV2CoqSTcrd+u9jSDAeaUIsIRTV4VV95cWwDnPNyT+KDJf2kExKyB0a6M6oF3FJSaznM2y1ZI+FPEug71gJiJSIebKQeDLU6rnLWRkvDQb82uOBI9MYeFcLaRFgutCYFoT8n5PV5wvnausWjopwcdsOfSLwAPSSldCtj56viv1boaNIawR+k/AoQGVuw1VuQfGOQr/QkV5q9/PZDfr7vMM8ftG6/88P0Poa4NS1kFZzr61dnnCTn9gnBXJ1NHwnnXUYz5j99Gw9HVe2698tePq/m8ARaRTjOFQZ3ABW2KzwwZ9M8qG5zt9Xk8WIgiF8n6hV06fWLCGfWVdqKYanJDOKp1vcLr1brL5EPtqhGFRv/joebYbb/8s/v5TyoMGcEHYR41Nak5AYx5gUBAbzarJu8wddAApS6lKAHlFqrZrD45CqPKnzqVBRHyVVWQJS1CQDCBiDWiay71ABuYhFDETCuK0oSc9tqCTs5ofEK4T8FI9saJJTJvmdfMUNMAIHctUdCs9SOQUxeXa3/j2QjVf/4q1cCS+hQWdQy09yzSAkiDOTHjFUdn905yFM6lqppLJ3mzzbGzekE2p9TPg4OBY3EivogC5YwsAIThoo4I5lFTzz/5USaTcuFSjrHPkg3M+mTcCjNlePYIJBhSxeUeyxzhuN1tD3fZubxtkUcE8DoJd4XaAxJn8PIp7rAgsQoNG3kFj7Hdbkf33bhdLHfXrrGvUubUdZySzYErI7KxQPZcSNekphqtgNHDAmlN3MUccwouxRaVCVm1xDmh2gHLqMJ8woWLSiOXxaleKZoFyeCXX/xRxkN36kzXtuDItk1JuWT1lPfWpeRTB4udciBLJBhOxNeLdWxCC43zDRPl1PUbKUYEJuaoZTDq5ah+lSpKXigXn7jgdXr3xdxUrTh2XmvQmLLNsSfZxeD+BmWbe58Isp33AAAAAElFTkSuQmCC', - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAIAAABvFaqvAAAET0lEQVR4ASVUxZosNw89kuzCwcvM2x+T1V3mHbLLK2SbV8su+3BWYeYLgz3dRbYUucdfjcufSpZ0YJree/t5E3lKdtjFaztBDatJU7Yx5cAcmMyMCF0twjRlXbIFILD1FQmBgBhEoOE/d9vz0bqaVXMy7PdV39lvL07e//z3d54/vn3Yrcd00Mmc0VZQ8zQis75mIjrbLIERmZaMsBpyG+yDr17//Or83bceNYFON3o+5MfX9+fFouYHu8Rsg+VOqI6cs2Y1YvXtWs8idHqRhDnc3YUZnh1WjeynxUTS9cr+f6d9sB8qsRfn048JwrTb8IHKzYo5YphUE2Jkj4PoYLeCIXz089oMXcVPr9avVvPpRtQsMO/W8XDH+2GzFF7WU/7zKM3T0laF0CjUJJqT51IVaFg0vHm/dYKCkGcAUOJJcbRKw5gd48OrEYTAuLpDlcMyNlCj8HwyIwOTrcZcBwq/HE/+IuB4UJ9/rwtRuBL0FS7G/POrrGpTUoGuu6drrf/Xfmvgo5U2kZhpzjrM5jKFL/4a9usQGV5FQD++dN3x4DAWExj2ajlaL4ct+7XzV7/8eDStd7MwudBuBYC8R1KYIRwUIe10zEPiF6sUhPrIv58uUby0ZNVacLTJQsSYnxzg7wsfMM/ZTiadsu3Wcn83EFn4952WgNNRnW/XvnWBDcwUGCmp83pzN6xnZSImmNqtvXIWgRoWxZLRCAAL+zWY6aANzlRT+bGYpWvjnMFmagaUFzP5mhU5ayRjQso2LTomnI2qamGdOYJGvwc3AbzceLZMy+giCsMMzEjqO7kIIbAncMWlPKkRNa54JafrFL57OfYVRaYuchtIlWshx2gGEQKwcdbVdmoR03HMC9OrU4fsqrE/VbAlm6UcpnmpSbpGlpRfnWsGtp+FYL4xF/uZYjMvixoAb5myXkx5XGxMCvgnHbOGk1H92Rl1ySAqekfRnJdKSM2ywpuPyYTRl5EZBCZ43DFGFjdBCmijOEBWs8b5rpgILqcQAtOi8LhXqYUfH0oVSEoJRw1TuA+ZMCy6nulGG4tqNzpuhISL8MJwmvY6qQO7KL4ik2eXElwGxHYRG4E2i/oQbc9z0jkhePacLYB2iJxmIlv7f1lOQ5mF5mRaEkoDYvjyTdWSmlOTDC5LVltNOWhp5Ij4YtIXq2wokvUVB8Ywm6plM6LS30cmYNj+Elz683RI8LsVR0bwvyG5CsgKJhw0LHw5GiAlOGXycYThMmfDjts1kEt5PqkT2mwt7vGwVQcp6c3d6GfX7mJWqoqthKCOl21WPd4UjN6g2vbn4KpbIFwyUwq5LqW30MmQx+35sBUvd7wkJsBgBG8emALDd6fjEq8wVluvqhURg4cYSAaPtIG9/JBst6LZ/AAuPQuoJWsUch/2tXghj7TRSxMTRUZWCr2/Ad/GBIdARFvvkC+14t2jTbreB95q6gRdmos9bVEAgeD91PAPxy3t5DnD7Z8AAAAASUVORK5CYII=', - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAIAAABvFaqvAAAEwklEQVR4AVWURZrEuhGAVVWSLLt5mHke4zJMl0hukV3oAllnGThKVmFmGmae6W7bbRRE7cf1lQss6RcLvv3DHzHGHGPGMfLeOQYMpsaxRgBgahuHyBBwGoF3DBshACLghGQZxOcH81Trtdc+xDXyGZCngNcG2ASIjQISAiFynzggW1eD7DBne2MMyGn/BxCds+Bcg3LQOPwsxUsTI5GnsSkvGsxN+MCKQBCKQCGRKVJPASREhvhRS2yaNepDgg9iapKpZYEU/U4nUCEglfHw/O+/s4e/X6PMU9hH3X7wUUOCD3Ce4aWB8bquZasdKFXHZjJ8uDk7Nlf/S57GawtqxWxKRec6mhJYQ0RGiB/NCKjhAjEP5J+fL355Uz2cnRyf/o8dXdVnB7w7O6RICJrUMGOvGfUvYYFzjswh4QcIbz8aHfOGe9D2YvRw/Kef/+RnK2/ukgPJ0LeJuJIsZzKsk5uFoLSADzSPQhIwAKApCHnjGgoSkbc4v7HV2XlFKrA1QyLIkiWTGSVlfBtxG2O7o2g2LA5yRBVy5rHEPxoLNYvMvaEGeHlyU5e5rStXVT6toxBVx1pWZBOl06ysZiPYZC/CaeRCciSvxIVX7/jUIjqLzBXMaONE1FbOAOBNmoMK70usWOCS+42eMNaVyctadSfBgm/PSfAPRXIKBMfn23tktmIIRKLVksDA1UZwsjnnBFw8lMH9KAvRzgSgi7Tt8hAdehSiIJpS+NRiaawAM2QTrGp2faoYa0Vh2IpcVW+pOnAGUNwMJ2mpy6IMmK51zfOXSGdEqAQPBXkbiA+OltWMsZhh5PHMBVW558q2CrVjWZHP8vKtRVUVxUiTJ1YOQ9D88UDUWRgIT1GimVpVVtbodcb6rdBoE3AqS/3P3/4LiP/vLj6I8SIXyaQYSLvCs54SWZ49PD1P4nFxdxKADQMZNCCOzJHglZ9Rb/BYuvDhhGZXl7eXct7Sxsz2ew/Poz/+/mh/ayW/u3zl1VcH7UhKg2mZ17UzdRT2wDlEpP3ttadxfhbrDPA+nMXBRiQxardvNb8q6HjsmK1b3P79+vlPxw+Jpevnoc4mDtAxjEfx4tJKr9sLpIDNL31DW7vdVc6au8dRIjtrLfSXjnfm5rq9AqRwuh+ilPzx9haZM0EUIoSsXlpeCVQrVPy11996GsY0o7gss+eXURonripsOtZp9nR1Mbq7sOCUK1qhII7pOL26vv/HyZFOk8PnYa/d8ZUpCN57//2r25sffPf7oHoD62zzsAIScXBcCESKszyUQijV6bQxUCeF3VT01ubK0urqaog9CSXj3dWdrZ1do+txnMDM/JwkTCvDmvfBOLexvFBaFnFhPJy4I3htc2WvL934ZbC55xbXO9kLpk/1YKO9tmOMJXBCCP4yNEwXMxtzEmyqbRdJZsl/x+U3396OBv2zYbrbDd/ZCBb6rccHuC3r5+fhqyw1lo0NFqOEEAgAseRfe3dha6n/24vkdhgrQrCWWSM5gypblDNauK4tyVQ3k/ZdSanOotnlNBn7aZVBO08zcJYIJef8W1/a73bb56OD/xze7q31k8lEq7AKRFY7VRcrEu8dL5/Nem/MskK1Z16Go0etQEjx9CI49xSvBdb/B4pjTyOoxDJeAAAAAElFTkSuQmCC', - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAIAAABvFaqvAAAFhklEQVR4ARVTRYLzsNkWWZbZjp1kggP/X2Zm5p6gN+kJuuslui8zL8vcfgzz4YTjGGVZstXpC6uX4YHf/Po3dtunrzb3vdEVkfn0fM7z8vl/fvGOt79FZDxNcynhwPGw3aeVaDQQPe3T/HJ2trwIiCk1b4QEozMXe3G8P5ZJMsZQeK7x8NGdv9zbpT00LaOHjmuxaEBtmxADvqg568H/J6AlCFHLIiZA+riVXUt7UmASe+No8J63vev61SY7PrDC+W+fvAxU+6OXbYirN85MA1p1IwvBt1kPQf+mUe/S/vd3tOGbuJOdxMkkhkCQyXgsFJ9Moo/Sd9+s/dX2X66JnND7MKRTFzumXXPnWPIatY5WRSH//drQQnalfHrj+Ff0dpSsbUKrIiFGjWoevb6+nC0AnPzn0V/OEx+wxqiKYRAjOu2EZv1N4LsgRPvN6cmL0oE4YIDC3osHxyMS1bEwCfnCu5IX2+Zfd/7ke67FyP+0KUmLKkGlEqhLe2X1mtgtDke2EHLWdBJoWZqRCymua1C9WlFqY7KYFowFjp8IkUPV3mx3RMfTs+BA+d1VcTHeeszZaFjW8nR9AkrHcbSrgMXCTne8ak0TUc+kqCY4CBzDKrN8yjKg5HRxqXpMiDEhoixIx/5/7FNVPDkUWOpuGA5W3MCMWpScsvJmXy/GcJQMDMNBXHAaRdC/uD7tw8nZpz/3Lg6avKw5Yl4EwzByo+XycjxIWIdQjW1iBoyahHTnSwtRyjk97A9ZXqE0V5zX73jzNPJCYNmG4/7l4ctj2tw7CIBsD69Vu8e2HcTacDQJ2eX/uYnf8rZTkruu04KI2qaXOMR3zcePVi/L7TzsVpudify3jIgQlV7XrTevRe3hFwJQTTgxqMWsONK6UjnXJisHZ6OiaKIgCYYEf+5j//eD717/8rePjYFwaDB0R7vd5vXrbBwmSYCmMYVdqzFVqk9Tuj80RbrNpD8fNINhx1EEDN1LAfsUA2v+6Gnxzje5DHVOFHLO0/VL2epwQObBrZuRqqEEPoMd7KtDptKcWsHAsunuoKBFiMW06uvyQGIHeoFZ8X5kEiDzvD7wkl+dD///8raZtuMEY1sDKLt2Op9g23zx6gBQ9+IAjR6PbJYeat+CZQuJlXgEnhybtB24HHlAG48fbmzLfOPF//u+cXj9dPX6UNZtIzMpHY2JF8LbPoFqmQcx0bCVWVsDaqCHWzUZW6IrLybh29/0VoRuk6BTJe88XvdspNz/X633ZYE77ZVCUgfHszCKSRyT4Zlrsd6wnGODBwHG1yvnox85T0L9rjdfaUS/9cN/tFLzutk32o3HIs+LtBhOBq7bEds0HWqHpu3bgmtiYspwEmjAHJzv8Ne++qZDxt/0hovF+fy4O6kuXRcwMRAjBqpT0mYS2wACijshGtFB26MamNdP0rJtHN8nIiuO6y5vUCUh9OGrU9PUmYTIsygDzWhkf/wDiygwdc9tcMjTmmihJS+yZrsqb14cu7Ygpvb1QRTpq51JmcJV7iugGwGYbv5x/bIUXZPKwDMv5qHugYYAaM1s7LPMJEHbGzXnPK8VUlcXYb16LRtZw+FiKPH47OzV4/IWE5ZPA0cRamCFTzvhexBBIIR0QiuKQoJdFsXaAL1ute6dkBX7NEu1aei8Dy8nHfJD9tYPJ299h2dbuutZUcpOg/Wq/c7PXp6qNkyozULfChWheVuqrpbytgmEADydSgQbbNHlZSS1ScqDePubR4tlZLYn6rAbLjYPq3jmSNlFsTWanRmSaJQRo0etFo1WmvRAUQoHiYlVpwzrKumazEDm7ehjLw6dN1wOHI9Oxu5H3jctVL9csMXMifwEQQhR5fsGcyhznKLod7sT1AqBFuG+16pp1Wjo/hedTBUz0I7/BQAAAABJRU5ErkJggg==', -]; - -export const recordCardAttachment = - 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QCMRXhpZgAATU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAABIAAAAAQAAAEgAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQAAAFCgAwAEAAAAAQAAAFAAAAAA/+0AOFBob3Rvc2hvcCAzLjAAOEJJTQQEAAAAAAAAOEJJTQQlAAAAAAAQ1B2M2Y8AsgTpgAmY7PhCfv/AABEIAFAAUAMBIgACEQEDEQH/xAAfAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgv/xAC1EAACAQMDAgQDBQUEBAAAAX0BAgMABBEFEiExQQYTUWEHInEUMoGRoQgjQrHBFVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4eLj5OXm5+jp6vHy8/T19vf4+fr/xAAfAQADAQEBAQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/xAC1EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2wBDAAICAgMDAwMEBAMFBQUFBQcGBgYGBwoHCAcIBwoPCgsKCgsKDw4RDg0OEQ4YExERExgcGBcYHCIfHyIrKSs4OEv/2wBDAQICAgMDAwMEBAMFBQUFBQcGBgYGBwoHCAcIBwoPCgsKCgsKDw4RDg0OEQ4YExERExgcGBcYHCIfHyIrKSs4OEv/3QAEAAX/2gAMAwEAAhEDEQA/APC7PS+ny12tnpnyr8tbVlo/IGK7W10bAX5a+4xOPXc8bKeHml8JzNlpJ4wtdTa6SD1Sums9K6cV1NppfT5a8Svjt9T9Cy/I0ktDlrPSBkfLXaaXoPmHAwPrWta6SeOK7PTNOZSOK8bE4x2dmfZ5flcYtXjoZVv4ax1YH6c1sw6LsxgD8RXeWdgzAVtQ6IX5IrwauMl1Z9PSwlOOyscBDprHBxWmunIo+5z6mu/i0LjO01MdHXH3a4pYlM6koL7SP//Q7aw0hBg7ea6q10rJHy1uWmmdPlrrLHTeny1jWzRa+8fq2CyRRSXIc/Z6QePlrq7TRs4+Wt+z0vp8tdTaacOOK8utma/mPo8PlnKtjnbTRuny11NjpS8fLW9baegA5P5U3+39Ft9Ws9Il1S2TULmF54LRpFE0scXDuqdSozya86pj+a9nc7XGFFatI1bLTwMfLk119norMOSMnoBzTLO1HFdzpNiWK1wTxF3ueLmeZunF2laxkQ+G5j1WpJfDbAfdNev2ViFQZq5JaIwxgV3wy6rOHMj4WpxTVU7dL9D/0foqzeU4wRXTwPJGhZpVRQOWbAA/E1+X0Xxh8ZX052eKbvHQpZ28SAZ/3lJ/WtZrm71NlOpmfU2J4OoXW5Rj0jj+XP5V50uFsZp7XE04r+6m3+PKfc0fFXLaiawmBr1JJ2vUahH74c7/AAPu+9+O3gvSZAtz480lTu2kbhJtI6hjGTj8a7jwt8cPB2t3EdvY+L9LuJX+4kUnzPjrgN1/CvzRGqaNZTJb+ZpMcm4N5ENj5zlScEYjaRia3YYbeSWI/wBjXbbgG3tZw2sKkfxBZTuB9OCRWdfhnD2/j1U2tJNJL8l+YYTxGzGUrvC4VpSXNCLk5Jdrpu33H6PfHP4tr8OPBOseInRrprfyo4IVAVXmuHEUYdjnam5gWPp0r4V/YEu5PF/jzxJ4p1/Wf7T1ltLja1m+0iZreKeeRZVZNxMZyMJHjCKeOtKPG3h6eGax1bxM1zGylH09JJLoYxgB1RmUL9QMViyePNF0T5dKtraOKTqZWjttwHPGMs3rgcVz4fIqsMNXpJvnqP8AicrSsumpWa8UYevjcJiZ1oKlRTfsVJTlzPq1Felrn7Jt4s0fRgv2/WLaDIOPtEqIT9M4JrV0z42+DI4JJo9ZjuAgPy26NISR2GBjNfh1F8adOl1OSSG5t7iQIPm3wIS3OQDu3sF9Miur034iy3sbvdWwjj4xJGJVIA9slfxrzJ8Pzoq7rOTVulkKWfZfjqjU5tJt6LTbrezR+2b/ALSPhOOKEkXW6RtqIUGSfwJxXB+Lf2oDaIxsNLVwFLEu24gDjkDFflOfiA9oYIYb1XwuVjuZtkmWHTgHGe2SK6rwTrWpR+bJcO7x7tyoV3MpPUZUZIrLFV8wUF+/5YLS0Uk38z6LKsBw66sIfValSbTu5ycoxaXW1rn/0vzSTx/I4xea1amPtHaW5uWOemJJCEGPoahPi2zmj3T6c06jo17NmMD/AK5qEX8hXAWa6NaOu+QzuOynr/hXVf24I0Qw2kKHIIbaGb82zX1kKa1bab7X2PySvmE1yxpwnGP8zVm/vO40/wCNjaNalNOgUHbgfZISqL/3yMV51rXjHxNr7u0q3PlsclDJtDZ/vknJHt0o1TUbqby2ZmIxnngE/TiqYudQnlH+mCBSMHykXOB9KzlRjzX5bP8AupL8wjmlf2PL7ZySv/Fm3+EepfRNTtoCss6QRqAVSHOwH/awOv0zUZ1OGISA3KXEjHk/Z/Mc8dmkzjFYF9rIt5fLWATsvWS5cnn2UcCqDa7eTjyvPSMHnEabB9Kyqyh8PM9P68h4aNf43Sirre9tPRX/ABPRtM1UvPGFR9wU5XaEX2zxn8a7uPxbbabEri6LzBshC7SCM/7KbsDHYmvCINEv3UsSHAG7G8dPoSM1ShlDNhQ3HYDPT6V5WKpylo479zto42UOZ06ibXVbL/gn0la+Jrq+UyRrNMRydw3cnntnmum8NfGi80e4BZ5F25QgM3GeMFa8n8K+OIbKyjgOFdS2T03ZPWuX8R+IBe30s0SYJABck8sOpA4596+VeEnKpUpzp+537n0H9u18LDDV8PjP3ytzK22h/9P8P4L5omAEiOe3atZ9eYIA90xA7RjaPpnkmvPNQtXjc9cGlhiklA/dyNj+6pau/wCszjdW/E+enldCoozck16I65vEfP7uL8WJYj8TWpb+KLxVAWTqwCoFGT3rm7LSpnbb5TqB13Lt/PNegm1j0uw8y3u1ikMio8hUFzg/dTdxt9a2outLmk5tJLX+tDycdHBwcKapKcpOyT1+92ZhyXMkzeZKPvMVyP73ofersDWyuFlnVM/dJ5H4ntXeWdnpviCyYXd2hvYoQ6XMUaxs8Sk5DqDlmX7vT864q+0DdH5kV3azorHMyZidcdpEbBDDuKc1NK9r32OeSpbN8qWjtsuxafWZbd2idOh4IBAI/GqouZYQJQjxl87fXHtWQ1xdIoklkFwiMV4yGHtn09KzY5t7vjcMnIyefxxXLOpJtXbv08iaeAhZtRj/AHmtU/k7HUSan50eNsgk9QBg/wC9/wDWqGWSQEtkgdg7gn/69c/NckDiYE+mSGFPGoRyjY9oi+jxjDZ9znBrlqyjfVs2jguVLlgrf13P/9k='; diff --git a/packages/sdk/stories/helpers/categorize_style_props.ts b/packages/sdk/stories/helpers/categorize_style_props.ts deleted file mode 100644 index e1addfa07..000000000 --- a/packages/sdk/stories/helpers/categorize_style_props.ts +++ /dev/null @@ -1,64 +0,0 @@ -import {entries} from '../../src/private_utils'; -import { - backgroundColor, - boxShadow, - opacity, - borderRadius, - border, - dimensionsSet, - flexContainerSet, - flexItemSet, - positionSet, - margin, - padding, - typographySet, - display, - overflow, -} from '../../src/ui/system/'; - -const categories = { - Appearance: [ - ...backgroundColor.propNames!, - ...borderRadius.propNames!, - ...boxShadow.propNames!, - ...opacity.propNames!, - ], - Border: border.propNames!, - Dimensions: dimensionsSet.propNames!, - 'Flex container': flexContainerSet.propNames!, - 'Flex item': flexItemSet.propNames!, - Position: positionSet.propNames!, - Margin: margin.propNames!, - Padding: padding.propNames!, - Typography: typographySet.propNames!, - Other: [...display.propNames!, ...overflow.propNames!], -}; - -let categoriesByStyleProp: {[styleProp: string]: string} = {}; -for (const [category, styleProps] of entries(categories)) { - styleProps.forEach(styleProp => (categoriesByStyleProp[styleProp] = category)); -} - -/** - * Given a list of style prop names, map each to its corresponding category and return - * a map of style props grouped by category name. - * e.g. - * input: ['alignContent', 'justifyItems', 'fontSize'] - * output: { - * 'Flex container': ['alignContent', 'justifyItems'], - * 'Typography': ['fontSize'], - * } - */ -export default function categorizeStyleProps( - styleProps: Array, -): {[category: string]: Array} { - const result: {[category: string]: Array} = {}; - styleProps.forEach(styleProp => { - const category = categoriesByStyleProp[styleProp]; - if (!result[category]) { - result[category] = []; - } - result[category].push(styleProp); - }); - return result; -} diff --git a/packages/sdk/stories/helpers/choice_options.ts b/packages/sdk/stories/helpers/choice_options.ts deleted file mode 100644 index d9be492ac..000000000 --- a/packages/sdk/stories/helpers/choice_options.ts +++ /dev/null @@ -1,27 +0,0 @@ -export default [ - { - id: 'a', - name: 'TypeScript', - color: 'yellowLight2', - }, - { - id: 'b', - name: 'JavaScript', - color: 'orangeLight2', - }, - { - id: 'c', - name: 'Python', - color: 'green', - }, - { - id: 'd', - name: 'Ruby', - color: 'redBright', - }, - { - id: 'e', - name: 'C++', - color: 'blue', - }, -]; diff --git a/packages/sdk/stories/helpers/code_block.tsx b/packages/sdk/stories/helpers/code_block.tsx deleted file mode 100644 index 76d06b4db..000000000 --- a/packages/sdk/stories/helpers/code_block.tsx +++ /dev/null @@ -1,115 +0,0 @@ -/* eslint-disable react/jsx-closing-bracket-location */ -import React from 'react'; -import Highlight, {defaultProps, PrismTheme} from 'prism-react-renderer'; - -const theme: PrismTheme = { - plain: { - color: '#393A34', - }, - styles: [ - { - types: ['comment', 'prolog', 'doctype', 'cdata'], - style: { - color: '#999988', - fontStyle: 'italic', - }, - }, - { - types: ['namespace'], - style: { - opacity: 0.7, - }, - }, - { - types: ['string', 'attr-value'], - style: { - color: '#e3116c', - }, - }, - { - types: ['punctuation', 'operator'], - style: { - color: '#393A34', - }, - }, - { - types: [ - 'entity', - 'url', - 'symbol', - 'number', - 'boolean', - 'variable', - 'constant', - 'property', - 'regex', - 'inserted', - ], - style: { - color: '#36acaa', - }, - }, - { - types: ['atrule', 'keyword', 'attr-name', 'selector'], - style: { - color: '#00a4db', - }, - }, - { - types: ['function', 'deleted', 'tag'], - style: { - color: '#d73a49', - }, - }, - { - types: ['function-variable'], - style: { - color: '#6f42c1', - }, - }, - { - types: ['tag', 'selector', 'keyword'], - style: { - color: 'hsl(216, 100%, 33%)', - }, - }, - ], -}; - -export default function CodeBlock({code}: {code: string}) { - const formattedExampleCodeString = code - .trim() - .split('\n') - .filter(line => !line.startsWith('```')) - .join('\n'); - return ( -
    - - {({className, style, tokens, getLineProps, getTokenProps}) => ( -
    -                        {tokens.map((line, i) => (
    -                            
    - {line.map((token, key) => ( - - ))} -
    - ))} -
    - )} -
    -
    - ); -} diff --git a/packages/sdk/stories/helpers/code_utils.ts b/packages/sdk/stories/helpers/code_utils.ts deleted file mode 100644 index e7c6a40cc..000000000 --- a/packages/sdk/stories/helpers/code_utils.ts +++ /dev/null @@ -1,87 +0,0 @@ -import {has} from '../../src/private_utils'; - -/** - * Helper function to turn an object keyed by prop name into JSX properties. - * - * ``` - * valuesAsJsxProps({ size: 'large', disabled: true }) => `size="large" disabled="true"` - * ``` - * - * False boolean values are ignored: - * - * ``` - * valuesAsJsxProps({ disabled: false }) => `` - * ``` - * - * Provide a `mappingConfig` object to map certain keys: - * - * ``` - * valuesAsJsxProps( - * { icon: true }, - * { - * icon: value => (value ? 'edit' : null) - * } - * ) => `icon="edit"` - * ``` - */ -export function createJsxPropsStringFromValuesMap( - values: {[key: string]: string | boolean | number | null}, - mappingConfig?: { - [key: string]: ( - value: string | boolean | number | null, - ) => string | boolean | null | number; - }, -): string { - const output = []; - for (const valueKey of Object.keys(values)) { - let value; - if (mappingConfig && has(mappingConfig, valueKey)) { - value = mappingConfig[valueKey](values[valueKey]); - } else { - value = values[valueKey]; - } - - if (value === null) { - continue; - } - - switch (typeof value) { - case 'string': { - if (value !== 'default') { - output.push(`${valueKey}="${value}"`); - } - break; - } - case 'number': { - output.push(`${valueKey}={${value}}`); - break; - } - case 'boolean': { - if (value === true) { - output.push(`${valueKey}={${value}}`); - } - break; - } - - default: - // eslint-disable-next-line @airtable/blocks/no-throw-new - throw new Error('Unsupported value type'); - } - } - - return output.join(' '); -} - -export function createJsxComponentString( - name: string, - props: Array, - children?: string | null, -): string { - const propsAsString = props.join(' '); - if (children) { - return `<${name} ${propsAsString}>${children}`; - } - return `<${name} ${propsAsString} />`; -} - -export const CONTROL_WIDTH = '320px'; diff --git a/packages/sdk/stories/helpers/collaborator_options.ts b/packages/sdk/stories/helpers/collaborator_options.ts deleted file mode 100644 index cb4405711..000000000 --- a/packages/sdk/stories/helpers/collaborator_options.ts +++ /dev/null @@ -1,26 +0,0 @@ -export default [ - { - id: 'a', - name: 'Jess Patel', - profilePicUrl: - 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/AABEIABgAGAMBEQACEQEDEQH/xACBAAEBAQEAAAAAAAAAAAAAAAAKCQcIEAACAgMAAQMDBAMAAAAAAAAEBQIDAQYHCAARExIUFQkKFiFBUYEBAAICAwEAAAAAAAAAAAAAAAYHBQkDBAoIEQACAwABAwIEBQUBAAAAAAACAwEEBQYHERIAIQgTMUEUIjNRYQkykaHB8P/aAAwDAQACEQMRAD8AujGOZyjCOPeU5YhHGP8AMpZ+nGP+5zjHoo/3/H7+qZ/+d5/f2j3/AI+3/p+nop3kx+4j3rhPll2fmuo8d530fjnPNrZaEhw5Z7bpu4tHWl3WIdwdS2QBfsAMwTdxEdLFi23V6YBBpKzPyh1baEAoxl8haQjAkAlMR3ghKfGe0z/H5ont7F3iO8H2kfXsLjXw0Z25xLE0bmtq0Nu/Tr37J1Ro3aKg0FruVa/4Zra8+aaL6puYFth/OsGliVMrmJW+/T186VPn9xtz1xNybdOTUa9ttmlHrtrPUtl7R2MnXt2V2quFmapNVaz8mOtPIMXLShXEC1hAsCgSIVbiHjYCTGJiO/b37TE+31iY+sf47T7faZJDdTunb+m22nGsa9TUZYqzdD5CH1bNdHz2IWN2s/8ARY2VkxULMxNPgyPIDAy626L0zS+Ua7btO8PRESyNv2os7/ksJOPnXOdIa8OiNhRZEsQzOz4q/jGpxIkq2iiErPRVxniHJuZ325nFcW7t301H3Tq0hXMihAzM/Mc5ia6jaXimuDWgVh5ilUGZRAhWNnTqadGjNmpRTYt1U2dHReFTNzazrCl2NDStnEhVoU0mdi085iAQphx37ehMLvAvl+0bQduvXbnOyvtge7zsDERheagu2J7vG1nbPRIn8GwqfsCwKCiFlStKwKEbkknFCYMsvHl6UWoO9nalzL0su9l6dRp/i87RpPqXKIgZCUvrXA8lpg/7rbIiuXaWC2AKfXYl0U+BDotucG41oZfJaPUvErcfugHKeO6Y0+K7rLE0wpbdvc43fVcz8zMy1BVim7Xq169Z7bOius+JlC1vB/QiuNcZ1vjNXDiuRJ9RAsZVmiEat/GtpebGZc22FuuWp3ZzdWYxZkXHlAtVoWBI3Vi1XX/DmMCTIKwKFobSZXEVS1TZOSEga4zVXITUoxcpBqGe0tAhDvJgckseej+opgfD7a6y8n6mdA+vHTHnuHy7lVrLjpHwji3M8G/0lo8eyq2WumzR2MipxrkOEd/Psop7ONcKxfsWYtTTJJnZI/3ZPIrpnduhMt72d8YNHN142s68vvnWl1NB888hp1YucZrlb8WYWN2t8JmuWP1lETrGqACEt44Dw3I6f4FfDwglcR8tujeIRi3r6HhAuvXGdvIh7+S6dSO6aNbstUEwrD7CDz+O5WTSCkqsDJiAl9hsRL3ugYljSL8sjHn+mtfiIBAjHeZMy3Lxk6nPkG5a50456ZlANfZVuaom7GE0teLtxU+tJpvkRIg4P6a3IRkrIEfdhDVV4xCc6/WXqT04xupfE9Gpfq0nb66Do45r2q6Dv5WmmIOolOgYHbipbasaT65PKrCbMsFAuADCF09TkFTM0uGYent5eLq3k6drDxtrZz8bU1QB4o07WBnXquI/RILVhL7zM1l16msXZsuAQEEphmiMRBWABNRgB4o5oJlE8WUlhmU1kilUzj/UqiB7arYZx7+8Z+/+8RqccltdzUWFml6GsS5RxMGpyjkGrOJiCggMSCfKPqP39IpgmsiUyCAllISufb5ZB3Eh8O0QPjMeMRH2j6RER6//2Q==', - }, - { - id: 'b', - name: 'Casey Park', - profilePicUrl: - 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/AABEIABgAGAMBEQACEQEDEQH/xAB7AAADAQAAAAAAAAAAAAAAAAAJCgsIEAABBAIBBAIDAAAAAAAAAAAFAgMEBgEHCAkRExQAFQohIgEAAgMBAAAAAAAAAAAAAAAABQcDBggJEQACAgEDAgUCBgMAAAAAAAABAgMEBQYREhMhAAcUIjEyQQgVFkJhcVGB8P/aAAwDAQACEQMRAD8AdCkPtRY7syS4iPDYQt1+ZIcRHiMNN47rdflPqbjMtoThSluOutoThOcqVjGO/wAbzOqAlmUcQWO522A+T9zt/r+tz4MH/iew7fz8ff7/AB8/Y+F++Yf5KPTs4nbDl6tDkbzyWtwMjLEW9/RKqW9SKqVgoT7oZ7Y15s9Wp9hOwH3GoxQTVCRl8XJy7HnKafjSWWgFvUNWBykQM5U8WdeJjH+dnMkYY/B2UkD93H3BYGnQEKoLse3s2I7/AGBPye/wBt/Pf2lX4Vc1NF8+dEBeQnH0sZIU0iUI1ouLswaRXrXT7gE8P3dSs4h/LiIxUd7DDrb8OVMGE4UiMRGS5EKS26orSvQ3oupETuCQ6H6kI27EjdT2ZTuCVPIEFgQfEoIZVbiwDc+PJSp3jkaGUKG2LdOZGik234SIUcBg4WSzyC6lXOHlMoyzvHk7uzYY6wsYjl6/YNiHYlMkxkvLkuRnddVF2ra3fgqdW6tUSVTJaPDlEZOcstNoSt2sWLLbPNK/LuweWQxqsYZi3TG8ftXdmKwM7BQuxCoiincklmZ234j62I7H2gIeKAktsWAUntyJCrx1VxI6ZN523sPUqbhPDlTl3PVqCJ1f6C5Sy0k74IgiuGibr0cUEix0TWJRRoeOItghcGcqMiVge2w65dSeQet8NoCvrMRQWHf0lm/iKy2JMvi8VcVRHkbA39Ky13ZfXVVWP0EU0Usl14lmbxaPw5aw8mtVec1XS3mZqVNNadp0srlYcjkaMF3Dajtafp3L+Q0qvK1UkiyWVx9d/wBMjqsc1loPy2NcdM1e01Jnp3dPil8CtbzqqCLtFTp5I77WKCYnBKFXGoUVtvIaoVjJF+MpK5aXJJG1FWnrObX4I70yMFgiQ8Cjad0++I69izckuW7RVvcqLFUi6cKmtBxjjklQvGZurY5SdSWTp9OIiNXX51+cWE8xItO6X0LobHeX3lzowZIadw8fQvZy9PlLklu1lc5ljVWdHlMh9NgaM64TGO9ieOtYydzJZK3MA4y8Kyb5WPcd1CS9cjCiUCSGoxWC3DIm8spjEGJ51l/yzB4bz5abQNcixSJRLbvn8EBWEStD+TH4d7uUmi1JrqpexmNq2K01DB3KXpruY6ZisrPcitKLOPxyuqxtXlrwXrg5rJ6aEvE+A9a+ZVaCBsfp2xWuzzRTRz5GvYEtenuXgZIJIj0rFj6iJkkeCElWBdhyU/uh7kQ11e6hsoMxiSU10fBX0fHytLfvu1YxCK4GqdV/TH28eHIFLkpwr1US8v474ZQhW8Bg6+oK2UwdktHBmcRkcQ7hBtWGTqvj4bCqdnb0ck6WhGpKv6fpb+7l4zBls1b0+1HNVW5T4jLUMvGg4A2VxNlclYo82V0j/Mq1eah6goxr+qNhQXRfD+1Rs467Vat3IQlxIq218LZxqXkqQ6mCfHRysZDqVYxlLjbUpKF/rHdSc57YxnGPnLm5Umx9y3QsbGejas0pivderUmevJt/BaIkbcht+7t7ui1OzHeqVbsJJhuVoLUW4IPTsxJMgIO3cKwHcb/14//Z', - }, - { - id: 'd', - name: 'Sandy Hagen', - profilePicUrl: - 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/AABEIABgAGAMBEQACEQEDEQH/xAB2AAEBAAAAAAAAAAAAAAAAAAAKCRAAAgMBAQACAQQDAAAAAAAABAUCAwYBBwgJEgARFBUKExYBAAIDAQAAAAAAAAAAAAAAAAUIBgcJBBEAAgIBAwMEAgEFAAAAAAAAAgMBBAUGBxEAEhMIFCFBIjHwIzJhsdH/2gAMAwEAAhEDEQA/AGH+jekYryfItdxv9KhymbUVc7ez0T5LnAbSreS4GuoY6A9Yt/sGFsf44I9pcJX3d5CHJd7+36qtSmOYK1jJEU8fETPHzxzPHMwP+YiePvjq8bFhVVJucUCAxM/MxElMRz2jzxyXH1/v4jqLvg3+QD8OvkZ87Mr8IPN1WzLN2MH6bP8ArxBmNL87bb1Fmi9hDLLC0WlbnkjHpk70Ch7eAIs/6VXJFWXewneEKXs4N1WodomgRhwUqGJiOySgSmSIRjvgpHiIlncBcjMTE9oChqQMjkVUkVWzDQbImI+Q5JSieZQtfeyUioGExnZHj7PyGR5MbsQsrs7PkJxl2qyVVsYyj2VVsf2/Kq2PJSlVbDnednVZyFkOd52UOflz8gn8/n/fvqTyMxAzMTEGMGM8TEEM/ohmYiCGfoh5Gf1E/HU+fsi+t/xz7OvFMx4h7LpNtkFmP9IWem5nRYOGXLbg6MPP6DJ2VXKtoj0WZaBnItS1E7WxVXWBE20MALaDhqL6u6hkGY5jGgKyE1+M4Mf0EGtnIlEiQTErjmYmOQkgn8SLoRl8WrKIBbDJcqPyicdvEcAYzBQQGMjAnM89slE8TH9vQV8v9bif4V/KP0gbO+npvY1uKcOcblPQ0FhaT+2F4UsFZiu8howhDcZu8No85aq/s8s/bJ2RNVrBEUuG/hLFrOK9O++eWwuKzWN0DfsUMzjKuTreHJYWLM1b9eLFZhobkEs8NyjYS8BPukkv4esC5A5n6WvUN6VNstWZnK7h7u0NI6owr9RaSWDU6hfTy1HLpVjM3LDp4O2jFWce2rcw8urZASyEOyFaxWNC1MJ5P1tq2y74ceTFPS0DZtooabVE6FE8L09ujoe6doUtdabRn9sMc7QhZwSjVlkEGT42Fuo4YTGnls121Zp/MaU1HltOagxtrD5vEWfaZPGXBUNinbFazaqRSxqvHPkg0+NhDKTCefymOprvLuPoPdnX2T11thlTzG3+Xo4QNL2wprxuNGlSxNWpYr4DGqWldDTyb6rcYWr7euxdIgl1dLjMBnf9v/z323nBo/xU8JfmZjXukojr13fpCmIulzmeb/lNHhMVcrq6xE0GqCqJY6XQriQjsznuABLD1zfRjt1Dl+kzYbHalUe52saCMjja11lLSOEupCzRyF+nMRdzeRqHBjcp45srr4+m9JULt2XNt+evj3U7SA7/AO7VrCunRGnrZ1bR0xs6iyNZp1bdSpagxp4yre7lhQsXRB1i5aS4r1SopK64Jfkq1+qdXDMrxdFIHQNm5k3NRVhoJJ9bnTn38rlf26+J15lKqRfbP3YFnsT3B0p9lbK0rvJQ0pqjYTK1VfABBxC0Olqq6AXETBhXqdiq/txGISiIBIQPjgAXHSQZKKzazHmtxqjthltEQZO8xdhLO1cg7N47BSXlsx/VKSJ0mbS7+mpfXxumu++Ivj7B4taq26BOdhi6HVMKGJVOJamZ5W3vhWCsr7x2mDXNKrq14VRECv8AfTT2qcJyxh9TGnKumt69bV6Lqr6WWt1tSVypGZ1lFn6ichbqJJjbBTFK821UKJe7xkqVy0iEoHSjYPPv1DtXpd9tVxVvGpt6fs+/WtVlx4C7YxarjAWpARGQrVk31mKFC1NlbhUoGLAf/9k=', - }, - { - id: 'e', - name: 'Logan Grandmont', - profilePicUrl: - 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/AABEIABgAGAMBEQACEQEDEQH/xAB3AAEBAQAAAAAAAAAAAAAAAAAJCggQAAICAgICAgEFAAAAAAAAAAQFAwYCBwEICRIRExUAChQYIgEAAwEBAQAAAAAAAAAAAAAABggJBAcFEQACAgEDAgcAAgMAAAAAAAACAwEEBQYHERITAAgUFSEiMQkWIyRS/9oADAMBAAIRAxEAPwBydt7e1lofXtk2vuK7V/XeuqiNAVYbbZjcQVa/EsqEAEfjL/UxbFmwJGWqVYUJLFqxKGBXizlTRxZ0Ey+WxuDouyOUsrqVEREm0+f0p6QAAHk2MM5gVrASMymIAZnmPB6xq0hLHHC1j+kXPEzMxEDHHMyUzMRER8z+Rxzz4mu7lfuVdbURkhrvS2hi7jyKXSs7Fe9pqLxRUoefMvoKmrdLkVBWtvPjBhMazfOuUSeCPIURRg7n5ZZKuHam3rTWJa9P1VEER/ms5UXIAiIuBBKOkG8f9G008kUAruyJ9ryH5b7QFNcsLiSkjA/yI5nhY/biB5kiL4iPznmeneXiY8uAXkWwvdIuuvU2r9w0BGqtnANesRbar3qnHsJkhztCM7BAfqWNbexRB2JTLE0XDiOEB4jyeRgSEEWbd7ijrBlrH20V05Cqtbuqq6TS1RlITHQcAxTQKIIgnr5BiyEiEpENVK9NnlbV9pvRLAj7RDREhFkiJDE8rkgg5iSiOoeZiZjwffnT7b9qLhHtvpfq/qBd3OmwhKdPaN6NtNbXt2FgfgxJ7zG01O+raWSnV4WpGZjrMbkawatPzK6wC4pgQ8Q2mQRunqPN3iyGBr4eyvF1DRJ3W4y+w3tX2rU2KtuFRXQlcz2oYMuImA6C7UR1eMOTsPaTaoIKFiwBlpKaRGf1kZWUBI8TLOn6ScycSExBxx4nk8T2j6Zt7uRT6fuvXyOx6qKr1vmvOV0+lTXkIRSaWGvsyZ2eI2A8xtg/iKVEoxIjDKYqctWZDyrNminb5nrm4q9pMu/aN2oP72eV0wrTydL15sXrjr2brVXBLYaqnXoKquO3ftX2+1VqiCZkv9Q5YHYtiNt8tqjcAMTf0NldQ4+ni8vcz1ScRbcWLpKqNhV21UlUWw67ivSVewmbbbJGNTg61kgv66l9LOunUde8g0bpeg65ysmI2BVhrwGRlocKR8pChwn9tbsnD9sNGYcUUvgiZfhoxMsOYRYs+cZCjb+OTSvmyxGoNdak8yun9xIRqTCUsVpTJajzGiUYLBlpfLXTyiQ0liMt7pjreq7OXFmKzGOxmRxeTxmn2OyGRrdWI9wJN0qm3NFVLG6GHCVW0bryySKdHJe4N9QkQWFjJWlHDox5IaNmo6yDqz7ELGtBjYkMF+TPt/ZV1m/rLrd+YjA+lULtd0hayCujzrHDEUv18OSHNgaoXRJCIHdqnFlEPNhYq18E+IP5WPJtt8tw7k5BujcPYKtWrgo85aT0w24x6+4ONW6Jk1VwSS23O30TYJq6xHKRspNrvKRsZgnYbF7s6vpqydzK5eaWiMPbQFilUTTtlSsaluVHqKvdsPyKrNTDCwbFWqGPyVxqZsjSLwbSacUeKGGMeCKPGPgaLCOHDHDEePHniIf4wj5+IsMeOefr598MfbPn049suP0sszJfv5PxPx+xPHPMTPE/EccfWJj4nn9Gp+PalRCQpBbCiB7qxiGEIT1hBNGO+UifLOZMiE+WTPVMkSgeO/tJbzdosutFuMle1nlCya6yOnj9zqlJWRFszalSnY5fJddKXF5MKwORhkSgmXtl+BkykhUvXsxsPr7J+5L0VkmlbxzKjm4VzZiX491QYYeO7ks62UnV5Y2oMpP0M1Wo7spdUQiZXng2UwSQy+7+m6gY7JhqClV1rWr8xVyxZuGDW1F2JnhGUXkFTWzLVGKcj66jZOsF8L1y3//Z', - }, -]; diff --git a/packages/sdk/stories/helpers/example.tsx b/packages/sdk/stories/helpers/example.tsx deleted file mode 100644 index 864622f5f..000000000 --- a/packages/sdk/stories/helpers/example.tsx +++ /dev/null @@ -1,222 +0,0 @@ -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 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'; - -injectGlobal(` - html { - box-sizing: border-box; - } - *, *:before, *:after { - box-sizing: inherit; - } -`); - -interface SelectOption { - type: 'select'; - label: string; - options: Array; - defaultValue?: string | number; - renderLabel?: (label: string) => string; -} - -interface SelectButtonsOption { - type: 'selectButtons'; - label: string; - options: Array; - defaultValue?: string | number; - renderLabel?: (label: string) => string; -} - -interface SwitchOption { - type: 'switch'; - label: string; - defaultValue?: boolean; -} - -type ArrayType> = T extends Array ? U : never; - -type OptionType = T extends SelectOption - ? ArrayType - : T extends SwitchOption - ? boolean - : T extends SelectButtonsOption - ? ArrayType - : never; - -type Option = SelectOption | SwitchOption | SelectButtonsOption; -interface OptionMap { - [key: string]: Option; -} -type OptionMapType = {[K in keyof T]: OptionType}; - -interface Props { - options?: T; - styleProps?: Array; - children: (values: OptionMapType) => React.ReactNode; - renderCodeFn?: (values: OptionMapType) => string; - containerPadding?: number; -} - -export default function Example(props: Props) { - const {options} = props; - const defaultValues: any = {}; - - if (options) { - for (const optionKey of Object.keys(options)) { - const option = options[optionKey]; - switch (option.type) { - case 'select': - case 'selectButtons': - if (option.defaultValue) { - defaultValues[optionKey] = option.defaultValue; - } else if (option.options.includes('default')) { - defaultValues[optionKey] = 'default'; - } else { - defaultValues[optionKey] = option.options[0]; - } - break; - case 'switch': - defaultValues[optionKey] = option.defaultValue ?? true; - break; - default: - throw spawnUnknownSwitchCaseError('option.type', option, 'type'); - } - } - } - - const [values, setValues] = useState(defaultValues); - - const children = props.children(values); - - function _setValue(key: string, value: any) { - console.log(); - setValues({...values, [key]: value}); - } - - return ( - - - {children} - - - - - Props - - {options ? ( - Object.keys(options).map(optionKey => { - const option = options[optionKey]; - switch (option.type) { - case 'select': - case 'selectButtons': { - const optionsType = typeof option.options[0]; - const renderLabel = option.renderLabel ?? capitalize; - const sharedProps = { - size: 'small' as const, - value: String(values[optionKey]), - options: option.options.map(String).map(value => ({ - label: renderLabel(value), - value, - })), - onChange: (newValue: SelectOptionValue) => { - switch (optionsType) { - case 'number': - _setValue(optionKey, Number(newValue)); - break; - case 'string': - _setValue(optionKey, newValue); - break; - } - }, - }; - switch (option.type) { - case 'select': - return ( - - setSearchQuery(e.target.value)} - placeholder="Search for icons..." - /> - -
    - {filteredIconNamesArray.length > 0 ? ( - - {filteredIconNamesArray.map(iconName => { - return ( - setCheckedIconName(iconName)} - /> - ); - })} - - ) : ( - - No results - - )} -
    -
    - ); - }} - - ); -} diff --git a/packages/sdk/stories/input.stories.tsx b/packages/sdk/stories/input.stories.tsx deleted file mode 100644 index 763f716e7..000000000 --- a/packages/sdk/stories/input.stories.tsx +++ /dev/null @@ -1,208 +0,0 @@ -/* 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 Example from './helpers/example'; -import {createJsxPropsStringFromValuesMap, CONTROL_WIDTH} from './helpers/code_utils'; - -const stories = storiesOf('Input', module); - -const sharedExampleProps = { - options: { - size: { - type: 'selectButtons', - label: 'Size', - options: keys(theme.inputSizes), - }, - disabled: { - type: 'switch', - label: 'Disabled', - defaultValue: false, - }, - }, - styleProps: Object.keys(inputStylePropTypes), -} as const; - -function InputExample() { - const [value, setValue] = useState(''); - return ( - { - const props = createJsxPropsStringFromValuesMap(values); - - return ` - import React, {useState} from 'react'; - import {Input} from '@airtable/blocks/ui'; - - const InputExample = () => { - const [value, setValue] = useState(''); - - return ( - setValue(e.target.value)} - placeholder="Placeholder" - ${props} - width="${CONTROL_WIDTH}" - /> - ); - }; - `; - }} - > - {values => { - return ( - setValue(e.target.value)} - placeholder="Placeholder" - {...values} - width={CONTROL_WIDTH} - /> - ); - }} - - ); -} - -stories.add('example', () => ); - -function InputSyncedExample() { - const [value, setValue] = useState(''); - return ( - { - const props = createJsxPropsStringFromValuesMap(values); - - return ` - import React, {useState} from 'react'; - import {InputSynced} from '@airtable/blocks/ui'; - - const InputSyncedExample = () => { - return ( - - ); - }; - `; - }} - > - {values => { - return ( - setValue(e.target.value)} - placeholder="Placeholder" - {...values} - width={CONTROL_WIDTH} - /> - ); - }} - - ); -} - -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)} - /> - - ); - }), -); diff --git a/packages/sdk/stories/label.stories.tsx b/packages/sdk/stories/label.stories.tsx deleted file mode 100644 index ec5a6382b..000000000 --- a/packages/sdk/stories/label.stories.tsx +++ /dev/null @@ -1,154 +0,0 @@ -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 Example from './helpers/example'; -import {createJsxPropsStringFromValuesMap, CONTROL_WIDTH} from './helpers/code_utils'; - -const stories = storiesOf('Label', module); - -function LabelExample() { - const [value, setValue] = React.useState(''); - return ( - { - const props = createJsxPropsStringFromValuesMap(values); - return ` - import {Label, Box, Input} from '@airtable/blocks/ui'; - - // You might want to consider using \`FormField\` instead. - const LabelExample = () => { - const [value, setValue] = React.useState(''); - - return ( - - - setValue(e.target.value)} /> - - ); - }; - `; - }} - > - {values => ( - - - setValue(e.target.value)} /> - - )} - - ); -} - -stories.add('example', () => ); - -stories.add('with input', () => ( - - - - {}} value="" /> - - -)); - -stories.add('with select', () => ( - - - - setValue(newValue as string)} - {...values} - width={CONTROL_WIDTH} - /> - ); - }} - - ); -} - -stories.add('ViewPicker example', () => ); - -function ViewPickerSyncedExample() { - const [value, setValue] = useState(null); - return ( - { - const props = createJsxPropsStringFromValuesMap(values); - - return ` - import React from 'react'; - import {useBase, ViewPickerSynced} from '@airtable/blocks/ui'; - - const ViewPickerSyncedExample = () => { - const base = useBase(); - const table = base.getTableByNameIfExists('Tasks'); - // If table is null or undefined, the ViewPickerSynced will not render. - - return - }; - `; - }} - > - {values => { - const placeholder = values.shouldAllowPickingNone ? 'None' : 'Pick a view...'; - - return ( - setValue(newValue as string)} - {...values} - width={CONTROL_WIDTH} - /> - ); - }} - - ); -} - -stories.add('TablePicker example', () => ); - -function TablePickerSyncedExample() { - const [value, setValue] = useState(null); - return ( - { - const props = createJsxPropsStringFromValuesMap(values); - - return ` - import React from 'react'; - import {TablePickerSynced} from '@airtable/blocks/ui'; - - const TablePickerSyncedExample = () => ( - - ); - `; - }} - > - {values => { - const placeholder = values.shouldAllowPickingNone ? 'None' : 'Pick a table...'; - - return ( - setValue(newValue as string)} - {...values} - width={CONTROL_WIDTH} - /> - ); - }} - - ); -} - -stories.add('FieldPicker example', () => ); - -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(newValue)} ${props} width="${CONTROL_WIDTH}"/> - }; - `; - }} - > - {values => { - return ( - setValue(newValue as string)} - {...values} - width={CONTROL_WIDTH} - /> - ); - }} - - ); -} - -stories.add('synced example', () => ); - -stories.add('sizes', () => ( - - - - - - - -)); - -stories.add('disabled', () => ( - - - - -)); - -stories.add('ref', () => ( - - -
    -
    -)); - -stories.add('with tooltip', () => ( - - - 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 deleted file mode 100644 index 45b1411cd..000000000 --- a/packages/sdk/stories/switch.stories.tsx +++ /dev/null @@ -1,354 +0,0 @@ -/* 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 Example from './helpers/example'; -import {createJsxPropsStringFromValuesMap, CONTROL_WIDTH} from './helpers/code_utils'; - -const stories = storiesOf('Switch', module); - -const sharedExampleProps = { - options: { - variant: { - type: 'selectButtons', - label: 'Variant', - options: keys(theme.switchVariants), - }, - size: { - type: 'selectButtons', - label: 'Size', - options: keys(theme.switchSizes), - }, - disabled: { - type: 'switch', - label: 'Disabled', - defaultValue: false, - }, - backgroundColor: { - type: 'switch', - label: 'Hide background', - defaultValue: false, - }, - }, - styleProps: Object.keys(switchStylePropTypes), -} as const; - -function SwitchExample() { - const [isEnabled, setIsEnabled] = useState(true); - return ( - { - const props = createJsxPropsStringFromValuesMap(values, { - backgroundColor: value => (value ? 'transparent' : null), - }); - - return ` - import React, {useState} from 'react'; - import {Switch} from '@airtable/blocks/ui'; - - const SwitchExample = () => { - const [isEnabled, setIsEnabled] = useState(true); - - return ( - setIsEnabled(newValue)} - label="Is switch enabled" - ${props} - width="${CONTROL_WIDTH}" - /> - ); - }; - `; - }} - > - {({backgroundColor, ...values}) => { - return ( - setIsEnabled(newValue)} - label="Is switch enabled" - {...values} - backgroundColor={backgroundColor ? 'transparent' : undefined} - width={CONTROL_WIDTH} - /> - ); - }} - - ); -} - -stories.add('example', () => ); - -function SwitchSyncedExample() { - const [isEnabled, setIsEnabled] = useState(true); - return ( - { - const props = createJsxPropsStringFromValuesMap(values, { - backgroundColor: value => (value ? 'transparent' : null), - }); - - return ` - import React, {useState} from 'react'; - import {SwitchSynced} from '@airtable/blocks/ui'; - - const SwitchSyncedExample = () => { - return ( - - ); - }; - `; - }} - > - {({backgroundColor, ...values}) => { - return ( - setIsEnabled(newValue)} - label="Is switch enabled" - backgroundColor={backgroundColor ? 'transparent' : undefined} - {...values} - width={CONTROL_WIDTH} - /> - ); - }} - - ); -} - -stories.add('example synced', () => ); - -stories.add('sizes', () => - React.createElement(() => { - const [isChecked, setIsChecked] = useState(true); - return ( - - - - - - ); - }), -); - -stories.add('variants', () => - React.createElement(() => { - const [isChecked, setIsChecked] = useState(true); - return ( - - - - - ); - }), -); - -stories.add('disabled', () => - React.createElement(() => { - const [isChecked, setIsChecked] = useState(true); - return ( - - - - ); - }), -); - -stories.add('override backgroundColor', () => - React.createElement(() => { - const [isChecked, setIsChecked] = useState(true); - return ( - - - - ); - }), -); - -stories.add('override width', () => - 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" - /> - - ); - }), -); - -stories.add('responsive size', () => - React.createElement(() => { - const [isChecked, setIsChecked] = useState(true); - return ( - - - - ); - }), -); - -stories.add('custom classname', () => - React.createElement(() => { - const [isChecked, setIsChecked] = useState(true); - return ( - - - - ); - }), -); - -stories.add('id attribute', () => - React.createElement(() => { - const [isChecked, setIsChecked] = useState(true); - return ( - - - - ); - }), -); - -stories.add('style attribute', () => - 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 - - - ); - }), -); - -stories.add('truncate', () => - React.createElement(() => { - const [isChecked, setIsChecked] = useState(true); - return ( - - - - ); - }), -); diff --git a/packages/sdk/stories/text.size.stories.tsx b/packages/sdk/stories/text.size.stories.tsx deleted file mode 100644 index 7a729defc..000000000 --- a/packages/sdk/stories/text.size.stories.tsx +++ /dev/null @@ -1,55 +0,0 @@ -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'; - -const stories = storiesOf('Text/size', module); - -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. - - - ))} - -)); - -stories.add('responsive size', () => ( - - 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 deleted file mode 100644 index fced8178e..000000000 --- a/packages/sdk/stories/text.stories.tsx +++ /dev/null @@ -1,176 +0,0 @@ -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 Example from './helpers/example'; -import {createJsxPropsStringFromValuesMap} from './helpers/code_utils'; - -const stories = storiesOf('Text', module); - -const childrenForVariant = { - default: 'The brown fox jumped over the lazy dog', - paragraph: - 'Consectetur accusamus accusamus repudiandae est eveniet. Doloribus eligendi et recusandae voluptatem recusandae. Nostrum vitae minus atque blanditiis aliquid voluptate sint.', -} as const; - -function TextExample() { - return ( - { - const props = createJsxPropsStringFromValuesMap({ - ...values, - maxWidth: values.variant === 'paragraph' ? '30em' : null, - }); - - return ` - import {Text} from '@airtable/blocks/ui'; - - const textExample = ( - ${childrenForVariant[values.variant]} - ); - `; - }} - > - {values => ( - - {childrenForVariant[values.variant]} - - )} - - ); -} - -stories.add('example', () => ); - -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} - - ))} - -)); - -stories.add('textColor', () => ( - - (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 - - -)); - -stories.add('custom className', () => ( - - Inspect element to see class name - -)); - -stories.add('id attribute', () => ( - - Inspect element to see id - -)); - -stories.add('style attribute', () => ( - - - Inspect element to see style attribute - - -)); - -stories.add('data attributes', () => ( - - - Inspect element to see data attributes - - -)); - -stories.add('role attribute', () => ( - - Inspect element to see role attribute - -)); - -stories.add('aria attributes', () => ( - - - Inspect element to see aria attributes - - -)); diff --git a/packages/sdk/stories/text_button.stories.tsx b/packages/sdk/stories/text_button.stories.tsx deleted file mode 100644 index 3fcc97df7..000000000 --- a/packages/sdk/stories/text_button.stories.tsx +++ /dev/null @@ -1,229 +0,0 @@ -/* 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 {createJsxPropsStringFromValuesMap, createJsxComponentString} from './helpers/code_utils'; - -const stories = storiesOf('TextButton', module); - -function TextButtonExample() { - const {textButtonVariants, textStyles} = useTheme(); - return ( - { - const props = createJsxPropsStringFromValuesMap(restOfValues as any, { - icon: value => (value ? 'edit' : null), - }); - - const ariaLabel = hasLabel ? '' : 'aria-label="Edit"'; - const ariaLabelComment = hasLabel - ? '' - : '// Make sure to add an "aria-label" prop when only using an icon.'; - - const children = hasLabel ? 'Text button' : null; - - const buttonComponentString = createJsxComponentString( - 'TextButton', - ["onClick={() => console.log('Button clicked')}", props, ariaLabel], - children, - ); - - return ` - import {TextButton} from '@airtable/blocks/ui'; - - ${ariaLabelComment} - const buttonExample = ( - ${buttonComponentString} - ); - `; - }} - > - {({icon, hasLabel, ...restOfValues}) => ( - console.log('Button clicked')} - {...restOfValues} - icon={icon ? 'edit' : undefined} - aria-label={hasLabel ? undefined : 'Edit'} - > - {hasLabel ? 'Text button' : null} - - )} - - ); -} - -stories.add('example', () => ); - -stories.add('variants', () => ( - - - Default - - - Dark - - - Light - - -)); - -stories.add('sizes', () => ( - - - Small - - - Default - - - Large - - - Xlarge - - -)); - -stories.add('inside of text', () => ( - - - Some text with a{' '} - - text button - - - -)); - -stories.add('with icon', () => ( - - - Import - - - Export - - -)); - -stories.add('with all icons', () => ( - - {values(iconNames).map(iconName => ( - - {}}> - {iconName.substr(0, 1).toUpperCase()} - {iconName.substr(1)} - - - ))} - -)); - -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 - - - -)); - -stories.add('custom icon', () => ( - - - - - - } - size="large" - paddingX={1} - marginX={-1} - > - Custom icon - - - -)); - -stories.add('with tooltip', () => ( - - Text button with tooltip - -)); diff --git a/packages/sdk/stories/tooltip.stories.tsx b/packages/sdk/stories/tooltip.stories.tsx deleted file mode 100644 index 5b714be08..000000000 --- a/packages/sdk/stories/tooltip.stories.tsx +++ /dev/null @@ -1,71 +0,0 @@ -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 {createJsxPropsStringFromValuesMap} from './helpers/code_utils'; -import {injectGlobal} from 'emotion'; - -const stories = storiesOf('Tooltip', module); - -injectGlobal` - @keyframes opacityFadeIn { - from { opacity: 0.0; } - to { opacity: 1.0; } - } -`; - -function TextExample() { - return ( - { - const props = createJsxPropsStringFromValuesMap(restOfValues as any); - const placementXProp = `placementX={Tooltip.placements.${placementX.toUpperCase()}}`; - const placementYProp = `placementY={Tooltip.placements.${placementY.toUpperCase()}}`; - return ` - import {Tooltip, Button} from '@airtable/blocks/ui'; - - const tooltipExample = ( - -
    }> , - el, ); }); + return await promise; }; describe('expandRecord', () => { diff --git a/packages/sdk/test/ui/expand_record_list.test.tsx b/packages/sdk/test/base/ui/expand_record_list.test.tsx similarity index 72% rename from packages/sdk/test/ui/expand_record_list.test.tsx rename to packages/sdk/test/base/ui/expand_record_list.test.tsx index 3c49ba898..9820d34c3 100644 --- a/packages/sdk/test/ui/expand_record_list.test.tsx +++ b/packages/sdk/test/base/ui/expand_record_list.test.tsx @@ -1,37 +1,43 @@ import React from 'react'; -import ReactDOM from 'react-dom'; +import {act, render} from '@testing-library/react'; 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'; - -let mockAirtableInterface: jest.Mocked; -jest.mock('../../src/injected/airtable_interface', () => ({ - __esModule: true, - default() { - if (!mockAirtableInterface) { - mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); - } - return mockAirtableInterface; - }, -})); +import {expandRecordList, useBase, useRecords} from '../../../src/base/ui/ui'; +import {SdkContext} from '../../../src/shared/ui/sdk_context'; +import {__sdk as sdk} from '../../../src/base'; +import getAirtableInterface from '../../../src/injected/airtable_interface'; +import {createPromiseWithResolveAndReject} from '../../test_helpers'; + +jest.mock('../../../src/injected/airtable_interface', () => { + let mockAirtableInterface: jest.Mocked; + return { + __esModule: true, + default() { + if (!mockAirtableInterface) { + mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); + } + return mockAirtableInterface; + }, + }; +}); + +const mockAirtableInterface = getAirtableInterface() as jest.Mocked; const TestProvider = ({children}: {children: React.ReactNode}) => { return {children}; }; -const renderAsync = async (TestBody: Function) => { - const el = document.createElement('div'); - return new Promise(resolve => { - ReactDOM.render( +const renderAsync = async (TestBody: React.FunctionComponent): Promise => { + const {promise, resolve} = createPromiseWithResolveAndReject(); + await act(async () => { + render( }> , - el, ); }); + return await promise; }; describe('expandRecordList', () => { diff --git a/packages/sdk/test/ui/expand_record_picker_async.test.tsx b/packages/sdk/test/base/ui/expand_record_picker_async.test.tsx similarity index 76% rename from packages/sdk/test/ui/expand_record_picker_async.test.tsx rename to packages/sdk/test/base/ui/expand_record_picker_async.test.tsx index de6e7fc38..001bf7848 100644 --- a/packages/sdk/test/ui/expand_record_picker_async.test.tsx +++ b/packages/sdk/test/base/ui/expand_record_picker_async.test.tsx @@ -1,21 +1,27 @@ import React from 'react'; -import ReactDOM from 'react-dom'; +import {act, render} from '@testing-library/react'; 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'; - -let mockAirtableInterface: jest.Mocked; -jest.mock('../../src/injected/airtable_interface', () => ({ - __esModule: true, - default() { - if (!mockAirtableInterface) { - mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); - } - return mockAirtableInterface; - }, -})); +import {expandRecordPickerAsync, useBase, useRecords} from '../../../src/base/ui/ui'; +import {type Record} from '../../../src/base/models/models'; +import {SdkContext} from '../../../src/shared/ui/sdk_context'; +import {__sdk as sdk} from '../../../src/base'; +import getAirtableInterface from '../../../src/injected/airtable_interface'; +import {createPromiseWithResolveAndReject} from '../../test_helpers'; + +jest.mock('../../../src/injected/airtable_interface', () => { + let mockAirtableInterface: jest.Mocked; + return { + __esModule: true, + default() { + if (!mockAirtableInterface) { + mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); + } + return mockAirtableInterface; + }, + }; +}); + +const mockAirtableInterface = getAirtableInterface() as jest.Mocked; const TestProvider = ({children}: {children: React.ReactNode}) => { return {children}; @@ -26,18 +32,18 @@ interface TestBodyParams { reject: () => null; } -const renderAsync = async (TestBody: Function): Promise => { - const el = document.createElement('div'); - return new Promise((resolve, reject) => { - ReactDOM.render( +const renderAsync = async (TestBody: React.FunctionComponent): Promise => { + const {promise, resolve} = createPromiseWithResolveAndReject(); + await act(async () => { + render( }> - + , - el, ); }); + return await promise; }; describe('expandRecordPickerAsync', () => { diff --git a/packages/sdk/test/base/ui/global_alert.test.tsx b/packages/sdk/test/base/ui/global_alert.test.tsx new file mode 100644 index 000000000..ed64325e1 --- /dev/null +++ b/packages/sdk/test/base/ui/global_alert.test.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import {fireEvent, render} from '@testing-library/react'; +import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; +import {GlobalAlert} from '../../../src/base/ui/ui'; +import {SdkContext} from '../../../src/shared/ui/sdk_context'; +import {__sdk as sdk} from '../../../src/base'; +import getAirtableInterface from '../../../src/injected/airtable_interface'; + +jest.mock('../../../src/injected/airtable_interface', () => { + let mockAirtableInterface: jest.Mocked; + return { + __esModule: true, + default() { + if (!mockAirtableInterface) { + mockAirtableInterface = MockAirtableInterface.linkedRecordsExample(); + } + return mockAirtableInterface; + }, + }; +}); + +const mockAirtableInterface = getAirtableInterface() as jest.Mocked; + +const TestProvider = ({children}: {children: React.ReactNode}) => { + return {children}; +}; + +describe('GlobalAlert', () => { + it('renders within a Blocks Sdk context', () => { + const globalAlert = new GlobalAlert(); + globalAlert.showReloadPrompt(); + render({globalAlert.__alertInfo!.content}); + }); + + it('sends a "reload" request when clicked', () => { + const globalAlert = new GlobalAlert(); + globalAlert.showReloadPrompt(); + const {container} = render({globalAlert.__alertInfo!.content}); + mockAirtableInterface.reloadFrame.mockImplementation(() => {}); + + const clickableElement = container.querySelector('span span'); + fireEvent.click(clickableElement!); + + expect(mockAirtableInterface.reloadFrame).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/sdk/test/base/ui/initialize_block.test.tsx b/packages/sdk/test/base/ui/initialize_block.test.tsx new file mode 100644 index 000000000..435b62d64 --- /dev/null +++ b/packages/sdk/test/base/ui/initialize_block.test.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import {act} from '@testing-library/react'; +import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; +import {initializeBlock} from '../../../src/base/ui/ui'; +import {__resetHasBeenInitialized} from '../../../src/base/ui/initialize_block'; + +jest.mock('../../../src/injected/airtable_interface', () => { + let mockAirtableInterface: jest.Mocked; + return { + __esModule: true, + default() { + if (!mockAirtableInterface) { + mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); + } + return mockAirtableInterface; + }, + }; +}); + +describe('initializeBlock', () => { + afterEach(() => { + __resetHasBeenInitialized(); + }); + it("functions without explicitly importing the SDK's main entry point", () => { + act(() => { + initializeBlock(() =>
    ); + }); + }); + it('functions as a view and a block through one entry point', () => { + act(() => { + initializeBlock({dashboard: () =>
    , view: () =>
    }); + }); + }); +}); diff --git a/packages/sdk/test/ui/remote_utils.test.ts b/packages/sdk/test/base/ui/remote_utils.test.ts similarity index 89% rename from packages/sdk/test/ui/remote_utils.test.ts rename to packages/sdk/test/base/ui/remote_utils.test.ts index 1f0634bb9..064ec7aba 100644 --- a/packages/sdk/test/ui/remote_utils.test.ts +++ b/packages/sdk/test/base/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/base/ui/ui.test.tsx b/packages/sdk/test/base/ui/ui.test.tsx new file mode 100644 index 000000000..848ad60ce --- /dev/null +++ b/packages/sdk/test/base/ui/ui.test.tsx @@ -0,0 +1,24 @@ +import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; + +jest.mock('../../../src/injected/airtable_interface', () => ({ + __esModule: true, + default() { + return MockAirtableInterface.projectTrackerExample(); + }, +})); + +const run = (bindingIdentifier: string) => { + const exported = require('../../../src/base/ui/ui')[bindingIdentifier]; + + expect(exported).toBeTruthy(); + + expect(exported).toBe( + require('../../../src/base/ui/unstable_standalone_ui')[bindingIdentifier], + ); +}; + +describe('ui entry point', () => { + test('loadCSSFromString', () => run('loadCSSFromString')); + test('loadCSSFromURLAsync', () => run('loadCSSFromURLAsync')); + test('loadScriptFromURLAsync', () => run('loadScriptFromURLAsync')); +}); diff --git a/packages/sdk/test/ui/unstable_standalone_ui.test.tsx b/packages/sdk/test/base/ui/unstable_standalone_ui.test.tsx similarity index 64% rename from packages/sdk/test/ui/unstable_standalone_ui.test.tsx rename to packages/sdk/test/base/ui/unstable_standalone_ui.test.tsx index a2b3d8613..84b9a5ec7 100644 --- a/packages/sdk/test/ui/unstable_standalone_ui.test.tsx +++ b/packages/sdk/test/base/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/base/ui/use_array_identity.test.tsx similarity index 74% rename from packages/sdk/test/ui/use_array_identity.test.tsx rename to packages/sdk/test/base/ui/use_array_identity.test.tsx index 4b21daa86..2dc7dfdb6 100644 --- a/packages/sdk/test/ui/use_array_identity.test.tsx +++ b/packages/sdk/test/base/ui/use_array_identity.test.tsx @@ -1,7 +1,6 @@ import React from 'react'; -import {mount} from 'enzyme'; -import {act} from 'react-dom/test-utils'; -import useArrayIdentity from '../../src/ui/use_array_identity'; +import {render} from '@testing-library/react'; +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 () => { @@ -14,14 +13,12 @@ describe('useArrayIdentity', () => { }; const startingArray = [1, 2, 3]; - const wrapper = mount(); + const {rerender} = render(); expect(inputArray).toBe(startingArray); expect(outputArray).toBe(startingArray); const nextArray = [1, 2, 3]; - act(() => { - wrapper.setProps({array: nextArray}); - }); + rerender(); expect(inputArray).toBe(nextArray); expect(outputArray).toBe(startingArray); }); @@ -36,14 +33,12 @@ describe('useArrayIdentity', () => { }; const startingArray = [1, 2, 3]; - const wrapper = mount(); + const {rerender} = render(); expect(inputArray).toBe(startingArray); expect(outputArray).toBe(startingArray); const nextArray = [3, 2, 1]; - act(() => { - wrapper.setProps({array: nextArray}); - }); + rerender(); expect(inputArray).toBe(nextArray); expect(outputArray).toBe(nextArray); }); diff --git a/packages/sdk/test/base/ui/use_color_scheme.test.tsx b/packages/sdk/test/base/ui/use_color_scheme.test.tsx new file mode 100644 index 000000000..01c5da36d --- /dev/null +++ b/packages/sdk/test/base/ui/use_color_scheme.test.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import {render, act} from '@testing-library/react'; +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 {getByText} = render(); + expect(getByText('light')).toBeInTheDocument(); + }); + + 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 {getByText} = render(); + expect(getByText('dark')).toBeInTheDocument(); + }); + + 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 {getByText} = render(); + expect(getByText('dark')).toBeInTheDocument(); + + 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(getByText('light')).toBeInTheDocument(); + }); +}); diff --git a/packages/sdk/test/ui/use_loadable.test.tsx b/packages/sdk/test/base/ui/use_loadable.test.tsx similarity index 72% rename from packages/sdk/test/ui/use_loadable.test.tsx rename to packages/sdk/test/base/ui/use_loadable.test.tsx index a4c0fa5fa..dc8dfc068 100644 --- a/packages/sdk/test/ui/use_loadable.test.tsx +++ b/packages/sdk/test/base/ui/use_loadable.test.tsx @@ -1,16 +1,14 @@ 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 {act, render} from '@testing-library/react'; +import AbstractModelWithAsyncData from '../../../src/base/models/abstract_model_with_async_data'; +import useLoadable from '../../../src/base/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(); async function tickAsync() { - await new Promise(resolve => process.nextTick(resolve)); + await new Promise((resolve) => process.nextTick(resolve)); jest.advanceTimersByTime(0); } @@ -40,7 +38,7 @@ class Thing extends AbstractModelWithAsyncData<{name: string}, 'name' | 'isDataL } async _loadDataAsync(): Promise> { - return new Promise(resolve => { + return new Promise((resolve) => { this._resolve = resolve; }); } @@ -83,17 +81,17 @@ describe('useLoadable', () => { it('without suspense, it loads the passed model on mount and re-renders, then unloads on unmount', async () => { const thing = new Thing(sdk); - const wrapper = mount(); - expect(wrapper.find('span').text()).toBe('not loaded, no name'); + const {getByText, unmount} = render(); + expect(getByText('not loaded, no name')).toBeInTheDocument(); await act(async () => { thing.resolveLoading('foo'); await tickAsync(); }); - expect(wrapper.find('span').text()).toBe('loaded, foo'); + expect(getByText('loaded, foo')).toBeInTheDocument(); act(() => { - wrapper.unmount(); + unmount(); }); jest.runAllTimers(); expect(thing.isDataLoaded).toBe(false); @@ -107,15 +105,15 @@ describe('useLoadable', () => { await tickAsync(); - const wrapper = mount(); - expect(wrapper.find('span').text()).toBe('loaded, foo'); + const {getByText, unmount} = render(); + expect(getByText('loaded, foo')).toBeInTheDocument(); thing.unloadData(); jest.runAllTimers(); expect(thing.isDataLoaded).toBe(true); act(() => { - wrapper.unmount(); + unmount(); }); jest.runAllTimers(); expect(thing.isDataLoaded).toBe(false); @@ -124,14 +122,18 @@ describe('useLoadable', () => { it('with suspense, suspend until loaded then render then unload when unmounted', async () => { const thing = new Thing(sdk); - const el = document.createElement('div'); - act(() => { - ReactDOM.render( - suspended}> - - , - el, - ); + const {container: el, unmount} = await new Promise<{ + container: HTMLElement; + unmount: () => void; + }>((resolve) => { + act(() => { + const {container, unmount} = render( + suspended}> + + , + ); + resolve({container, unmount}); + }); }); expect(el.textContent).toBe('suspended'); @@ -142,9 +144,8 @@ describe('useLoadable', () => { }); expect(el.textContent).toBe('loaded, foo'); - act(() => { - ReactDOM.unmountComponentAtNode(el); - }); + unmount(); + jest.runAllTimers(); expect(thing.isDataLoaded).toBe(false); }); @@ -163,8 +164,9 @@ describe('useLoadable', () => { const status = things .map( (thing, i) => - `${i}: ${thing.isDataLoaded ? 'loaded' : 'not loaded'}, ${thing.name || - 'no name'}`, + `${i}: ${thing.isDataLoaded ? 'loaded' : 'not loaded'}, ${ + thing.name || 'no name' + }`, ) .join('; '); @@ -174,25 +176,25 @@ describe('useLoadable', () => { it('without suspense, it loads the passed models on mount, re-renders when any of their load-states change, then unloads on unmount', async () => { const thing1 = new Thing(sdk); const thing2 = new Thing(sdk); - const wrapper = mount(); - expect(wrapper.find('span').text()).toBe( - '0: not loaded, no name; 1: not loaded, no name', + const {getByText, unmount} = render( + , ); + expect(getByText('0: not loaded, no name; 1: not loaded, no name')).toBeInTheDocument(); await act(async () => { thing1.resolveLoading('one'); await tickAsync(); }); - expect(wrapper.find('span').text()).toBe('0: loaded, one; 1: not loaded, no name'); + expect(getByText('0: loaded, one; 1: not loaded, no name')).toBeInTheDocument(); await act(async () => { thing2.resolveLoading('two'); await tickAsync(); }); - expect(wrapper.find('span').text()).toBe('0: loaded, one; 1: loaded, two'); + expect(getByText('0: loaded, one; 1: loaded, two')).toBeInTheDocument(); act(() => { - wrapper.unmount(); + unmount(); }); jest.runAllTimers(); expect(thing1.isDataLoaded).toBe(false); @@ -203,31 +205,32 @@ describe('useLoadable', () => { const thing1 = new Thing(sdk); const thing2 = new Thing(sdk); const thing3 = new Thing(sdk); - const wrapper = mount(); - wrapper.mount(); - expect(wrapper.find('span').text()).toBe('0: not loaded, no name'); + const {getByText, rerender, unmount} = render( + , + ); + expect(getByText('0: not loaded, no name')).toBeInTheDocument(); await act(async () => { thing1.resolveLoading('one'); await tickAsync(); }); - expect(wrapper.find('span').text()).toBe('0: loaded, one'); + expect(getByText('0: loaded, one')).toBeInTheDocument(); act(() => { - wrapper.setProps({things: [thing1, thing2]}); + rerender(); }); - expect(wrapper.find('span').text()).toBe('0: loaded, one; 1: not loaded, no name'); + expect(getByText('0: loaded, one; 1: not loaded, no name')).toBeInTheDocument(); await act(async () => { thing2.resolveLoading('two'); await tickAsync(); }); - expect(wrapper.find('span').text()).toBe('0: loaded, one; 1: loaded, two'); + expect(getByText('0: loaded, one; 1: loaded, two')).toBeInTheDocument(); act(() => { - wrapper.setProps({things: [thing3, thing2]}); + rerender(); }); - expect(wrapper.find('span').text()).toBe('0: not loaded, no name; 1: loaded, two'); + expect(getByText('0: not loaded, no name; 1: loaded, two')).toBeInTheDocument(); jest.runAllTimers(); expect(thing1.isDataLoaded).toBe(false); @@ -236,10 +239,10 @@ describe('useLoadable', () => { thing3.resolveLoading('three'); await tickAsync(); }); - expect(wrapper.find('span').text()).toBe('0: loaded, three; 1: loaded, two'); + expect(getByText('0: loaded, three; 1: loaded, two')).toBeInTheDocument(); act(() => { - wrapper.unmount(); + unmount(); }); jest.runAllTimers(); expect(thing2.isDataLoaded).toBe(false); @@ -258,8 +261,8 @@ describe('useLoadable', () => { await tickAsync(); - const wrapper = mount(); - expect(wrapper.find('span').text()).toBe('0: loaded, one; 1: loaded, two'); + const {getByText, unmount} = render(); + expect(getByText('0: loaded, one; 1: loaded, two')).toBeInTheDocument(); thing1.unloadData(); thing2.unloadData(); @@ -268,7 +271,7 @@ describe('useLoadable', () => { expect(thing2.isDataLoaded).toBe(true); act(() => { - wrapper.unmount(); + unmount(); }); jest.runAllTimers(); expect(thing1.isDataLoaded).toBe(false); @@ -301,20 +304,21 @@ describe('useLoadable', () => { const thing2 = new Thing(sdk); const thing3 = new Thing(sdk); - const el = document.createElement('div'); let wrapper: any; - await act(() => { - return new Promise(resolve => { - ReactDOM.render( + const {container: el, unmount} = await new Promise<{ + container: HTMLElement; + unmount: () => void; + }>((resolve) => { + act(() => { + const {container, unmount} = render( { + ref={(wrapperRef) => { wrapper = wrapperRef; - resolve(); }} />, - el, ); + resolve({container, unmount}); }); }); @@ -349,9 +353,8 @@ describe('useLoadable', () => { jest.runAllTimers(); expect(thing1.isDataLoaded).toBe(false); - act(() => { - ReactDOM.unmountComponentAtNode(el); - }); + unmount(); + jest.runAllTimers(); expect(thing2.isDataLoaded).toBe(false); expect(thing3.isDataLoaded).toBe(false); diff --git a/packages/sdk/test/ui/use_record_action_data.test.tsx b/packages/sdk/test/base/ui/use_record_action_data.test.tsx similarity index 54% rename from packages/sdk/test/ui/use_record_action_data.test.tsx rename to packages/sdk/test/base/ui/use_record_action_data.test.tsx index 387ffaa31..ff2f4edb9 100644 --- a/packages/sdk/test/ui/use_record_action_data.test.tsx +++ b/packages/sdk/test/base/ui/use_record_action_data.test.tsx @@ -1,12 +1,12 @@ import React, {Suspense} from 'react'; -import ReactDOM from 'react-dom'; -import {act} from 'react-dom/test-utils'; +import {render, act} from '@testing-library/react'; 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 {type RecordActionData} from '../../../src/base/types/record_action_data'; +import {SdkContext} from '../../../src/shared/ui/sdk_context'; +import {createPromiseWithResolveAndReject} from '../../test_helpers'; describe('useRecordActionData', () => { let sdk: Sdk; @@ -27,18 +27,19 @@ describe('useRecordActionData', () => { it('loads', async () => { mockAirtableInterface.fetchAndSubscribeToPerformRecordActionAsync.mockResolvedValue(null); - const recordActionData: RecordActionData = await new Promise(resolve => { - act(() => { - ReactDOM.render( - - - - - , - document.createElement('div'), - ); - }); + + const {promise, resolve} = createPromiseWithResolveAndReject(); + await act(async () => { + render( + + + + + , + ); }); + const recordActionData = await promise; + expect(recordActionData).toBe(null); }); }); diff --git a/packages/sdk/test/ui/use_records.test.tsx b/packages/sdk/test/base/ui/use_records.test.tsx similarity index 68% rename from packages/sdk/test/ui/use_records.test.tsx rename to packages/sdk/test/base/ui/use_records.test.tsx index 9dd15d635..e99cc08bd 100644 --- a/packages/sdk/test/ui/use_records.test.tsx +++ b/packages/sdk/test/base/ui/use_records.test.tsx @@ -1,16 +1,16 @@ 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 {render, act} from '@testing-library/react'; +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 type Record from '../../../src/base/models/record'; +import type Table from '../../../src/base/models/table'; +import {type TableId} from '../../../src/shared/types/hyper_ids'; +import {createPromiseWithResolveAndReject} from '../../test_helpers'; const mockAirtableInterface = MockAirtableInterface.linkedRecordsExample(); -jest.mock('../../src/injected/airtable_interface', () => ({ +jest.mock('../../../src/injected/airtable_interface', () => ({ __esModule: true, default: () => mockAirtableInterface, })); @@ -43,16 +43,15 @@ describe('useRecords', () => { recordsById, }); - const records: Record[] = await new Promise(resolve => { - act(() => { - ReactDOM.render( - - - , - document.createElement('div'), - ); - }); + const {promise, resolve} = createPromiseWithResolveAndReject(); + await act(async () => { + render( + + + , + ); }); + const records: Record[] = await promise; const ids = records.map(({id}) => id).sort(); expect(ids).toStrictEqual(['recA', 'recB', 'recC']); @@ -72,16 +71,15 @@ describe('useRecords', () => { recordsById, }); - const records: Record[] = await new Promise(resolve => { - act(() => { - ReactDOM.render( - - - , - document.createElement('div'), - ); - }); + const {promise, resolve} = createPromiseWithResolveAndReject(); + await act(async () => { + render( + + + , + ); }); + const records: Record[] = await promise; const ids = records.map(({id}) => id).sort(); expect(ids).toStrictEqual(['recA', 'recC']); @@ -113,19 +111,18 @@ describe('useRecords', () => { const query = await sdk.base.tables[0].selectRecordsAsync(); const record = query.getRecordById('recA'); - const records: Record[] = await new Promise(resolve => { - act(() => { - ReactDOM.render( - - - , - document.createElement('div'), - ); - }); + const {promise, resolve} = createPromiseWithResolveAndReject(); + await act(async () => { + render( + + + , + ); }); + const records: Record[] = await promise; const ids = records.map(({id}) => id).sort(); expect(ids).toStrictEqual(['recD']); diff --git a/packages/sdk/test/ui/use_view_metadata.test.tsx b/packages/sdk/test/base/ui/use_view_metadata.test.tsx similarity index 70% rename from packages/sdk/test/ui/use_view_metadata.test.tsx rename to packages/sdk/test/base/ui/use_view_metadata.test.tsx index dcd3861c6..e87ce238c 100644 --- a/packages/sdk/test/ui/use_view_metadata.test.tsx +++ b/packages/sdk/test/base/ui/use_view_metadata.test.tsx @@ -1,15 +1,15 @@ 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 {act, render} from '@testing-library/react'; +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 type ViewMetadataQueryResult from '../../../src/base/models/view_metadata_query_result'; +import type Table from '../../../src/base/models/table'; +import {createPromiseWithResolveAndReject} from '../../test_helpers'; const mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); -jest.mock('../../src/injected/airtable_interface', () => ({ +jest.mock('../../../src/injected/airtable_interface', () => ({ __esModule: true, default: () => mockAirtableInterface, })); @@ -49,16 +49,15 @@ describe('useViewMetadata', () => { recordsById, }); - const metadata: ViewMetadataQueryResult = await new Promise(resolve => { - act(() => { - ReactDOM.render( - - - , - document.createElement('div'), - ); - }); + const {promise, resolve} = createPromiseWithResolveAndReject(); + await act(async () => { + render( + + + , + ); }); + const metadata: ViewMetadataQueryResult = await promise; const ids = metadata.allFields.map(({id}) => id); expect(ids).toStrictEqual(['fldPrjctName', 'fldPrjctClient', 'fldPrjctCtgry']); diff --git a/packages/sdk/test/ui/use_watchable.test.tsx b/packages/sdk/test/base/ui/use_watchable.test.tsx similarity index 77% rename from packages/sdk/test/ui/use_watchable.test.tsx rename to packages/sdk/test/base/ui/use_watchable.test.tsx index 3218b7902..2b28b4e1f 100644 --- a/packages/sdk/test/ui/use_watchable.test.tsx +++ b/packages/sdk/test/base/ui/use_watchable.test.tsx @@ -1,8 +1,7 @@ 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 {act, render} from '@testing-library/react'; +import Watchable from '../../../src/shared/watchable'; +import useWatchable from '../../../src/shared/ui/use_watchable'; jest.useFakeTimers(); @@ -81,13 +80,13 @@ describe('useWatchable', () => { }; const thing = new Thing('foo'); - const wrapper = mount(); - expect(wrapper.find('span').text()).toEqual('foo'); + const {getByText} = render(); + expect(getByText('foo')).toBeInTheDocument(); act(() => { thing.setName('bar'); }); - expect(wrapper.find('span').text()).toEqual('bar'); + expect(getByText('bar')).toBeInTheDocument(); }); it('supports non-array watch keys', () => { @@ -97,13 +96,13 @@ describe('useWatchable', () => { }; const thing = new Thing('foo'); - const wrapper = mount(); - expect(wrapper.find('span').text()).toEqual('foo'); + const {getByText} = render(); + expect(getByText('foo')).toBeInTheDocument(); act(() => { thing.setName('bar'); }); - expect(wrapper.find('span').text()).toEqual('bar'); + expect(getByText('bar')).toBeInTheDocument(); }); it('only renders once on initial mount', () => { @@ -114,7 +113,7 @@ describe('useWatchable', () => { return null; }; - mount(); + render(); expect(renderCount).toEqual(1); }); @@ -129,14 +128,14 @@ describe('useWatchable', () => { }; const thing = new Thing('foo'); - const wrapper = mount(); - expect(wrapper.find('span').text()).toEqual('foo very'); + const {getByText} = render(); + expect(getByText('foo very')).toBeInTheDocument(); act(() => thing.setName('bar')); - expect(wrapper.find('span').text()).toEqual('bar very'); + expect(getByText('bar very')).toBeInTheDocument(); act(() => thing.setSpice('not very')); - expect(wrapper.find('span').text()).toEqual('bar not very'); + expect(getByText('bar not very')).toBeInTheDocument(); }); it('will call the provided callback when values change', () => { @@ -147,7 +146,7 @@ describe('useWatchable', () => { }; const thing = new Thing('foo'); - mount(); + render(); act(() => thing.setName('bar')); expect(callback).toHaveBeenCalledTimes(1); @@ -160,7 +159,7 @@ describe('useWatchable', () => { return null; }; - mount(); + render(); }); it('will not accept undefined as the keys', () => { @@ -172,7 +171,7 @@ describe('useWatchable', () => { return null; }; - expect(() => mount()).toThrowError( + expect(() => render()).toThrowError( 'Invalid call to useWatchable: keys cannot be undefined. ' + 'Pass a key or array of keys corresponding to the model being watched as the ' + 'second argument.', @@ -184,18 +183,18 @@ describe('useWatchable', () => { it('re-renders when a watched key changes', () => { const Component = ({things}: {things: Array}) => { useWatchable(things, ['name']); - return {things.map(thing => thing.name).join(', ')}; + return {things.map((thing) => thing.name).join(', ')}; }; const thing1 = new Thing('one'); const thing2 = new Thing('two'); - const wrapper = mount(); - expect(wrapper.find('span').text()).toEqual('one, two'); + const {getByText} = render(); + expect(getByText('one, two')).toBeInTheDocument(); act(() => { thing1.setName('bar'); }); - expect(wrapper.find('span').text()).toEqual('bar, two'); + expect(getByText('bar, two')).toBeInTheDocument(); }); it('only renders once on initial mount', () => { @@ -206,7 +205,7 @@ describe('useWatchable', () => { return null; }; - mount(); + render(); expect(renderCount).toEqual(1); }); @@ -214,32 +213,32 @@ describe('useWatchable', () => { const Component = ({things}: {things: Array}) => { useWatchable(things, ['name', 'spice', null]); return ( - {things.map(thing => `${thing.name} ${thing.spice}`).join(', ')} + {things.map((thing) => `${thing.name} ${thing.spice}`).join(', ')} ); }; const thing1 = new Thing('one'); const thing2 = new Thing('two'); - const wrapper = mount(); - expect(wrapper.find('span').text()).toEqual('one very, two very'); + const {getByText} = render(); + expect(getByText('one very, two very')).toBeInTheDocument(); act(() => thing1.setName('bar')); - expect(wrapper.find('span').text()).toEqual('bar very, two very'); + expect(getByText('bar very, two very')).toBeInTheDocument(); act(() => thing2.setSpice('not very')); - expect(wrapper.find('span').text()).toEqual('bar very, two not very'); + expect(getByText('bar very, two not very')).toBeInTheDocument(); }); it('will call the provided callback when values change', () => { const callback = jest.fn(); const Component = ({things}: {things: Array}) => { useWatchable(things, ['name'], callback); - return {things.map(thing => thing.name).join(', ')}; + return {things.map((thing) => thing.name).join(', ')}; }; const thing1 = new Thing('one'); const thing2 = new Thing('two'); - mount(); + render(); act(() => thing1.setName('bar')); expect(callback).toHaveBeenCalledTimes(1); @@ -253,32 +252,34 @@ describe('useWatchable', () => { it("won't let ref-count hit 0 when changing keys/models", () => { const Component = ({things, keys}: {things: Array; keys: Array}) => { useWatchable(things, keys); - return {things.map(thing => thing.name).join(', ')}; + return {things.map((thing) => thing.name).join(', ')}; }; const thing1 = new Thing('one'); const thing2 = new Thing('two'); - const wrapper = mount(); + const {rerender, unmount} = render( + , + ); expect(thing1.setupNameSideEffect).toHaveBeenCalledTimes(1); expect(thing2.setupNameSideEffect).toHaveBeenCalledTimes(1); expect(thing1.teardownNameSideEffect).toHaveBeenCalledTimes(0); expect(thing2.teardownNameSideEffect).toHaveBeenCalledTimes(0); - wrapper.setProps({keys: ['name', 'spice']}); + rerender(); expect(thing1.setupNameSideEffect).toHaveBeenCalledTimes(1); expect(thing2.setupNameSideEffect).toHaveBeenCalledTimes(1); expect(thing1.teardownNameSideEffect).toHaveBeenCalledTimes(0); expect(thing2.teardownNameSideEffect).toHaveBeenCalledTimes(0); - wrapper.setProps({things: [thing1]}); + rerender(); jest.runAllTimers(); expect(thing1.setupNameSideEffect).toHaveBeenCalledTimes(1); expect(thing2.setupNameSideEffect).toHaveBeenCalledTimes(1); expect(thing1.teardownNameSideEffect).toHaveBeenCalledTimes(0); expect(thing2.teardownNameSideEffect).toHaveBeenCalledTimes(1); - wrapper.unmount(); + unmount(); jest.runAllTimers(); expect(thing1.setupNameSideEffect).toHaveBeenCalledTimes(1); expect(thing2.setupNameSideEffect).toHaveBeenCalledTimes(1); 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 deleted file mode 100644 index aafe1a853..000000000 --- a/packages/sdk/test/get_style_props_for_responsive_prop.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import getStylePropsForResponsiveProp from '../src/ui/system/utils/get_style_props_for_responsive_prop'; - -describe('getStylePropForResponsiveProp', () => { - it('returns responsive style props for scale', () => { - const result = getStylePropsForResponsiveProp( - { - smallViewport: 'xsmall', - mediumViewport: 'small', - }, - { - xsmall: { - fontSize: 1, - textColor: 'dark', - lineHeight: '14px', - fontWeight: 400, - fontFamily: 'default', - marginY: 0, - }, - small: { - fontSize: 2, - textColor: 'dark', - lineHeight: '16px', - fontWeight: 400, - fontFamily: 'default', - marginY: 0, - }, - }, - ); - - expect(result).toStrictEqual({ - fontSize: { - smallViewport: 1, - mediumViewport: 2, - }, - textColor: 'dark', - lineHeight: { - smallViewport: '14px', - mediumViewport: '16px', - }, - fontWeight: 400, - fontFamily: 'default', - marginY: 0, - }); - }); -}); diff --git a/packages/sdk/test/index.test.ts b/packages/sdk/test/index.test.ts deleted file mode 100644 index 021f0ee4e..000000000 --- a/packages/sdk/test/index.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -// 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'; - -let mockAirtableInterface: jest.Mocked; -jest.mock('../src/injected/airtable_interface', () => ({ - __esModule: true, - default() { - if (!mockAirtableInterface) { - mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); - } - return mockAirtableInterface; - }, -})); - -describe('index', () => { - describe('legacy `cursor` property', () => { - test('value', () => { - expect(sdk.cursor.activeTableId).toBe('tblDesignProjects'); - }); - - test('enumerability', () => { - expect(Object.keys(sdk).some(key => key === 'cursor')).toBe(true); - }); - }); - - describe('legacy `session` property', () => { - test('value', () => { - expect(sdk.session.currentUser).toEqual({ - email: 'collab10@example.com', - id: 'usrGalSamari', - name: 'Gal Samari', - profilePicUrl: - 'https://dl.airtable.com/profilePics/qy4E6kRaaku2JJwXpjQb_headshot-purple-2.png', - }); - }); - - test('enumerability', () => { - expect(Object.keys(sdk).some(key => key === 'session')).toBe(true); - }); - }); - - describe('legacy `UI` property', () => { - test('value', () => { - expect((sdk as any).UI).toBe(UI); - }); - - test('enumerability', () => { - expect(Object.keys(sdk).some(key => key === 'UI')).toBe(true); - }); - }); - - 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); - }); - - test('enumerability', () => { - expect(Object.keys(sdk).some(key => key === 'undoRedo')).toBe(true); - }); - }); -}); diff --git a/packages/sdk/test/interface/airtable_interface_mocks/fixture_data.ts b/packages/sdk/test/interface/airtable_interface_mocks/fixture_data.ts new file mode 100644 index 000000000..3df1274e8 --- /dev/null +++ b/packages/sdk/test/interface/airtable_interface_mocks/fixture_data.ts @@ -0,0 +1,182 @@ +import { + type BaseId, + type TableId, + type FieldId, + type RecordId, +} from '../../../src/shared/types/hyper_ids'; +import {type TableData} from '../../../src/interface/types/table'; +import {FieldType} from '../../../src/shared/types/field_core'; +import {type FieldData} from '../../../src/interface/types/field'; +import {type CollaboratorData} from '../../../src/shared/types/collaborator'; +import {type ObjectMap, keyBy, getId} from '../../../src/shared/private_utils'; +import { + BlockRunContextType, + type SdkInitData, +} from '../../../src/interface/types/airtable_interface'; +import {type RecordData} from '../../../src/interface/types/record'; + +const MOCK_BLOCK_INSTALLATION_ID = 'blicPfOILwejF6HL2'; +const MOCK_BLOCK_RUN_CONTEXT_TYPE = BlockRunContextType.PAGE_ELEMENT_IN_QUERY_CONTAINER; +const MOCK_PAGE_ID = 'pag0000KEVINWILDE'; +const MOCK_BASE_PERMISSION_LEVEL = 'create'; +const MOCK_CURRENT_USER_ID = 'usrGalSamari'; +const MOCK_BASE_COLOR = 'purple'; +const MOCK_BILLING_GROUP = 'pro'; +const MOCK_WORKSPACE_ID = 'wspUai0ZmWFWfSBtb'; + +export function convertFixtureDataToSdkInitData(fixtureData: FixtureData): SdkInitData { + const { + base: {id, name, color, tables, collaborators}, + } = fixtureData; + return { + isDevelopmentMode: false, + blockInstallationId: MOCK_BLOCK_INSTALLATION_ID, + isFirstRun: false, + initialKvValuesByKey: {}, + runContext: { + type: MOCK_BLOCK_RUN_CONTEXT_TYPE, + pageId: MOCK_PAGE_ID, + isPageElementInEditMode: false, + }, + baseData: { + id, + name, + color: color ?? MOCK_BASE_COLOR, + tableOrder: tables.map((t) => t.id), + tablesById: keyBy( + tables.map(convertTableFixtureDataToTableData), + getId, + ), + permissionLevel: MOCK_BASE_PERMISSION_LEVEL, + currentUserId: MOCK_CURRENT_USER_ID, + enabledFeatureNames: [], + collaboratorsById: keyBy( + collaborators.map((c) => { + const {email, profilePicUrl} = c; + return {id: c.id, name: c.name, email, profilePicUrl}; + }), + getId, + ), + activeCollaboratorIds: collaborators.filter((c) => c.isActive).map((c) => c.id), + billingPlanGrouping: MOCK_BILLING_GROUP, + appInterface: {}, + isBlockDevelopmentRestrictionEnabled: false, + workspaceId: MOCK_WORKSPACE_ID, + }, + intentData: null, + }; +} + +function convertTableFixtureDataToTableData(tableFixtureData: TableFixtureData): TableData { + const {id, name, description, fields, records} = tableFixtureData; + return { + id, + name, + description, + primaryFieldId: fields[0].id, + fieldsById: keyBy(fields.map(convertFieldFixtureDataToFieldData), getId), + recordsById: keyBy( + records.map(convertRecordFixtureDataToRecordData), + getId, + ), + recordOrder: records.map((r) => r.id), + lock: null, + externalSyncById: null, + isRecordExpansionEnabled: true, + canCreateRecordsInline: true, + canEditRecordsInline: true, + canDestroyRecordsInline: true, + }; +} + +function convertFieldFixtureDataToFieldData(fieldFixtureData: FieldFixtureData): FieldData { + const {id, name, description, type, options} = fieldFixtureData; + return { + id, + name, + type, + description, + typeOptions: options, + lock: null, + isSynced: false, + isEditable: true, + canCreateNewForeignRecords: type === FieldType.MULTIPLE_RECORD_LINKS ? true : undefined, + }; +} + +function convertRecordFixtureDataToRecordData(recordFixtureData: RecordFixtureData): RecordData { + const {id, createdTime, cellValuesByFieldId} = recordFixtureData; + return { + id, + createdTime, + cellValuesByFieldId, + }; +} + +/** + * A complete set of information necessary to initialize a simulated Airtable + * Base in automated test environments. This is currently copied from + * block-testing. + * TODO(fredz): consider moving this into src/testing and exporting it as + * part of unstable_testing_utils. + * + * Unlike SdkInitData which does not contain record data by design, + * FixtureData contains it in fixtureData.base.tables[number].records, + * and relies on dependent modules to simulate the hyperbase behavior of + * only making those records after corresponding necessary calls to + * AirtableInterface. + */ +export interface FixtureData { + /** A representation of the state of an Airtable Base */ + base: { + id: BaseId; + name: string; + color?: string; + tables: Array; + collaborators: Array; + }; +} + +/** A representation of the state of a Table */ +interface TableFixtureData { + /** A unique identifier for the simulated Table */ + id: TableId; + /** The name to assign to the simulated Table */ + name: string; + /** The description to assign to the simulated Table */ + description: string | null; + /** + * Fixture data for the simulated Fields that should be present in the + * simulated Table when it is initialized. + */ + fields: Array; + /** + * Fixture data for the simulated Records that should be present in the + * simulated Table when it is initialized. + */ + records: Array; +} + +/** A representation of the state of a Field */ +interface FieldFixtureData { + /** A unique identifier for the simulated Field */ + id: FieldId; + /** The name to assign to the simulated Field */ + name: string; + /** The description to assign to the simulated Field */ + description: string | null; + /** The type of the simulated Field */ + type: FieldType; + /** Options associated with the simulated Field */ + options: null | {[key: string]: unknown}; +} + +/** A representation of the state of a Record */ +interface RecordFixtureData { + /** A unique identifier for the simulated Record */ + id: RecordId; + /** The time the simulated record should appear to have been created */ + createdTime: string; + /** A mapping of field identifiers to cell values */ + cellValuesByFieldId: ObjectMap; +} diff --git a/packages/sdk/test/interface/airtable_interface_mocks/linked_records.ts b/packages/sdk/test/interface/airtable_interface_mocks/linked_records.ts new file mode 100644 index 000000000..0ea3e0f5f --- /dev/null +++ b/packages/sdk/test/interface/airtable_interface_mocks/linked_records.ts @@ -0,0 +1,92 @@ +import {FieldType} from '../../../src/shared/types/field_core'; +import {type FixtureData} from './fixture_data'; + +const linkedRecords: FixtureData = { + base: { + id: 'app97Vimdj1OP7QKF', + name: 'Linked Records Table', + color: 'purpleLight2', + tables: [ + { + id: 'tblFirst', + name: 'First Table', + fields: [ + { + id: 'fld1stPrimary', + name: 'Name', + type: FieldType.SINGLE_LINE_TEXT, + options: null, + description: '', + }, + { + id: 'fld1stLinked', + name: 'linked records', + type: FieldType.MULTIPLE_RECORD_LINKS, + options: { + linkedTableId: 'tblSecond', + relationship: 'many', + symmetricColumnId: 'fld2ndLinked', + unreversed: true, + }, + description: '', + }, + { + id: 'fldMockLookup', + name: 'lookup', + type: FieldType.MULTIPLE_LOOKUP_VALUES, + options: null, + description: '', + }, + ], + description: '', + records: [], + }, + { + id: 'tblSecond', + name: 'Second Table', + fields: [ + { + id: 'fld2ndPrimary', + name: 'Name', + type: FieldType.SINGLE_LINE_TEXT, + options: null, + description: '', + }, + { + id: 'fld2ndSecondary', + name: 'Name', + type: FieldType.SINGLE_LINE_TEXT, + options: null, + description: '', + }, + { + id: 'fld2ndLinked', + name: 'linked records', + type: FieldType.MULTIPLE_RECORD_LINKS, + options: { + linkedTableId: 'tblFirst', + relationship: 'many', + symmetricColumnId: 'fld1stLinked', + unreversed: true, + }, + description: '', + }, + ], + description: '', + records: [], + }, + ], + collaborators: [ + { + id: 'usrGalSamari', + email: 'collab10@example.com', + name: 'Gal Samari', + profilePicUrl: + 'https://dl.airtable.com/profilePics/qy4E6kRaaku2JJwXpjQb_headshot-purple-2.png', + isActive: true, + }, + ], + }, +}; + +export default linkedRecords; diff --git a/packages/sdk/test/interface/airtable_interface_mocks/mock_airtable_interface.ts b/packages/sdk/test/interface/airtable_interface_mocks/mock_airtable_interface.ts new file mode 100644 index 000000000..4924c62d0 --- /dev/null +++ b/packages/sdk/test/interface/airtable_interface_mocks/mock_airtable_interface.ts @@ -0,0 +1,84 @@ +import {type FieldTypeProvider, type IdGenerator} from '../../../src/base/types/airtable_interface'; +import {AbstractMockAirtableInterface} from '../../../src/testing/interface/abstract_mock_airtable_interface'; +import {spawnError} from '../../../src/shared/error_utils'; +import {type GlobalConfigHelpers} from '../../../src/shared/types/airtable_interface_core'; +import projectTrackerData from './project_tracker'; +import linkedRecordsData from './linked_records'; +import {type FixtureData, convertFixtureDataToSdkInitData} from './fixture_data'; + +const resetSpies = (target: {[key: string]: any}, names: string[]) => { + for (const name of names) { + if (jest.isMockFunction(target[name])) { + target[name].mockRestore(); + } + + if (typeof target[name] === 'function') { + jest.spyOn(target as any, name); + } + } +}; + +/** + * An implementation of the AbstractMockAirtableInterface designed for use in the + * Blocks SDK internal automated test suite. Provides Jest spies for all + * available methods (and which resets the state of those spies with every call + * to `reset`). + */ +export class MockAirtableInterface extends AbstractMockAirtableInterface { + static projectTrackerExample() { + return MockAirtableInterface.createFromFixtureData(projectTrackerData); + } + + static linkedRecordsExample() { + return MockAirtableInterface.createFromFixtureData(linkedRecordsData); + } + + static createFromFixtureData(fixtureData: FixtureData) { + const sdkInitData = convertFixtureDataToSdkInitData(fixtureData); + return new MockAirtableInterface(sdkInitData) as jest.Mocked; + } + + get fieldTypeProvider() { + return super.fieldTypeProvider as jest.Mocked; + } + + get globalConfigHelpers() { + return super.globalConfigHelpers as jest.Mocked; + } + + get idGenerator() { + return super.idGenerator as jest.Mocked; + } + + /** + * Revert the mock interface to its initial state. This includes: + * + * - removing all event listeners + * - restoring the database schema + * - recreating the Jest "spies" for every instance method + */ + reset() { + super.reset(); + + resetSpies(this, Object.getOwnPropertyNames(MockAirtableInterface.prototype)); + resetSpies(this, Object.getOwnPropertyNames(AbstractMockAirtableInterface.prototype)); + resetSpies(this.fieldTypeProvider, Object.keys(this.fieldTypeProvider)); + resetSpies(this.globalConfigHelpers, Object.keys(this.globalConfigHelpers)); + resetSpies(this.idGenerator, Object.keys(this.idGenerator)); + } + + expandRecord(tableId: string, recordId: string) { + throw spawnError('expandRecord unimplemented'); + } + reloadFrame() { + throw spawnError('reloadFrame unimplemented'); + } + trackEvent() { + throw spawnError('trackEvent unimplemented'); + } + trackExposure() { + } + sendStat() { + throw spawnError('sendStat unimplemented'); + } +} diff --git a/packages/sdk/test/interface/airtable_interface_mocks/project_tracker.ts b/packages/sdk/test/interface/airtable_interface_mocks/project_tracker.ts new file mode 100644 index 000000000..c5feb8e2c --- /dev/null +++ b/packages/sdk/test/interface/airtable_interface_mocks/project_tracker.ts @@ -0,0 +1,327 @@ +import {FieldType} from '../../../src/shared/types/field_core'; +import {type FixtureData} from './fixture_data'; + +const projectTracker: FixtureData = { + base: { + id: 'app97Vimdj1OP7QKF', + name: 'Project tracker', + color: 'purple', + tables: [ + { + id: 'tblDesignProjects', + name: 'Design projects', + fields: [ + { + id: 'fldPrjctName', + name: 'Name', + type: FieldType.SINGLE_LINE_TEXT, + options: null, + description: '', + }, + { + id: 'fldPrjctClient', + name: 'Client', + type: FieldType.MULTIPLE_RECORD_LINKS, + options: { + foreignTableId: 'tblClients', + relationship: 'many', + symmetricColumnId: 'fldClientProjects', + }, + description: 'the project client', + }, + { + id: 'fldPrjctCtgry', + name: 'Category', + type: FieldType.SINGLE_SELECT, + options: { + choiceOrder: [ + 'selPrjctBrand', + 'selPrjctIndstrl', + 'selPrjctHealth', + 'selPrjctTech', + ], + choices: { + selPrjctBrand: { + name: 'Brand identity', + id: 'selPrjctBrand', + color: 'cyanDark', + }, + selPrjctIndstrl: { + name: 'Industrial design', + id: 'selPrjctIndstrl', + color: 'redDark', + }, + selPrjctHealth: { + name: 'Healthcare design', + id: 'selPrjctHealth', + color: 'yellowDark', + }, + selPrjctTech: { + name: 'Technology design', + id: 'selPrjctTech', + color: 'greenDark', + }, + }, + }, + description: '', + }, + { + id: 'fldPrjctCmplt', + name: 'Complete', + type: FieldType.CHECKBOX, + options: { + color: 'orange', + icon: 'check', + }, + description: '', + }, + { + id: 'fldPrjctTasks', + name: 'Tasks', + type: FieldType.MULTIPLE_RECORD_LINKS, + options: { + foreignTableId: 'tblTasks', + relationship: 'many', + symmetricColumnId: 'fldTaskProject', + }, + description: '', + }, + { + id: 'fldPrjctLead', + name: 'Project lead', + type: FieldType.SINGLE_COLLABORATOR, + options: { + shouldNotify: true, + }, + description: '', + }, + { + id: 'fldPrjctTeam', + name: 'Project team', + type: FieldType.MULTIPLE_COLLABORATORS, + options: { + shouldNotify: true, + }, + description: '', + }, + { + id: 'fldPrjctDue', + name: 'Due date', + type: FieldType.DATE, + options: { + isDateTime: false, + dateFormat: 'Local', + }, + description: '', + }, + { + id: 'fldPrjctKickoff', + name: 'Kickoff date', + type: FieldType.DATE, + options: { + isDateTime: false, + dateFormat: 'Local', + }, + description: '', + }, + { + id: 'fldPrjctNotes', + name: 'Notes', + type: FieldType.MULTILINE_TEXT, + options: null, + description: '', + }, + { + id: 'fldPrjctImages', + name: 'Project images', + type: FieldType.MULTIPLE_ATTACHMENTS, + options: null, + description: '', + }, + ], + records: [], + description: 'description for design projects table', + }, + { + id: 'tblTasks', + name: 'Tasks', + fields: [ + { + id: 'fldTaskName', + name: 'Name', + type: FieldType.SINGLE_LINE_TEXT, + options: null, + description: '', + }, + { + id: 'fldTaskNotes', + name: 'Notes', + type: FieldType.MULTILINE_TEXT, + options: null, + description: '', + }, + { + id: 'fldTaskProject', + name: 'Design project', + type: FieldType.MULTIPLE_RECORD_LINKS, + options: { + foreignTableId: 'tblDesignProjects', + symmetricColumnId: 'fldPrjctTasks', + relationship: 'many', + }, + description: '', + }, + { + id: 'fldTaskTime', + name: 'Time estimate (days)', + type: FieldType.NUMBER, + options: { + format: 'decimal', + precision: 1, + negative: false, + validatorName: 'positive', + }, + description: '', + }, + { + id: 'fldTaskCompleted', + name: 'Completed', + type: FieldType.CHECKBOX, + options: { + color: 'gray', + icon: 'check', + }, + description: '', + }, + { + id: 'fldTaskAssignee', + name: 'Assignee', + type: FieldType.SINGLE_COLLABORATOR, + options: { + shouldNotify: true, + }, + description: '', + }, + ], + records: [], + description: '', + }, + { + id: 'tblClients', + name: 'Clients', + fields: [ + { + id: 'fldClientName', + name: 'Name', + type: FieldType.SINGLE_LINE_TEXT, + options: null, + description: '', + }, + { + id: 'fldClientAbout', + name: 'About', + type: FieldType.MULTILINE_TEXT, + options: null, + description: '', + }, + { + id: 'fldClientLogo', + name: 'Logo', + type: FieldType.MULTIPLE_ATTACHMENTS, + options: null, + description: '', + }, + { + id: 'fldClientProjects', + name: 'Projects', + type: FieldType.MULTIPLE_RECORD_LINKS, + options: { + foreignTableId: 'tblDesignProjects', + symmetricColumnId: 'fldPrjctClient', + relationship: 'many', + }, + description: '', + }, + ], + records: [], + description: '', + }, + ], + collaborators: [ + { + id: 'usrGalSamari', + email: 'collab10@example.com', + name: 'Gal Samari', + profilePicUrl: + 'https://dl.airtable.com/profilePics/qy4E6kRaaku2JJwXpjQb_headshot-purple-2.png', + isActive: true, + }, + { + id: 'usrSamEpps', + email: 'collab35@example.com', + name: 'Sam Epps', + profilePicUrl: + 'https://dl.airtable.com/profilePics/ybh33aqqTrKhPFYFj47K_headshot-orange-2.png', + isActive: false, + }, + { + id: 'usrParisFotiou', + email: 'collab26@example.com', + name: 'Paris Fotiou', + profilePicUrl: + 'https://dl.airtable.com/profilePics/xoafD4NRXGRLcx3qilRg_Screen%20Shot%202019-01-17%20at%201.20.14%20PM.png', + isActive: false, + }, + { + id: 'usrBaileyMirza', + email: 'collab5@example.com', + name: 'Bailey Mirza', + profilePicUrl: + 'https://dl.airtable.com/profilePics/7pprdNqqQuSWWN7zeavM_headshot-pink-1.png', + isActive: false, + }, + { + id: 'usrJordanPeretz', + email: 'collab16@example.com', + name: 'Jordan Peretz', + profilePicUrl: + 'https://dl.airtable.com/profilePics/jCMoXFziQcD0XkHMxhwQ_Screen%20Shot%202019-01-17%20at%201.19.59%20PM.png', + isActive: false, + }, + { + id: 'usrLeslieWalker', + email: 'collab23@example.com', + name: 'Leslie Walker', + profilePicUrl: + 'https://dl.airtable.com/profilePics/zMyV7nBhTI0fwMiWOi6g_headshot-blue-1.png', + isActive: false, + }, + { + id: 'usrAshQuintana', + email: 'collab4@example.com', + name: 'Ash Quintana', + profilePicUrl: + 'https://dl.airtable.com/profilePics/7KX9bnbqQyGvWGArbTXB_headshot-yellow-1.png', + isActive: false, + }, + { + id: 'usrSkylerXu', + email: 'collab37@example.com', + name: 'Skyler Xu', + profilePicUrl: + 'https://dl.airtable.com/profilePics/WB8Q1EQRTJW3YBfv403V_headshot-orange-1.png', + isActive: false, + }, + { + id: 'usrCameronToth', + email: 'collab7@example.com', + name: 'Cameron Toth', + profilePicUrl: + 'https://dl.airtable.com/profilePics/1Paw52jFSLa7vRHwxCRd_headshot-pink-2.png', + isActive: false, + }, + ], + }, +}; + +export default projectTracker; diff --git a/packages/sdk/test/interface/models/base.test.ts b/packages/sdk/test/interface/models/base.test.ts new file mode 100644 index 000000000..769f71a95 --- /dev/null +++ b/packages/sdk/test/interface/models/base.test.ts @@ -0,0 +1,378 @@ +import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; +import {type Base} from '../../../src/interface/models/base'; +import {InterfaceBlockSdk} from '../../../src/interface/sdk'; +import {Table} from '../../../src/interface/models/table'; + +const mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); +jest.mock('../../../src/injected/airtable_interface', () => ({ + __esModule: true, + default() { + return mockAirtableInterface; + }, +})); + +describe('Base', () => { + let base: Base; + let sdk: InterfaceBlockSdk; + + beforeEach(() => { + mockAirtableInterface.reset(); + sdk = new InterfaceBlockSdk(mockAirtableInterface); + base = sdk.base; + }); + + describe('__sdk', () => { + it('exposes its instance', () => { + expect(base.__sdk).toBe(sdk); + }); + }); + + describe('tables', () => { + it('lists all tables in the expected order', () => { + expect(base.tables.map(({id}) => id)).toMatchInlineSnapshot(` + Array [ + "tblDesignProjects", + "tblTasks", + "tblClients", + ] + `); + }); + + it('omits tables which have no corresponding schema', async () => { + const {tablesById} = mockAirtableInterface.sdkInitData.baseData; + delete tablesById.tblDesignProjects; + base = new InterfaceBlockSdk(mockAirtableInterface).base; + + expect(base.tables.map(({id}) => id)).toMatchInlineSnapshot(` + Array [ + "tblTasks", + "tblClients", + ] + `); + }); + }); + + describe('getCollaboratorIfExists', () => { + it('returns collaborator by id', () => { + const collaborator1 = base.getCollaboratorIfExists('usrAshQuintana'); + const collaborator2 = base.getCollaboratorIfExists('usrParisFotiou'); + expect(collaborator1).toMatchInlineSnapshot(` + Object { + "email": "collab4@example.com", + "id": "usrAshQuintana", + "name": "Ash Quintana", + "profilePicUrl": "https://dl.airtable.com/profilePics/7KX9bnbqQyGvWGArbTXB_headshot-yellow-1.png", + } + `); + expect(collaborator2).toMatchInlineSnapshot(` + Object { + "email": "collab26@example.com", + "id": "usrParisFotiou", + "name": "Paris Fotiou", + "profilePicUrl": "https://dl.airtable.com/profilePics/xoafD4NRXGRLcx3qilRg_Screen%20Shot%202019-01-17%20at%201.20.14%20PM.png", + } + `); + }); + + it('returns collaborator by name', () => { + const collaborator1 = base.getCollaboratorIfExists('Bailey Mirza'); + const collaborator2 = base.getCollaboratorIfExists('Gal Samari'); + expect(collaborator1).toMatchInlineSnapshot(` + Object { + "email": "collab5@example.com", + "id": "usrBaileyMirza", + "name": "Bailey Mirza", + "profilePicUrl": "https://dl.airtable.com/profilePics/7pprdNqqQuSWWN7zeavM_headshot-pink-1.png", + } + `); + expect(collaborator2).toMatchInlineSnapshot(` + Object { + "email": "collab10@example.com", + "id": "usrGalSamari", + "name": "Gal Samari", + "profilePicUrl": "https://dl.airtable.com/profilePics/qy4E6kRaaku2JJwXpjQb_headshot-purple-2.png", + } + `); + }); + + it('returns collaborator by email', () => { + const collaborator1 = base.getCollaboratorIfExists('collab16@example.com'); + const collaborator2 = base.getCollaboratorIfExists('collab4@example.com'); + expect(collaborator1).toMatchInlineSnapshot(` + Object { + "email": "collab16@example.com", + "id": "usrJordanPeretz", + "name": "Jordan Peretz", + "profilePicUrl": "https://dl.airtable.com/profilePics/jCMoXFziQcD0XkHMxhwQ_Screen%20Shot%202019-01-17%20at%201.19.59%20PM.png", + } + `); + expect(collaborator2).toMatchInlineSnapshot(` + Object { + "email": "collab4@example.com", + "id": "usrAshQuintana", + "name": "Ash Quintana", + "profilePicUrl": "https://dl.airtable.com/profilePics/7KX9bnbqQyGvWGArbTXB_headshot-yellow-1.png", + } + `); + }); + + it('gracefully handles collaborators with identical names', () => { + const {collaboratorsById} = mockAirtableInterface.sdkInitData.baseData; + collaboratorsById.usrFAKE0000000001 = { + email: 'fake1@example.com', + id: 'usrFAKE0000000001', + name: 'Gristle McThornbody', + profilePicUrl: 'https://example.com/fake1.jpg', + }; + collaboratorsById.usrFAKE0000000002 = { + email: 'fake2@example.com', + id: 'usrFAKE0000000002', + name: 'Gristle McThornbody', + profilePicUrl: 'https://example.com/fake2.jpg', + }; + base = new InterfaceBlockSdk(mockAirtableInterface).base; + expect(base.getCollaboratorIfExists('fake1@example.com')).toMatchInlineSnapshot(` + Object { + "email": "fake1@example.com", + "id": "usrFAKE0000000001", + "name": "Gristle McThornbody", + "profilePicUrl": "https://example.com/fake1.jpg", + } + `); + expect(base.getCollaboratorIfExists('fake2@example.com')).toMatchInlineSnapshot(` + Object { + "email": "fake2@example.com", + "id": "usrFAKE0000000002", + "name": "Gristle McThornbody", + "profilePicUrl": "https://example.com/fake2.jpg", + } + `); + expect([ + collaboratorsById.usrFAKE0000000001, + collaboratorsById.usrFAKE0000000002, + ]).toContainEqual(base.getCollaboratorIfExists('Gristle McThornbody')); + }); + + it('returns null if no collaborator is found', () => { + expect(base.getCollaboratorIfExists('usr3VLCpyqgc1FAKE')).toBe(null); + expect(base.getCollaboratorIfExists('fake@example.com')).toBe(null); + expect(base.getCollaboratorIfExists('Mary Face')).toBe(null); + }); + }); + + describe('getCollaborator', () => { + beforeEach(() => { + base = new InterfaceBlockSdk(mockAirtableInterface).base; + }); + + it('returns collaborator by id', () => { + const collaborator1 = base.getCollaborator('usrAshQuintana'); + const collaborator2 = base.getCollaborator('usrParisFotiou'); + expect(collaborator1).toMatchInlineSnapshot(` + Object { + "email": "collab4@example.com", + "id": "usrAshQuintana", + "name": "Ash Quintana", + "profilePicUrl": "https://dl.airtable.com/profilePics/7KX9bnbqQyGvWGArbTXB_headshot-yellow-1.png", + } + `); + expect(collaborator2).toMatchInlineSnapshot(` + Object { + "email": "collab26@example.com", + "id": "usrParisFotiou", + "name": "Paris Fotiou", + "profilePicUrl": "https://dl.airtable.com/profilePics/xoafD4NRXGRLcx3qilRg_Screen%20Shot%202019-01-17%20at%201.20.14%20PM.png", + } + `); + }); + + it('returns collaborator by name', () => { + const collaborator1 = base.getCollaborator('Bailey Mirza'); + const collaborator2 = base.getCollaborator('Gal Samari'); + expect(collaborator1).toMatchInlineSnapshot(` + Object { + "email": "collab5@example.com", + "id": "usrBaileyMirza", + "name": "Bailey Mirza", + "profilePicUrl": "https://dl.airtable.com/profilePics/7pprdNqqQuSWWN7zeavM_headshot-pink-1.png", + } + `); + expect(collaborator2).toMatchInlineSnapshot(` + Object { + "email": "collab10@example.com", + "id": "usrGalSamari", + "name": "Gal Samari", + "profilePicUrl": "https://dl.airtable.com/profilePics/qy4E6kRaaku2JJwXpjQb_headshot-purple-2.png", + } + `); + }); + + it('returns collaborator by email', () => { + const collaborator1 = base.getCollaborator('collab16@example.com'); + const collaborator2 = base.getCollaborator('collab4@example.com'); + expect(collaborator1).toMatchInlineSnapshot(` + Object { + "email": "collab16@example.com", + "id": "usrJordanPeretz", + "name": "Jordan Peretz", + "profilePicUrl": "https://dl.airtable.com/profilePics/jCMoXFziQcD0XkHMxhwQ_Screen%20Shot%202019-01-17%20at%201.19.59%20PM.png", + } + `); + expect(collaborator2).toMatchInlineSnapshot(` + Object { + "email": "collab4@example.com", + "id": "usrAshQuintana", + "name": "Ash Quintana", + "profilePicUrl": "https://dl.airtable.com/profilePics/7KX9bnbqQyGvWGArbTXB_headshot-yellow-1.png", + } + `); + }); + + it('returns throws if no collaborator is found', () => { + expect(() => + base.getCollaborator('usr3VLCpyqgc1FAKE'), + ).toThrowErrorMatchingInlineSnapshot( + `"No collaborator with ID, name, or email of 'usr3VLCpyqgc1FAKE' is in base 'Project tracker'"`, + ); + expect(() => + base.getCollaborator('fake@example.com'), + ).toThrowErrorMatchingInlineSnapshot( + `"No collaborator with ID, name, or email of 'fake@example.com' is in base 'Project tracker'"`, + ); + expect(() => base.getCollaborator('Mary Face')).toThrowErrorMatchingInlineSnapshot( + `"No collaborator with ID, name, or email of 'Mary Face' is in base 'Project tracker'"`, + ); + }); + }); + + describe('getCollaboratorById', () => { + it('returns collaborator by id', () => { + const collaborator1 = base.getCollaboratorById('usrAshQuintana'); + const collaborator2 = base.getCollaboratorById('usrParisFotiou'); + expect(collaborator1).toMatchInlineSnapshot(` + Object { + "email": "collab4@example.com", + "id": "usrAshQuintana", + "name": "Ash Quintana", + "profilePicUrl": "https://dl.airtable.com/profilePics/7KX9bnbqQyGvWGArbTXB_headshot-yellow-1.png", + } + `); + expect(collaborator2).toMatchInlineSnapshot(` + Object { + "email": "collab26@example.com", + "id": "usrParisFotiou", + "name": "Paris Fotiou", + "profilePicUrl": "https://dl.airtable.com/profilePics/xoafD4NRXGRLcx3qilRg_Screen%20Shot%202019-01-17%20at%201.20.14%20PM.png", + } + `); + }); + + it('throws when no collaborator is found', () => { + expect(() => + base.getCollaboratorById('usrDoesNotExist'), + ).toThrowErrorMatchingInlineSnapshot( + `"No collaborator with ID usrDoesNotExist has access to base 'Project tracker'"`, + ); + }); + }); + + describe('getTableIfExists', () => { + it('returns table by id', () => { + const table1 = base.getTableIfExists('tblDesignProjects'); + const table2 = base.getTableIfExists('tblTasks'); + expect(table1).toBeInstanceOf(Table); + expect(table1?.id).toBe('tblDesignProjects'); + expect(table2).toBeInstanceOf(Table); + expect(table2?.id).toBe('tblTasks'); + }); + + it('returns table by name', () => { + const table1 = base.getTableIfExists('Design projects'); + const table2 = base.getTableIfExists('Tasks'); + expect(table1).toBeInstanceOf(Table); + expect(table1?.id).toBe('tblDesignProjects'); + expect(table2).toBeInstanceOf(Table); + expect(table2?.id).toBe('tblTasks'); + }); + + it('returns null when not found', () => { + expect(base.getTableIfExists('tbly388E8NA1cFAKE')).toBe(null); + expect(base.getTableIfExists('Injustices')).toBe(null); + }); + }); + + describe('getTable', () => { + it('returns table by id', () => { + const table1 = base.getTable('tblDesignProjects'); + const table2 = base.getTable('tblTasks'); + expect(table1).toBeInstanceOf(Table); + expect(table1.id).toBe('tblDesignProjects'); + expect(table2).toBeInstanceOf(Table); + expect(table2.id).toBe('tblTasks'); + }); + + it('returns table by name', () => { + const table1 = base.getTable('Design projects'); + const table2 = base.getTable('Tasks'); + expect(table1).toBeInstanceOf(Table); + expect(table1.id).toBe('tblDesignProjects'); + expect(table2).toBeInstanceOf(Table); + expect(table2.id).toBe('tblTasks'); + }); + + it('throws when not found', () => { + expect(() => base.getTable('tbly388E8NA1cFAKE')).toThrowErrorMatchingInlineSnapshot( + `"No table with ID or name 'tbly388E8NA1cFAKE' in base 'Project tracker'"`, + ); + expect(() => base.getTable('Injustices')).toThrowErrorMatchingInlineSnapshot( + `"No table with ID or name 'Injustices' in base 'Project tracker'"`, + ); + }); + }); + + describe('getTableById', () => { + it('returns table', () => { + const table1 = base.getTableById('tblDesignProjects'); + const table2 = base.getTableById('tblTasks'); + expect(table1).toBeInstanceOf(Table); + expect(table1.id).toBe('tblDesignProjects'); + expect(table2).toBeInstanceOf(Table); + expect(table2.id).toBe('tblTasks'); + }); + + it('throws when not found', () => { + expect(() => base.getTableById('tbly388E8NA1cFAKE')).toThrowErrorMatchingInlineSnapshot( + `"No table with ID tbly388E8NA1cFAKE in base 'Project tracker'"`, + ); + }); + }); + + describe('getTableByName', () => { + it('returns table', () => { + const table1 = base.getTableByName('Design projects'); + const table2 = base.getTableByName('Tasks'); + expect(table1).toBeInstanceOf(Table); + expect(table1.id).toBe('tblDesignProjects'); + expect(table2).toBeInstanceOf(Table); + expect(table2.id).toBe('tblTasks'); + }); + + it('throws when not found', () => { + expect(() => base.getTableByName('Susan')).toThrowErrorMatchingInlineSnapshot( + `"No table named 'Susan' in base 'Project tracker'"`, + ); + }); + }); + + describe('getMaxRecordsPerTable', () => { + it('returns the correct limit', () => { + expect(base.getMaxRecordsPerTable()).toEqual(100000); + }); + }); + + describe('workspaceId', () => { + it('returns the correct workspace id', () => { + expect(base.workspaceId).toEqual('wspUai0ZmWFWfSBtb'); + }); + }); +}); diff --git a/packages/sdk/test/interface/models/field.test.ts b/packages/sdk/test/interface/models/field.test.ts new file mode 100644 index 000000000..2d410266a --- /dev/null +++ b/packages/sdk/test/interface/models/field.test.ts @@ -0,0 +1,396 @@ +import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; +import {Field} from '../../../src/interface/models/field'; +import {FieldType} from '../../../src/shared/types/field_core'; +import {InterfaceBlockSdk} from '../../../src/interface/sdk'; + +const mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); +jest.mock('../../../src/injected/airtable_interface', () => ({ + __esModule: true, + default: () => mockAirtableInterface, +})); + +describe('Field', () => { + let sdk: InterfaceBlockSdk; + let field: Field; + + const makeFieldWithIsSynced = (fieldType: FieldType, isSynced: boolean | null) => { + const fieldId = 'fldTest'; + const baseData = mockAirtableInterface.sdkInitData.baseData; + const parentTable = baseData.tablesById.tblDesignProjects; + parentTable.fieldsById[fieldId] = { + id: fieldId, + name: 'Field 1', + type: '', + typeOptions: null, + description: null, + lock: null, + isSynced: isSynced, + isEditable: true, + canCreateNewForeignRecords: + fieldType === FieldType.MULTIPLE_RECORD_LINKS ? true : undefined, + }; + + const newField = new Field(sdk, sdk.base.getTableById('tblDesignProjects'), fieldId); + + Object.defineProperty(newField, 'type', { + get: jest.fn(() => fieldType), + }); + + return newField; + }; + + beforeEach(() => { + sdk = new InterfaceBlockSdk(mockAirtableInterface); + field = sdk.base.tables[0].fields[1]; + }); + + afterEach(() => { + mockAirtableInterface.reset(); + }); + + describe('#convertStringToCellValue', () => { + test('request to AirtableInterface: conversion', () => { + field.convertStringToCellValue('hello'); + + expect( + mockAirtableInterface.fieldTypeProvider.convertStringToCellValue, + ).toHaveBeenCalledTimes(1); + expect( + mockAirtableInterface.fieldTypeProvider.convertStringToCellValue, + ).toHaveBeenCalledWith(sdk.__appInterface, 'hello', field._data, { + parseDateCellValueInColumnTimeZone: false, + }); + }); + + test('computed value (no validation applied)', () => { + mockAirtableInterface.fieldTypeProvider.convertStringToCellValue.mockReturnValue( + 'converted value 1', + ); + mockAirtableInterface.fieldTypeProvider.validateCellValueForUpdate.mockReturnValue({ + isValid: false, + reason: '', + }); + mockAirtableInterface.fieldTypeProvider.isComputed.mockReturnValue(true); + + expect(field.convertStringToCellValue('hello')).toBe('converted value 1'); + }); + + test('request to AirtableInterface: validation', () => { + mockAirtableInterface.fieldTypeProvider.convertStringToCellValue.mockReturnValue( + 'converted value 2', + ); + + field.convertStringToCellValue('hello'); + + expect( + mockAirtableInterface.fieldTypeProvider.validateCellValueForUpdate, + ).toHaveBeenCalledTimes(1); + expect( + mockAirtableInterface.fieldTypeProvider.validateCellValueForUpdate, + ).toHaveBeenCalledWith(sdk.__appInterface, 'converted value 2', null, field._data); + }); + + test('non-computed value, passing validation', () => { + mockAirtableInterface.fieldTypeProvider.convertStringToCellValue.mockReturnValue( + 'converted value 3', + ); + mockAirtableInterface.fieldTypeProvider.validateCellValueForUpdate.mockReturnValue({ + isValid: true, + }); + mockAirtableInterface.fieldTypeProvider.isComputed.mockReturnValue(false); + + expect(field.convertStringToCellValue('hello')).toBe('converted value 3'); + }); + + test('non-computed value, passing validation', () => { + mockAirtableInterface.fieldTypeProvider.convertStringToCellValue.mockReturnValue( + 'converted value 4', + ); + mockAirtableInterface.fieldTypeProvider.validateCellValueForUpdate.mockReturnValue({ + isValid: false, + reason: '', + }); + mockAirtableInterface.fieldTypeProvider.isComputed.mockReturnValue(false); + + expect(field.convertStringToCellValue('hello')).toBe(null); + }); + }); + + test('#description', () => { + expect(field.description).toBe('the project client'); + }); + + describe('#isFieldSynced', () => { + test('null', () => { + const newField = makeFieldWithIsSynced(FieldType.SINGLE_SELECT, null); + expect(newField.isFieldSynced).toBe(false); + }); + + test('affirmative', () => { + const newField = makeFieldWithIsSynced(FieldType.SINGLE_SELECT, true); + expect(newField.isFieldSynced).toBe(true); + }); + + test('negative', () => { + const newField = makeFieldWithIsSynced(FieldType.SINGLE_SELECT, false); + expect(newField.isFieldSynced).toBe(false); + }); + }); + + describe('#isComputed', () => { + test('affirmative', () => { + mockAirtableInterface.fieldTypeProvider.isComputed.mockReturnValue(true); + + expect(field.isComputed).toBe(true); + }); + + test('negative', () => { + expect(field.isComputed).toBe(false); + }); + }); + + describe('#isDeleted', () => { + test('affirmative', () => { + mockAirtableInterface.triggerModelUpdates([ + { + path: ['tablesById', 'tblDesignProjects', 'fieldsById', 'fldPrjctClient'], + value: null, + }, + ]); + + expect(field.isDeleted).toBe(true); + }); + + test('negative', () => { + expect(field.isDeleted).toBe(false); + }); + }); + + describe('#isPrimaryField', () => { + test('affirmative', () => { + expect(sdk.base.tables[0].fields[0].isPrimaryField).toBe(true); + }); + + test('negative', () => { + expect(field.isPrimaryField).toBe(false); + }); + }); + + test('#name', () => { + expect(field.name).toBe('Client'); + }); + + describe('#options', () => { + test('no options available', () => { + expect(sdk.base.tables[0].fields[0].options).toBe(null); + }); + + test('options available', () => { + expect(field.options).toStrictEqual({ + foreignTableId: 'tblClients', + relationship: 'many', + symmetricColumnId: 'fldClientProjects', + }); + }); + }); + + describe('#type', () => { + test('lookup type', () => { + mockAirtableInterface.sdkInitData.baseData.tablesById.tblDesignProjects.fieldsById.fldPrjctClient.type = + 'lookup'; + expect(field.type).toBe(FieldType.MULTIPLE_LOOKUP_VALUES); + }); + + test('ensure cache for type updates changes', () => { + mockAirtableInterface.sdkInitData.baseData.tablesById.tblDesignProjects.fieldsById.fldPrjctClient.type = + 'lookup'; + expect(field.type).toBe(FieldType.MULTIPLE_LOOKUP_VALUES); + + mockAirtableInterface.triggerModelUpdates([ + { + path: [ + 'tablesById', + 'tblDesignProjects', + 'fieldsById', + 'fldPrjctClient', + 'type', + ], + value: FieldType.SINGLE_SELECT, + }, + ]); + expect(field.type).toBe(FieldType.SINGLE_SELECT); + }); + + test('other types', () => { + expect(sdk.base.tables[0].fields[3].type).toBe(FieldType.CHECKBOX); + }); + }); + + describe('#config', () => { + test('lookup config', () => { + mockAirtableInterface.sdkInitData.baseData.tablesById.tblDesignProjects.fieldsById.fldPrjctClient.type = + 'lookup'; + expect(field.config).toStrictEqual({ + type: FieldType.MULTIPLE_LOOKUP_VALUES, + options: { + foreignTableId: 'tblClients', + relationship: 'many', + symmetricColumnId: 'fldClientProjects', + }, + }); + }); + }); + + describe('#watch', () => { + let mocks: {[key: string]: jest.Mock}; + + beforeEach(() => { + mocks = { + description: jest.fn(), + isComputed: jest.fn(), + name: jest.fn(), + options: jest.fn(), + type: jest.fn(), + isFieldSynced: jest.fn(), + }; + field.watch('description', mocks.description); + field.watch('isComputed', mocks.isComputed); + field.watch('name', mocks.name); + field.watch('options', mocks.options); + field.watch('type', mocks.type); + field.watch('isFieldSynced', mocks.isFieldSynced); + }); + + test('key: description', () => { + mockAirtableInterface.triggerModelUpdates([ + { + path: [ + 'tablesById', + 'tblDesignProjects', + 'fieldsById', + 'fldPrjctClient', + 'description', + ], + value: 'some other description', + }, + ]); + + expect(mocks.description).toHaveBeenCalledTimes(1); + expect(mocks.isComputed).toHaveBeenCalledTimes(0); + expect(mocks.name).toHaveBeenCalledTimes(0); + expect(mocks.options).toHaveBeenCalledTimes(0); + expect(mocks.type).toHaveBeenCalledTimes(0); + expect(mocks.isFieldSynced).toHaveBeenCalledTimes(0); + }); + + test('key: isComputed', () => { + mockAirtableInterface.triggerModelUpdates([ + { + path: [ + 'tablesById', + 'tblDesignProjects', + 'fieldsById', + 'fldPrjctClient', + 'type', + ], + value: 'select', + }, + ]); + + expect(mocks.description).toHaveBeenCalledTimes(0); + expect(mocks.isComputed).toHaveBeenCalledTimes(1); + expect(mocks.name).toHaveBeenCalledTimes(0); + expect(mocks.options).toHaveBeenCalledTimes(0); + expect(mocks.type).toHaveBeenCalledTimes(1); + expect(mocks.isFieldSynced).toHaveBeenCalledTimes(0); + }); + + test('key: name', () => { + mockAirtableInterface.triggerModelUpdates([ + { + path: [ + 'tablesById', + 'tblDesignProjects', + 'fieldsById', + 'fldPrjctClient', + 'name', + ], + value: 'some other name', + }, + ]); + + expect(mocks.description).toHaveBeenCalledTimes(0); + expect(mocks.isComputed).toHaveBeenCalledTimes(0); + expect(mocks.name).toHaveBeenCalledTimes(1); + expect(mocks.options).toHaveBeenCalledTimes(0); + expect(mocks.type).toHaveBeenCalledTimes(0); + expect(mocks.isFieldSynced).toHaveBeenCalledTimes(0); + }); + + test('key: options', () => { + mockAirtableInterface.triggerModelUpdates([ + { + path: [ + 'tablesById', + 'tblDesignProjects', + 'fieldsById', + 'fldPrjctClient', + 'typeOptions', + ], + value: {}, + }, + ]); + + expect(mocks.description).toHaveBeenCalledTimes(0); + expect(mocks.isComputed).toHaveBeenCalledTimes(0); + expect(mocks.name).toHaveBeenCalledTimes(0); + expect(mocks.options).toHaveBeenCalledTimes(1); + expect(mocks.type).toHaveBeenCalledTimes(0); + expect(mocks.isFieldSynced).toHaveBeenCalledTimes(0); + }); + + test('key: type', () => { + mockAirtableInterface.triggerModelUpdates([ + { + path: [ + 'tablesById', + 'tblDesignProjects', + 'fieldsById', + 'fldPrjctClient', + 'type', + ], + value: 'select', + }, + ]); + + expect(mocks.description).toHaveBeenCalledTimes(0); + expect(mocks.isComputed).toHaveBeenCalledTimes(1); + expect(mocks.name).toHaveBeenCalledTimes(0); + expect(mocks.options).toHaveBeenCalledTimes(0); + expect(mocks.type).toHaveBeenCalledTimes(1); + expect(mocks.isFieldSynced).toHaveBeenCalledTimes(0); + }); + + test('key: isFieldSynced', () => { + mockAirtableInterface.triggerModelUpdates([ + { + path: [ + 'tablesById', + 'tblDesignProjects', + 'fieldsById', + 'fldPrjctClient', + 'isSynced', + ], + value: 'true', + }, + ]); + + expect(mocks.description).toHaveBeenCalledTimes(0); + expect(mocks.isComputed).toHaveBeenCalledTimes(0); + expect(mocks.name).toHaveBeenCalledTimes(0); + expect(mocks.options).toHaveBeenCalledTimes(0); + expect(mocks.type).toHaveBeenCalledTimes(0); + expect(mocks.isFieldSynced).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/packages/sdk/test/interface/models/mutations.test.ts b/packages/sdk/test/interface/models/mutations.test.ts new file mode 100644 index 000000000..fcda94832 --- /dev/null +++ b/packages/sdk/test/interface/models/mutations.test.ts @@ -0,0 +1,1090 @@ +import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; +import {type Base} from '../../../src/interface/models/base'; +import {Mutations} from '../../../src/interface/models/mutations'; +import {InterfaceBlockSdk} from '../../../src/interface/sdk'; +import {type ModelChange} from '../../../src/shared/types/base_core'; +import {MutationTypes} from '../../../src/interface/types/mutations'; +import {Session} from '../../../src/interface/models/session'; +import {type FieldId, type RecordId} from '../../../src/shared/types/hyper_ids'; +import {type ObjectMap} from '../../../src/shared/private_utils'; + +const mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); +jest.mock('../../../src/injected/airtable_interface', () => ({ + __esModule: true, + default: () => mockAirtableInterface, +})); + +jest.mock('../../../src/shared/types/mutation_constants', () => ({ + MAX_NUM_FIELDS_PER_TABLE: 10, + MAX_FIELD_NAME_LENGTH: 20, + MAX_FIELD_DESCRIPTION_LENGTH: 50, + MAX_TABLE_NAME_LENGTH: 20, +})); + +const compareUpdatePath = (a: ModelChange, b: ModelChange) => { + return a.path.join('') > b.path.join('') ? 1 : -1; +}; + +const createdTime = new Date().toJSON(); +const mockRecordsById = { + recA: {id: 'recA', cellValuesByFieldId: {fldMockLookup: null}, createdTime}, + recB: {id: 'recB', cellValuesByFieldId: {fldMockLookup: null}, createdTime}, + recC: {id: 'recC', cellValuesByFieldId: {fldMockLookup: null}, createdTime}, +}; + +describe('Mutations (Interface)', () => { + let base: Base; + let mutations: Mutations; + let applyModelChanges: jest.Mock; + let applyGlobalConfigUpdates: jest.Mock; + + beforeEach(() => { + const sdk = new InterfaceBlockSdk(mockAirtableInterface); + base = sdk.base; + + const session = new Session(sdk); + applyModelChanges = jest.fn(); + applyGlobalConfigUpdates = jest.fn(); + + mutations = new Mutations(sdk, session, base, applyModelChanges, applyGlobalConfigUpdates); + }); + + afterEach(() => { + mockAirtableInterface.reset(); + }); + + const makeRecord = ( + id: RecordId, + cellValuesByFieldId: ObjectMap, + createdTime: string, + ) => { + const baseData = mockAirtableInterface.sdkInitData.baseData; + const parentTableData = baseData.tablesById.tblDesignProjects; + parentTableData.recordsById[id] = { + id, + cellValuesByFieldId, + createdTime, + }; + parentTableData.recordOrder.push(id); + + const parentRecordStore = base.__getRecordStore(parentTableData.id); + + const newRecord = parentRecordStore.getRecordByIdIfExists(id); + + return newRecord!; + }; + + describe('_assertMutationIsValid', () => { + describe('SET_MULTIPLE_RECORDS_CELL_VALUES', () => { + it('throws error when table does not exist', () => { + const mutation = { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblNonExistent', + records: [], + }; + + expect(() => mutations._assertMutationIsValid(mutation)).toThrow( + "Can't set cell values: No table with id tblNonExistent exists", + ); + }); + + it('throws error when field does not exist', () => { + makeRecord('recA', {}, ''); + + const mutation = { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recA', + cellValuesByFieldId: { + fldNonExistent: 'value', + }, + }, + ], + }; + + expect(() => mutations._assertMutationIsValid(mutation)).toThrow( + "Can't set cell values: No field with id fldNonExistent exists in table 'Design projects'", + ); + }); + + it('throws error when field is computed', () => { + makeRecord('recA', {}, ''); + mockAirtableInterface.fieldTypeProvider.isComputed.mockReturnValue(true); + + const mutation = { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recA', + cellValuesByFieldId: { + fldPrjctCtgry: 'value', + }, + }, + ], + }; + + expect(() => mutations._assertMutationIsValid(mutation)).toThrow( + "Can't set cell values: Field 'Category' is computed and cannot be set", + ); + + mockAirtableInterface.fieldTypeProvider.isComputed.mockReturnValue(false); + }); + + it('throws error when record does not exist and record store is ready', () => { + const mutation = { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recNonExistent', + cellValuesByFieldId: { + fldPrjctCtgry: 'value', + }, + }, + ], + }; + + expect(() => mutations._assertMutationIsValid(mutation)).toThrow( + "Can't set cell values: No record with id recNonExistent exists", + ); + }); + + it('throws error when cell value is invalid for existing record', () => { + makeRecord('recA', {}, ''); + + mockAirtableInterface.fieldTypeProvider.validateCellValueForUpdate.mockReturnValue({ + isValid: false, + reason: 'Invalid value type', + }); + + const mutation = { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recA', + cellValuesByFieldId: { + fldPrjctCtgry: 'invalid', + }, + }, + ], + }; + + expect(() => mutations._assertMutationIsValid(mutation)).toThrow( + "Can't set cell values: invalid cell value for field 'Category'.\nInvalid value type", + ); + + mockAirtableInterface.fieldTypeProvider.validateCellValueForUpdate.mockReturnValue({ + isValid: true, + }); + }); + + it('succeeds with valid mutation', () => { + makeRecord('recA', {}, ''); + + const mutation = { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recA', + cellValuesByFieldId: { + fldPrjctCtgry: 'valid', + }, + }, + ], + }; + + expect(() => mutations._assertMutationIsValid(mutation)).not.toThrow(); + }); + }); + + describe('DELETE_MULTIPLE_RECORDS', () => { + it('throws error when table does not exist', () => { + const mutation = { + type: MutationTypes.DELETE_MULTIPLE_RECORDS, + tableId: 'tblNonExistent', + recordIds: ['recA'], + }; + + expect(() => mutations._assertMutationIsValid(mutation)).toThrow( + "Can't delete records: No table with id tblNonExistent exists", + ); + }); + + it('throws error when record does not exist and record store is ready', () => { + const mutation = { + type: MutationTypes.DELETE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + recordIds: ['recNonExistent'], + }; + + expect(() => mutations._assertMutationIsValid(mutation)).toThrow( + "Can't delete records: No record with id recNonExistent exists in table 'Design projects'", + ); + }); + + it('succeeds with valid mutation', () => { + makeRecord('recA', {}, ''); + + const mutation = { + type: MutationTypes.DELETE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + recordIds: ['recA'], + }; + + expect(() => mutations._assertMutationIsValid(mutation)).not.toThrow(); + }); + }); + + describe('CREATE_MULTIPLE_RECORDS', () => { + it('throws error when table does not exist', () => { + const mutation = { + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblNonExistent', + records: [], + }; + + expect(() => mutations._assertMutationIsValid(mutation)).toThrow( + "Can't create records: No table with id tblNonExistent exists", + ); + }); + + it('throws error when field does not exist', () => { + const mutation = { + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recNew', + cellValuesByFieldId: { + fldNonExistent: 'value', + }, + }, + ], + }; + + expect(() => mutations._assertMutationIsValid(mutation)).toThrow( + "Can't create records: No field with id fldNonExistent exists in table 'Design projects'", + ); + }); + + it('throws error when field is computed', () => { + mockAirtableInterface.fieldTypeProvider.isComputed.mockReturnValue(true); + + const mutation = { + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recNew', + cellValuesByFieldId: { + fldPrjctCtgry: 'value', + }, + }, + ], + }; + + expect(() => mutations._assertMutationIsValid(mutation)).toThrow( + "Can't set cell values: Field 'Category' is computed and cannot be set", + ); + + mockAirtableInterface.fieldTypeProvider.isComputed.mockReturnValue(false); + }); + + it('throws error when cell value is invalid', () => { + mockAirtableInterface.fieldTypeProvider.validateCellValueForUpdate.mockReturnValue({ + isValid: false, + reason: 'Invalid value for new record', + }); + + const mutation = { + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recNew', + cellValuesByFieldId: { + fldPrjctCtgry: 'invalid', + }, + }, + ], + }; + + expect(() => mutations._assertMutationIsValid(mutation)).toThrow( + "Can't create records: invalid cell value for field 'Category'.\nInvalid value for new record", + ); + + mockAirtableInterface.fieldTypeProvider.validateCellValueForUpdate.mockReturnValue({ + isValid: true, + }); + }); + + it('succeeds with valid mutation', () => { + const mutation = { + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recNew', + cellValuesByFieldId: { + fldPrjctCtgry: 'valid', + }, + }, + ], + }; + + expect(() => mutations._assertMutationIsValid(mutation)).not.toThrow(); + }); + }); + + describe('SET_MULTIPLE_GLOBAL_CONFIG_PATHS', () => { + it('succeeds without validation (handled by globalConfig)', () => { + const mutation = { + type: MutationTypes.SET_MULTIPLE_GLOBAL_CONFIG_PATHS, + updates: [ + { + path: ['test', 'path'], + value: 'test value', + }, + ], + }; + + expect(() => mutations._assertMutationIsValid(mutation)).not.toThrow(); + }); + }); + + describe('unhandled mutation type', () => { + it('throws error for unknown mutation type', () => { + const mutation = { + type: 'UNKNOWN_TYPE' as any, + tableId: 'tblDesignProjects', + records: [], + } as any; + + expect(() => mutations._assertMutationIsValid(mutation)).toThrow( + 'unhandled mutation type: UNKNOWN_TYPE', + ); + }); + }); + }); + + describe('_getOptimisticModelChangesForMutation', () => { + describe('CREATE_MULTIPLE_RECORDS', () => { + it('uses default optimistic behavior for CREATE_MULTIPLE_RECORDS', () => { + const mutation = { + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recNew1', + cellValuesByFieldId: { + fldPrjctName: 'New Project', + }, + }, + ], + }; + + const modelChanges = mutations._getOptimisticModelChangesForMutation(mutation); + + expect(modelChanges).toBeDefined(); + expect(Array.isArray(modelChanges)).toBe(true); + }); + }); + + describe('DELETE_MULTIPLE_RECORDS', () => { + beforeEach(() => { + const recordStore = { + recordIds: ['rec1', 'rec2', 'rec3', 'rec4'], + }; + jest.spyOn(base, '__getRecordStore').mockReturnValue(recordStore as any); + }); + + it('optimistically removes records from recordOrder and calls super', () => { + const mutation = { + type: MutationTypes.DELETE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + recordIds: ['rec2', 'rec4'], + }; + + const modelChanges = mutations._getOptimisticModelChangesForMutation(mutation); + + const recordOrderChange = modelChanges.find((change) => + change.path.includes('recordOrder'), + ); + expect(recordOrderChange).toBeDefined(); + expect(recordOrderChange!.path).toEqual([ + 'tablesById', + 'tblDesignProjects', + 'recordOrder', + ]); + expect(recordOrderChange!.value).toEqual(['rec1', 'rec3']); + + expect(modelChanges.length).toBeGreaterThan(1); + }); + + it('handles empty recordIds gracefully', () => { + const mutation = { + type: MutationTypes.DELETE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + recordIds: [], + }; + + const modelChanges = mutations._getOptimisticModelChangesForMutation(mutation); + + const recordOrderChange = modelChanges.find((change) => + change.path.includes('recordOrder'), + ); + expect(recordOrderChange).toBeDefined(); + expect(recordOrderChange!.value).toEqual(['rec1', 'rec2', 'rec3', 'rec4']); + }); + + it('handles deleting all records', () => { + const mutation = { + type: MutationTypes.DELETE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + recordIds: ['rec1', 'rec2', 'rec3', 'rec4'], + }; + + const modelChanges = mutations._getOptimisticModelChangesForMutation(mutation); + + const recordOrderChange = modelChanges.find((change) => + change.path.includes('recordOrder'), + ); + expect(recordOrderChange).toBeDefined(); + expect(recordOrderChange!.value).toEqual([]); + }); + }); + + describe('other mutation types', () => { + it('delegates to super for other mutation types', () => { + const mutation = { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [ + { + id: 'rec1', + cellValuesByFieldId: { + fldPrjctName: 'Updated Project', + }, + }, + ], + }; + + const modelChanges = mutations._getOptimisticModelChangesForMutation( + mutation as any, + ); + + expect(modelChanges).toBeDefined(); + expect(Array.isArray(modelChanges)).toBe(true); + }); + }); + }); + + describe('applyMutationAsync', () => { + describe('SET_MULTIPLE_RECORDS_CELL_VALUES', () => { + const validRecord = { + id: 'recA', + cellValuesByFieldId: { + fldPrjctCtgry: 9, + }, + }; + + it('checks that the table exists', async () => { + await expect( + mutations.applyMutationAsync({ + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblNonExistentTableId', + records: [], + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Can't set cell values: No table with id tblNonExistentTableId exists"`, + ); + expect(applyModelChanges.mock.calls.length).toBe(0); + }); + + it('checks that the field exists', async () => { + makeRecord(validRecord.id, validRecord.cellValuesByFieldId, ''); + + await expect( + mutations.applyMutationAsync({ + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [{id: validRecord.id, cellValuesByFieldId: {fldNonExistent: {}}}], + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Can't set cell values: No field with id fldNonExistent exists in table 'Design projects'"`, + ); + expect(applyModelChanges.mock.calls.length).toBe(0); + }); + + it('checks that the field is not computed', async () => { + makeRecord(validRecord.id, validRecord.cellValuesByFieldId, ''); + + mockAirtableInterface.fieldTypeProvider.isComputed.mockReturnValue(true); + await expect( + mutations.applyMutationAsync({ + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [{id: validRecord.id, cellValuesByFieldId: {fldPrjctCtgry: {}}}], + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Can't set cell values: Field 'Category' is computed and cannot be set"`, + ); + expect(applyModelChanges.mock.calls.length).toBe(0); + }); + + it('fails when batch size limit is exceeded', async () => { + makeRecord(validRecord.id, validRecord.cellValuesByFieldId, ''); + + await expect( + mutations.applyMutationAsync({ + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: Array.from(new Array(51)).map(() => validRecord), + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Request exceeds maximum batch size limit of 50 items"`, + ); + expect(applyModelChanges.mock.calls.length).toBe(0); + }); + + it('fails when permission is denied', async () => { + makeRecord(validRecord.id, validRecord.cellValuesByFieldId, ''); + + const mutation = { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [validRecord], + }; + mockAirtableInterface.checkPermissionsForMutation.mockReturnValue({ + hasPermission: false, + reasonDisplayString: 'mock reason', + }); + + await expect( + mutations.applyMutationAsync(mutation), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Cannot apply setMultipleRecordsCellValues mutation: mock reason"`, + ); + + expect(mockAirtableInterface.checkPermissionsForMutation.mock.calls.length).toBe(1); + expect( + mockAirtableInterface.checkPermissionsForMutation.mock.calls[0][0], + ).toStrictEqual(mutation); + expect(applyModelChanges.mock.calls.length).toBe(0); + }); + + it('succeeds when input is valid', async () => { + makeRecord(validRecord.id, validRecord.cellValuesByFieldId, ''); + + await mutations.applyMutationAsync({ + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: Array.from(new Array(50)).map(() => validRecord), + }); + expect(applyModelChanges.mock.calls.length).toBe(1); + }); + + describe('loaded records', () => { + beforeEach(() => { + Object.values(mockRecordsById).forEach((record) => { + makeRecord(record.id, record.cellValuesByFieldId, record.createdTime); + }); + }); + + it('checks that the record exists', async () => { + expect( + mutations.applyMutationAsync({ + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recNonExistent', + cellValuesByFieldId: { + fldNonExistent: {}, + }, + }, + ], + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Can't set cell values: No record with id recNonExistent exists"`, + ); + expect(applyModelChanges.mock.calls.length).toBe(0); + }); + + it('checks that value is valid', async () => { + mockAirtableInterface.fieldTypeProvider.validateCellValueForUpdate.mockReturnValue( + {isValid: false, reason: 'Mock reason'}, + ); + await expect( + mutations.applyMutationAsync({ + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recA', + cellValuesByFieldId: { + fldPrjctCtgry: 9, + }, + }, + ], + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` +"Can't set cell values: invalid cell value for field 'Category'. +Mock reason" +`); + expect(applyModelChanges.mock.calls.length).toBe(0); + }); + + it('succeeds when input is valid', async () => { + await mutations.applyMutationAsync({ + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [validRecord], + }); + expect(applyModelChanges.mock.calls.length).toBe(1); + }); + }); + }); + + describe('DELETE_MULTIPLE_RECORDS', () => { + it('checks that the table exists', async () => { + expect( + mutations.applyMutationAsync({ + type: MutationTypes.DELETE_MULTIPLE_RECORDS, + tableId: 'tblNonExistentTableId', + recordIds: [], + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Can't delete records: No table with id tblNonExistentTableId exists"`, + ); + expect(applyModelChanges.mock.calls.length).toBe(0); + }); + + it('fails when batch size limit is exceeded', async () => { + const recA = mockRecordsById.recA; + makeRecord(recA.id, recA.cellValuesByFieldId, recA.createdTime); + + await expect( + mutations.applyMutationAsync({ + type: MutationTypes.DELETE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + recordIds: Array.from(new Array(51)).map(() => 'recA'), + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Request exceeds maximum batch size limit of 50 items"`, + ); + expect(applyModelChanges.mock.calls.length).toBe(0); + }); + + it('fails when permission is denied', async () => { + const recA = mockRecordsById.recA; + makeRecord(recA.id, recA.cellValuesByFieldId, recA.createdTime); + + const mutation = { + type: MutationTypes.DELETE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + recordIds: ['recA'], + }; + mockAirtableInterface.checkPermissionsForMutation.mockReturnValue({ + hasPermission: false, + reasonDisplayString: 'mock reason', + }); + + await expect( + mutations.applyMutationAsync(mutation), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Cannot apply deleteMultipleRecords mutation: mock reason"`, + ); + + expect(mockAirtableInterface.checkPermissionsForMutation.mock.calls.length).toBe(1); + expect( + mockAirtableInterface.checkPermissionsForMutation.mock.calls[0][0], + ).toStrictEqual(mutation); + expect(applyModelChanges.mock.calls.length).toBe(0); + }); + + it('succeeds when input is valid', async () => { + const recA = mockRecordsById.recA; + makeRecord(recA.id, recA.cellValuesByFieldId, recA.createdTime); + + await mutations.applyMutationAsync({ + type: MutationTypes.DELETE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + recordIds: Array.from(new Array(50)).map(() => 'recA'), + }); + + expect(applyModelChanges.mock.calls.length).toBe(1); + }); + + describe('loaded records', () => { + beforeEach(async () => { + Object.values(mockRecordsById).forEach((record) => { + makeRecord(record.id, record.cellValuesByFieldId, record.createdTime); + }); + }); + + it('checks that the record exists', async () => { + await expect( + mutations.applyMutationAsync({ + type: MutationTypes.DELETE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + recordIds: ['recNonExistent'], + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Can't delete records: No record with id recNonExistent exists in table 'Design projects'"`, + ); + expect(applyModelChanges.mock.calls.length).toBe(0); + }); + + it('succeeds when input is valid', async () => { + const recA = mockRecordsById.recA; + makeRecord(recA.id, recA.cellValuesByFieldId, recA.createdTime); + const recC = mockRecordsById.recC; + makeRecord(recC.id, recC.cellValuesByFieldId, recC.createdTime); + + await mutations.applyMutationAsync({ + type: MutationTypes.DELETE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + recordIds: ['recA', 'recC'], + }); + expect(applyModelChanges.mock.calls.length).toBe(1); + expect( + applyModelChanges.mock.calls[0][0].sort(compareUpdatePath), + ).toStrictEqual([ + { + path: ['tablesById', 'tblDesignProjects', 'recordOrder'], + value: ['recB'], + }, + { + path: ['tablesById', 'tblDesignProjects', 'recordsById', 'recA'], + value: undefined, + }, + { + path: ['tablesById', 'tblDesignProjects', 'recordsById', 'recC'], + value: undefined, + }, + ]); + }); + }); + }); + + describe('CREATE_MULTIPLE_RECORDS', () => { + const validRecord = { + id: 'recD', + cellValuesByFieldId: { + fldPrjctName: 9, + fldPrjctCtgry: 10, + }, + }; + + it('checks that the table exists', async () => { + await expect( + mutations.applyMutationAsync({ + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblNonExistentTableId', + records: [], + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Can't create records: No table with id tblNonExistentTableId exists"`, + ); + expect(applyModelChanges.mock.calls.length).toBe(0); + }); + + it('checks that the field exists', async () => { + await expect( + mutations.applyMutationAsync({ + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recNonExistent', + cellValuesByFieldId: { + fldNonExistent: {}, + }, + }, + ], + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Can't create records: No field with id fldNonExistent exists in table 'Design projects'"`, + ); + expect(applyModelChanges.mock.calls.length).toBe(0); + }); + + it('checks that the field is not computed', async () => { + mockAirtableInterface.fieldTypeProvider.isComputed.mockReturnValue(true); + await expect( + mutations.applyMutationAsync({ + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recNonExistent', + cellValuesByFieldId: { + fldPrjctCtgry: {}, + }, + }, + ], + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Can't set cell values: Field 'Category' is computed and cannot be set"`, + ); + expect(applyModelChanges.mock.calls.length).toBe(0); + }); + + it('checks that value is valid', async () => { + mockAirtableInterface.fieldTypeProvider.validateCellValueForUpdate.mockReturnValue({ + isValid: false, + reason: 'Mock reason', + }); + await expect( + mutations.applyMutationAsync({ + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recA', + cellValuesByFieldId: { + fldPrjctCtgry: 9, + }, + }, + ], + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` +"Can't create records: invalid cell value for field 'Category'. +Mock reason" +`); + expect(applyModelChanges.mock.calls.length).toBe(0); + }); + + it('fails when batch size limit is exceeded', async () => { + await expect( + mutations.applyMutationAsync({ + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: Array.from(new Array(51)).map(() => validRecord), + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Request exceeds maximum batch size limit of 50 items"`, + ); + expect(applyModelChanges.mock.calls.length).toBe(0); + }); + + it('fails when permission is denied', async () => { + const mutation = { + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: [validRecord], + }; + mockAirtableInterface.checkPermissionsForMutation.mockReturnValue({ + hasPermission: false, + reasonDisplayString: 'mock reason', + }); + + await expect( + mutations.applyMutationAsync(mutation), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Cannot apply createMultipleRecords mutation: mock reason"`, + ); + + expect(mockAirtableInterface.checkPermissionsForMutation.mock.calls.length).toBe(1); + expect( + mockAirtableInterface.checkPermissionsForMutation.mock.calls[0][0], + ).toStrictEqual(mutation); + expect(applyModelChanges.mock.calls.length).toBe(0); + }); + + it('succeeds when input is valid', async () => { + await mutations.applyMutationAsync({ + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: Array.from(new Array(50)).map(() => validRecord), + }); + expect(applyModelChanges.mock.calls.length).toBe(1); + }); + + describe('loaded records', () => { + beforeEach(async () => { + Object.values(mockRecordsById).forEach((record) => { + makeRecord(record.id, record.cellValuesByFieldId, record.createdTime); + }); + }); + + it('succeeds when input is valid', async () => { + await mutations.applyMutationAsync({ + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: [validRecord], + }); + expect(applyModelChanges.mock.calls.length).toBe(1); + const changes = applyModelChanges.mock.calls[0][0].sort(compareUpdatePath); + expect(Date.parse(changes[0].value.createdTime)).toBeTruthy(); + changes[0].value.createdTime = ''; + expect(changes).toStrictEqual([ + { + path: [ + 'tablesById', + 'tblDesignProjects', + 'recordsById', + validRecord.id, + ], + value: { + id: validRecord.id, + cellValuesByFieldId: validRecord.cellValuesByFieldId, + createdTime: '', + }, + }, + ]); + }); + }); + }); + + describe('SET_MULTIPLE_GLOBAL_CONFIG_PATHS', () => { + const validUpdate = { + path: ['foo'], + value: 'bar', + }; + + it('fails when permission is denied', async () => { + const mutation = { + type: MutationTypes.SET_MULTIPLE_GLOBAL_CONFIG_PATHS, + tableId: 'tblDesignProjects', + updates: [], + }; + mockAirtableInterface.checkPermissionsForMutation.mockReturnValue({ + hasPermission: false, + reasonDisplayString: 'mock reason', + }); + + await expect( + mutations.applyMutationAsync(mutation), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Cannot apply setMultipleGlobalConfigPaths mutation: mock reason"`, + ); + + expect(mockAirtableInterface.checkPermissionsForMutation.mock.calls.length).toBe(1); + expect( + mockAirtableInterface.checkPermissionsForMutation.mock.calls[0][0], + ).toStrictEqual(mutation); + expect(applyGlobalConfigUpdates.mock.calls.length).toBe(0); + }); + + it('fails when batch size limit is exceeded', async () => { + await expect( + mutations.applyMutationAsync({ + type: MutationTypes.SET_MULTIPLE_GLOBAL_CONFIG_PATHS, + updates: Array.from(new Array(51)).map(() => validUpdate), + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Request exceeds maximum batch size limit of 50 items"`, + ); + expect(applyGlobalConfigUpdates.mock.calls.length).toBe(0); + }); + + it('forwards updates to injected function', () => { + mutations.applyMutationAsync({ + type: MutationTypes.SET_MULTIPLE_GLOBAL_CONFIG_PATHS, + updates: [validUpdate], + }); + expect(applyGlobalConfigUpdates.mock.calls.length).toBe(1); + expect(applyGlobalConfigUpdates.mock.calls[0].length).toBe(1); + expect(applyGlobalConfigUpdates.mock.calls[0][0]).toStrictEqual([validUpdate]); + }); + }); + + it('rejects enormous mutations', async () => { + const recA = mockRecordsById.recA; + makeRecord(recA.id, recA.cellValuesByFieldId, recA.createdTime); + + await expect( + mutations.applyMutationAsync({ + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recA', + cellValuesByFieldId: { + fldPrjctCtgry: 'x'.repeat(2 * 2 ** 20), + }, + }, + ], + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Request exceeds maximum size limit of 1992294.4 bytes"`, + ); + expect(applyModelChanges.mock.calls.length).toBe(0); + }); + + it('rejects unrecognized mutation types', async () => { + const circular: {[key: string]: object} = {}; + circular.circular = circular; + + await expect( + mutations.applyMutationAsync({ + type: 'foo' as typeof MutationTypes.DELETE_MULTIPLE_RECORDS, + tableId: 'tblNonExistentTableId', + recordIds: [circular as unknown as string], + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(`"unhandled mutation type: foo"`); + expect(applyModelChanges.mock.calls.length).toBe(0); + }); + + it('rejects when synchronization failures occur and no state changes have been optimistically applied', async () => { + }); + + describe('uncaught exception after optimistic update', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + }); + + function flushPromises() { + return new Promise((resolve) => setImmediate(resolve)); + } + + it('produces an uncaught exception when synchronization failures occur and a state change has been optimistically applied', async () => { + const recA = mockRecordsById.recA; + makeRecord(recA.id, recA.cellValuesByFieldId, recA.createdTime); + + mockAirtableInterface.applyMutationAsync.mockRejectedValue(new Error('foobar')); + + mutations + .applyMutationAsync({ + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recA', + cellValuesByFieldId: { + fldPrjctCtgry: 9, + }, + }, + ], + }) + .then(() => { + // eslint-disable-next-line @airtable/blocks/no-throw-new + throw new Error('Unexpected fulfillment'); + }); + + await flushPromises(); + + let wasErrorCaught = false; + try { + jest.runAllTimers(); + } catch (e) { + wasErrorCaught = true; + expect(e).toMatchInlineSnapshot(`[Error: foobar]`); + } + expect(wasErrorCaught).toBe(true); + }); + }); + }); +}); diff --git a/packages/sdk/test/interface/models/record.test.ts b/packages/sdk/test/interface/models/record.test.ts new file mode 100644 index 000000000..a7dfa7c5d --- /dev/null +++ b/packages/sdk/test/interface/models/record.test.ts @@ -0,0 +1,922 @@ +import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; +import {InterfaceBlockSdk} from '../../../src/interface/sdk'; +import AbstractModel from '../../../src/shared/models/abstract_model'; +import {type Base} from '../../../src/interface/models/base'; +import {Record} from '../../../src/interface/models/record'; +import {type Table} from '../../../src/interface/models/table'; +import {type ObjectMap} from '../../../src/shared/private_utils'; + +const mockAirtableInterface = MockAirtableInterface.linkedRecordsExample(); +jest.mock('../../../src/injected/airtable_interface', () => ({ + __esModule: true, + default: () => mockAirtableInterface, +})); + +describe('Record', () => { + let sdk: InterfaceBlockSdk; + let base: Base; + let table: Table; + let recordA: Record; + let recordB: Record; + let recordC: Record; + + const createdTime = '2020-10-23T16:34:04.281Z'; + + const makeRecord = (recordId: string, cellValuesByFieldId: ObjectMap) => { + const baseData = mockAirtableInterface.sdkInitData.baseData; + const parentTableData = baseData.tablesById.tblFirst; + parentTableData.recordsById[recordId] = { + id: recordId, + cellValuesByFieldId, + createdTime, + }; + parentTableData.recordOrder.push(recordId); + + const parentRecordStore = sdk.base.__getRecordStore(parentTableData.id); + + const newRecord = parentRecordStore.getRecordByIdIfExists(recordId); + + return newRecord!; + }; + + beforeEach(async () => { + sdk = new InterfaceBlockSdk(mockAirtableInterface); + base = sdk.base; + table = base.getTable('First Table'); + + recordA = makeRecord('recA', { + fld1stPrimary: 'Bonjour!', + fld1stLinked: {id: 'recB'}, + fldMockLookup: null, + }); + recordB = makeRecord('recB', { + fld1stPrimary: 'Hello!', + fld1stLinked: {id: 'recC'}, + fldMockLookup: { + linkedRecordIds: ['recLink1', 'recLink2', 'recLink3', 'recLink4'], + valuesByLinkedRecordId: { + recLink1: null, + recLink2: 'abc123', + recLink3: {id: 'sel123abc', name: 'recLink3'}, + recLink4: [1, 2, 3], + }, + }, + }); + recordC = makeRecord('recC', { + fld1stPrimary: '¡Hola!', + fld1stLinked: {id: 'recA'}, + fldMockLookup: { + linkedRecordIds: ['recLink1', 'recLink2', 'recLink3', 'recLink4'], + valuesByLinkedRecordId: { + recLink1: null, + recLink2: 'XYZ', + recLink3: {id: 'selFooBarBaz', name: 'recLink3'}, + recLink4: [1, 2, 3], + }, + }, + }); + }); + + afterEach(() => { + mockAirtableInterface.reset(); + }); + + describe('constructor', () => { + test('instance of Record', () => { + expect(recordA).toBeInstanceOf(Record); + }); + test('Record subclass of AbstractModel', () => { + expect(recordA).toBeInstanceOf(AbstractModel); + }); + }); + + describe('internal', () => { + test('throws when record is missing', () => { + expect(recordA.isDeleted).toBe(false); + + mockAirtableInterface.triggerModelUpdates([ + { + path: ['tablesById', 'tblFirst', 'recordsById', 'recA'], + value: undefined, + }, + ]); + + expect(recordA.isDeleted).toBe(true); + }); + + test('throws when table data is missing', () => { + mockAirtableInterface.triggerModelUpdates([ + { + path: ['tablesById', 'tblFirst'], + value: null, + }, + { + path: ['tableOrder'], + value: ['tblSecond'], + }, + { + path: ['activeTableId'], + value: 'tblSecond', + }, + ]); + }); + }); + + describe('properties', () => { + test('#createdTime', () => { + expect(recordA.createdTime.toISOString()).toBe(createdTime); + expect(() => { + // @ts-ignore + recordA.createdTime = 1; + }).toThrowErrorMatchingInlineSnapshot( + `"Cannot set property createdTime of [object Object] which has only a getter"`, + ); + expect(recordA.createdTime.toISOString()).toBe(createdTime); + }); + test('#id', () => { + expect(recordA.id).toBe('recA'); + expect(() => { + // @ts-ignore + recordA.id = 1; + }).toThrowErrorMatchingInlineSnapshot( + `"Cannot set property id of [object Object] which has only a getter"`, + ); + expect(recordA.id).toBe('recA'); + }); + test('#isDeleted', () => { + expect(recordA.isDeleted).toBe(false); + expect(() => { + // @ts-ignore + recordA.isDeleted = 1; + }).toThrowErrorMatchingInlineSnapshot( + `"Cannot set property isDeleted of [object Object] which has only a getter"`, + ); + expect(recordA.isDeleted).toBe(false); + + mockAirtableInterface.triggerModelUpdates([ + { + path: ['tablesById', 'tblFirst', 'recordsById', 'recA'], + value: undefined, + }, + ]); + + expect(recordA.isDeleted).toBe(true); + }); + test('#name', () => { + expect(recordA.name).toBe(''); + expect(() => { + // @ts-ignore + recordA.name = 1; + }).toThrowErrorMatchingInlineSnapshot( + `"Cannot set property name of [object Object] which has only a getter"`, + ); + expect(recordA.name).toBe(''); + }); + }); + + describe('methods', () => { + describe('#getCellValue()', () => { + test('#getCellValue(fieldOrFieldIdOrFieldName), Field', () => { + expect(recordA.getCellValue(table.fields[0])).toBe('Bonjour!'); + expect(recordB.getCellValue(table.fields[0])).toBe('Hello!'); + expect(recordC.getCellValue(table.fields[0])).toBe('¡Hola!'); + expect(recordA.getCellValue(table.fields[1])).toMatchObject({id: 'recB'}); + expect(recordB.getCellValue(table.fields[1])).toMatchObject({id: 'recC'}); + expect(recordC.getCellValue(table.fields[1])).toMatchObject({id: 'recA'}); + expect(recordA.getCellValue(table.fields[2])).toBe(null); + expect(recordB.getCellValue(table.fields[2])).toMatchInlineSnapshot(` + Array [ + Object { + "linkedRecordId": "recLink1", + "value": null, + }, + Object { + "linkedRecordId": "recLink2", + "value": "abc123", + }, + Object { + "linkedRecordId": "recLink3", + "value": Object { + "id": "sel123abc", + "name": "recLink3", + }, + }, + Object { + "linkedRecordId": "recLink4", + "value": 1, + }, + Object { + "linkedRecordId": "recLink4", + "value": 2, + }, + Object { + "linkedRecordId": "recLink4", + "value": 3, + }, + ] + `); + expect(recordC.getCellValue(table.fields[2])).toMatchInlineSnapshot(` + Array [ + Object { + "linkedRecordId": "recLink1", + "value": null, + }, + Object { + "linkedRecordId": "recLink2", + "value": "XYZ", + }, + Object { + "linkedRecordId": "recLink3", + "value": Object { + "id": "selFooBarBaz", + "name": "recLink3", + }, + }, + Object { + "linkedRecordId": "recLink4", + "value": 1, + }, + Object { + "linkedRecordId": "recLink4", + "value": 2, + }, + Object { + "linkedRecordId": "recLink4", + "value": 3, + }, + ] + `); + }); + + test('#getCellValue(fieldOrFieldIdOrFieldName), FieldId', () => { + const fld1stPrimary = 'fld1stPrimary'; + const fld1stLinked = 'fld1stLinked'; + const fldMockLookup = 'fldMockLookup'; + + expect(recordA.getCellValue(fld1stPrimary)).toBe('Bonjour!'); + expect(recordB.getCellValue(fld1stPrimary)).toBe('Hello!'); + expect(recordC.getCellValue(fld1stPrimary)).toBe('¡Hola!'); + expect(recordA.getCellValue(fld1stLinked)).toMatchObject({id: 'recB'}); + expect(recordB.getCellValue(fld1stLinked)).toMatchObject({id: 'recC'}); + expect(recordC.getCellValue(fld1stLinked)).toMatchObject({id: 'recA'}); + expect(recordA.getCellValue(fldMockLookup)).toBe(null); + expect(recordB.getCellValue(fldMockLookup)).toMatchInlineSnapshot(` + Array [ + Object { + "linkedRecordId": "recLink1", + "value": null, + }, + Object { + "linkedRecordId": "recLink2", + "value": "abc123", + }, + Object { + "linkedRecordId": "recLink3", + "value": Object { + "id": "sel123abc", + "name": "recLink3", + }, + }, + Object { + "linkedRecordId": "recLink4", + "value": 1, + }, + Object { + "linkedRecordId": "recLink4", + "value": 2, + }, + Object { + "linkedRecordId": "recLink4", + "value": 3, + }, + ] + `); + expect(recordC.getCellValue(fldMockLookup)).toMatchInlineSnapshot(` + Array [ + Object { + "linkedRecordId": "recLink1", + "value": null, + }, + Object { + "linkedRecordId": "recLink2", + "value": "XYZ", + }, + Object { + "linkedRecordId": "recLink3", + "value": Object { + "id": "selFooBarBaz", + "name": "recLink3", + }, + }, + Object { + "linkedRecordId": "recLink4", + "value": 1, + }, + Object { + "linkedRecordId": "recLink4", + "value": 2, + }, + Object { + "linkedRecordId": "recLink4", + "value": 3, + }, + ] + `); + }); + + test('#getCellValue(fieldOrFieldIdOrFieldName), string name', () => { + const fld1stPrimary = 'Name'; + const fld1stLinked = 'linked records'; + const fldMockLookup = 'lookup'; + + expect(recordA.getCellValue(fld1stPrimary)).toBe('Bonjour!'); + expect(recordB.getCellValue(fld1stPrimary)).toBe('Hello!'); + expect(recordC.getCellValue(fld1stPrimary)).toBe('¡Hola!'); + expect(recordA.getCellValue(fld1stLinked)).toMatchObject({id: 'recB'}); + expect(recordB.getCellValue(fld1stLinked)).toMatchObject({id: 'recC'}); + expect(recordC.getCellValue(fld1stLinked)).toMatchObject({id: 'recA'}); + expect(recordA.getCellValue(fldMockLookup)).toBe(null); + expect(recordB.getCellValue(fldMockLookup)).toMatchInlineSnapshot(` + Array [ + Object { + "linkedRecordId": "recLink1", + "value": null, + }, + Object { + "linkedRecordId": "recLink2", + "value": "abc123", + }, + Object { + "linkedRecordId": "recLink3", + "value": Object { + "id": "sel123abc", + "name": "recLink3", + }, + }, + Object { + "linkedRecordId": "recLink4", + "value": 1, + }, + Object { + "linkedRecordId": "recLink4", + "value": 2, + }, + Object { + "linkedRecordId": "recLink4", + "value": 3, + }, + ] + `); + expect(recordC.getCellValue(fldMockLookup)).toMatchInlineSnapshot(` + Array [ + Object { + "linkedRecordId": "recLink1", + "value": null, + }, + Object { + "linkedRecordId": "recLink2", + "value": "XYZ", + }, + Object { + "linkedRecordId": "recLink3", + "value": Object { + "id": "selFooBarBaz", + "name": "recLink3", + }, + }, + Object { + "linkedRecordId": "recLink4", + "value": 1, + }, + Object { + "linkedRecordId": "recLink4", + "value": 2, + }, + Object { + "linkedRecordId": "recLink4", + "value": 3, + }, + ] + `); + }); + + test('reformatting is disabled when isUsingNewLookupCellValueFormat = true', () => { + mockAirtableInterface.sdkInitData.isUsingNewLookupCellValueFormat = true; + + expect(recordA.getCellValue('fldMockLookup')).toMatchInlineSnapshot(`null`); + expect(recordB.getCellValue('fldMockLookup')).toMatchInlineSnapshot(` + Object { + "linkedRecordIds": Array [ + "recLink1", + "recLink2", + "recLink3", + "recLink4", + ], + "valuesByLinkedRecordId": Object { + "recLink1": null, + "recLink2": "abc123", + "recLink3": Object { + "id": "sel123abc", + "name": "recLink3", + }, + "recLink4": Array [ + 1, + 2, + 3, + ], + }, + } + `); + }); + + test('reformatting is enabled when isUsingNewLookupCellValueFormat = undefined', () => { + mockAirtableInterface.sdkInitData.isUsingNewLookupCellValueFormat = undefined; + + expect(recordA.getCellValue('fldMockLookup')).toMatchInlineSnapshot(`null`); + expect(recordB.getCellValue('fldMockLookup')).toMatchInlineSnapshot(` + Array [ + Object { + "linkedRecordId": "recLink1", + "value": null, + }, + Object { + "linkedRecordId": "recLink2", + "value": "abc123", + }, + Object { + "linkedRecordId": "recLink3", + "value": Object { + "id": "sel123abc", + "name": "recLink3", + }, + }, + Object { + "linkedRecordId": "recLink4", + "value": 1, + }, + Object { + "linkedRecordId": "recLink4", + "value": 2, + }, + Object { + "linkedRecordId": "recLink4", + "value": 3, + }, + ] + `); + }); + + test('tablesById[id] is undefined', async () => { + mockAirtableInterface.triggerModelUpdates([ + { + path: ['tablesById', 'tblFirst'], + value: undefined, + }, + { + path: ['tableOrder'], + value: ['tblSecond'], + }, + { + path: ['activeTableId'], + value: 'tblSecond', + }, + ]); + + expect(() => { + recordA.getCellValue('fld1stPrimary'); + }).toThrowErrorMatchingInlineSnapshot(`"TableCore has been deleted"`); + }); + + test('recordsById is undefined', async () => { + mockAirtableInterface.triggerModelUpdates([ + { + path: ['tablesById', 'tblFirst', 'recordsById'], + value: undefined, + }, + ]); + expect(() => { + recordA.getCellValue('fld1stPrimary'); + }).toThrowErrorMatchingInlineSnapshot(`"Record data is not loaded"`); + }); + + test('cellValuesByFieldId is undefined', async () => { + mockAirtableInterface.triggerModelUpdates([ + { + path: [ + 'tablesById', + 'tblFirst', + 'recordsById', + 'recA', + 'cellValuesByFieldId', + ], + value: undefined, + }, + ]); + + expect(recordA.getCellValue('fld1stPrimary')).toBe(null); + }); + + test('cellValuesByFieldId[id] is undefined', async () => { + mockAirtableInterface.triggerModelUpdates([ + { + path: [ + 'tablesById', + 'tblFirst', + 'recordsById', + 'recA', + 'cellValuesByFieldId', + 'fld1stPrimary', + ], + value: undefined, + }, + ]); + expect(recordA.getCellValue('fld1stPrimary')).toBe(null); + }); + + test('cellValuesByFieldId is null', async () => { + mockAirtableInterface.triggerModelUpdates([ + { + path: [ + 'tablesById', + 'tblFirst', + 'recordsById', + 'recA', + 'cellValuesByFieldId', + ], + value: null, + }, + ]); + + expect(recordA.getCellValue('fld1stPrimary')).toBe(null); + }); + + test('cellValuesByFieldId[id] is null', async () => { + mockAirtableInterface.triggerModelUpdates([ + { + path: [ + 'tablesById', + 'tblFirst', + 'recordsById', + 'recA', + 'cellValuesByFieldId', + 'fld1stPrimary', + ], + value: null, + }, + ]); + expect(recordA.getCellValue('fld1stPrimary')).toBe(null); + }); + }); + + describe('#getCellValueAsString()', () => { + beforeEach(() => { + mockAirtableInterface.fieldTypeProvider.convertCellValueToString = jest.fn( + (appInterface, cellValue, fieldData) => String(cellValue), + ); + }); + + test('#getCellValueAsString(fieldOrFieldIdOrFieldName), Field', async () => { + expect(recordA.getCellValueAsString(table.fields[0])).toBe('Bonjour!'); + expect(recordB.getCellValueAsString(table.fields[0])).toBe('Hello!'); + expect(recordC.getCellValueAsString(table.fields[0])).toBe('¡Hola!'); + expect(mockAirtableInterface.fieldTypeProvider.convertCellValueToString.mock.calls) + .toMatchInlineSnapshot(` + Array [ + Array [ + Object {}, + "Bonjour!", + Object { + "description": "", + "id": "fld1stPrimary", + "isEditable": true, + "isSynced": false, + "lock": null, + "name": "Name", + "type": "singleLineText", + "typeOptions": null, + }, + ], + Array [ + Object {}, + "Hello!", + Object { + "description": "", + "id": "fld1stPrimary", + "isEditable": true, + "isSynced": false, + "lock": null, + "name": "Name", + "type": "singleLineText", + "typeOptions": null, + }, + ], + Array [ + Object {}, + "¡Hola!", + Object { + "description": "", + "id": "fld1stPrimary", + "isEditable": true, + "isSynced": false, + "lock": null, + "name": "Name", + "type": "singleLineText", + "typeOptions": null, + }, + ], + ] + `); + }); + + test('#getCellValueAsString(fieldOrFieldIdOrFieldName), FieldId', async () => { + expect(recordA.getCellValueAsString('fld1stPrimary')).toBe('Bonjour!'); + expect(recordB.getCellValueAsString('fld1stPrimary')).toBe('Hello!'); + expect(recordC.getCellValueAsString('fld1stPrimary')).toBe('¡Hola!'); + expect(mockAirtableInterface.fieldTypeProvider.convertCellValueToString.mock.calls) + .toMatchInlineSnapshot(` + Array [ + Array [ + Object {}, + "Bonjour!", + Object { + "description": "", + "id": "fld1stPrimary", + "isEditable": true, + "isSynced": false, + "lock": null, + "name": "Name", + "type": "singleLineText", + "typeOptions": null, + }, + ], + Array [ + Object {}, + "Hello!", + Object { + "description": "", + "id": "fld1stPrimary", + "isEditable": true, + "isSynced": false, + "lock": null, + "name": "Name", + "type": "singleLineText", + "typeOptions": null, + }, + ], + Array [ + Object {}, + "¡Hola!", + Object { + "description": "", + "id": "fld1stPrimary", + "isEditable": true, + "isSynced": false, + "lock": null, + "name": "Name", + "type": "singleLineText", + "typeOptions": null, + }, + ], + ] + `); + }); + + test('#getCellValueAsString(fieldOrFieldIdOrFieldName), string name', async () => { + expect(recordA.getCellValueAsString('Name')).toBe('Bonjour!'); + expect(recordB.getCellValueAsString('Name')).toBe('Hello!'); + expect(recordC.getCellValueAsString('Name')).toBe('¡Hola!'); + expect(mockAirtableInterface.fieldTypeProvider.convertCellValueToString.mock.calls) + .toMatchInlineSnapshot(` + Array [ + Array [ + Object {}, + "Bonjour!", + Object { + "description": "", + "id": "fld1stPrimary", + "isEditable": true, + "isSynced": false, + "lock": null, + "name": "Name", + "type": "singleLineText", + "typeOptions": null, + }, + ], + Array [ + Object {}, + "Hello!", + Object { + "description": "", + "id": "fld1stPrimary", + "isEditable": true, + "isSynced": false, + "lock": null, + "name": "Name", + "type": "singleLineText", + "typeOptions": null, + }, + ], + Array [ + Object {}, + "¡Hola!", + Object { + "description": "", + "id": "fld1stPrimary", + "isEditable": true, + "isSynced": false, + "lock": null, + "name": "Name", + "type": "singleLineText", + "typeOptions": null, + }, + ], + ] + `); + }); + + test('tablesById[id] is undefined', async () => { + mockAirtableInterface.triggerModelUpdates([ + { + path: ['tablesById', 'tblFirst'], + value: undefined, + }, + { + path: ['tableOrder'], + value: ['tblSecond'], + }, + { + path: ['activeTableId'], + value: 'tblSecond', + }, + ]); + expect(() => { + recordA.getCellValueAsString('fld1stPrimary'); + }).toThrowErrorMatchingInlineSnapshot(`"TableCore has been deleted"`); + }); + + test('recordsById is undefined', async () => { + mockAirtableInterface.triggerModelUpdates([ + { + path: ['tablesById', 'tblFirst', 'recordsById'], + value: undefined, + }, + ]); + expect(() => { + recordA.getCellValueAsString('fld1stPrimary'); + }).toThrowErrorMatchingInlineSnapshot(`"Record data is not loaded"`); + }); + + test('cellValuesByFieldId is undefined', async () => { + mockAirtableInterface.triggerModelUpdates([ + { + path: [ + 'tablesById', + 'tblFirst', + 'recordsById', + 'recA', + 'cellValuesByFieldId', + ], + value: undefined, + }, + ]); + expect(recordA.getCellValueAsString('fld1stPrimary')).toBe(''); + }); + + test('cellValuesByFieldId[id] is undefined', async () => { + mockAirtableInterface.triggerModelUpdates([ + { + path: [ + 'tablesById', + 'tblFirst', + 'recordsById', + 'recA', + 'cellValuesByFieldId', + 'fld1stPrimary', + ], + value: undefined, + }, + ]); + expect(recordA.getCellValueAsString('fld1stPrimary')).toBe(''); + }); + + test('cellValuesByFieldId is null', async () => { + mockAirtableInterface.triggerModelUpdates([ + { + path: [ + 'tablesById', + 'tblFirst', + 'recordsById', + 'recA', + 'cellValuesByFieldId', + ], + value: null, + }, + ]); + + expect(recordA.getCellValueAsString('fld1stPrimary')).toBe(''); + }); + + test('cellValuesByFieldId[id] is null', async () => { + mockAirtableInterface.triggerModelUpdates([ + { + path: [ + 'tablesById', + 'tblFirst', + 'recordsById', + 'recA', + 'cellValuesByFieldId', + 'fld1stPrimary', + ], + value: null, + }, + ]); + expect(recordA.getCellValueAsString('fld1stPrimary')).toBe(''); + }); + }); + + describe('#toString()', () => { + test('returns a debugging string', async () => { + expect(recordA.toString()).toMatchInlineSnapshot(`"[Record recA]"`); + }); + }); + + describe('#watch()', () => { + const recordPath = ['tablesById', 'tblFirst', 'recordsById', 'recA']; + + const trigger = (recId: string, path: Array, value: any) => { + mockAirtableInterface.triggerModelUpdates([ + { + path, + value, + }, + ]); + }; + + test('#watch(invalid key) throws', () => { + expect(() => + recordA.watch('isDeleted', () => {}), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid key to watch for Record: isDeleted"`, + ); + }); + + describe('#watch(valid key)', () => { + test('#watch("cellValues")', () => { + const fn = jest.fn(); + recordA.watch('cellValues', fn); + + expect(fn).toHaveBeenCalledTimes(0); + + trigger( + 'recA', + [...recordPath, 'cellValuesByFieldId', 'fld1stPrimary'], + 'Something else', + ); + + expect(fn).toHaveBeenCalledTimes(1); + expect(fn).toHaveBeenCalledWith(recordA, 'cellValues', ['fld1stPrimary']); + }); + }); + }); + + describe('#unwatch()', () => { + const recordPath = ['tablesById', 'tblFirst', 'recordsById', 'recA']; + + const trigger = (recId: string, path: Array, value: any) => { + mockAirtableInterface.triggerModelUpdates([ + { + path, + value, + }, + ]); + }; + + describe('#unwatch(valid key)', () => { + test('#unwatch("cellValues")', () => { + const fn = jest.fn(); + recordA.watch('cellValues', fn); + + expect(fn).toHaveBeenCalledTimes(0); + + trigger( + 'recA', + [...recordPath, 'cellValuesByFieldId', 'fld1stPrimary'], + 'Something else', + ); + + expect(fn).toHaveBeenCalledTimes(1); + + recordA.unwatch('cellValues', fn); + + trigger( + 'recA', + [...recordPath, 'cellValuesByFieldId', 'fld1stPrimary'], + 'Something else', + ); + + expect(fn).toHaveBeenCalledTimes(1); + }); + }); + }); + }); +}); diff --git a/packages/sdk/test/interface/models/table.test.ts b/packages/sdk/test/interface/models/table.test.ts new file mode 100644 index 000000000..2c0fa5746 --- /dev/null +++ b/packages/sdk/test/interface/models/table.test.ts @@ -0,0 +1,1807 @@ +import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; +import {type Base} from '../../../src/interface/models/base'; +import {type Table} from '../../../src/interface/models/table'; +import {Field} from '../../../src/interface/models/field'; +import {type TableId, type FieldId, type RecordId} from '../../../src/shared/types/hyper_ids'; +import {MutationTypes} from '../../../src/interface/types/mutations'; +import {InterfaceBlockSdk} from '../../../src/interface/sdk'; + +const mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); +jest.mock('../../../src/injected/airtable_interface', () => ({ + __esModule: true, + default: () => mockAirtableInterface, +})); + +const deleteTable = (id: TableId) => { + const newOrder = ['tblDesignProjects', 'tblTasks', 'tblClients'].filter( + (other) => other !== id, + ); + + mockAirtableInterface.triggerModelUpdates([ + {path: ['tableOrder'], value: newOrder}, + {path: ['tablesById', id], value: undefined}, + ]); +}; + +const deleteField = (id: FieldId) => { + mockAirtableInterface.triggerModelUpdates([ + { + path: ['tablesById', 'tblDesignProjects', 'fieldsById', id], + value: undefined, + }, + ]); +}; + +describe('Table', () => { + let sdk: InterfaceBlockSdk; + let base: Base; + let table: Table; + + beforeEach(() => { + sdk = new InterfaceBlockSdk(mockAirtableInterface); + base = sdk.base; + table = base.getTableByName('Design projects'); + }); + + afterEach(() => { + mockAirtableInterface.reset(); + }); + + const makeEmptyRecord = (id: RecordId) => { + const baseData = mockAirtableInterface.sdkInitData.baseData; + const parentTableData = baseData.tablesById.tblDesignProjects; + parentTableData.recordsById[id] = { + id, + cellValuesByFieldId: {}, + createdTime: '2020-10-28T01:40:24.913Z', + }; + parentTableData.recordOrder.push(id); + + const parentRecordStore = sdk.base.__getRecordStore(parentTableData.id); + + const newRecord = parentRecordStore.getRecordByIdIfExists(id); + + return newRecord!; + }; + + describe('getFieldIfExists', () => { + it('returns field by id', () => { + const field1 = table.getFieldIfExists('fldPrjctName') as Field; + const field2 = table.getFieldIfExists('fldPrjctClient') as Field; + expect(field1).toBeInstanceOf(Field); + expect(field1.id).toBe('fldPrjctName'); + expect(field2).toBeInstanceOf(Field); + expect(field2.id).toBe('fldPrjctClient'); + }); + + it('returns field by name', () => { + const field1 = table.getFieldIfExists('Category') as Field; + const field2 = table.getFieldIfExists('Complete') as Field; + expect(field1).toBeInstanceOf(Field); + expect(field1.id).toBe('fldPrjctCtgry'); + expect(field2).toBeInstanceOf(Field); + expect(field2.id).toBe('fldPrjctCmplt'); + }); + + it('returns null when field not found', () => { + expect(table.getFieldIfExists('fldHOlUIpjmlyFAKE')).toBe(null); + expect(table.getFieldIfExists('A made up field')).toBe(null); + }); + }); + + describe('getField', () => { + it('returns field by id', () => { + const field1 = table.getField('fldPrjctName'); + const field2 = table.getField('fldPrjctClient'); + expect(field1).toBeInstanceOf(Field); + expect(field1.id).toBe('fldPrjctName'); + expect(field2).toBeInstanceOf(Field); + expect(field2.id).toBe('fldPrjctClient'); + }); + + it('returns field by name', () => { + const field1 = table.getField('Category'); + const field2 = table.getField('Complete'); + expect(field1).toBeInstanceOf(Field); + expect(field1.id).toBe('fldPrjctCtgry'); + expect(field2).toBeInstanceOf(Field); + expect(field2.id).toBe('fldPrjctCmplt'); + }); + + it('throws when field not found', () => { + expect(() => table.getField('fldHOlUIpjmlyFAKE')).toThrowErrorMatchingInlineSnapshot( + `"No field with ID or name 'fldHOlUIpjmlyFAKE' in table 'Design projects'"`, + ); + expect(() => table.getField('A made up field')).toThrowErrorMatchingInlineSnapshot( + `"No field with ID or name 'A made up field' in table 'Design projects'"`, + ); + }); + }); + + describe('#checkPermissionsForCreateRecord', () => { + it('correctly queries AirtableInterface when no field data is provided', () => { + table.checkPermissionsForCreateRecord(); + + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: [ + { + id: undefined, + cellValuesByFieldId: undefined, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('correctly queries AirtableInterface when a field ID is provided', () => { + table.checkPermissionsForCreateRecord({fldPrjctName: 99}); + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: [ + { + id: undefined, + cellValuesByFieldId: { + fldPrjctName: 99, + }, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('correctly queries AirtableInterface when a field name is provided', () => { + table.checkPermissionsForCreateRecord({'Project images': 99}); + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: [ + { + id: undefined, + cellValuesByFieldId: { + fldPrjctImages: 99, + }, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('throws when a deleted field ID is provided', () => { + deleteField('fldPrjctNotes'); + + expect(() => { + table.checkPermissionsForCreateRecord({fldPrjctNotes: '86'}); + }).toThrowErrorMatchingInlineSnapshot( + `"Field 'fldPrjctNotes' does not exist in table 'Design projects'"`, + ); + }); + + it('returns affirmative responses from AirtableInterface', () => { + mockAirtableInterface.checkPermissionsForMutation.mockReturnValueOnce({ + hasPermission: true, + }); + + const result = table.checkPermissionsForCreateRecord(); + + expect(result).toEqual({hasPermission: true}); + }); + + it('returns negative responses from AirtableInterface', () => { + mockAirtableInterface.checkPermissionsForMutation.mockReturnValueOnce({ + hasPermission: false, + reasonDisplayString: 'spoon', + }); + + const result = table.checkPermissionsForCreateRecord(); + + expect(result).toEqual({ + hasPermission: false, + reasonDisplayString: 'spoon', + }); + }); + }); + + describe('#checkPermissionsForCreateRecords', () => { + it('correctly queries AirtableInterface when no records are provided', () => { + table.checkPermissionsForCreateRecords(); + + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: undefined, + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('correctly queries AirtableInterface when an empty set of records is provided', () => { + table.checkPermissionsForCreateRecords([]); + + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: [], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('correctly queries AirtableInterface when a nondescript record specifier is provided', () => { + table.checkPermissionsForCreateRecords([{}]); + + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: [ + { + id: undefined, + cellValuesByFieldId: undefined, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('correctly queries AirtableInterface when only a field ID is provided', () => { + table.checkPermissionsForCreateRecords([{fields: {fldPrjctName: 99}}]); + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: [ + { + id: undefined, + cellValuesByFieldId: { + fldPrjctName: 99, + }, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('correctly queries AirtableInterface when only a field name is provided', () => { + table.checkPermissionsForCreateRecords([{fields: {'Project images': 99}}]); + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: [ + { + id: undefined, + cellValuesByFieldId: { + fldPrjctImages: 99, + }, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('correctly queries AirtableInterface when multiple records and fields are provided', () => { + table.checkPermissionsForCreateRecords([ + {fields: {fldPrjctName: 99, fldPrjctClient: 98}}, + {fields: {fldPrjctCtgry: 97, fldPrjctCmplt: 96}}, + ]); + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: [ + { + id: undefined, + cellValuesByFieldId: { + fldPrjctName: 99, + fldPrjctClient: 98, + }, + }, + { + id: undefined, + cellValuesByFieldId: { + fldPrjctCtgry: 97, + fldPrjctCmplt: 96, + }, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('throws when a deleted field ID is provided', () => { + deleteField('fldPrjctNotes'); + + expect(() => { + table.checkPermissionsForCreateRecords([{fields: {fldPrjctNotes: '86'}}]); + }).toThrowErrorMatchingInlineSnapshot( + `"Field 'fldPrjctNotes' does not exist in table 'Design projects'"`, + ); + }); + + it('returns affirmative responses from AirtableInterface', () => { + mockAirtableInterface.checkPermissionsForMutation.mockReturnValueOnce({ + hasPermission: true, + }); + + const result = table.checkPermissionsForCreateRecords(); + + expect(result).toEqual({hasPermission: true}); + }); + + it('returns negative responses from AirtableInterface', () => { + mockAirtableInterface.checkPermissionsForMutation.mockReturnValueOnce({ + hasPermission: false, + reasonDisplayString: 'spoon', + }); + + const result = table.checkPermissionsForCreateRecords(); + + expect(result).toEqual({ + hasPermission: false, + reasonDisplayString: 'spoon', + }); + }); + }); + + describe('#checkPermissionsForDeleteRecord', () => { + it('correctly queries AirtableInterface when no record is provided', () => { + table.checkPermissionsForDeleteRecord(); + + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.DELETE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + recordIds: undefined, + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('correctly queries AirtableInterface when a record ID is provided', () => { + table.checkPermissionsForDeleteRecord('recQ'); + + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.DELETE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + recordIds: ['recQ'], + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('correctly queries AirtableInterface when a record model is provided', async () => { + const record = makeEmptyRecord('recT'); + table.checkPermissionsForDeleteRecord(record); + + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.DELETE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + recordIds: ['recT'], + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('returns affirmative responses from AirtableInterface', () => { + mockAirtableInterface.checkPermissionsForMutation.mockReturnValueOnce({ + hasPermission: true, + }); + + const result = table.checkPermissionsForDeleteRecord(); + + expect(result).toEqual({hasPermission: true}); + }); + + it('returns negative responses from AirtableInterface', () => { + mockAirtableInterface.checkPermissionsForMutation.mockReturnValueOnce({ + hasPermission: false, + reasonDisplayString: 'spoon', + }); + + const result = table.checkPermissionsForDeleteRecord(); + + expect(result).toEqual({ + hasPermission: false, + reasonDisplayString: 'spoon', + }); + }); + }); + + describe('#checkPermissionsForDeleteRecords', () => { + it('correctly queries AirtableInterface when no records are provided', () => { + table.checkPermissionsForDeleteRecords(); + + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.DELETE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + recordIds: undefined, + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('correctly queries AirtableInterface when an empty set of records is provided', () => { + table.checkPermissionsForDeleteRecords([]); + + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.DELETE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + recordIds: [], + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('correctly queries AirtableInterface when a record ID is provided', () => { + table.checkPermissionsForDeleteRecords(['recQ']); + + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.DELETE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + recordIds: ['recQ'], + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('correctly queries AirtableInterface when a record model is provided', async () => { + const record = makeEmptyRecord('recT'); + table.checkPermissionsForDeleteRecords([record]); + + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.DELETE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + recordIds: ['recT'], + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('correctly queries AirtableInterface when multiple records are provided', () => { + table.checkPermissionsForDeleteRecords(['recG', 'recH']); + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.DELETE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + recordIds: ['recG', 'recH'], + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('returns affirmative responses from AirtableInterface', () => { + mockAirtableInterface.checkPermissionsForMutation.mockReturnValueOnce({ + hasPermission: true, + }); + + const result = table.checkPermissionsForDeleteRecords(); + + expect(result).toEqual({hasPermission: true}); + }); + + it('returns negative responses from AirtableInterface', () => { + mockAirtableInterface.checkPermissionsForMutation.mockReturnValueOnce({ + hasPermission: false, + reasonDisplayString: 'spoon', + }); + + const result = table.checkPermissionsForDeleteRecords(); + + expect(result).toEqual({ + hasPermission: false, + reasonDisplayString: 'spoon', + }); + }); + }); + + describe('#checkPermissionsForUpdateRecord', () => { + it('correctly queries AirtableInterface when no record is provided', () => { + table.checkPermissionsForUpdateRecord(); + + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [ + { + id: undefined, + cellValuesByFieldId: undefined, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('correctly queries AirtableInterface when only a record ID is provided', () => { + table.checkPermissionsForUpdateRecord('recQ'); + + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recQ', + cellValuesByFieldId: undefined, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('correctly queries AirtableInterface when only a record model is provided', async () => { + const record = makeEmptyRecord('recF'); + table.checkPermissionsForUpdateRecord(record); + + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recF', + cellValuesByFieldId: undefined, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('correctly queries AirtableInterface when only a field ID is provided', () => { + table.checkPermissionsForUpdateRecord(undefined, {fldPrjctName: 99}); + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [ + { + id: undefined, + cellValuesByFieldId: { + fldPrjctName: 99, + }, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('correctly queries AirtableInterface when only a field name is provided', () => { + table.checkPermissionsForUpdateRecord(undefined, {'Project images': 99}); + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [ + { + id: undefined, + cellValuesByFieldId: { + fldPrjctImages: 99, + }, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('throws when a deleted field ID is provided', () => { + deleteField('fldPrjctNotes'); + + expect(() => { + table.checkPermissionsForUpdateRecord(undefined, {fldPrjctNotes: '86'}); + }).toThrowErrorMatchingInlineSnapshot( + `"Field 'fldPrjctNotes' does not exist in table 'Design projects'"`, + ); + }); + + it('returns affirmative responses from AirtableInterface', () => { + mockAirtableInterface.checkPermissionsForMutation.mockReturnValueOnce({ + hasPermission: true, + }); + + const result = table.checkPermissionsForUpdateRecord(); + + expect(result).toEqual({hasPermission: true}); + }); + + it('returns negative responses from AirtableInterface', () => { + mockAirtableInterface.checkPermissionsForMutation.mockReturnValueOnce({ + hasPermission: false, + reasonDisplayString: 'spoon', + }); + + const result = table.checkPermissionsForUpdateRecord(); + + expect(result).toEqual({ + hasPermission: false, + reasonDisplayString: 'spoon', + }); + }); + }); + + describe('#checkPermissionsForUpdateRecords', () => { + it('correctly queries AirtableInterface when no records are provided', () => { + table.checkPermissionsForUpdateRecords(); + + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: undefined, + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('correctly queries AirtableInterface when an empty set of records is provided', () => { + table.checkPermissionsForUpdateRecords([]); + + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('correctly queries AirtableInterface when a nondescript record specifier is provided', () => { + table.checkPermissionsForUpdateRecords([{}]); + + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [ + { + id: undefined, + cellValuesByFieldId: undefined, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('correctly queries AirtableInterface when only a record ID is provided', () => { + table.checkPermissionsForUpdateRecords([{id: 'recQ'}]); + + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recQ', + cellValuesByFieldId: undefined, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('correctly queries AirtableInterface when only a field ID is provided', () => { + table.checkPermissionsForUpdateRecords([{fields: {fldPrjctName: 99}}]); + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [ + { + id: undefined, + cellValuesByFieldId: { + fldPrjctName: 99, + }, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('correctly queries AirtableInterface when only a field name is provided', () => { + table.checkPermissionsForUpdateRecords([{fields: {'Project images': 99}}]); + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [ + { + id: undefined, + cellValuesByFieldId: { + fldPrjctImages: 99, + }, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('correctly queries AirtableInterface when multiple records and fields are provided', () => { + table.checkPermissionsForUpdateRecords([ + {id: 'recG', fields: {fldPrjctName: 99, fldPrjctClient: 98}}, + {id: 'recH', fields: {fldPrjctCtgry: 97, fldPrjctCmplt: 96}}, + ]); + expect(mockAirtableInterface.checkPermissionsForMutation).toHaveBeenLastCalledWith( + { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recG', + cellValuesByFieldId: { + fldPrjctName: 99, + fldPrjctClient: 98, + }, + }, + { + id: 'recH', + cellValuesByFieldId: { + fldPrjctCtgry: 97, + fldPrjctCmplt: 96, + }, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + mockAirtableInterface.sdkInitData.baseData, + ); + }); + + it('throws when a deleted field ID is provided', () => { + deleteField('fldPrjctNotes'); + + expect(() => { + table.checkPermissionsForUpdateRecords([{fields: {fldPrjctNotes: '86'}}]); + }).toThrowErrorMatchingInlineSnapshot( + `"Field 'fldPrjctNotes' does not exist in table 'Design projects'"`, + ); + }); + + it('returns affirmative responses from AirtableInterface', () => { + mockAirtableInterface.checkPermissionsForMutation.mockReturnValueOnce({ + hasPermission: true, + }); + + const result = table.checkPermissionsForUpdateRecords(); + + expect(result).toEqual({hasPermission: true}); + }); + + it('returns negative responses from AirtableInterface', () => { + mockAirtableInterface.checkPermissionsForMutation.mockReturnValueOnce({ + hasPermission: false, + reasonDisplayString: 'spoon', + }); + + const result = table.checkPermissionsForUpdateRecords(); + + expect(result).toEqual({ + hasPermission: false, + reasonDisplayString: 'spoon', + }); + }); + }); + + describe('#createRecordAsync', () => { + it('tolerates omitted field set', async () => { + const result = await table.createRecordAsync(); + + expect(mockAirtableInterface.applyMutationAsync).toHaveBeenLastCalledWith( + { + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recGeneratedMockId', + cellValuesByFieldId: {}, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + {holdForMs: 100}, + ); + expect(result).toEqual('recGeneratedMockId'); + }); + + it('tolerates empty field set', async () => { + const result = await table.createRecordAsync({}); + + expect(mockAirtableInterface.applyMutationAsync).toHaveBeenLastCalledWith( + { + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recGeneratedMockId', + cellValuesByFieldId: {}, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + {holdForMs: 100}, + ); + expect(result).toEqual('recGeneratedMockId'); + }); + + it('tolerates fields specified by ID', async () => { + const result = await table.createRecordAsync({ + fldPrjctName: 'foo', + fldPrjctNotes: 'bar', + }); + + expect(mockAirtableInterface.applyMutationAsync).toHaveBeenLastCalledWith( + { + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recGeneratedMockId', + cellValuesByFieldId: { + fldPrjctName: 'foo', + fldPrjctNotes: 'bar', + }, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + {holdForMs: 100}, + ); + expect(result).toEqual('recGeneratedMockId'); + }); + + it('tolerates fields specified by name', async () => { + const result = await table.createRecordAsync({ + Name: 'spike', + Notes: 'porkchop', + }); + + expect(mockAirtableInterface.applyMutationAsync).toHaveBeenLastCalledWith( + { + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recGeneratedMockId', + cellValuesByFieldId: { + fldPrjctName: 'spike', + fldPrjctNotes: 'porkchop', + }, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + {holdForMs: 100}, + ); + expect(result).toEqual('recGeneratedMockId'); + }); + }); + + describe('#createRecordsAsync', () => { + it('tolerates empty record sets', async () => { + const result = await table.createRecordsAsync([]); + expect(mockAirtableInterface.applyMutationAsync).toHaveBeenLastCalledWith( + { + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: [], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + {holdForMs: 100}, + ); + expect(result).toEqual([]); + }); + + it('tolerates empty field sets', async () => { + const result = await table.createRecordsAsync([{fields: {}}]); + + expect(mockAirtableInterface.applyMutationAsync).toHaveBeenLastCalledWith( + { + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recGeneratedMockId', + cellValuesByFieldId: {}, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + {holdForMs: 100}, + ); + expect(result).toEqual(['recGeneratedMockId']); + }); + + it('tolerates fields specified by ID', async () => { + const result = await table.createRecordsAsync([ + { + fields: { + fldPrjctName: 'foo', + fldPrjctNotes: 'bar', + }, + }, + ]); + + expect(mockAirtableInterface.applyMutationAsync).toHaveBeenLastCalledWith( + { + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recGeneratedMockId', + cellValuesByFieldId: { + fldPrjctName: 'foo', + fldPrjctNotes: 'bar', + }, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + {holdForMs: 100}, + ); + expect(result).toEqual(['recGeneratedMockId']); + }); + + it('tolerates fields specified by name', async () => { + const result = await table.createRecordsAsync([ + { + fields: { + Name: 'spike', + Notes: 'porkchop', + }, + }, + ]); + + expect(mockAirtableInterface.applyMutationAsync).toHaveBeenLastCalledWith( + { + type: MutationTypes.CREATE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recGeneratedMockId', + cellValuesByFieldId: { + fldPrjctName: 'spike', + fldPrjctNotes: 'porkchop', + }, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + {holdForMs: 100}, + ); + expect(result).toEqual(['recGeneratedMockId']); + }); + + it('rejects missing field sets', async () => { + // @ts-ignore + const createPromise = table.createRecordsAsync([{}]); + await expect(createPromise).rejects.toThrowErrorMatchingInlineSnapshot( + `"Invalid record format. Please define field mappings using a \`fields\` key for each record definition object"`, + ); + }); + + it('rejects extraneous information', async () => { + // @ts-ignore + const createPromise = table.createRecordsAsync([{foo: [], fields: {}}]); + await expect(createPromise).rejects.toThrowErrorMatchingInlineSnapshot( + `"Invalid record format. Please define field mappings using a \`fields\` key for each record definition object"`, + ); + }); + }); + + describe('#deleteRecordAsync', () => { + it('includes all record specified by ID', async () => { + makeEmptyRecord('recA'); + makeEmptyRecord('recB'); + await table.deleteRecordAsync('recA'); + + expect(mockAirtableInterface.applyMutationAsync).toHaveBeenLastCalledWith( + { + type: MutationTypes.DELETE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + recordIds: ['recA'], + }, + {holdForMs: 100}, + ); + }); + + it('includes record specified by model', async () => { + const record = makeEmptyRecord('recK'); + + await table.deleteRecordAsync(record); + + expect(mockAirtableInterface.applyMutationAsync).toHaveBeenLastCalledWith( + { + type: MutationTypes.DELETE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + recordIds: ['recK'], + }, + {holdForMs: 100}, + ); + }); + + it('updates local model', async () => { + const record = makeEmptyRecord('recD'); + + await table.deleteRecordAsync(record); + + const result = table._recordStore.getRecordByIdIfExists('recD'); + + expect(result).toBeNull(); + }); + }); + + describe('#deleteRecordsAsync', () => { + it('tolerates empty record sets', async () => { + await table.deleteRecordsAsync([]); + + expect(mockAirtableInterface.applyMutationAsync).toHaveBeenLastCalledWith( + { + type: MutationTypes.DELETE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + recordIds: [], + }, + {holdForMs: 100}, + ); + }); + + it('includes all records (specified by ID)', async () => { + makeEmptyRecord('recA'); + makeEmptyRecord('recB'); + await table.deleteRecordsAsync(['recA', 'recB']); + + expect(mockAirtableInterface.applyMutationAsync).toHaveBeenLastCalledWith( + { + type: MutationTypes.DELETE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + recordIds: ['recA', 'recB'], + }, + {holdForMs: 100}, + ); + }); + + it('includes all records (specified by model)', async () => { + const record = makeEmptyRecord('recK'); + + await table.deleteRecordsAsync([record]); + + expect(mockAirtableInterface.applyMutationAsync).toHaveBeenLastCalledWith( + { + type: MutationTypes.DELETE_MULTIPLE_RECORDS, + tableId: 'tblDesignProjects', + recordIds: ['recK'], + }, + {holdForMs: 100}, + ); + }); + + it('updates local model', async () => { + const record = makeEmptyRecord('recD'); + + await table.deleteRecordsAsync([record]); + + const result = table._recordStore.getRecordByIdIfExists('recD'); + + expect(result).toBeNull(); + }); + }); + + describe('#description', () => { + it('returns the descrption of the table', () => { + expect(table.description).toBe('description for design projects table'); + }); + + it('throws when table has been deleted', () => { + deleteTable('tblDesignProjects'); + + expect(() => table.description).toThrowErrorMatchingInlineSnapshot( + `"TableCore has been deleted"`, + ); + }); + }); + + describe('#getFieldById', () => { + it('returns the requested field', () => { + expect(table.getFieldById('fldPrjctClient')).toBe(table.fields[1]); + }); + + it('throws when field has been deleted', () => { + deleteField('fldPrjctClient'); + + expect(() => table.getFieldById('fldPrjctClient')).toThrowErrorMatchingInlineSnapshot( + `"No field with ID fldPrjctClient in table 'Design projects'"`, + ); + }); + + it('throws when table has been deleted', () => { + deleteTable('tblDesignProjects'); + + expect(() => table.getFieldById('fldPrjctClient')).toThrowErrorMatchingInlineSnapshot( + `"TableCore has been deleted"`, + ); + }); + }); + + describe('#getFieldByName', () => { + it('returns the requested field', () => { + expect(table.getFieldByName('Category')).toBe(table.fields[2]); + }); + + it('throws when field has been deleted', () => { + deleteField('fldPrjctCtgry'); + + expect(() => table.getFieldByName('Category')).toThrowErrorMatchingInlineSnapshot( + `"No field named 'Category' in table 'Design projects'"`, + ); + }); + + it('throws when table has been deleted', () => { + deleteTable('tblDesignProjects'); + + expect(() => table.getFieldByName('Category')).toThrowErrorMatchingInlineSnapshot( + `"TableCore has been deleted"`, + ); + }); + }); + + describe('#hasPermissionToCreateRecord', () => { + it('returns affirmative responses from AirtableInterface', () => { + mockAirtableInterface.checkPermissionsForMutation.mockReturnValueOnce({ + hasPermission: true, + }); + + expect(table.hasPermissionToCreateRecord()).toEqual(true); + }); + + it('returns negative responses from AirtableInterface', () => { + mockAirtableInterface.checkPermissionsForMutation.mockReturnValueOnce({ + hasPermission: false, + reasonDisplayString: 'spoon', + }); + + expect(table.hasPermissionToCreateRecord()).toEqual(false); + }); + }); + + describe('#hasPermissionToCreateRecords', () => { + it('returns affirmative responses from AirtableInterface', () => { + mockAirtableInterface.checkPermissionsForMutation.mockReturnValueOnce({ + hasPermission: true, + }); + + expect(table.hasPermissionToCreateRecords()).toEqual(true); + }); + + it('returns negative responses from AirtableInterface', () => { + mockAirtableInterface.checkPermissionsForMutation.mockReturnValueOnce({ + hasPermission: false, + reasonDisplayString: 'spoon', + }); + + expect(table.hasPermissionToCreateRecords()).toEqual(false); + }); + }); + + describe('#hasPermissionToDeleteRecord', () => { + it('returns affirmative responses from AirtableInterface', () => { + mockAirtableInterface.checkPermissionsForMutation.mockReturnValueOnce({ + hasPermission: true, + }); + + expect(table.hasPermissionToDeleteRecord()).toEqual(true); + }); + + it('returns negative responses from AirtableInterface', () => { + mockAirtableInterface.checkPermissionsForMutation.mockReturnValueOnce({ + hasPermission: false, + reasonDisplayString: 'spoon', + }); + + expect(table.hasPermissionToDeleteRecord()).toEqual(false); + }); + }); + + describe('#hasPermissionToDeleteRecords', () => { + it('returns affirmative responses from AirtableInterface', () => { + mockAirtableInterface.checkPermissionsForMutation.mockReturnValueOnce({ + hasPermission: true, + }); + + expect(table.hasPermissionToDeleteRecords()).toEqual(true); + }); + + it('returns negative responses from AirtableInterface', () => { + mockAirtableInterface.checkPermissionsForMutation.mockReturnValueOnce({ + hasPermission: false, + reasonDisplayString: 'spoon', + }); + + expect(table.hasPermissionToDeleteRecords()).toEqual(false); + }); + }); + + describe('#hasPermissionToUpdateRecord', () => { + it('returns affirmative responses from AirtableInterface', () => { + mockAirtableInterface.checkPermissionsForMutation.mockReturnValueOnce({ + hasPermission: true, + }); + + expect(table.hasPermissionToUpdateRecord()).toEqual(true); + }); + + it('returns negative responses from AirtableInterface', () => { + mockAirtableInterface.checkPermissionsForMutation.mockReturnValueOnce({ + hasPermission: false, + reasonDisplayString: 'spoon', + }); + + expect(table.hasPermissionToUpdateRecord()).toEqual(false); + }); + }); + + describe('#hasPermissionToUpdateRecords', () => { + it('returns affirmative responses from AirtableInterface', () => { + mockAirtableInterface.checkPermissionsForMutation.mockReturnValueOnce({ + hasPermission: true, + }); + + expect(table.hasPermissionToUpdateRecords()).toEqual(true); + }); + + it('returns negative responses from AirtableInterface', () => { + mockAirtableInterface.checkPermissionsForMutation.mockReturnValueOnce({ + hasPermission: false, + reasonDisplayString: 'spoon', + }); + + expect(table.hasPermissionToUpdateRecords()).toEqual(false); + }); + }); + + describe('#name', () => { + it('returns the name of the table', () => { + expect(table.name).toBe('Design projects'); + }); + + it('throws when table has been deleted', () => { + deleteTable('tblDesignProjects'); + + expect(() => table.name).toThrowErrorMatchingInlineSnapshot( + `"TableCore has been deleted"`, + ); + }); + }); + + describe('#parentBase', () => { + it('returns the parent base', () => { + expect(table.parentBase).toBe(base); + }); + + it('returns the parent base when table has been deleted', () => { + deleteTable('tblDesignProjects'); + + expect(table.parentBase).toBe(base); + }); + }); + + describe('#primaryField', () => { + it('returns the primary field', () => { + expect(table.primaryField).toBe(table.fields[0]); + }); + + it('throws when table has been deleted', () => { + deleteTable('tblDesignProjects'); + + expect(() => table.primaryField).toThrowErrorMatchingInlineSnapshot( + `"TableCore has been deleted"`, + ); + }); + }); + + describe('#updateRecordAsync', () => { + it('tolerates empty field value maps', async () => { + makeEmptyRecord('recA'); + await table.updateRecordAsync('recA', {}); + + expect(mockAirtableInterface.applyMutationAsync).toHaveBeenLastCalledWith( + { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [{id: 'recA', cellValuesByFieldId: {}}], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + {holdForMs: 100}, + ); + }); + + it('rejects deleted fields (specified by ID)', async () => { + makeEmptyRecord('recA'); + const target = 'fldPrjctNotes'; + deleteField(target); + + await expect( + table.updateRecordAsync('recA', {[target]: 'hello'}), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Field 'fldPrjctNotes' does not exist in table 'Design projects'"`, + ); + }); + + it('rejects deleted fields (specified by name)', async () => { + makeEmptyRecord('recA'); + deleteField('fldPrjctNotes'); + + await expect( + table.updateRecordAsync('recA', {Notes: 'hello'}), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Field 'Notes' does not exist in table 'Design projects'"`, + ); + }); + + it('includes all field values (record by ID and fields by ID)', async () => { + makeEmptyRecord('recA'); + await table.updateRecordAsync('recA', { + fldPrjctName: 'one', + fldPrjctNotes: 'two', + }); + + expect(mockAirtableInterface.applyMutationAsync).toHaveBeenLastCalledWith( + { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recA', + cellValuesByFieldId: { + fldPrjctName: 'one', + fldPrjctNotes: 'two', + }, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + {holdForMs: 100}, + ); + }); + + it('includes all field values (record by ID and fields by name)', async () => { + makeEmptyRecord('recA'); + await table.updateRecordAsync('recA', { + Name: 'one', + Notes: 'two', + }); + + expect(mockAirtableInterface.applyMutationAsync).toHaveBeenLastCalledWith( + { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recA', + cellValuesByFieldId: { + fldPrjctName: 'one', + fldPrjctNotes: 'two', + }, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + {holdForMs: 100}, + ); + }); + + it('includes all field values (record by model and fields by ID)', async () => { + const record = makeEmptyRecord('recB'); + + await table.updateRecordAsync(record, { + fldPrjctName: 'one', + fldPrjctNotes: 'two', + }); + + expect(mockAirtableInterface.applyMutationAsync).toHaveBeenLastCalledWith( + { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recB', + cellValuesByFieldId: { + fldPrjctName: 'one', + fldPrjctNotes: 'two', + }, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + {holdForMs: 100}, + ); + }); + + it('includes all field values (record by model and fields by name)', async () => { + const record = makeEmptyRecord('recB'); + + await table.updateRecordAsync(record, { + Name: 'one', + Notes: 'two', + }); + + expect(mockAirtableInterface.applyMutationAsync).toHaveBeenLastCalledWith( + { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recB', + cellValuesByFieldId: { + fldPrjctName: 'one', + fldPrjctNotes: 'two', + }, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + {holdForMs: 100}, + ); + }); + + it('updates local model', async () => { + const record = makeEmptyRecord('recC'); + + await table.updateRecordAsync('recC', { + fldPrjctName: 'sasquatch', + fldPrjctNotes: 'yeti', + }); + + expect(record.getCellValue('fldPrjctName')).toBe('sasquatch'); + expect(record.getCellValue('fldPrjctNotes')).toBe('yeti'); + }); + }); + + describe('#updateRecordsAsync', () => { + it('tolerates empty record sets', async () => { + await table.updateRecordsAsync([]); + + expect(mockAirtableInterface.applyMutationAsync).toHaveBeenLastCalledWith( + { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + {holdForMs: 100}, + ); + }); + + it('tolerates empty field value maps', async () => { + makeEmptyRecord('recA'); + await table.updateRecordsAsync([{id: 'recA', fields: {}}]); + + expect(mockAirtableInterface.applyMutationAsync).toHaveBeenLastCalledWith( + { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [{id: 'recA', cellValuesByFieldId: {}}], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + {holdForMs: 100}, + ); + }); + + it('rejects deleted fields (specified by ID)', async () => { + makeEmptyRecord('recA'); + const target = 'fldPrjctNotes'; + deleteField(target); + + await expect( + table.updateRecordsAsync([{id: 'recA', fields: {[target]: 'hello'}}]), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Field 'fldPrjctNotes' does not exist in table 'Design projects'"`, + ); + }); + + it('rejects deleted fields (specified by name)', async () => { + const target = 'fldPrjctNotes'; + deleteField(target); + + await expect( + table.updateRecordsAsync([{id: 'recA', fields: {Notes: 'hello'}}]), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Field 'Notes' does not exist in table 'Design projects'"`, + ); + }); + + it('includes all field values for all records (fields specified by ID)', async () => { + makeEmptyRecord('recA'); + makeEmptyRecord('recB'); + await table.updateRecordsAsync([ + { + id: 'recA', + fields: { + fldPrjctName: 'one', + fldPrjctNotes: 'two', + }, + }, + { + id: 'recB', + fields: { + fldPrjctName: 'three', + fldPrjctNotes: 'four', + }, + }, + ]); + + expect(mockAirtableInterface.applyMutationAsync).toHaveBeenLastCalledWith( + { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recA', + cellValuesByFieldId: { + fldPrjctName: 'one', + fldPrjctNotes: 'two', + }, + }, + { + id: 'recB', + cellValuesByFieldId: { + fldPrjctName: 'three', + fldPrjctNotes: 'four', + }, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + {holdForMs: 100}, + ); + }); + + it('includes all field values for all records (fields specified by name)', async () => { + makeEmptyRecord('recA'); + makeEmptyRecord('recB'); + await table.updateRecordsAsync([ + { + id: 'recA', + fields: { + Name: 'one', + Notes: 'two', + }, + }, + { + id: 'recB', + fields: { + Name: 'three', + Notes: 'four', + }, + }, + ]); + + expect(mockAirtableInterface.applyMutationAsync).toHaveBeenLastCalledWith( + { + type: MutationTypes.SET_MULTIPLE_RECORDS_CELL_VALUES, + tableId: 'tblDesignProjects', + records: [ + { + id: 'recA', + cellValuesByFieldId: { + fldPrjctName: 'one', + fldPrjctNotes: 'two', + }, + }, + { + id: 'recB', + cellValuesByFieldId: { + fldPrjctName: 'three', + fldPrjctNotes: 'four', + }, + }, + ], + opts: { + parseDateCellValueInColumnTimeZone: true, + includesForeignRowsThatShouldBeCreated: false, + }, + }, + {holdForMs: 100}, + ); + }); + + it('updates local model', async () => { + const record = makeEmptyRecord('recD'); + + await table.updateRecordsAsync([ + { + id: 'recD', + fields: { + fldPrjctName: 'bigfoot', + fldPrjctNotes: 'sasquatch again', + }, + }, + ]); + + expect(record.getCellValue('fldPrjctName')).toBe('bigfoot'); + expect(record.getCellValue('fldPrjctNotes')).toBe('sasquatch again'); + }); + }); + + describe('#watch', () => { + describe('key: fields', () => { + it('ignores unrecognized fields', () => { + const spy = jest.fn(); + table.watch('fields', spy); + + mockAirtableInterface.triggerModelUpdates([ + { + path: [ + 'tablesById', + 'tblDesignProjects', + 'fieldsById', + 'fldUnrecognized', + 'name', + ], + value: 'a brand new name', + }, + ]); + + expect(spy).toHaveBeenCalledTimes(0); + }); + }); + }); +}); diff --git a/packages/sdk/test/interface/ui/block_wrapper.test.tsx b/packages/sdk/test/interface/ui/block_wrapper.test.tsx new file mode 100644 index 000000000..ae4913a49 --- /dev/null +++ b/packages/sdk/test/interface/ui/block_wrapper.test.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import {render} from '@testing-library/react'; +import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; +import {BlockWrapper} from '../../../src/interface/ui/block_wrapper'; +import {InterfaceBlockSdk} from '../../../src/interface/sdk'; + +const mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); +jest.mock('../../../src/injected/airtable_interface', () => ({ + __esModule: true, + default: () => mockAirtableInterface, +})); + +describe('BlockWrapper', () => { + let sdk: InterfaceBlockSdk; + + beforeEach(() => { + sdk = new InterfaceBlockSdk(mockAirtableInterface); + }); + + afterEach(() => { + mockAirtableInterface.reset(); + }); + + it('renders outside of a blocks context', () => { + render( + +
    +
    , + ); + }); +}); diff --git a/packages/sdk/test/interface/ui/cell_renderer.test.tsx b/packages/sdk/test/interface/ui/cell_renderer.test.tsx new file mode 100644 index 000000000..566529620 --- /dev/null +++ b/packages/sdk/test/interface/ui/cell_renderer.test.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import {render} from '@testing-library/react'; +import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; +import {CellRenderer} from '../../../src/interface/ui/ui'; +import {SdkContext} from '../../../src/shared/ui/sdk_context'; +import {InterfaceBlockSdk} from '../../../src/interface/sdk'; +import getAirtableInterface from '../../../src/injected/airtable_interface'; + +jest.mock('../../../src/injected/airtable_interface', () => { + let mockAirtableInterface: jest.Mocked; + return { + __esModule: true, + default() { + if (!mockAirtableInterface) { + mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); + } + return mockAirtableInterface; + }, + }; +}); + +const mockAirtableInterface = getAirtableInterface() as jest.Mocked; + +describe('CellRenderer', () => { + let sdk: InterfaceBlockSdk; + + beforeEach(() => { + sdk = new InterfaceBlockSdk(mockAirtableInterface); + }); + + afterEach(() => { + mockAirtableInterface.reset(); + }); + + const TestProvider = ({children}: {children: React.ReactNode}) => { + return {children}; + }; + + it('renders within a Blocks Sdk context', () => { + const field = sdk.base.getTable('Design projects').getField('Name'); + render( + + + , + ); + }); +}); diff --git a/packages/sdk/test/interface/ui/expand_record.test.tsx b/packages/sdk/test/interface/ui/expand_record.test.tsx new file mode 100644 index 000000000..c062c4463 --- /dev/null +++ b/packages/sdk/test/interface/ui/expand_record.test.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import {act, render} from '@testing-library/react'; +import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; +import {expandRecord, useBase, useRecords} from '../../../src/interface/ui/ui'; +import {SdkContext} from '../../../src/shared/ui/sdk_context'; +import {InterfaceBlockSdk} from '../../../src/interface/sdk'; +import {type FieldId, type RecordId} from '../../../src/shared/types/hyper_ids'; +import {type ObjectMap} from '../../../src/shared/private_utils'; +import getAirtableInterface from '../../../src/injected/airtable_interface'; +import {createPromiseWithResolveAndReject} from '../../test_helpers'; + +jest.mock('../../../src/injected/airtable_interface', () => { + let mockAirtableInterface: jest.Mocked; + return { + __esModule: true, + default() { + if (!mockAirtableInterface) { + mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); + } + return mockAirtableInterface; + }, + }; +}); + +const mockAirtableInterface = getAirtableInterface() as jest.Mocked; + +const TestProvider = ({sdk, children}: {sdk: InterfaceBlockSdk; children: React.ReactNode}) => { + return {children}; +}; + +const renderAsync = async ( + sdk: InterfaceBlockSdk, + TestBody: React.FunctionComponent, +): Promise => { + const {promise, resolve} = createPromiseWithResolveAndReject(); + await act(async () => { + render( + +
    }> + + + , + ); + }); + return await promise; +}; + +describe('expandRecord', () => { + let sdk: InterfaceBlockSdk; + + beforeEach(() => { + mockAirtableInterface.expandRecord.mockImplementation(() => {}); + sdk = new InterfaceBlockSdk(mockAirtableInterface); + sdk.base.getTableByName('Tasks'); + makeRecord('recA', {}, '2020-11-19T20:51:04.281Z'); + }); + + afterEach(() => { + mockAirtableInterface.reset(); + }); + + const makeRecord = ( + id: RecordId, + cellValuesByFieldId: ObjectMap, + createdTime: string, + ) => { + const baseData = mockAirtableInterface.sdkInitData.baseData; + const parentTableData = baseData.tablesById.tblTasks; + parentTableData.recordsById[id] = { + id, + cellValuesByFieldId, + createdTime, + }; + parentTableData.recordOrder.push(id); + + const parentRecordStore = sdk.base.__getRecordStore(parentTableData.id); + + const newRecord = parentRecordStore.getRecordByIdIfExists(id); + + return newRecord!; + }; + + it('provides the minimal information to AirtableInterface', async () => { + const TestBody = ({resolve}: {resolve: Function}) => { + const base = useBase(); + const table = base.getTableByName('Tasks'); + const records = useRecords(table); + + expandRecord(records[0]); + resolve(); + + return
    ; + }; + + await renderAsync(sdk, TestBody); + + expect(mockAirtableInterface.expandRecord).toHaveBeenCalledWith('tblTasks', 'recA'); + }); +}); diff --git a/packages/sdk/test/interface/ui/ui.test.ts b/packages/sdk/test/interface/ui/ui.test.ts new file mode 100644 index 000000000..df44f4d27 --- /dev/null +++ b/packages/sdk/test/interface/ui/ui.test.ts @@ -0,0 +1,33 @@ +import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; + +jest.mock('../../../src/injected/airtable_interface', () => ({ + __esModule: true, + default() { + return MockAirtableInterface.projectTrackerExample(); + }, +})); + +const run = (bindingIdentifier: string) => { + const exported = require('../../../src/interface/ui/ui')[bindingIdentifier]; + + expect(exported).toBeTruthy(); +}; + +describe('ui entry point', () => { + test('expandRecord', () => run('expandRecord')); + test('initializeBlock', () => run('initializeBlock')); + test('useBase', () => run('useBase')); + test('useColorScheme', () => run('useColorScheme')); + test('useCustomProperties', () => run('useCustomProperties')); + test('useRecords', () => run('useRecords')); + test('useRunInfo', () => run('useRunInfo')); + test('useSession', () => run('useSession')); + test('useGlobalConfig', () => run('useGlobalConfig')); + test('useSynced', () => run('useSynced')); + test('useWatchable', () => run('useWatchable')); + test('colors', () => run('colors')); + test('colorUtils', () => run('colorUtils')); + test('loadCSSFromString', () => run('loadCSSFromString')); + test('loadCSSFromURLAsync', () => run('loadCSSFromURLAsync')); + test('loadScriptFromURLAsync', () => run('loadScriptFromURLAsync')); +}); diff --git a/packages/sdk/test/interface/ui/use_records.test.tsx b/packages/sdk/test/interface/ui/use_records.test.tsx new file mode 100644 index 000000000..467287c67 --- /dev/null +++ b/packages/sdk/test/interface/ui/use_records.test.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import {act, render} from '@testing-library/react'; +import {useRecords} from '../../../src/interface/ui/use_records'; + +import {MockAirtableInterface} from '../airtable_interface_mocks/mock_airtable_interface'; +import {InterfaceBlockSdk} from '../../../src/interface/sdk'; +import {type Record} from '../../../src/interface/models/record'; +import {type Table} from '../../../src/interface/models/table'; +import {type FieldId, type RecordId} from '../../../src/shared/types/hyper_ids'; +import {type ObjectMap} from '../../../src/shared/private_utils'; +import {BlockWrapper} from '../../../src/interface/ui/block_wrapper'; +import {createPromiseWithResolveAndReject} from '../../test_helpers'; + +const mockAirtableInterface = MockAirtableInterface.linkedRecordsExample(); +jest.mock('../../../src/injected/airtable_interface', () => ({ + __esModule: true, + default: () => mockAirtableInterface, +})); + +describe('useRecords', () => { + let sdk: InterfaceBlockSdk; + let table: Table; + const Component = ({table, resolve}: {table: Table; resolve: Function}) => { + const records = useRecords(table); + resolve(records); + return
    ; + }; + + beforeEach(() => { + sdk = new InterfaceBlockSdk(mockAirtableInterface); + table = sdk.base.getTableByName('First Table'); + }); + + afterEach(() => { + mockAirtableInterface.reset(); + }); + + const makeRecord = ( + id: RecordId, + cellValuesByFieldId: ObjectMap, + createdTime: string, + ) => { + const baseData = mockAirtableInterface.sdkInitData.baseData; + const parentTableData = baseData.tablesById.tblFirst; + parentTableData.recordsById[id] = { + id, + cellValuesByFieldId, + createdTime, + }; + parentTableData.recordOrder.push(id); + + const parentRecordStore = sdk.base.__getRecordStore(parentTableData.id); + + const newRecord = parentRecordStore.getRecordByIdIfExists(id); + + return newRecord!; + }; + + it('eventually returns all records from a table', async () => { + makeRecord('recA', {}, '2020-11-19T20:51:04.281Z'); + makeRecord('recB', {}, '2020-11-19T20:51:04.281Z'); + makeRecord('recC', {}, '2020-11-19T20:51:04.281Z'); + + const {promise, resolve} = createPromiseWithResolveAndReject(); + await act(async () => { + render( + + + , + ); + }); + const records: Record[] = await promise; + + const ids = records.map(({id}) => id).sort(); + expect(ids).toStrictEqual(['recA', 'recB', 'recC']); + }); +}); diff --git a/packages/sdk/test/setup_enzyme.ts b/packages/sdk/test/setup_enzyme.ts deleted file mode 100644 index 5374f679d..000000000 --- a/packages/sdk/test/setup_enzyme.ts +++ /dev/null @@ -1,4 +0,0 @@ -import Enzyme from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; - -Enzyme.configure({adapter: new Adapter()}); diff --git a/packages/sdk/test/setup_rtl.ts b/packages/sdk/test/setup_rtl.ts new file mode 100644 index 000000000..7b0828bfa --- /dev/null +++ b/packages/sdk/test/setup_rtl.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/packages/sdk/test/error_utils.test.ts b/packages/sdk/test/shared/error_utils.test.ts similarity index 98% rename from packages/sdk/test/error_utils.test.ts rename to packages/sdk/test/shared/error_utils.test.ts index 8a6e5a06c..19eefc9d9 100644 --- a/packages/sdk/test/error_utils.test.ts +++ b/packages/sdk/test/shared/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/private_utils.test.ts b/packages/sdk/test/shared/private_utils.test.ts similarity index 80% rename from packages/sdk/test/private_utils.test.ts rename to packages/sdk/test/shared/private_utils.test.ts index f25cb7399..7060691a5 100644 --- a/packages/sdk/test/private_utils.test.ts +++ b/packages/sdk/test/shared/private_utils.test.ts @@ -1,5 +1,6 @@ import { has, + isDeepEqual, isObjectEmpty, isNullOrUndefinedOrEmpty, compact, @@ -11,8 +12,8 @@ import { arrayDifference, debounce, cast, -} from '../src/private_utils'; -import {flowTest} from './test_helpers'; +} from '../../src/shared/private_utils'; +import {flowTest} from '../test_helpers'; jest.useFakeTimers(); @@ -38,6 +39,52 @@ describe('has', () => { }); }); +describe('isDeepEqual', () => { + it('returns true for primitive values', () => { + expect(isDeepEqual(1, 1)).toBe(true); + expect(isDeepEqual('foo', 'foo')).toBe(true); + expect(isDeepEqual(true, true)).toBe(true); + expect(isDeepEqual(false, false)).toBe(true); + expect(isDeepEqual(null, null)).toBe(true); + }); + + it('returns false for primitive values', () => { + expect(isDeepEqual(1, 2)).toBe(false); + expect(isDeepEqual('foo', 'bar')).toBe(false); + expect(isDeepEqual(true, false)).toBe(false); + expect(isDeepEqual(null, undefined)).toBe(false); + }); + + it('returns true for arrays', () => { + expect(isDeepEqual([1, 2, 3], [1, 2, 3])).toBe(true); + expect(isDeepEqual([{foo: 'bar'}], [{foo: 'bar'}])).toBe(true); + }); + + it('returns false for arrays', () => { + expect(isDeepEqual([1, 2], [1, 2, 3])).toBe(false); + expect(isDeepEqual([1, 2, 3], [1, 2])).toBe(false); + expect(isDeepEqual([1, 2], [1, 3])).toBe(false); + expect(isDeepEqual([1, 2], [2, 1])).toBe(false); + }); + + it('returns true if the objects have the same keys and values', () => { + expect(isDeepEqual({foo: 'bar'}, {foo: 'bar'})).toBe(true); + expect(isDeepEqual({foo: 1}, {foo: 1})).toBe(true); + expect(isDeepEqual({foo: 1, bar: true}, {foo: 1, bar: true})).toBe(true); + }); + + it('returns false if the objects have different keys', () => { + expect(isDeepEqual({foo: 'bar'}, {foo: 'bar', baz: 'qux'})).toBe(false); + }); + + it('returns false if the objects have the same keys but the values are not equal', () => { + expect(isDeepEqual({foo: 'bar'}, {foo: 'baz'})).toBe(false); + expect(isDeepEqual({foo: 1}, {foo: 2})).toBe(false); + expect(isDeepEqual({foo: true}, {foo: false})).toBe(false); + expect(isDeepEqual({foo: true}, {foo: 'bar'})).toBe(false); + }); +}); + describe('isObjectEmpty', () => { it('returns true if the object has no "own" properties', () => { expect(isObjectEmpty({})).toBe(true); @@ -138,13 +185,13 @@ describe('flattenDeep', () => { describe('keyBy', () => { it('converts arrays into objects keyed by the result of a given function', () => { - expect(keyBy([1, 2, 3, 4], o => String(o))).toEqual({ + expect(keyBy([1, 2, 3, 4], (o) => String(o))).toEqual({ '1': 1, '2': 2, '3': 3, '4': 4, }); - expect(keyBy([{id: 1}, {id: 2}, {id: 3}, {id: 4}], o => String(o.id))).toEqual({ + expect(keyBy([{id: 1}, {id: 2}, {id: 3}, {id: 4}], (o) => String(o.id))).toEqual({ '1': {id: 1}, '2': {id: 2}, '3': {id: 3}, @@ -161,7 +208,7 @@ describe('keyBy', () => { {id: 3, group: 'a'}, {id: 4, group: 'b'}, ], - o => o.group, + (o) => o.group, ), ).toEqual({ a: {id: 3, group: 'a'}, @@ -172,8 +219,8 @@ describe('keyBy', () => { describe('uniqBy', () => { it('removes non-unique array elements based on a given function', () => { - expect(uniqBy([1, 2, 3, 4], o => o)).toEqual([1, 2, 3, 4]); - expect(uniqBy([1, 1, 1, 1], o => o)).toEqual([1]); + expect(uniqBy([1, 2, 3, 4], (o) => o)).toEqual([1, 2, 3, 4]); + expect(uniqBy([1, 1, 1, 1], (o) => o)).toEqual([1]); expect( uniqBy( [ @@ -182,7 +229,7 @@ describe('uniqBy', () => { {id: 3, group: 'a'}, {id: 4, group: 'b'}, ], - o => o.group, + (o) => o.group, ), ).toEqual([ {id: 1, group: 'a'}, diff --git a/packages/sdk/test/unstable_private_utils.test.ts b/packages/sdk/test/shared/unstable_private_utils.test.ts similarity index 66% rename from packages/sdk/test/unstable_private_utils.test.ts rename to packages/sdk/test/shared/unstable_private_utils.test.ts index 96ab172b2..4471a2a7e 100644 --- a/packages/sdk/test/unstable_private_utils.test.ts +++ b/packages/sdk/test/shared/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/test/test_helpers.ts b/packages/sdk/test/test_helpers.ts index 9037d06c0..ae3ca48d5 100644 --- a/packages/sdk/test/test_helpers.ts +++ b/packages/sdk/test/test_helpers.ts @@ -1,5 +1,4 @@ -import {ReactWrapper} from 'enzyme'; -import Watchable from '../src/watchable'; +import type Watchable from '../src/shared/watchable'; /** * include a section of code that must pass flow but shouldn't actually be executed. Use it along @@ -8,19 +7,29 @@ import Watchable from '../src/watchable'; export function flowTest(description: string, fn: () => unknown): void { } -export function getComputedStylePropValue( - wrapper: ReactWrapper, - styleProp: string, -): string { - const domNode = wrapper.getDOMNode(); - return getComputedStyle(domNode).getPropertyValue(styleProp); +export function createPromiseWithResolveAndReject(): { + promise: Promise; + resolve: (value: T) => void; + reject: (error: any) => void; +} { + let resolve: (value: T) => void; + let reject: (error: any) => void; + const promise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }); + return {promise, resolve: resolve!, reject: reject!}; +} + +export function getComputedStylePropValue(element: Element, styleProp: string): string { + return getComputedStyle(element).getPropertyValue(styleProp); } export function waitForWatchKeyAsync( model: Watchable, key: Key, ): Promise { - return new Promise(resolve => { + return new Promise((resolve) => { const handler = () => { model.unwatch(key, handler); resolve(); diff --git a/packages/sdk/test/ui/base_provider.test.tsx b/packages/sdk/test/ui/base_provider.test.tsx deleted file mode 100644 index c919d78e4..000000000 --- a/packages/sdk/test/ui/base_provider.test.tsx +++ /dev/null @@ -1,34 +0,0 @@ -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'; - -let mockAirtableInterface: jest.Mocked; -jest.mock('../../src/injected/airtable_interface', () => ({ - __esModule: true, - default() { - if (!mockAirtableInterface) { - mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); - } - return mockAirtableInterface; - }, -})); - -describe('BaseProvider', () => { - it('enables rendering outside of a Blocks context', () => { - let base; - const Component = () => { - base = useBase(); - return null; - }; - - mount( - - - , - ); - - expect(base).toBe(sdk.base); - }); -}); diff --git a/packages/sdk/test/ui/block_wrapper.test.tsx b/packages/sdk/test/ui/block_wrapper.test.tsx deleted file mode 100644 index 801dbcc16..000000000 --- a/packages/sdk/test/ui/block_wrapper.test.tsx +++ /dev/null @@ -1,51 +0,0 @@ -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'; - -let mockAirtableInterface: jest.Mocked; -jest.mock('../../src/injected/airtable_interface', () => ({ - __esModule: true, - default() { - if (!mockAirtableInterface) { - mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); - } - return mockAirtableInterface; - }, -})); - -const render = (sdk_: Sdk): HTMLElement => { - const div = document.createElement('div'); - ReactDOM.render( - -
    -
    , - div, - ); - return div; -}; - -describe('BlockWrapper', () => { - afterEach(() => { - __reset(); - }); - - it('renders outside of a blocks context', () => { - mount( - -
    -
    , - ); - }); - - it('prompts user to resize viewport ("extension" terminology)', () => { - mockAirtableInterface.setFullscreenMaxSize.mockImplementation(() => {}); - mockAirtableInterface.enterFullscreen.mockImplementation(() => {}); - sdk.viewport.addMinSize({width: 2 ** 30}); - - expect(render(sdk).textContent).toMatch(/\bmake\s+this\s+extension\s+bigger\b/i); - }); -}); diff --git a/packages/sdk/test/ui/box.test.tsx b/packages/sdk/test/ui/box.test.tsx deleted file mode 100644 index e082b6389..000000000 --- a/packages/sdk/test/ui/box.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import {mount} from 'enzyme'; -import {Box} from '../../src/ui/unstable_standalone_ui'; - -describe('Box', () => { - it('renders outside of a blocks context', () => { - mount(); - }); -}); diff --git a/packages/sdk/test/ui/button.test.tsx b/packages/sdk/test/ui/button.test.tsx deleted file mode 100644 index ce80986aa..000000000 --- a/packages/sdk/test/ui/button.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import {mount} from 'enzyme'; -import {Button} from '../../src/ui/unstable_standalone_ui'; - -describe('Button', () => { - it('renders outside of a blocks context', () => { - mount(); - }); -}); diff --git a/packages/sdk/test/ui/cell_renderer.test.tsx b/packages/sdk/test/ui/cell_renderer.test.tsx deleted file mode 100644 index 929aed092..000000000 --- a/packages/sdk/test/ui/cell_renderer.test.tsx +++ /dev/null @@ -1,32 +0,0 @@ -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'; - -let mockAirtableInterface: jest.Mocked; -jest.mock('../../src/injected/airtable_interface', () => ({ - __esModule: true, - default() { - if (!mockAirtableInterface) { - mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); - } - return mockAirtableInterface; - }, -})); - -const TestProvider = ({children}: {children: React.ReactNode}) => { - return {children}; -}; - -describe('CellRenderer', () => { - it('renders within a Blocks Sdk context', () => { - const field = sdk.base.getTable('Design projects').getField('Name'); - mount( - - - , - ); - }); -}); diff --git a/packages/sdk/test/ui/choice_token.test.tsx b/packages/sdk/test/ui/choice_token.test.tsx deleted file mode 100644 index 36e6a0fb6..000000000 --- a/packages/sdk/test/ui/choice_token.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import {mount} from 'enzyme'; -import {ChoiceToken} from '../../src/ui/unstable_standalone_ui'; - -describe('ChoiceToken', () => { - it('renders outside of a blocks context', () => { - mount(); - }); -}); diff --git a/packages/sdk/test/ui/collaborator_token.test.tsx b/packages/sdk/test/ui/collaborator_token.test.tsx deleted file mode 100644 index afa487872..000000000 --- a/packages/sdk/test/ui/collaborator_token.test.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import {mount} from 'enzyme'; -import {CollaboratorToken} from '../../src/ui/unstable_standalone_ui'; - -describe('StaticCollaboratorToken', () => { - it('renders outside of a blocks context', () => { - mount( - , - ); - }); -}); diff --git a/packages/sdk/test/ui/color_palette.test.tsx b/packages/sdk/test/ui/color_palette.test.tsx deleted file mode 100644 index 4a0eafc18..000000000 --- a/packages/sdk/test/ui/color_palette.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import {mount} from 'enzyme'; -import {ColorPalette} from '../../src/ui/unstable_standalone_ui'; - -describe('ColorPalette', () => { - it('renders outside of a blocks context', () => { - mount(); - }); -}); diff --git a/packages/sdk/test/ui/color_palette_synced.test.tsx b/packages/sdk/test/ui/color_palette_synced.test.tsx deleted file mode 100644 index 5a33ab27a..000000000 --- a/packages/sdk/test/ui/color_palette_synced.test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -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'; - -let mockAirtableInterface: jest.Mocked; -jest.mock('../../src/injected/airtable_interface', () => ({ - __esModule: true, - default() { - if (!mockAirtableInterface) { - mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); - } - return mockAirtableInterface; - }, -})); - -const TestProvider = ({children}: {children: React.ReactNode}) => { - return {children}; -}; - -describe('ColorPaletteSynced', () => { - it('renders within a blocks context', () => { - mount( - - - , - ); - }); -}); diff --git a/packages/sdk/test/ui/confirmation_dialog.test.tsx b/packages/sdk/test/ui/confirmation_dialog.test.tsx deleted file mode 100644 index abf205d4a..000000000 --- a/packages/sdk/test/ui/confirmation_dialog.test.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import {mount} from 'enzyme'; -import {ConfirmationDialog} from '../../src/ui/unstable_standalone_ui'; - -describe('ConfirmationDialog', () => { - it('renders outside of a blocks context', () => { - const noop = () => {}; - - mount(); - }); -}); diff --git a/packages/sdk/test/ui/dialog.test.tsx b/packages/sdk/test/ui/dialog.test.tsx deleted file mode 100644 index 7f18fdaac..000000000 --- a/packages/sdk/test/ui/dialog.test.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import {mount} from 'enzyme'; -import {Dialog} from '../../src/ui/unstable_standalone_ui'; - -describe('Dialog', () => { - it('renders outside of a blocks context', () => { - mount( - {}}> -
    -
    , - ); - }); -}); diff --git a/packages/sdk/test/ui/field_icon.test.tsx b/packages/sdk/test/ui/field_icon.test.tsx deleted file mode 100644 index 78775ea8e..000000000 --- a/packages/sdk/test/ui/field_icon.test.tsx +++ /dev/null @@ -1,39 +0,0 @@ -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'; - -let mockAirtableInterface: jest.Mocked; -jest.mock('../../src/injected/airtable_interface', () => ({ - __esModule: true, - default() { - if (!mockAirtableInterface) { - mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); - } - return mockAirtableInterface; - }, -})); - -const TestProvider = ({children}: {children: React.ReactNode}) => { - return {children}; -}; - -describe('FieldIcon', () => { - it('renders within a blocks context', () => { - const uiConfig = { - iconName: 'text', - desiredCellWidthForRecordCard: 10, - minimumCellWidthForRecordCard: 10, - }; - mockAirtableInterface.fieldTypeProvider.getUiConfig.mockReturnValue(uiConfig); - const field = sdk.base.getTable('Design projects').getField('Name'); - - mount( - - - , - ); - }); -}); diff --git a/packages/sdk/test/ui/field_picker.test.tsx b/packages/sdk/test/ui/field_picker.test.tsx deleted file mode 100644 index 4f10ec179..000000000 --- a/packages/sdk/test/ui/field_picker.test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -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'; - -let mockAirtableInterface: jest.Mocked; -jest.mock('../../src/injected/airtable_interface', () => ({ - __esModule: true, - default() { - if (!mockAirtableInterface) { - mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); - } - return mockAirtableInterface; - }, -})); - -const TestProvider = ({children}: {children: React.ReactNode}) => { - return {children}; -}; - -describe('FieldPicker', () => { - it('renders within a blocks context', () => { - mount( - - - , - ); - }); -}); diff --git a/packages/sdk/test/ui/field_picker_synced.test.tsx b/packages/sdk/test/ui/field_picker_synced.test.tsx deleted file mode 100644 index 40d0b4f49..000000000 --- a/packages/sdk/test/ui/field_picker_synced.test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -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'; - -let mockAirtableInterface: jest.Mocked; -jest.mock('../../src/injected/airtable_interface', () => ({ - __esModule: true, - default() { - if (!mockAirtableInterface) { - mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); - } - return mockAirtableInterface; - }, -})); - -const TestProvider = ({children}: {children: React.ReactNode}) => { - return {children}; -}; - -describe('FieldPickerSynced', () => { - it('renders within a blocks context', () => { - mount( - - - , - ); - }); -}); diff --git a/packages/sdk/test/ui/form_field.test.tsx b/packages/sdk/test/ui/form_field.test.tsx deleted file mode 100644 index 12b58a339..000000000 --- a/packages/sdk/test/ui/form_field.test.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import React from 'react'; -import {mount, render} from 'enzyme'; -import {FormField, Select, Input} from '../../src/ui/unstable_standalone_ui'; - -describe('FormField', () => { - it('renders outside of a blocks context', () => { - mount(); - }); - - describe(' - , - ); - expect($el.find('label').length).toEqual(1); - const labelTarget = $el.find('label').attr('for'); - expect($el.find('select').length).toEqual(1); - expect($el.find('select').attr('id')).toEqual(labelTarget); - }); - - it('creates and associates a description using aria-describedby', () => { - const $el = render( - - -
    from markup
    -
    , - ); - const $select = $el.find('select'); - - expect($select.length).toEqual(1); - const descriptionIds = $select.attr('aria-describedby'); - - expect(typeof descriptionIds).toEqual('string'); - const ids = (descriptionIds as string).split(/\s+/); - expect(ids.length).toEqual(2); - const descriptions = [ - $el.find(`[id="${ids[0]}"]`).text(), - $el.find(`[id="${ids[1]}"]`).text(), - ]; - expect(descriptions).toContain('from FormField'); - expect(descriptions).toContain('from markup'); - }); - }); - - describe(' integration', () => { - it('creates and associates a label', () => { - const $el = render( - - {}} /> - , - ); - expect($el.find('label').length).toEqual(1); - const labelTarget = $el.find('label').attr('for'); - expect($el.find('input').length).toEqual(1); - expect($el.find('input').attr('id')).toEqual(labelTarget); - }); - - it('creates and associates a description using aria-describedby', () => { - const $el = render( - - {}} /> - , - ); - const $input = $el.find('input'); - - expect($input.length).toEqual(1); - const descriptionId = $input.attr('aria-describedby'); - expect(typeof descriptionId).toEqual('string'); - const $desc = $el.find(`[id="${descriptionId}"]`); - expect($desc.length).toEqual(1); - expect($desc.text()).toEqual('my input description'); - }); - - it('creates and extends a description using aria-describedby', () => { - const $el = render( - - {}} /> -
    from markup
    -
    , - ); - const $input = $el.find('input'); - - expect($input.length).toEqual(1); - const descriptionIds = $input.attr('aria-describedby'); - expect(typeof descriptionIds).toEqual('string'); - - const ids = (descriptionIds as string).split(/\s+/); - expect(ids.length).toEqual(2); - const descriptions = [ - $el.find(`[id="${ids[0]}"]`).text(), - $el.find(`[id="${ids[1]}"]`).text(), - ]; - expect(descriptions).toContain('from FormField'); - expect(descriptions).toContain('from markup'); - }); - }); -}); diff --git a/packages/sdk/test/ui/global_alert.test.tsx b/packages/sdk/test/ui/global_alert.test.tsx deleted file mode 100644 index 59c234362..000000000 --- a/packages/sdk/test/ui/global_alert.test.tsx +++ /dev/null @@ -1,40 +0,0 @@ -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'; - -let mockAirtableInterface: jest.Mocked; -jest.mock('../../src/injected/airtable_interface', () => ({ - __esModule: true, - default() { - if (!mockAirtableInterface) { - mockAirtableInterface = MockAirtableInterface.linkedRecordsExample(); - } - return mockAirtableInterface; - }, -})); - -const TestProvider = ({children}: {children: React.ReactNode}) => { - return {children}; -}; - -describe('GlobalAlert', () => { - it('renders within a Blocks Sdk context', () => { - const globalAlert = new GlobalAlert(); - globalAlert.showReloadPrompt(); - mount({globalAlert.__alertInfo!.content}); - }); - - it('sends a "reload" request when clicked', () => { - const globalAlert = new GlobalAlert(); - globalAlert.showReloadPrompt(); - const wrapper = mount({globalAlert.__alertInfo!.content}); - mockAirtableInterface.reloadFrame.mockImplementation(() => {}); - - wrapper.find('span span').simulate('click'); - - expect(mockAirtableInterface.reloadFrame).toHaveBeenCalledTimes(1); - }); -}); diff --git a/packages/sdk/test/ui/heading.test.tsx b/packages/sdk/test/ui/heading.test.tsx deleted file mode 100644 index eaa9d3b91..000000000 --- a/packages/sdk/test/ui/heading.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import {mount} from 'enzyme'; -import {Heading} from '../../src/ui/unstable_standalone_ui'; - -describe('Heading', () => { - it('renders outside of a blocks context', () => { - mount(); - }); -}); diff --git a/packages/sdk/test/ui/icon.test.tsx b/packages/sdk/test/ui/icon.test.tsx deleted file mode 100644 index 32d553abb..000000000 --- a/packages/sdk/test/ui/icon.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import {mount} from 'enzyme'; -import {Icon} from '../../src/ui/unstable_standalone_ui'; - -describe('Icon', () => { - it('renders outside of a blocks context', () => { - mount(); - }); -}); diff --git a/packages/sdk/test/ui/initialize_block.test.tsx b/packages/sdk/test/ui/initialize_block.test.tsx deleted file mode 100644 index 858f97f68..000000000 --- a/packages/sdk/test/ui/initialize_block.test.tsx +++ /dev/null @@ -1,27 +0,0 @@ -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'; - -let mockAirtableInterface: jest.Mocked; -jest.mock('../../src/injected/airtable_interface', () => ({ - __esModule: true, - default() { - if (!mockAirtableInterface) { - mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); - } - return mockAirtableInterface; - }, -})); - -describe('initializeBlock', () => { - afterEach(() => { - __resetHasBeenInitialized(); - }); - it("functions without explicitly importing the SDK's main entry point", () => { - initializeBlock(() =>
    ); - }); - it('functions as a view and a block through one entry point', () => { - initializeBlock({dashboard: () =>
    , view: () =>
    }); - }); -}); diff --git a/packages/sdk/test/ui/input.test.tsx b/packages/sdk/test/ui/input.test.tsx deleted file mode 100644 index b3593d916..000000000 --- a/packages/sdk/test/ui/input.test.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import {mount} from 'enzyme'; -import {Input} from '../../src/ui/unstable_standalone_ui'; - -describe('Input', () => { - it('renders outside of a blocks context', () => { - mount( {}} />); - }); - - it('does not render aria-describedby when unspecified', () => { - const $el = mount( {}} />).render(); - - expect($el.is('[aria-describedby]')).toEqual(false); - }); - - it('recognizes aria-describedby', () => { - const $el = mount( {}} aria-describedby="foo" />).render(); - - expect($el.attr('aria-describedby')).toEqual('foo'); - }); -}); diff --git a/packages/sdk/test/ui/input_synced.test.tsx b/packages/sdk/test/ui/input_synced.test.tsx deleted file mode 100644 index 3ebabc156..000000000 --- a/packages/sdk/test/ui/input_synced.test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -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'; - -let mockAirtableInterface: jest.Mocked; -jest.mock('../../src/injected/airtable_interface', () => ({ - __esModule: true, - default() { - if (!mockAirtableInterface) { - mockAirtableInterface = MockAirtableInterface.projectTrackerExample(); - } - return mockAirtableInterface; - }, -})); - -const TestProvider = ({children}: {children: React.ReactNode}) => { - return {children}; -}; - -describe('InputSynced', () => { - it('renders within a blocks context', () => { - mount( - - {}} /> - , - ); - }); -}); diff --git a/packages/sdk/test/ui/label.test.tsx b/packages/sdk/test/ui/label.test.tsx deleted file mode 100644 index 72aebddd6..000000000 --- a/packages/sdk/test/ui/label.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import {mount} from 'enzyme'; -import {Label} from '../../src/ui/unstable_standalone_ui'; - -describe('Label', () => { - it('renders outside of a blocks context', () => { - mount(