├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .releaserc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── config.dist.json ├── lint-staged.config.js ├── package-lock.json ├── package.config.ts ├── package.json ├── renovate.json ├── sanity.json ├── src ├── components │ ├── TableComponent.tsx │ ├── TableIcon.tsx │ ├── TableInput.tsx │ ├── TableMenu.tsx │ └── TablePreview.tsx └── index.ts ├── tsconfig.json ├── tsconfig.lib.json ├── tsconfig.settings.json └── v2-incompatible.js /.editorconfig: -------------------------------------------------------------------------------- 1 | ; editorconfig.org 2 | root = true 3 | charset= utf8 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /lib 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "browser": true 6 | }, 7 | "extends": [ 8 | "sanity/typescript", 9 | "sanity/react", 10 | "plugin:prettier/recommended", 11 | "prettier" 12 | ], 13 | "plugins": ["simple-import-sort"], 14 | "rules": { 15 | "react/prop-types": "off", 16 | "react/require-default-props": "off", 17 | "simple-import-sort/imports": "error", 18 | "simple-import-sort/exports": "error", 19 | "no-undef": "off", 20 | "no-use-before-define": "off" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI & Release 3 | 4 | # Workflow name based on selected inputs. Fallback to default Github naming when expression evaluates to empty string 5 | run-name: >- 6 | ${{ 7 | inputs.release && inputs.test && 'Build ➤ Test ➤ Publish to NPM' || 8 | inputs.release && !inputs.test && 'Build ➤ Skip Tests ➤ Publish to NPM' || 9 | github.event_name == 'workflow_dispatch' && inputs.test && 'Build ➤ Test' || 10 | github.event_name == 'workflow_dispatch' && !inputs.test && 'Build ➤ Skip Tests' || 11 | '' 12 | }} 13 | 14 | on: 15 | # Build on pushes branches that have a PR (including drafts) 16 | pull_request: 17 | # Build on commits pushed to branches without a PR if it's in the allowlist 18 | push: 19 | branches: [main] 20 | # https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow 21 | workflow_dispatch: 22 | inputs: 23 | test: 24 | description: Run tests 25 | required: true 26 | default: true 27 | type: boolean 28 | release: 29 | description: Release new version 30 | required: true 31 | default: false 32 | type: boolean 33 | 34 | concurrency: 35 | # On PRs builds will cancel if new pushes happen before the CI completes, as it defines `github.head_ref` and gives it the name of the branch the PR wants to merge into 36 | # Otherwise `github.run_id` ensures that you can quickly merge a queue of PRs without causing tests to auto cancel on any of the commits pushed to main. 37 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 38 | cancel-in-progress: true 39 | 40 | jobs: 41 | log-the-inputs: 42 | name: Log inputs 43 | runs-on: ubuntu-latest 44 | steps: 45 | - run: | 46 | echo "Inputs: $INPUTS" 47 | env: 48 | INPUTS: ${{ toJSON(inputs) }} 49 | 50 | build: 51 | runs-on: ubuntu-latest 52 | name: Lint & Build 53 | steps: 54 | - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3 55 | - uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3 56 | with: 57 | cache: npm 58 | node-version: lts/* 59 | - run: npm ci 60 | # Linting can be skipped 61 | - run: npm run lint --if-present 62 | if: github.event.inputs.test != 'false' 63 | # But not the build script, as semantic-release will crash if this command fails so it makes sense to test it early 64 | - run: npm run prepublishOnly --if-present 65 | 66 | test: 67 | needs: build 68 | # The test matrix can be skipped, in case a new release needs to be fast-tracked and tests are already passing on main 69 | if: github.event.inputs.test != 'false' 70 | runs-on: ${{ matrix.os }} 71 | name: Node.js ${{ matrix.node }} / ${{ matrix.os }} 72 | strategy: 73 | # A test failing on windows doesn't mean it'll fail on macos. It's useful to let all tests run to its completion to get the full picture 74 | fail-fast: false 75 | matrix: 76 | # Run the testing suite on each major OS with the latest LTS release of Node.js 77 | os: [macos-latest, ubuntu-latest, windows-latest] 78 | node: [lts/*] 79 | # It makes sense to also test the oldest, and latest, versions of Node.js, on ubuntu-only since it's the fastest CI runner 80 | include: 81 | - os: ubuntu-latest 82 | # Test the oldest LTS release of Node that's still receiving bugfixes and security patches, versions older than that have reached End-of-Life 83 | node: lts/-2 84 | - os: ubuntu-latest 85 | # Test the actively developed version that will become the latest LTS release next October 86 | node: current 87 | steps: 88 | # It's only necessary to do this for windows, as mac and ubuntu are sane OS's that already use LF 89 | - name: Set git to use LF 90 | if: matrix.os == 'windows-latest' 91 | run: | 92 | git config --global core.autocrlf false 93 | git config --global core.eol lf 94 | - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3 95 | - uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3 96 | with: 97 | cache: npm 98 | node-version: ${{ matrix.node }} 99 | - run: npm i 100 | - run: npm test --if-present 101 | 102 | release: 103 | needs: [build, test] 104 | # only run if opt-in during workflow_dispatch 105 | if: always() && github.event.inputs.release == 'true' && needs.build.result != 'failure' && needs.test.result != 'failure' && needs.test.result != 'cancelled' 106 | runs-on: ubuntu-latest 107 | name: Semantic release 108 | steps: 109 | - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3 110 | with: 111 | # Need to fetch entire commit history to 112 | # analyze every commit since last release 113 | fetch-depth: 0 114 | - uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3 115 | with: 116 | cache: npm 117 | node-version: lts/* 118 | - run: npm ci 119 | # Branches that will release new versions are defined in .releaserc.json 120 | - run: npx semantic-release 121 | # Don't allow interrupting the release step if the job is cancelled, as it can lead to an inconsistent state 122 | # e.g. git tags were pushed but it exited before `npm publish` 123 | if: always() 124 | env: 125 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 126 | NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 127 | # Re-run semantic release with rich logs if it failed to publish for easier debugging 128 | - run: npx semantic-release --dry-run --debug 129 | if: failure() 130 | env: 131 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 132 | NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 133 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # macOS finder cache file 40 | .DS_Store 41 | 42 | # VS Code settings 43 | .vscode 44 | 45 | # IntelliJ 46 | .idea 47 | *.iml 48 | 49 | # Cache 50 | .cache 51 | 52 | # Yalc 53 | .yalc 54 | yalc.lock 55 | 56 | ##npm package zips 57 | *.tgz 58 | 59 | # Compiled plugin 60 | lib 61 | 62 | .eslintcache 63 | tsconfig.tsbuildinfo 64 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit "" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /test 2 | /coverage 3 | .editorconfig 4 | .eslintrc 5 | .gitignore 6 | .github 7 | .prettierrc 8 | .travis.yml 9 | .nyc_output 10 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "bracketSpacing": true, 6 | "arrowParens": "avoid" 7 | } 8 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sanity/semantic-release-preset", 3 | "branches": ["main"] 4 | } 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 📓 Changelog 4 | 5 | All notable changes to this project will be documented in this file. See 6 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 7 | 8 | ## [1.1.3](https://github.com/sanity-io/table/compare/v1.1.2...v1.1.3) (2024-12-18) 9 | 10 | ### Bug Fixes 11 | 12 | - make react 19 compatible ([#49](https://github.com/sanity-io/table/issues/49)) ([a2e5eff](https://github.com/sanity-io/table/commit/a2e5effbab9fff9feb598685f163b8a1cdb25d24)) 13 | 14 | ## [1.1.2](https://github.com/sanity-io/table/compare/v1.1.1...v1.1.2) (2024-01-17) 15 | 16 | ### Bug Fixes 17 | 18 | - correct config type ([c911c23](https://github.com/sanity-io/table/commit/c911c23cdafb6c6ec659ab4a081c8d76db4536c5)) 19 | 20 | ## [1.1.1](https://github.com/sanity-io/table/compare/v1.1.0...v1.1.1) (2024-01-17) 21 | 22 | ### Bug Fixes 23 | 24 | - pass row type to table component ([35d5457](https://github.com/sanity-io/table/commit/35d545728ad97419ee7cc7b0bb674bc5e8844a85)) 25 | 26 | ## [1.1.0](https://github.com/sanity-io/table/compare/v1.0.1...v1.1.0) (2024-01-12) 27 | 28 | ### Features 29 | 30 | - add configurable row type ([4d19b67](https://github.com/sanity-io/table/commit/4d19b67197a8507aeb6125020020d7467286c7bb)) 31 | 32 | ## [1.0.1](https://github.com/sanity-io/table/compare/v1.0.0...v1.0.1) (2022-11-25) 33 | 34 | ### Bug Fixes 35 | 36 | - **deps:** sanity ^3.0.0 (works with rc.3) ([ba5c124](https://github.com/sanity-io/table/commit/ba5c124daa0dafe66b2755e861ecb91ec3c1a705)) 37 | - preview props ([6ab72e7](https://github.com/sanity-io/table/commit/6ab72e76c400d9c1a5c6e073df79bb34e25b1990)) 38 | 39 | ## 1.0.0 (2022-11-20) 40 | 41 | ### ⚠ BREAKING CHANGES 42 | 43 | - Big thanks to the original contributors for their work! 44 | @rdunk @kMathisBullinger and @davydog187 45 | 46 | ### Features 47 | 48 | - initial Sanity Studio v3 release ([639df1e](https://github.com/sanity-io/table/commit/639df1ee074d6e7b46291c66f49382ee20da62d3)) 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 ʞunp ʇɹǝdnɹ 4 | Copyright (c) 2024 Mathis Bullinger 5 | Copyright (c) 2024 Dave Lucia 6 | Copyright (c) 2024 Sanity.io 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sanity Table Plugin 2 | 3 | > This is a **Sanity Studio v3** plugin. 4 | > For the v2 version, please refer to the [v2 repository](https://github.com/rdunk/sanity-plugin-table). 5 | 6 | This is a (triple) fork of the Sanity Plugin Table, migrated to [Sanity Studio V3](https://beta.sanity.io/docs/platform/studio/v2-to-v3). 7 | Only the v3 version is maintained by Sanity.io. 8 | 9 | ![example](https://user-images.githubusercontent.com/8467307/48703530-e369be00-ebeb-11e8-8299-14812461aee8.gif) 10 | 11 | ## Acknowledgements 12 | 13 | Big thanks to the original contributors for their work! 14 | 15 | - Original version: [rdunk/sanity-plugin-table](https://github.com/rdunk/sanity-plugin-table). 16 | - Further improvements in fork [MathisBullinger/sanity-plugin-another-table](https://github.com/MathisBullinger/sanity-plugin-another-table). 17 | - Initial V3 port: [bitfo/sanity-plugin-table](https://github.com/bitfo/sanity-plugin-table) 18 | 19 | ## Disclaimer 20 | 21 | Sometimes a table is just what you need. 22 | However, before using the Table plugin, consider if there are other ways to model your data that are: 23 | 24 | - easier to edit and validate 25 | - easier to query 26 | 27 | Approaching your schemas in a more structured manner can often pay dividends down the line. 28 | 29 | ## Install 30 | 31 | Install using npm 32 | 33 | ```bash 34 | $ npm i --save @sanity/table 35 | ``` 36 | 37 | ## Usage 38 | 39 | Add the plugin to your project configuration. Then use the type in your schemas 40 | 41 | ```js 42 | // sanity.config.ts 43 | 44 | import { defineConfig } from 'sanity'; 45 | 46 | import { table } from '@sanity/table'; 47 | 48 | export default defineConfig({ 49 | name: 'default', 50 | title: 'My Cool Project', 51 | projectId: 'my-project-id', 52 | dataset: 'production', 53 | plugins: [ 54 | // Include the table plugin 55 | table(), 56 | ], 57 | schema: { 58 | types: [ 59 | { 60 | name: 'product', 61 | title: 'Product', 62 | type: 'document', 63 | fields: [ 64 | { 65 | // Include the table as a field 66 | // Giving it a semantic title 67 | name: 'sizeChart', 68 | title: 'Size Chart', 69 | type: 'table', 70 | }, 71 | ], 72 | }, 73 | ], 74 | }, 75 | }); 76 | ``` 77 | 78 | ## Configuration 79 | 80 | You can optionally configure the `_type` used for the row object in the table schema by passing a `rowType` when adding the plugin. For most users this is unnecessary, but it can be useful if you are migrating from a legacy table plugin. 81 | 82 | ```js 83 | export default defineConfig({ 84 | // ... 85 | plugins: [ 86 | table({ 87 | rowType: 'my-custom-row-type', 88 | }), 89 | ], 90 | // ... 91 | }); 92 | ``` 93 | 94 | ## License 95 | 96 | [MIT](LICENSE) © ʞunp ʇɹǝdnɹ, Mathis Bullinger, Dave Lucia and Sanity.io 97 | 98 | ## Develop & test 99 | 100 | This plugin uses [@sanity/plugin-kit](https://github.com/sanity-io/plugin-kit) 101 | with default configuration for build & watch scripts. 102 | 103 | See [Testing a plugin in Sanity Studio](https://github.com/sanity-io/plugin-kit#testing-a-plugin-in-sanity-studio) 104 | on how to run this plugin with hotreload in the studio. 105 | 106 | ### Release new version 107 | 108 | Run ["CI & Release" workflow](https://github.com/sanity-io/table/actions/workflows/main.yml). 109 | Make sure to select the main branch and check "Release new version". 110 | 111 | Semantic release will only release on configured branches, so it is safe to run release on any branch. 112 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | -------------------------------------------------------------------------------- /config.dist.json: -------------------------------------------------------------------------------- 1 | { 2 | "rowType": "tableRow" 3 | } 4 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | '**/*.{js,jsx}': ['eslint'], 3 | '**/*.{ts,tsx}': ['eslint', () => 'tsc --noEmit'], 4 | }; 5 | -------------------------------------------------------------------------------- /package.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@sanity/pkg-utils'; 2 | 3 | export default defineConfig({ 4 | tsconfig: 'tsconfig.lib.json', 5 | dist: 'lib', 6 | // Remove this block to enable strict export validation 7 | extract: { 8 | rules: { 9 | 'ae-forgotten-export': 'off', 10 | 'ae-incompatible-release-tags': 'off', 11 | 'ae-internal-missing-underscore': 'off', 12 | 'ae-missing-release-tag': 'off', 13 | }, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sanity/table", 3 | "version": "1.1.3", 4 | "description": "Table schema type and input component for Sanity Studio", 5 | "keywords": [ 6 | "sanity", 7 | "sanity-plugin" 8 | ], 9 | "homepage": "https://github.com/sanity-io/table#readme", 10 | "bugs": { 11 | "url": "https://github.com/sanity-io/table/issues" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git@github.com:sanity-io/table.git" 16 | }, 17 | "license": "MIT", 18 | "author": "Dave Lucia ", 19 | "sideEffects": false, 20 | "type": "module", 21 | "exports": { 22 | ".": { 23 | "source": "./src/index.ts", 24 | "import": "./lib/index.js", 25 | "require": "./lib/index.cjs", 26 | "default": "./lib/index.js" 27 | }, 28 | "./package.json": "./package.json" 29 | }, 30 | "main": "./lib/index.cjs", 31 | "module": "./lib/index.js", 32 | "types": "./lib/index.d.ts", 33 | "files": [ 34 | "src", 35 | "lib", 36 | "v2-incompatible.js", 37 | "sanity.json" 38 | ], 39 | "scripts": { 40 | "prebuild": "npm run clean && plugin-kit verify-package --silent && pkg-utils", 41 | "build": "plugin-kit verify-package --silent && pkg-utils build --strict --check --clean", 42 | "clean": "rimraf lib", 43 | "format": "eslint . --fix --cache && prettier --write .", 44 | "link-watch": "plugin-kit link-watch", 45 | "lint": "eslint src --cache && prettier --check src", 46 | "prepare": "husky install", 47 | "prepublishOnly": "npm run build", 48 | "watch": "pkg-utils watch --strict" 49 | }, 50 | "browserslist": "extends @sanity/browserslist-config", 51 | "dependencies": { 52 | "@sanity/icons": "^3.5.3", 53 | "@sanity/incompatible-plugin": "^1.0.5", 54 | "@sanity/ui": "^2.10.11" 55 | }, 56 | "devDependencies": { 57 | "@commitlint/cli": "^19.6.1", 58 | "@commitlint/config-conventional": "^19.6.0", 59 | "@sanity/pkg-utils": "^6.12.0", 60 | "@sanity/plugin-kit": "^4.0.18", 61 | "@sanity/semantic-release-preset": "^5.0.0", 62 | "@types/react": "^18", 63 | "@types/react-dom": "^18", 64 | "@typescript-eslint/eslint-plugin": "^7.6.0", 65 | "@typescript-eslint/parser": "^7.6.0", 66 | "eslint": "^8.57.0", 67 | "eslint-config-prettier": "^9.1.0", 68 | "eslint-config-sanity": "^7.1.2", 69 | "eslint-plugin-prettier": "^4.0.0", 70 | "eslint-plugin-react": "^7.34.1", 71 | "eslint-plugin-react-hooks": "^4.6.0", 72 | "eslint-plugin-simple-import-sort": "^12.0.0", 73 | "husky": "^8.0.2", 74 | "lint-staged": "^13.0.3", 75 | "prettier": "^2.7.1", 76 | "prettier-plugin-packagejson": "^2.3.0", 77 | "react": "^18", 78 | "react-dom": "^18", 79 | "rimraf": "^3.0.2", 80 | "sanity": "^3.67.1", 81 | "semantic-release": "^24.2.0", 82 | "typescript": "^5.7.2" 83 | }, 84 | "peerDependencies": { 85 | "react": "^18 || ^19", 86 | "sanity": "^3.0.0" 87 | }, 88 | "engines": { 89 | "node": ">=18" 90 | }, 91 | "publishConfig": { 92 | "access": "public" 93 | }, 94 | "overrides": { 95 | "conventional-changelog-conventionalcommits": ">=8.0.0" 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>sanity-io/renovate-presets//ecosystem/auto", 5 | "github>sanity-io/renovate-presets//ecosystem/studio-v3" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /sanity.json: -------------------------------------------------------------------------------- 1 | { 2 | "parts": [ 3 | { 4 | "implements": "part:@sanity/base/sanity-root", 5 | "path": "./v2-incompatible.js" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/components/TableComponent.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable consistent-return */ 2 | import { AddIcon } from '@sanity/icons'; 3 | import { Box, Button, Card, Dialog, Flex, Inline, Text } from '@sanity/ui'; 4 | import { uuid } from '@sanity/uuid'; 5 | import { type FormEvent, useState } from 'react'; 6 | import { type ObjectInputProps, set, unset } from 'sanity'; 7 | 8 | import { TableInput } from './TableInput'; 9 | import { TableMenu } from './TableMenu'; 10 | 11 | const deepClone: (data: T) => T = 12 | globalThis.structuredClone ?? (data => JSON.parse(JSON.stringify(data))); 13 | 14 | export interface TableValue { 15 | _type: 'table'; 16 | rows: TableRow[]; 17 | } 18 | 19 | export type TableProps = ObjectInputProps; 20 | 21 | export type TableRow = { 22 | _type: string; 23 | _key: string; 24 | cells: string[]; 25 | }; 26 | 27 | // TODO refactor deeplone stuff to use proper patches 28 | // TODO use callback all the things 29 | 30 | export const TableComponent = (props: TableProps & { rowType?: string }) => { 31 | const { rowType = 'tableRow', value, onChange } = props; 32 | const [dialog, setDialog] = useState<{ 33 | type: string; 34 | callback: () => void; 35 | } | null>(null); 36 | 37 | const updateValue = (v?: Omit) => { 38 | return onChange(set(v)); 39 | }; 40 | 41 | const resetValue = () => { 42 | return onChange(unset()); 43 | }; 44 | 45 | const createTable = () => { 46 | const newValue: Omit = { 47 | rows: [ 48 | { 49 | _type: rowType, 50 | _key: uuid(), 51 | cells: ['', ''], 52 | }, 53 | { 54 | _type: rowType, 55 | _key: uuid(), 56 | cells: ['', ''], 57 | }, 58 | ], 59 | }; 60 | return updateValue({ ...value, ...newValue }); 61 | }; 62 | 63 | const confirmRemoveTable = () => { 64 | setDialog({ type: 'table', callback: removeTable }); 65 | }; 66 | 67 | const removeTable = () => { 68 | resetValue(); 69 | setDialog(null); 70 | }; 71 | 72 | const addRows = (count = 1) => { 73 | if (!value) { 74 | return; 75 | } 76 | const newValue = deepClone(value); 77 | // Calculate the column count from the first row 78 | const columnCount = value?.rows[0].cells.length ?? 0; 79 | for (let i = 0; i < count; i++) { 80 | // Add as many cells as we have columns 81 | newValue.rows.push({ 82 | _type: rowType, 83 | _key: uuid(), 84 | cells: Array(columnCount).fill(''), 85 | }); 86 | } 87 | // eslint-disable-next-line consistent-return 88 | return updateValue(newValue); 89 | }; 90 | 91 | const addRowAt = (index = 0) => { 92 | if (!value) { 93 | return; 94 | } 95 | const newValue = deepClone(value); 96 | // Calculate the column count from the first row 97 | const columnCount = value.rows[0].cells.length; 98 | 99 | newValue.rows.splice(index, 0, { 100 | _type: rowType, 101 | _key: uuid(), 102 | cells: Array(columnCount).fill(''), 103 | }); 104 | 105 | // eslint-disable-next-line consistent-return 106 | return updateValue(newValue); 107 | }; 108 | 109 | const removeRow = (index: number) => { 110 | if (!value) { 111 | return; 112 | } 113 | const newValue = deepClone(value); 114 | newValue.rows.splice(index, 1); 115 | updateValue(newValue); 116 | setDialog(null); 117 | }; 118 | 119 | const confirmRemoveRow = (index: number) => { 120 | if (!value) { 121 | return; 122 | } 123 | if (value.rows.length <= 1) return confirmRemoveTable(); 124 | return setDialog({ type: 'row', callback: () => removeRow(index) }); 125 | }; 126 | 127 | const confirmRemoveColumn = (index: number) => { 128 | if (!value) { 129 | return; 130 | } 131 | if (value.rows[0].cells.length <= 1) return confirmRemoveTable(); 132 | return setDialog({ type: 'column', callback: () => removeColumn(index) }); 133 | }; 134 | 135 | const addColumns = (count: number) => { 136 | if (!value) { 137 | return; 138 | } 139 | const newValue = deepClone(value); 140 | // Add a cell to each of the rows 141 | newValue.rows.forEach((_, i) => { 142 | for (let j = 0; j < count; j++) { 143 | newValue.rows[i].cells.push(''); 144 | } 145 | }); 146 | return updateValue(newValue); 147 | }; 148 | 149 | const addColumnAt = (index: number) => { 150 | if (!value) { 151 | return; 152 | } 153 | const newValue = deepClone(value); 154 | 155 | newValue.rows.forEach((_, i) => { 156 | newValue.rows[i].cells.splice(index, 0, ''); 157 | }); 158 | 159 | return updateValue(newValue); 160 | }; 161 | 162 | const removeColumn = (index: number) => { 163 | if (!value) { 164 | return; 165 | } 166 | const newValue = deepClone(value); 167 | newValue.rows.forEach(row => { 168 | row.cells.splice(index, 1); 169 | }); 170 | updateValue(newValue); 171 | setDialog(null); 172 | }; 173 | 174 | const updateCell = ( 175 | e: FormEvent, 176 | rowIndex: number, 177 | cellIndex: number 178 | ) => { 179 | if (!value) { 180 | return; 181 | } 182 | const newValue = deepClone(value); 183 | newValue.rows[rowIndex].cells[cellIndex] = ( 184 | e.target as HTMLInputElement 185 | ).value; 186 | return updateValue(newValue); 187 | }; 188 | 189 | return ( 190 |
191 | {dialog && ( 192 | setDialog(null)} 196 | zOffset={1000} 197 | > 198 | 199 | Are you sure you want to remove this {dialog.type}? 200 | 201 | 202 | 216 | )} 217 | 218 | 219 | {value?.rows?.length && ( 220 | 228 | )} 229 | 230 | 231 | {value?.rows?.length && ( 232 | 238 | )} 239 | {(!value || !value?.rows?.length) && ( 240 | 241 |
253 | ); 254 | }; 255 | 256 | export function createTableComponent(rowType: string) { 257 | return function Table(props: TableProps) { 258 | return ; 259 | }; 260 | } 261 | -------------------------------------------------------------------------------- /src/components/TableIcon.tsx: -------------------------------------------------------------------------------- 1 | export function TableIcon() { 2 | return ( 3 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/components/TableInput.tsx: -------------------------------------------------------------------------------- 1 | import { RemoveIcon } from '@sanity/icons'; 2 | import { Box, Button, TextInput } from '@sanity/ui'; 3 | import type { FormEvent } from 'react'; 4 | 5 | import type { TableRow } from './TableComponent'; 6 | 7 | interface TableInputProps { 8 | rows: TableRow[]; 9 | updateCell: ( 10 | e: FormEvent, 11 | rowIndex: number, 12 | cellIndex: number 13 | ) => void; 14 | removeRow: (index: number) => void; 15 | removeColumn: (index: number) => void; 16 | } 17 | 18 | export const TableInput = (props: TableInputProps) => { 19 | const updateCell = props.updateCell; 20 | 21 | const renderRowCell = (rowIndex: number) => 22 | function RowCell(cell: string, cellIndex: number) { 23 | return ( 24 | 25 | updateCell(e, rowIndex, cellIndex)} 30 | /> 31 | 32 | ); 33 | }; 34 | 35 | const renderRow = (row: TableRow, rowIndex: number) => { 36 | const renderCell = renderRowCell(rowIndex); 37 | 38 | return ( 39 | 40 | {row.cells.map(renderCell)} 41 | { 42 | 43 | 44 |