Move TuistLab's API to the Tuist's repository (#3209)
* Move lab-api to Tuist's repository * Centralize the Rubocop config at the root * Use the right Ruby version on CI * Update the Ruby version in the Gemfile * Use Node 16.4.0 * Add missing dependency * Remove encrypted credentials * Some renames * Disable test Co-authored-by: Pedro Piñera <pepibumur@gmail.com>
This commit is contained in:
parent
3d19f8b5ac
commit
43f3602e84
|
@ -1 +1,3 @@
|
|||
CHANGELOG merge=union
|
||||
|
||||
yarn.lock linguist-generated
|
|
@ -0,0 +1,226 @@
|
|||
name: TuistLab
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
paths:
|
||||
- projects/lab/**
|
||||
|
||||
env:
|
||||
RAILS_ENV: test
|
||||
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
|
||||
RUBY_VERSION: "3.0.1"
|
||||
NODE_VERSION: "16.4.0"
|
||||
CI: 1
|
||||
jobs:
|
||||
test:
|
||||
name: Test Ruby
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:11-alpine
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis:4.0.6
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
ruby-version: ${{ env.RUBY_VERSION }}
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- name: Install dependencies
|
||||
working-directory: projects/lab
|
||||
run: |
|
||||
sudo apt-get -yqq install libpq-dev
|
||||
gem install bundler
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs=4 --retry=3
|
||||
- name: Set up database (to detect schema breaking changes)
|
||||
working-directory: projects/lab
|
||||
run: |
|
||||
cp config/database.ci.yml config/database.yml
|
||||
bundle exec rails db:create
|
||||
bundle exec rails db:schema:load
|
||||
- name: Migrate database
|
||||
working-directory: projects/lab
|
||||
run: bin/rails db:migrate
|
||||
- name: Run Ruby tests
|
||||
working-directory: projects/lab
|
||||
run: |
|
||||
bundle exec rails test
|
||||
assets_precompile:
|
||||
name: Assets precompile
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
ruby-version: ${{ env.RUBY_VERSION }}
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- uses: actions/cache@v2
|
||||
name: Cache Yarn dependencies
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('projects/lab/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- name: Install dependencies
|
||||
working-directory: projects/lab
|
||||
run: |
|
||||
sudo apt-get -yqq install libpq-dev
|
||||
gem install bundler
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs=4 --retry=3
|
||||
yarn install --frozen-lockfile
|
||||
- name: Precompile assets
|
||||
working-directory: projects/lab
|
||||
run: |
|
||||
bin/rails assets:precompile
|
||||
detect_schema_breaking_changes:
|
||||
name: GraphQL breaking changes
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
ruby-version: ${{ env.RUBY_VERSION }}
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- uses: actions/cache@v2
|
||||
name: Cache Yarn dependencies
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('projects/lab/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- name: Install dependencies
|
||||
working-directory: projects/lab
|
||||
run: |
|
||||
sudo apt-get -yqq install libpq-dev
|
||||
gem install bundler
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs=4 --retry=3
|
||||
yarn install --frozen-lockfile
|
||||
- name: Precompile assets
|
||||
working-directory: projects/lab
|
||||
run: |
|
||||
mv schema.graphql old_schema.graphql
|
||||
bin/rails graphql:schema:dump
|
||||
bundle exec schema_comparator verify old_schema.graphql schema.graphql
|
||||
typescript_type_check:
|
||||
name: Typescript type-check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- uses: actions/cache@v2
|
||||
name: Cache Yarn dependencies
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('projects/lab/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- name: Install dependencies
|
||||
working-directory: projects/lab
|
||||
run: |
|
||||
sudo apt-get -yqq install libpq-dev
|
||||
yarn install --frozen-lockfile
|
||||
yarn install --frozen-lockfile --cwd website
|
||||
- name: Typecheck
|
||||
working-directory: projects/lab
|
||||
run: |
|
||||
yarn type-check
|
||||
frontend_tests:
|
||||
name: Frontend tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- uses: actions/cache@v2
|
||||
name: Cache Yarn dependencies
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('projects/lab/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- name: Install dependencies
|
||||
working-directory: projects/lab
|
||||
run: |
|
||||
sudo apt-get -yqq install libpq-dev
|
||||
yarn install --frozen-lockfile
|
||||
- name: Typecheck
|
||||
working-directory: projects/lab
|
||||
run: |
|
||||
yarn test
|
||||
eslint:
|
||||
name: ESLint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- uses: actions/cache@v2
|
||||
name: Cache Yarn dependencies
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('projects/lab/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- name: Install dependencies
|
||||
working-directory: projects/lab
|
||||
run: |
|
||||
sudo apt-get -yqq install libpq-dev
|
||||
yarn install --frozen-lockfile
|
||||
yarn install --frozen-lockfile --cwd website
|
||||
- name: ESLint
|
||||
working-directory: projects/lab
|
||||
run: |
|
||||
yarn lint
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy to Heroku
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Deploy to Heroku
|
||||
env:
|
||||
HEROKU_APP_NAME: ${{ secrets.HEROKU_APP_NAME }}
|
||||
HEROKU_API_TOKEN: ${{ secrets.HEROKU_API_TOKEN }}
|
||||
run: |
|
||||
git remote add heroku https://heroku:$HEROKU_API_TOKEN@git.heroku.com/$HEROKU_APP_NAME.git
|
||||
git push heroku --force HEAD:main
|
|
@ -4,6 +4,7 @@ inherit_gem:
|
|||
- config/rails.yml
|
||||
require:
|
||||
- rubocop-minitest
|
||||
- rubocop-rails
|
||||
|
||||
Style/ClassAndModuleChildren:
|
||||
Enabled: false
|
||||
|
@ -25,3 +26,8 @@ AllCops:
|
|||
- vendor/**/*
|
||||
- projects/website/node_modules/**/*
|
||||
- projects/tuist/fixtures/**/*
|
||||
- projects/lab/bin/**/*
|
||||
- projects/lab/db/schema.rb
|
||||
- projects/lab/node_modules/**/*
|
||||
- projects/lab/docs/**
|
||||
- projects/lab/website/**/*
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
"bradlc.vscode-tailwindcss",
|
||||
"rebornix.ruby",
|
||||
"misogi.ruby-rubocop",
|
||||
"shopify.vscode-shadowenv"
|
||||
"shopify.vscode-shadowenv",
|
||||
"editorconfig.editorconfig",
|
||||
"dracula-theme.theme-dracula",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -20,5 +20,9 @@
|
|||
"**/node_modules": true,
|
||||
"**/.DS_Store": true
|
||||
},
|
||||
"editor.formatOnSave": true
|
||||
"editor.formatOnSave": false,
|
||||
"workbench.colorTheme": "Dracula Soft",
|
||||
"editor.quickSuggestions": {
|
||||
"strings": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
release: bundle exec rake db:migrate
|
||||
web: bundle exec puma -C config/puma.rb
|
||||
worker: bundle exec sidekiq -C config/sidekiq.yml
|
|
@ -0,0 +1 @@
|
|||
defaults
|
|
@ -0,0 +1,30 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{js,json}]
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[{Gemfile,Gemfile.lock,*.rb,*.ru,*.erb}]
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{yml,yaml}]
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[{src,scripts}/**.{ts,json,js}]
|
||||
end_of_line = crlf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
|
@ -0,0 +1,6 @@
|
|||
node_modules
|
||||
app/javascript/graphql
|
||||
website/public/
|
||||
website/.cache/
|
||||
website/node_modules/
|
||||
docs
|
|
@ -0,0 +1,58 @@
|
|||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
jest: true,
|
||||
},
|
||||
globals: {
|
||||
BASE_URL: 'readonly',
|
||||
ENVIRONMENT: 'readonly',
|
||||
BUGSNAG_FRONTEND_API_KEY: 'readonly',
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
'import/resolver': {
|
||||
node: {
|
||||
extensions: [
|
||||
'.js',
|
||||
'.jsx',
|
||||
'.ts',
|
||||
'.tsx',
|
||||
'.css',
|
||||
'.scss',
|
||||
'.css.ts',
|
||||
],
|
||||
moduleDirectory: ['node_modules', 'src/'],
|
||||
},
|
||||
},
|
||||
},
|
||||
extends: [
|
||||
'plugin:react/recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'airbnb',
|
||||
'prettier',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
ecmaVersion: 12,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['react', '@typescript-eslint', 'prettier'],
|
||||
rules: {
|
||||
'no-use-before-define': 'off',
|
||||
'@typescript-eslint/no-use-before-define': ['error'],
|
||||
'prettier/prettier': ['error'],
|
||||
'react/jsx-filename-extension': [
|
||||
1,
|
||||
{ extensions: ['.js', '.jsx', '.ts', '.tsx'] },
|
||||
],
|
||||
'react/prop-types': 'off',
|
||||
'react/require-default-props': 'off',
|
||||
'import/extensions': 'off',
|
||||
},
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
# Mark the database schema as having been generated.
|
||||
db/schema.rb linguist-generated
|
||||
|
||||
# Mark any vendored files as having been vendored.
|
||||
vendor/* linguist-vendored
|
|
@ -0,0 +1,44 @@
|
|||
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
|
||||
#
|
||||
# If you find yourself ignoring temporary files generated by your text editor
|
||||
# or operating system, you probably want to add a global ignore instead:
|
||||
# git config --global core.excludesfile '~/.gitignore_global'
|
||||
|
||||
# Ignore bundler config.
|
||||
/.bundle
|
||||
|
||||
# Ignore all logfiles and tempfiles.
|
||||
/log/*
|
||||
/tmp/*
|
||||
!/log/.keep
|
||||
!/tmp/.keep
|
||||
|
||||
# Ignore pidfiles, but keep the directory.
|
||||
/tmp/pids/*
|
||||
!/tmp/pids/
|
||||
!/tmp/pids/.keep
|
||||
|
||||
# Ignore uploaded files in development.
|
||||
/storage/*
|
||||
!/storage/.keep
|
||||
|
||||
/public/assets
|
||||
.byebug_history
|
||||
|
||||
# Ignore master key for decrypting credentials and more.
|
||||
/config/master.key
|
||||
|
||||
/public/packs
|
||||
/public/packs-test
|
||||
/node_modules
|
||||
/yarn-error.log
|
||||
yarn-debug.log*
|
||||
.yarn-integrity
|
||||
node_modules/
|
||||
/config/credentials/production.key
|
||||
|
||||
.env.development
|
||||
|
||||
sorbet/rbi/hidden-definitions/errors.txt
|
||||
|
||||
TODO.md
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"semi": true,
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true,
|
||||
"printWidth": 70
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
source "https://rubygems.org"
|
||||
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
||||
|
||||
ruby "~> 3.0.1"
|
||||
|
||||
gem "bcrypt", "~> 3.1.7"
|
||||
gem "bootsnap", ">= 1.4.4", require: false
|
||||
gem "devise", "~> 4.8"
|
||||
gem "dotenv-rails"
|
||||
gem "jbuilder", "~> 2.7"
|
||||
gem "js_from_routes", "~> 2.0"
|
||||
gem "pg", "~> 1.1"
|
||||
gem "puma", "~> 5.3.2"
|
||||
gem "rails", "~> 6.1.4"
|
||||
gem "redis", "~> 4.0"
|
||||
gem "sass-rails", ">= 6"
|
||||
gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby]
|
||||
gem "tailwindcss-rails", "~> 0.3.3"
|
||||
gem "rolify", "~> 6.0"
|
||||
gem "pundit", "~> 2.1"
|
||||
gem "webpacker", "~> 5.4"
|
||||
gem "actionpack-page_caching", "~> 1.2"
|
||||
gem "front_matter_parser", "~> 1.0"
|
||||
gem "redcarpet", "~> 3.5"
|
||||
gem "rails_admin", "~> 2.1"
|
||||
gem "sidekiq", "~> 6.2"
|
||||
gem "bugsnag", "~> 6.21"
|
||||
gem "dig_bang", "~> 0.2.1"
|
||||
gem "octokit", "~> 4.21"
|
||||
gem "securecompare", "~> 1.0"
|
||||
gem "sidekiq-scheduler", "~> 3.1"
|
||||
gem "react-rails", "~> 2.6"
|
||||
gem "graphql", "~> 1.12"
|
||||
gem "graphiql-rails", group: :development
|
||||
gem "rexml", "~> 3.2"
|
||||
|
||||
# Omniauth
|
||||
gem "omniauth", "~> 2.0"
|
||||
gem "omniauth-github", "~> 2.0"
|
||||
gem "omniauth-rails_csrf_protection", "~> 1.0"
|
||||
|
||||
group :development, :test do
|
||||
gem "byebug", platforms: [:mri, :mingw, :x64_mingw]
|
||||
end
|
||||
|
||||
group :development do
|
||||
gem "foreman", "~> 0.87.2"
|
||||
gem "listen", "~> 3.3"
|
||||
# gem "rack-mini-profiler", "~> 2.0"
|
||||
gem "spring"
|
||||
gem "web-console", ">= 4.1.0"
|
||||
gem "graphql-schema_comparator", "~> 1.0"
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem "capybara", ">= 3.26"
|
||||
gem "mocha", "~> 1.12"
|
||||
gem "selenium-webdriver"
|
||||
gem "webdrivers"
|
||||
end
|
|
@ -0,0 +1,421 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (6.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (6.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
activejob (= 6.1.4)
|
||||
activerecord (= 6.1.4)
|
||||
activestorage (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
mail (>= 2.7.1)
|
||||
actionmailer (6.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
actionview (= 6.1.4)
|
||||
activejob (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.1.4)
|
||||
actionview (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
rack (~> 2.0, >= 2.0.9)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actionpack-page_caching (1.2.4)
|
||||
actionpack (>= 4.0.0)
|
||||
actiontext (6.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
activerecord (= 6.1.4)
|
||||
activestorage (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
activejob (6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
activemodel-serializers-xml (1.0.2)
|
||||
activemodel (> 5.x)
|
||||
activesupport (> 5.x)
|
||||
builder (~> 3.1)
|
||||
activerecord (6.1.4)
|
||||
activemodel (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
activestorage (6.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
activejob (= 6.1.4)
|
||||
activerecord (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
marcel (~> 1.0.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (6.1.4)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
tzinfo (~> 2.0)
|
||||
zeitwerk (~> 2.3)
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
babel-source (5.8.35)
|
||||
babel-transpiler (0.7.0)
|
||||
babel-source (>= 4.0, < 6)
|
||||
execjs (~> 2.0)
|
||||
bcrypt (3.1.16)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.7.5)
|
||||
msgpack (~> 1.0)
|
||||
bugsnag (6.21.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
builder (3.2.4)
|
||||
byebug (11.1.3)
|
||||
capybara (3.35.3)
|
||||
addressable
|
||||
mini_mime (>= 0.1.3)
|
||||
nokogiri (~> 1.8)
|
||||
rack (>= 1.6.0)
|
||||
rack-test (>= 0.6.3)
|
||||
regexp_parser (>= 1.5, < 3.0)
|
||||
xpath (~> 3.2)
|
||||
childprocess (3.0.0)
|
||||
concurrent-ruby (1.1.9)
|
||||
connection_pool (2.2.5)
|
||||
crass (1.0.6)
|
||||
devise (4.8.0)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
dig_bang (0.2.1)
|
||||
dotenv (2.7.6)
|
||||
dotenv-rails (2.7.6)
|
||||
dotenv (= 2.7.6)
|
||||
railties (>= 3.2)
|
||||
e2mmap (0.1.0)
|
||||
erubi (1.10.0)
|
||||
et-orbi (1.2.4)
|
||||
tzinfo
|
||||
execjs (2.8.1)
|
||||
faraday (1.4.2)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
faraday-net_http (~> 1.0)
|
||||
faraday-net_http_persistent (~> 1.1)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-net_http (1.0.1)
|
||||
faraday-net_http_persistent (1.1.0)
|
||||
ffi (1.15.1)
|
||||
foreman (0.87.2)
|
||||
front_matter_parser (1.0.0)
|
||||
fugit (1.5.0)
|
||||
et-orbi (~> 1.1, >= 1.1.8)
|
||||
raabro (~> 1.4)
|
||||
globalid (0.4.2)
|
||||
activesupport (>= 4.2.0)
|
||||
graphiql-rails (1.7.0)
|
||||
railties
|
||||
sprockets-rails
|
||||
graphql (1.12.13)
|
||||
graphql-schema_comparator (1.0.1)
|
||||
bundler (>= 1.14)
|
||||
graphql (~> 1.10)
|
||||
thor (>= 0.19, < 2.0)
|
||||
haml (5.2.1)
|
||||
temple (>= 0.8.0)
|
||||
tilt
|
||||
hashie (4.1.0)
|
||||
i18n (1.8.10)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jbuilder (2.11.2)
|
||||
activesupport (>= 5.0.0)
|
||||
jquery-rails (4.4.0)
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
jquery-ui-rails (6.0.1)
|
||||
railties (>= 3.2.16)
|
||||
js_from_routes (2.0.4)
|
||||
railties (>= 5.1, < 8)
|
||||
jwt (2.2.3)
|
||||
kaminari (1.2.1)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.2.1)
|
||||
kaminari-activerecord (= 1.2.1)
|
||||
kaminari-core (= 1.2.1)
|
||||
kaminari-actionview (1.2.1)
|
||||
actionview
|
||||
kaminari-core (= 1.2.1)
|
||||
kaminari-activerecord (1.2.1)
|
||||
activerecord
|
||||
kaminari-core (= 1.2.1)
|
||||
kaminari-core (1.2.1)
|
||||
listen (3.5.1)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
loofah (2.10.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.1)
|
||||
mini_mime (>= 0.1.1)
|
||||
marcel (1.0.1)
|
||||
method_source (1.0.0)
|
||||
mini_mime (1.1.0)
|
||||
mini_portile2 (2.5.3)
|
||||
minitest (5.14.4)
|
||||
mocha (1.13.0)
|
||||
msgpack (1.4.2)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.1.1)
|
||||
nested_form (0.3.2)
|
||||
nio4r (2.5.7)
|
||||
nokogiri (1.11.7)
|
||||
mini_portile2 (~> 2.5.0)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.11.7-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.11.7-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
oauth2 (1.4.7)
|
||||
faraday (>= 0.8, < 2.0)
|
||||
jwt (>= 1.0, < 3.0)
|
||||
multi_json (~> 1.3)
|
||||
multi_xml (~> 0.5)
|
||||
rack (>= 1.2, < 3)
|
||||
octokit (4.21.0)
|
||||
faraday (>= 0.9)
|
||||
sawyer (~> 0.8.0, >= 0.5.3)
|
||||
omniauth (2.0.4)
|
||||
hashie (>= 3.4.6)
|
||||
rack (>= 1.6.2, < 3)
|
||||
rack-protection
|
||||
omniauth-github (2.0.0)
|
||||
omniauth (~> 2.0)
|
||||
omniauth-oauth2 (~> 1.7.1)
|
||||
omniauth-oauth2 (1.7.1)
|
||||
oauth2 (~> 1.4)
|
||||
omniauth (>= 1.9, < 3)
|
||||
omniauth-rails_csrf_protection (1.0.0)
|
||||
actionpack (>= 4.2)
|
||||
omniauth (~> 2.0)
|
||||
orm_adapter (0.5.0)
|
||||
pg (1.2.3)
|
||||
public_suffix (4.0.6)
|
||||
puma (5.3.2)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.5.2)
|
||||
rack (2.2.3)
|
||||
rack-pjax (1.1.0)
|
||||
nokogiri (~> 1.5)
|
||||
rack (>= 1.1)
|
||||
rack-protection (2.1.0)
|
||||
rack
|
||||
rack-proxy (0.7.0)
|
||||
rack
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rails (6.1.4)
|
||||
actioncable (= 6.1.4)
|
||||
actionmailbox (= 6.1.4)
|
||||
actionmailer (= 6.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
actiontext (= 6.1.4)
|
||||
actionview (= 6.1.4)
|
||||
activejob (= 6.1.4)
|
||||
activemodel (= 6.1.4)
|
||||
activerecord (= 6.1.4)
|
||||
activestorage (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 6.1.4)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.3.0)
|
||||
loofah (~> 2.3)
|
||||
rails_admin (2.1.1)
|
||||
activemodel-serializers-xml (>= 1.0)
|
||||
builder (~> 3.1)
|
||||
haml (>= 4.0, < 6)
|
||||
jquery-rails (>= 3.0, < 5)
|
||||
jquery-ui-rails (>= 5.0, < 7)
|
||||
kaminari (>= 0.14, < 2.0)
|
||||
nested_form (~> 0.3)
|
||||
rack-pjax (>= 0.7)
|
||||
rails (>= 5.0, < 7)
|
||||
remotipart (~> 1.3)
|
||||
sassc-rails (>= 1.3, < 3)
|
||||
railties (6.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
method_source
|
||||
rake (>= 0.13)
|
||||
thor (~> 1.0)
|
||||
rake (13.0.3)
|
||||
rb-fsevent (0.11.0)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
react-rails (2.6.1)
|
||||
babel-transpiler (>= 0.7.0)
|
||||
connection_pool
|
||||
execjs
|
||||
railties (>= 3.2)
|
||||
tilt
|
||||
redcarpet (3.5.1)
|
||||
redis (4.3.1)
|
||||
regexp_parser (2.1.1)
|
||||
remotipart (1.4.4)
|
||||
responders (3.0.1)
|
||||
actionpack (>= 5.0)
|
||||
railties (>= 5.0)
|
||||
rexml (3.2.5)
|
||||
rolify (6.0.0)
|
||||
ruby2_keywords (0.0.4)
|
||||
rubyzip (2.3.0)
|
||||
rufus-scheduler (3.7.0)
|
||||
fugit (~> 1.1, >= 1.1.6)
|
||||
sass-rails (6.0.0)
|
||||
sassc-rails (~> 2.1, >= 2.1.1)
|
||||
sassc (2.4.0)
|
||||
ffi (~> 1.9)
|
||||
sassc-rails (2.1.2)
|
||||
railties (>= 4.0.0)
|
||||
sassc (>= 2.0)
|
||||
sprockets (> 3.0)
|
||||
sprockets-rails
|
||||
tilt
|
||||
sawyer (0.8.2)
|
||||
addressable (>= 2.3.5)
|
||||
faraday (> 0.8, < 2.0)
|
||||
securecompare (1.0.0)
|
||||
selenium-webdriver (3.142.7)
|
||||
childprocess (>= 0.5, < 4.0)
|
||||
rubyzip (>= 1.2.2)
|
||||
semantic_range (3.0.0)
|
||||
sidekiq (6.2.1)
|
||||
connection_pool (>= 2.2.2)
|
||||
rack (~> 2.0)
|
||||
redis (>= 4.2.0)
|
||||
sidekiq-scheduler (3.1.0)
|
||||
e2mmap
|
||||
redis (>= 3, < 5)
|
||||
rufus-scheduler (~> 3.2)
|
||||
sidekiq (>= 3)
|
||||
thwait
|
||||
tilt (>= 1.4.0)
|
||||
spring (2.1.1)
|
||||
sprockets (4.0.2)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
sprockets-rails (3.2.2)
|
||||
actionpack (>= 4.0)
|
||||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
tailwindcss-rails (0.3.3)
|
||||
rails (>= 6.0.0)
|
||||
temple (0.8.2)
|
||||
thor (1.1.0)
|
||||
thwait (0.2.0)
|
||||
e2mmap
|
||||
tilt (2.0.10)
|
||||
tzinfo (2.0.4)
|
||||
concurrent-ruby (~> 1.0)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
web-console (4.1.0)
|
||||
actionview (>= 6.0.0)
|
||||
activemodel (>= 6.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
webdrivers (4.6.0)
|
||||
nokogiri (~> 1.6)
|
||||
rubyzip (>= 1.3.0)
|
||||
selenium-webdriver (>= 3.0, < 4.0)
|
||||
webpacker (5.4.0)
|
||||
activesupport (>= 5.2)
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
semantic_range (>= 2.3.0)
|
||||
websocket-driver (0.7.5)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.4.2)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
x86_64-darwin-19
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
actionpack-page_caching (~> 1.2)
|
||||
bcrypt (~> 3.1.7)
|
||||
bootsnap (>= 1.4.4)
|
||||
bugsnag (~> 6.21)
|
||||
byebug
|
||||
capybara (>= 3.26)
|
||||
devise (~> 4.8)
|
||||
dig_bang (~> 0.2.1)
|
||||
dotenv-rails
|
||||
foreman (~> 0.87.2)
|
||||
front_matter_parser (~> 1.0)
|
||||
graphiql-rails
|
||||
graphql (~> 1.12)
|
||||
graphql-schema_comparator (~> 1.0)
|
||||
jbuilder (~> 2.7)
|
||||
js_from_routes (~> 2.0)
|
||||
listen (~> 3.3)
|
||||
mocha (~> 1.12)
|
||||
octokit (~> 4.21)
|
||||
omniauth (~> 2.0)
|
||||
omniauth-github (~> 2.0)
|
||||
omniauth-rails_csrf_protection (~> 1.0)
|
||||
pg (~> 1.1)
|
||||
puma (~> 5.3.2)
|
||||
pundit (~> 2.1)
|
||||
rails (~> 6.1.4)
|
||||
rails_admin (~> 2.1)
|
||||
react-rails (~> 2.6)
|
||||
redcarpet (~> 3.5)
|
||||
redis (~> 4.0)
|
||||
rexml (~> 3.2)
|
||||
rolify (~> 6.0)
|
||||
sass-rails (>= 6)
|
||||
securecompare (~> 1.0)
|
||||
selenium-webdriver
|
||||
sidekiq (~> 6.2)
|
||||
sidekiq-scheduler (~> 3.1)
|
||||
spring
|
||||
tailwindcss-rails (~> 0.3.3)
|
||||
tzinfo-data
|
||||
web-console (>= 4.1.0)
|
||||
webdrivers
|
||||
webpacker (~> 5.4)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.0.1p64
|
||||
|
||||
BUNDLED WITH
|
||||
2.2.21
|
|
@ -0,0 +1,2 @@
|
|||
web: bundle exec rails s
|
||||
webpacker: ../bin/webpack-dev-server
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Add your own tasks in files placed in lib/tasks ending in .rake,
|
||||
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
|
||||
|
||||
require_relative "config/application"
|
||||
require "graphql/rake_task"
|
||||
|
||||
GraphQL::RakeTask.new(schema_name: "TuistLabSchema")
|
||||
|
||||
desc("Starts Rails")
|
||||
task start: :environment do
|
||||
system("bundle exec foreman start -f Procfile.dev") || abort
|
||||
end
|
||||
|
||||
Rails.application.load_tasks
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
||||
* listed below.
|
||||
*
|
||||
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
|
||||
* vendor/assets/stylesheets directory can be referenced here using a relative path.
|
||||
*
|
||||
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
||||
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
||||
* files in this directory. Styles in this file should be added after the last require_* statement.
|
||||
* It is generally better to create a new file per style scope.
|
||||
*
|
||||
*= require_tree .
|
||||
*= require_self
|
||||
*/
|
|
@ -0,0 +1,7 @@
|
|||
# typed: ignore
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ApplicationCable
|
||||
class Channel < ActionCable::Channel::Base
|
||||
end
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
# typed: ignore
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ApplicationCable
|
||||
class Connection < ActionCable::Connection::Base
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
class AppController < ApplicationController
|
||||
include Authenticatable
|
||||
before_action :authenticate_authenticatable!
|
||||
|
||||
def index
|
||||
render(component: "App", prerender: false)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
class ApplicationController < ActionController::Base
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
module Authenticatable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
devise_group :authenticatable, contains: [:user, :project]
|
||||
end
|
||||
end
|
|
@ -0,0 +1,59 @@
|
|||
# frozen_string_literal: true
|
||||
class GraphqlController < ApplicationController
|
||||
include Authenticatable
|
||||
|
||||
# If accessing from outside this domain, nullify the session
|
||||
# This allows for outside API access while preventing CSRF attacks,
|
||||
# but you'll have to authenticate your user separately
|
||||
# protect_from_forgery with: :null_session
|
||||
|
||||
def execute
|
||||
if current_user.nil?
|
||||
url = "#{Rails.application.config.defaults[:urls][:app]}#{user_session_path}"
|
||||
error_message = "Authentication is required to interact with the GraphQL API. Authenticate through #{url}"
|
||||
error_extensions = { code: "AUTHENTICATION_ERROR" }
|
||||
raise GraphQL::ExecutionError.new(error_message, extensions: error_extensions)
|
||||
end
|
||||
|
||||
variables = prepare_variables(params[:variables])
|
||||
query = params[:query]
|
||||
operation_name = params[:operationName]
|
||||
context = {
|
||||
current_user: current_user,
|
||||
}
|
||||
result = TuistLabSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
|
||||
render(json: result)
|
||||
rescue StandardError => e
|
||||
raise e unless Rails.env.development?
|
||||
handle_error_in_development(e)
|
||||
end
|
||||
|
||||
private
|
||||
# Handle variables in form data, JSON body, or a blank value
|
||||
def prepare_variables(variables_param)
|
||||
case variables_param
|
||||
when String
|
||||
if variables_param.present?
|
||||
JSON.parse(variables_param) || {}
|
||||
else
|
||||
{}
|
||||
end
|
||||
when Hash
|
||||
variables_param
|
||||
when ActionController::Parameters
|
||||
variables_param.to_unsafe_hash # GraphQL-Ruby will validate name and type of incoming variables.
|
||||
when nil
|
||||
{}
|
||||
else
|
||||
raise ArgumentError, "Unexpected parameter: #{variables_param}"
|
||||
end
|
||||
end
|
||||
|
||||
def handle_error_in_development(e)
|
||||
logger.error(e.message)
|
||||
logger.error(e.backtrace.join("\n"))
|
||||
|
||||
render(json: { errors: [{ message: e.message, backtrace: e.backtrace }], data: {} },
|
||||
status: :internal_server_error)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,27 @@
|
|||
# typed: ignore
|
||||
# frozen_string_literal: true
|
||||
module Users
|
||||
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||
layout "application"
|
||||
|
||||
def github
|
||||
@user = UserCreateService.call(auth: auth_data)
|
||||
|
||||
if @user.persisted?
|
||||
sign_in_and_redirect(@user, event: :authentication)
|
||||
else
|
||||
data = auth_data.except("extra")
|
||||
session["devise.oauth.data"] = data
|
||||
redirect_to(new_user_registration_url)
|
||||
end
|
||||
end
|
||||
|
||||
def failure
|
||||
redirect_to(root_path)
|
||||
end
|
||||
|
||||
def auth_data
|
||||
request.env["omniauth.auth"]
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,54 @@
|
|||
# typed: ignore
|
||||
# frozen_string_literal: true
|
||||
|
||||
class WebsiteController < ApplicationController
|
||||
layout "website"
|
||||
|
||||
def landing
|
||||
render("website/landing")
|
||||
end
|
||||
|
||||
def privacy
|
||||
render("website/privacy")
|
||||
end
|
||||
|
||||
def terms
|
||||
render("website/terms")
|
||||
end
|
||||
|
||||
def cookie
|
||||
render("website/cookie")
|
||||
end
|
||||
|
||||
def acceptable_use_policy
|
||||
render("website/acceptable_use_policy")
|
||||
end
|
||||
|
||||
def changelog
|
||||
@changelogs = changelogs
|
||||
render("website/changelog")
|
||||
end
|
||||
|
||||
def changelog_feed
|
||||
head(:ok)
|
||||
end
|
||||
|
||||
private
|
||||
def changelogs
|
||||
changelog_dir = Rails.root.join("app/content/changelog")
|
||||
Dir.glob(File.join(changelog_dir, "*")).map do |file_path|
|
||||
parsed_file = FrontMatterParser::Parser.parse_file(file_path)
|
||||
date_string = File.basename(file_path).split("-").first
|
||||
OpenStruct.new(
|
||||
date: Date.strptime(date_string, "%Y%m%d"),
|
||||
title: parsed_file.front_matter["title"],
|
||||
type: parsed_file.front_matter["type"],
|
||||
html: markdown_parser.render(parsed_file.content)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def markdown_parser
|
||||
@markdown_parser ||= Redcarpet::Markdown.new(Redcarpet::Render::HTML, autolink: true, tables: true)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,95 @@
|
|||
# frozen_string_literal: true
|
||||
class ApiTokenDeviseStrategy < Devise::Strategies::Base
|
||||
Token = Struct.new(:model_name, :id, :token) do
|
||||
def self.decode(encoded)
|
||||
decoded = Base64.urlsafe_decode64(encoded)
|
||||
model_name, id, token = decoded.split(":")
|
||||
|
||||
new(model_name, id, token)
|
||||
end
|
||||
|
||||
def valid?
|
||||
[model_name, id, token].all?(&:present?)
|
||||
end
|
||||
|
||||
def encode
|
||||
Base64.urlsafe_encode64(to_a.join(":"), padding: false)
|
||||
end
|
||||
end
|
||||
|
||||
def valid?
|
||||
encoded_token.present?
|
||||
end
|
||||
|
||||
def authenticate!
|
||||
return fail! unless model_is_authenticatable?
|
||||
return fail! unless token_format_valid?
|
||||
return fail! unless scope_match?
|
||||
return fail! unless token_match?
|
||||
|
||||
skip_trackable
|
||||
|
||||
success!(model_object)
|
||||
end
|
||||
|
||||
def store?
|
||||
false
|
||||
end
|
||||
|
||||
def clean_up_csrf?
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
def skip_trackable
|
||||
env["devise.skip_trackable"] = true
|
||||
end
|
||||
|
||||
def fail!
|
||||
# TODO: Change the format to be:
|
||||
# {
|
||||
# "status": "error",
|
||||
# "errors": [
|
||||
# {
|
||||
# "code": "unauthorized",
|
||||
# "message": "The session is not present or is not valid."
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
super("invalid token")
|
||||
end
|
||||
|
||||
def model_is_authenticatable?
|
||||
model.class.include?(TokenAuthenticatable)
|
||||
end
|
||||
|
||||
def token_format_valid?
|
||||
decoded_token.valid?
|
||||
rescue ArgumentError
|
||||
false
|
||||
end
|
||||
|
||||
def scope_match?
|
||||
model.name == decoded_token.model_name
|
||||
end
|
||||
|
||||
def model_object
|
||||
@model_object ||= model.find(decoded_token.id)
|
||||
end
|
||||
|
||||
def token_match?
|
||||
Devise.secure_compare(model_object&.authentication_token, decoded_token.token)
|
||||
end
|
||||
|
||||
def model
|
||||
mapping.to
|
||||
end
|
||||
|
||||
def decoded_token
|
||||
@decoded_token ||= Token.decode(encoded_token)
|
||||
end
|
||||
|
||||
def encoded_token
|
||||
request.headers["Authorization"].to_s.remove("Bearer ")
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
module Mutations
|
||||
class BaseMutation < GraphQL::Schema::RelayClassicMutation
|
||||
argument_class Types::BaseArgument
|
||||
field_class Types::BaseField
|
||||
input_object_class Types::BaseInputObject
|
||||
object_class Types::BaseObject
|
||||
end
|
||||
end
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
module Mutations
|
||||
class OrganizationCreate < BaseMutation
|
||||
description "Create a new organization"
|
||||
|
||||
# Arguments
|
||||
argument :name, String, "The name of the organization", required: true
|
||||
|
||||
# Fields
|
||||
field :organization, Types::OrganizationType, null: true do
|
||||
description "The created organization"
|
||||
end
|
||||
field :errors, [String], null: false do
|
||||
description "A list of errors if the creation of the organization failed"
|
||||
end
|
||||
|
||||
def resolve(name:)
|
||||
organization = OrganizationCreateService.call(name: name, admin: context[:current_user])
|
||||
{
|
||||
organization: organization,
|
||||
errors: [],
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
class TuistLabSchema < GraphQL::Schema
|
||||
mutation(Types::MutationType)
|
||||
query(Types::QueryType)
|
||||
|
||||
# Union and Interface Resolution
|
||||
def self.resolve_type(_abstract_type, _obj, _ctx)
|
||||
# TODO: Implement this function
|
||||
# to return the correct object type for `obj`
|
||||
raise(GraphQL::RequiredImplementationMissingError)
|
||||
end
|
||||
|
||||
# Relay-style Object Identification:
|
||||
|
||||
# Return a string UUID for `object`
|
||||
def self.id_from_object(object, type_definition, query_ctx)
|
||||
# Here's a simple implementation which:
|
||||
# - joins the type name & object.id
|
||||
# - encodes it with base64:
|
||||
# GraphQL::Schema::UniqueWithinType.encode(type_definition.name, object.id)
|
||||
end
|
||||
|
||||
# Given a string UUID, find the object
|
||||
def self.object_from_id(id, query_ctx)
|
||||
# For example, to decode the UUIDs generated above:
|
||||
# type_name, item_id = GraphQL::Schema::UniqueWithinType.decode(id)
|
||||
#
|
||||
# Then, based on `type_name` and `id`
|
||||
# find an object in your application
|
||||
# ...
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
module Types
|
||||
class BaseArgument < GraphQL::Schema::Argument
|
||||
end
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
module Types
|
||||
class BaseConnection < Types::BaseObject
|
||||
# add `nodes` and `pageInfo` fields, as well as `edge_type(...)` and `node_nullable(...)` overrides
|
||||
include GraphQL::Types::Relay::ConnectionBehaviors
|
||||
end
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
module Types
|
||||
class BaseEdge < Types::BaseObject
|
||||
# add `node` and `cursor` fields, as well as `node_type(...)` override
|
||||
include GraphQL::Types::Relay::EdgeBehaviors
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
module Types
|
||||
class BaseEnum < GraphQL::Schema::Enum
|
||||
end
|
||||
end
|
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
module Types
|
||||
class BaseField < GraphQL::Schema::Field
|
||||
argument_class Types::BaseArgument
|
||||
end
|
||||
end
|
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
module Types
|
||||
class BaseInputObject < GraphQL::Schema::InputObject
|
||||
argument_class Types::BaseArgument
|
||||
end
|
||||
end
|
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
module Types
|
||||
module BaseInterface
|
||||
include GraphQL::Schema::Interface
|
||||
edge_type_class(Types::BaseEdge)
|
||||
connection_type_class(Types::BaseConnection)
|
||||
|
||||
field_class Types::BaseField
|
||||
end
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
module Types
|
||||
class BaseObject < GraphQL::Schema::Object
|
||||
edge_type_class(Types::BaseEdge)
|
||||
connection_type_class(Types::BaseConnection)
|
||||
field_class Types::BaseField
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
module Types
|
||||
class BaseScalar < GraphQL::Schema::Scalar
|
||||
end
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
module Types
|
||||
class BaseUnion < GraphQL::Schema::Union
|
||||
edge_type_class(Types::BaseEdge)
|
||||
connection_type_class(Types::BaseConnection)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
module Types
|
||||
class MutationType < Types::BaseObject
|
||||
field :organization_create, mutation: Mutations::OrganizationCreate
|
||||
end
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
module Types
|
||||
module NodeType
|
||||
include Types::BaseInterface
|
||||
# Add the `id` field
|
||||
include GraphQL::Types::Relay::NodeBehaviors
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
module Types
|
||||
class OrganizationType < Types::BaseObject
|
||||
field :id, String, null: false
|
||||
field :name, String, null: false
|
||||
|
||||
def name
|
||||
object.account.name
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
module Types
|
||||
class QueryType < Types::BaseObject
|
||||
# Add `node(id: ID!) and `nodes(ids: [ID!]!)`
|
||||
include GraphQL::Types::Relay::HasNodeField
|
||||
include GraphQL::Types::Relay::HasNodesField
|
||||
|
||||
# Add root-level fields here.
|
||||
# They will be entry points for queries on your schema.
|
||||
|
||||
# Fields
|
||||
field :me, UserType, "Returns the authenticated user.", null: false
|
||||
|
||||
# Resolvers
|
||||
def me
|
||||
context[:current_user]
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
module Types
|
||||
class UserType < Types::BaseObject
|
||||
field :id, String, null: false
|
||||
field :email, String, null: false
|
||||
field :avatar_url, String, null: false
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ApplicationHelper
|
||||
end
|
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
module MetaTagsHelper
|
||||
def meta_title(title)
|
||||
content_for(:meta_title, title)
|
||||
end
|
||||
|
||||
def content_for_meta_title
|
||||
content_for?(:meta_title) ? "TuistLab | #{content_for(:meta_title)}" : Rails.application.config.defaults.dig(
|
||||
:site_metadata, :title)
|
||||
end
|
||||
|
||||
def meta_description(description)
|
||||
content_for(:meta_description, description)
|
||||
end
|
||||
|
||||
def content_for_meta_description
|
||||
content_for?(:meta_description) ? content_for(:meta_description) : Rails.application.config.defaults.dig(
|
||||
:site_metadata, :description)
|
||||
end
|
||||
|
||||
def meta_keywords(keywords)
|
||||
content_for(:meta_keywords, keywords.join(","))
|
||||
end
|
||||
|
||||
def content_for_meta_keywords
|
||||
content_for?(:meta_keywords) ? content_for(:meta_keywords) : Rails.application.config.defaults.dig(:site_metadata,
|
||||
:keywords)
|
||||
end
|
||||
|
||||
def content_for_meta_image
|
||||
URI.join(Rails.application.config.defaults.dig(:urls, :app),
|
||||
asset_pack_path("media/images/logo-with-background.png")).to_s
|
||||
end
|
||||
|
||||
def content_for_meta_twitter_handle
|
||||
Rails.application.config.defaults.dig(:site_metadata, :twitter)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,54 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
BrowserRouter as Router,
|
||||
Switch,
|
||||
Route,
|
||||
} from 'react-router-dom';
|
||||
import { AppProvider } from '@shopify/polaris';
|
||||
import ClientProvider from '../networking/ClientProvider';
|
||||
import RESTClientProvider from '../networking/RESTClientProvider';
|
||||
|
||||
import ErrorBoundary from '../utilities/ErrorBoundary';
|
||||
import HomePage from '../pages/HomePage';
|
||||
import ProjectNewPage from '../pages/ProjectNewPage';
|
||||
import OrganizationNewPage from '../pages/OrganizationNewPage';
|
||||
import SettingsPage from '../pages/SettingsPage';
|
||||
import {
|
||||
projectNewPath,
|
||||
organizationNewPath,
|
||||
settingsPath,
|
||||
} from '../utilities/routes';
|
||||
import EnvironmentProvider from '../utilities/EnvironmentProvider';
|
||||
import theme from '../polaris/theme';
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<RESTClientProvider>
|
||||
<ClientProvider>
|
||||
<AppProvider theme={theme} i18n={{}}>
|
||||
<EnvironmentProvider>
|
||||
<Router>
|
||||
<Switch>
|
||||
<Route path="/" component={HomePage} exact />
|
||||
<Route
|
||||
path={projectNewPath}
|
||||
component={ProjectNewPage}
|
||||
/>
|
||||
<Route
|
||||
path={organizationNewPath}
|
||||
component={OrganizationNewPage}
|
||||
/>
|
||||
<Route
|
||||
path={settingsPath}
|
||||
component={SettingsPage}
|
||||
/>
|
||||
</Switch>
|
||||
</Router>
|
||||
</EnvironmentProvider>
|
||||
</AppProvider>
|
||||
</ClientProvider>
|
||||
</RESTClientProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
query Me {
|
||||
me {
|
||||
email
|
||||
avatarUrl
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
mutation OrganizationCreate(
|
||||
$organization: OrganizationCreateInput!
|
||||
) {
|
||||
organizationCreate(input: $organization) {
|
||||
errors
|
||||
organization {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
export type Maybe<T> = T | null;
|
||||
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
||||
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
|
||||
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
|
||||
const defaultOptions = {}
|
||||
/** All built-in and custom scalars, mapped to their actual values */
|
||||
export type Scalars = {
|
||||
ID: string;
|
||||
String: string;
|
||||
Boolean: boolean;
|
||||
Int: number;
|
||||
Float: number;
|
||||
};
|
||||
|
||||
export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
/** Create a new organization */
|
||||
organizationCreate?: Maybe<OrganizationCreatePayload>;
|
||||
};
|
||||
|
||||
|
||||
export type MutationOrganizationCreateArgs = {
|
||||
input: OrganizationCreateInput;
|
||||
};
|
||||
|
||||
export type Organization = {
|
||||
__typename?: 'Organization';
|
||||
id: Scalars['String'];
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
/** Autogenerated input type of OrganizationCreate */
|
||||
export type OrganizationCreateInput = {
|
||||
/** A unique identifier for the client performing the mutation. */
|
||||
clientMutationId?: Maybe<Scalars['String']>;
|
||||
/** The name of the organization */
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
/** Autogenerated return type of OrganizationCreate */
|
||||
export type OrganizationCreatePayload = {
|
||||
__typename?: 'OrganizationCreatePayload';
|
||||
/** A unique identifier for the client performing the mutation. */
|
||||
clientMutationId?: Maybe<Scalars['String']>;
|
||||
/** A list of errors if the creation of the organization failed */
|
||||
errors: Array<Scalars['String']>;
|
||||
/** The created organization */
|
||||
organization?: Maybe<Organization>;
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
/** Returns the authenticated user. */
|
||||
me: User;
|
||||
};
|
||||
|
||||
export type User = {
|
||||
__typename?: 'User';
|
||||
avatarUrl: Scalars['String'];
|
||||
email: Scalars['String'];
|
||||
id: Scalars['String'];
|
||||
};
|
||||
|
||||
export type MeQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type MeQuery = (
|
||||
{ __typename?: 'Query' }
|
||||
& { me: (
|
||||
{ __typename?: 'User' }
|
||||
& Pick<User, 'email' | 'avatarUrl'>
|
||||
) }
|
||||
);
|
||||
|
||||
export type OrganizationCreateMutationVariables = Exact<{
|
||||
organization: OrganizationCreateInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type OrganizationCreateMutation = (
|
||||
{ __typename?: 'Mutation' }
|
||||
& { organizationCreate?: Maybe<(
|
||||
{ __typename?: 'OrganizationCreatePayload' }
|
||||
& Pick<OrganizationCreatePayload, 'errors'>
|
||||
& { organization?: Maybe<(
|
||||
{ __typename?: 'Organization' }
|
||||
& Pick<Organization, 'name'>
|
||||
)> }
|
||||
)> }
|
||||
);
|
||||
|
||||
|
||||
export const MeDocument = gql`
|
||||
query Me {
|
||||
me {
|
||||
email
|
||||
avatarUrl
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useMeQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useMeQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useMeQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useMeQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useMeQuery(baseOptions?: Apollo.QueryHookOptions<MeQuery, MeQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<MeQuery, MeQueryVariables>(MeDocument, options);
|
||||
}
|
||||
export function useMeLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MeQuery, MeQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<MeQuery, MeQueryVariables>(MeDocument, options);
|
||||
}
|
||||
export type MeQueryHookResult = ReturnType<typeof useMeQuery>;
|
||||
export type MeLazyQueryHookResult = ReturnType<typeof useMeLazyQuery>;
|
||||
export type MeQueryResult = Apollo.QueryResult<MeQuery, MeQueryVariables>;
|
||||
export const OrganizationCreateDocument = gql`
|
||||
mutation OrganizationCreate($organization: OrganizationCreateInput!) {
|
||||
organizationCreate(input: $organization) {
|
||||
errors
|
||||
organization {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type OrganizationCreateMutationFn = Apollo.MutationFunction<OrganizationCreateMutation, OrganizationCreateMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useOrganizationCreateMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useOrganizationCreateMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useOrganizationCreateMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [organizationCreateMutation, { data, loading, error }] = useOrganizationCreateMutation({
|
||||
* variables: {
|
||||
* organization: // value for 'organization'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useOrganizationCreateMutation(baseOptions?: Apollo.MutationHookOptions<OrganizationCreateMutation, OrganizationCreateMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<OrganizationCreateMutation, OrganizationCreateMutationVariables>(OrganizationCreateDocument, options);
|
||||
}
|
||||
export type OrganizationCreateMutationHookResult = ReturnType<typeof useOrganizationCreateMutation>;
|
||||
export type OrganizationCreateMutationResult = Apollo.MutationResult<OrganizationCreateMutation>;
|
||||
export type OrganizationCreateMutationOptions = Apollo.BaseMutationOptions<OrganizationCreateMutation, OrganizationCreateMutationVariables>;
|
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
|
@ -0,0 +1,6 @@
|
|||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="828" y="184" width="111" height="660" rx="5" fill="#F09038"/>
|
||||
<rect x="670" y="274" width="111" height="560" rx="5" transform="rotate(7.60437 670 274)" fill="#F07A38"/>
|
||||
<rect x="505" y="340" width="111" height="495.343" rx="5" transform="rotate(19.3331 505 340)" fill="#F06438"/>
|
||||
<rect x="282.441" y="405" width="111" height="436.385" rx="5" transform="rotate(27.3432 282.441 405)" fill="#F05138"/>
|
||||
</svg>
|
After Width: | Height: | Size: 519 B |
|
@ -0,0 +1,5 @@
|
|||
describe('it works', () => {
|
||||
it('works', () => {
|
||||
expect(true).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,51 @@
|
|||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
// @ts-nocheck
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
ApolloClient,
|
||||
InMemoryCache,
|
||||
ApolloProvider,
|
||||
concat,
|
||||
} from '@apollo/client';
|
||||
import { ApolloLink } from 'apollo-link';
|
||||
import { createHttpLink } from 'apollo-link-http';
|
||||
|
||||
const httpLink = createHttpLink({ uri: '/graphql' });
|
||||
|
||||
const middlewareLink = new ApolloLink((operation, forward) => {
|
||||
const csrfToken = document
|
||||
.querySelector('meta[name=csrf-token]')
|
||||
.getAttribute('content');
|
||||
operation.setContext({
|
||||
credentials: 'same-origin',
|
||||
headers: { 'X-CSRF-Token': csrfToken },
|
||||
});
|
||||
return forward(operation);
|
||||
});
|
||||
|
||||
const defaultOptions = {
|
||||
watchQuery: {
|
||||
fetchPolicy: 'no-cache',
|
||||
errorPolicy: 'ignore',
|
||||
},
|
||||
query: {
|
||||
fetchPolicy: 'no-cache',
|
||||
errorPolicy: 'all',
|
||||
},
|
||||
};
|
||||
|
||||
const client = new ApolloClient({
|
||||
link: concat(middlewareLink, httpLink),
|
||||
uri: `${BASE_URL}/graphql`,
|
||||
cache: new InMemoryCache(),
|
||||
defaultOptions,
|
||||
});
|
||||
|
||||
const ClientProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => <ApolloProvider client={client}>{children}</ApolloProvider>;
|
||||
|
||||
export default ClientProvider;
|
|
@ -0,0 +1,32 @@
|
|||
import React, { useEffect, useContext } from 'react';
|
||||
import type { AxiosInstance } from 'axios';
|
||||
import restClient from './restClient';
|
||||
|
||||
const HTTPClientContext =
|
||||
React.createContext<AxiosInstance>(restClient);
|
||||
|
||||
const RESTClientProvider = ({ children }) => {
|
||||
useEffect(() => {
|
||||
restClient.interceptors.request.use((config) => {
|
||||
const csrfToken = document
|
||||
.querySelector('meta[name=csrf-token]')
|
||||
.getAttribute('content');
|
||||
/* eslint-disable no-param-reassign */
|
||||
config.headers['X-CSRF-Token'] = csrfToken;
|
||||
config.headers['Content-Type'] = 'application/json';
|
||||
config.withCredentials = true;
|
||||
/* eslint-enable no-param-reassign */
|
||||
return config;
|
||||
});
|
||||
}, [restClient]);
|
||||
|
||||
return (
|
||||
<HTTPClientContext.Provider value={restClient}>
|
||||
{children}
|
||||
</HTTPClientContext.Provider>
|
||||
);
|
||||
};
|
||||
const useRestClient = () => useContext(HTTPClientContext);
|
||||
|
||||
export { useRestClient, restClient };
|
||||
export default RESTClientProvider;
|
|
@ -0,0 +1,6 @@
|
|||
import axios from 'axios';
|
||||
import type { AxiosInstance } from 'axios';
|
||||
|
||||
const restClient: AxiosInstance = axios.create();
|
||||
|
||||
export default restClient;
|
|
@ -0,0 +1,17 @@
|
|||
import { signOutPath } from '../utilities/routes';
|
||||
import restClient from './restClient';
|
||||
|
||||
export const redirectToSignIn = () => {
|
||||
window.location.pathname = '/';
|
||||
};
|
||||
|
||||
const signOut = async () => {
|
||||
try {
|
||||
await restClient.delete(signOutPath);
|
||||
} catch {
|
||||
console.log('Logged out');
|
||||
}
|
||||
redirectToSignIn();
|
||||
};
|
||||
|
||||
export default signOut;
|
|
@ -0,0 +1,26 @@
|
|||
/* eslint no-console:0 */
|
||||
import Bugsnag from '@bugsnag/js';
|
||||
import BugsnagPluginReact from '@bugsnag/plugin-react';
|
||||
import RailsUJS from '@rails/ujs';
|
||||
import ReactRailsUJS from 'react_ujs';
|
||||
|
||||
// Styles
|
||||
import '@shopify/polaris/dist/styles.css';
|
||||
|
||||
RailsUJS.start();
|
||||
|
||||
// Bugsnag
|
||||
if (ENVIRONMENT !== 'development') {
|
||||
Bugsnag.start({
|
||||
apiKey: BUGSNAG_FRONTEND_API_KEY,
|
||||
plugins: [new BugsnagPluginReact()],
|
||||
});
|
||||
}
|
||||
|
||||
// Images
|
||||
require.context('../images', true);
|
||||
|
||||
// React
|
||||
const componentRequireContext = require.context('components', true);
|
||||
|
||||
ReactRailsUJS.useContext(componentRequireContext);
|
|
@ -0,0 +1,8 @@
|
|||
/* eslint-disable */
|
||||
import 'stylesheets/server-side';
|
||||
|
||||
const images = require.context('../images', true);
|
||||
const imagePath = (name) => images(name, true);
|
||||
|
||||
// Rails
|
||||
require('@rails/ujs').start();
|
|
@ -0,0 +1,5 @@
|
|||
import ReactRailsUJS from 'react_ujs';
|
||||
|
||||
const componentRequireContext = require.context('components', true);
|
||||
|
||||
ReactRailsUJS.useContext(componentRequireContext);
|
|
@ -0,0 +1,321 @@
|
|||
import React, { useCallback, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
AppProvider,
|
||||
ActionList,
|
||||
Card,
|
||||
TextField,
|
||||
TextContainer,
|
||||
ContextualSaveBar,
|
||||
FormLayout,
|
||||
Modal,
|
||||
Frame,
|
||||
Layout,
|
||||
Loading,
|
||||
Navigation,
|
||||
Page,
|
||||
SkeletonBodyText,
|
||||
SkeletonDisplayText,
|
||||
SkeletonPage,
|
||||
Toast,
|
||||
TopBar,
|
||||
} from '@shopify/polaris';
|
||||
import { HomeMajor, OrdersMajor } from '@shopify/polaris-icons';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import theme from '../polaris/theme';
|
||||
import TopBarUserMenu from '../polaris/TopBarUserMenu';
|
||||
|
||||
export default function HomePage() {
|
||||
const defaultState = useRef({
|
||||
emailFieldValue: 'pedro@ppinera.es',
|
||||
nameFieldValue: 'Jaded Pixel',
|
||||
});
|
||||
const skipToContentRef = useRef(null);
|
||||
|
||||
const [toastActive, setToastActive] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
const [searchActive, setSearchActive] = useState(false);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [mobileNavigationActive, setMobileNavigationActive] =
|
||||
useState(false);
|
||||
const [modalActive, setModalActive] = useState(false);
|
||||
const [nameFieldValue, setNameFieldValue] = useState(
|
||||
defaultState.current.nameFieldValue,
|
||||
);
|
||||
const [emailFieldValue, setEmailFieldValue] = useState(
|
||||
defaultState.current.emailFieldValue,
|
||||
);
|
||||
const [, setStoreName] = useState(
|
||||
defaultState.current.nameFieldValue,
|
||||
);
|
||||
const [supportSubject, setSupportSubject] = useState('');
|
||||
const [supportMessage, setSupportMessage] = useState('');
|
||||
|
||||
const handleSubjectChange = useCallback(
|
||||
(value) => setSupportSubject(value),
|
||||
[],
|
||||
);
|
||||
const handleMessageChange = useCallback(
|
||||
(value) => setSupportMessage(value),
|
||||
[],
|
||||
);
|
||||
const handleDiscard = useCallback(() => {
|
||||
setEmailFieldValue(defaultState.current.emailFieldValue);
|
||||
setNameFieldValue(defaultState.current.nameFieldValue);
|
||||
setIsDirty(false);
|
||||
}, []);
|
||||
const handleSave = useCallback(() => {
|
||||
defaultState.current.nameFieldValue = nameFieldValue;
|
||||
defaultState.current.emailFieldValue = emailFieldValue;
|
||||
|
||||
setIsDirty(false);
|
||||
setToastActive(true);
|
||||
setStoreName(defaultState.current.nameFieldValue);
|
||||
}, [emailFieldValue, nameFieldValue]);
|
||||
const handleNameFieldChange = useCallback((value) => {
|
||||
setNameFieldValue(value);
|
||||
// value && setIsDirty(true);
|
||||
}, []);
|
||||
const handleEmailFieldChange = useCallback((value) => {
|
||||
setEmailFieldValue(value);
|
||||
// value && setIsDirty(true);
|
||||
}, []);
|
||||
const handleSearchResultsDismiss = useCallback(() => {
|
||||
setSearchActive(false);
|
||||
setSearchValue('');
|
||||
}, []);
|
||||
const handleSearchFieldChange = useCallback((value) => {
|
||||
setSearchValue(value);
|
||||
setSearchActive(value.length > 0);
|
||||
}, []);
|
||||
const toggleToastActive = useCallback(
|
||||
() => setToastActive((_toastActive) => !_toastActive),
|
||||
[],
|
||||
);
|
||||
|
||||
const toggleMobileNavigationActive = useCallback(
|
||||
() =>
|
||||
setMobileNavigationActive(
|
||||
(_mobileNavigationActive) => !_mobileNavigationActive,
|
||||
),
|
||||
[],
|
||||
);
|
||||
const toggleIsLoading = useCallback(
|
||||
() => setIsLoading((_isLoading) => !_isLoading),
|
||||
[],
|
||||
);
|
||||
const toggleModalActive = useCallback(
|
||||
() => setModalActive((_modalActive) => !_modalActive),
|
||||
[],
|
||||
);
|
||||
|
||||
const toastMarkup = toastActive ? (
|
||||
<Toast onDismiss={toggleToastActive} content="Changes saved" />
|
||||
) : null;
|
||||
|
||||
const contextualSaveBarMarkup = isDirty ? (
|
||||
<ContextualSaveBar
|
||||
message="Unsaved changes"
|
||||
saveAction={{
|
||||
onAction: handleSave,
|
||||
}}
|
||||
discardAction={{
|
||||
onAction: handleDiscard,
|
||||
}}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const searchResultsMarkup = (
|
||||
<ActionList
|
||||
items={[
|
||||
{ content: 'Shopify help center' },
|
||||
{ content: 'Community forums' },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
const searchFieldMarkup = (
|
||||
<TopBar.SearchField
|
||||
onChange={handleSearchFieldChange}
|
||||
value={searchValue}
|
||||
placeholder="Search"
|
||||
/>
|
||||
);
|
||||
const userMenuMarkup = <TopBarUserMenu />;
|
||||
|
||||
const topBarMarkup = (
|
||||
<TopBar
|
||||
showNavigationToggle
|
||||
userMenu={userMenuMarkup}
|
||||
searchResultsVisible={searchActive}
|
||||
searchField={searchFieldMarkup}
|
||||
searchResults={searchResultsMarkup}
|
||||
onSearchResultsDismiss={handleSearchResultsDismiss}
|
||||
onNavigationToggle={toggleMobileNavigationActive}
|
||||
/>
|
||||
);
|
||||
|
||||
const navigationMarkup = (
|
||||
<Navigation location="/">
|
||||
<Navigation.Section
|
||||
title="Project"
|
||||
items={[
|
||||
{
|
||||
label: 'Deploys',
|
||||
icon: HomeMajor,
|
||||
onClick: toggleIsLoading,
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
icon: OrdersMajor,
|
||||
onClick: toggleIsLoading,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Navigation>
|
||||
);
|
||||
|
||||
const loadingMarkup = isLoading ? <Loading /> : null;
|
||||
|
||||
const skipToContentTarget = (
|
||||
/* eslint-disable jsx-a11y/anchor-has-content,jsx-a11y/anchor-is-valid */
|
||||
<a
|
||||
id="SkipToContentTarget"
|
||||
ref={skipToContentRef}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
/* eslint-enable jsx-a11y/anchor-has-content,jsx-a11y/anchor-is-valid */
|
||||
);
|
||||
|
||||
const actualPageMarkup = (
|
||||
<Page title="Account">
|
||||
<Layout>
|
||||
{skipToContentTarget}
|
||||
<Layout.AnnotatedSection
|
||||
title="Account details"
|
||||
description="Jaded Pixel will use this as your account information."
|
||||
>
|
||||
<Card sectioned>
|
||||
<FormLayout>
|
||||
<TextField
|
||||
label="Full name"
|
||||
value={nameFieldValue}
|
||||
onChange={handleNameFieldChange}
|
||||
/>
|
||||
<TextField
|
||||
type="email"
|
||||
label="Email"
|
||||
value={emailFieldValue}
|
||||
onChange={handleEmailFieldChange}
|
||||
/>
|
||||
</FormLayout>
|
||||
</Card>
|
||||
</Layout.AnnotatedSection>
|
||||
</Layout>
|
||||
</Page>
|
||||
);
|
||||
|
||||
const loadingPageMarkup = (
|
||||
<SkeletonPage>
|
||||
<Layout>
|
||||
<Layout.Section>
|
||||
<Card sectioned>
|
||||
<TextContainer>
|
||||
<SkeletonDisplayText size="small" />
|
||||
<SkeletonBodyText lines={9} />
|
||||
</TextContainer>
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
</Layout>
|
||||
</SkeletonPage>
|
||||
);
|
||||
|
||||
const pageMarkup = isLoading ? loadingPageMarkup : actualPageMarkup;
|
||||
|
||||
const modalMarkup = (
|
||||
<Modal
|
||||
open={modalActive}
|
||||
onClose={toggleModalActive}
|
||||
title="Contact support"
|
||||
primaryAction={{
|
||||
content: 'Send',
|
||||
onAction: toggleModalActive,
|
||||
}}
|
||||
>
|
||||
<Modal.Section>
|
||||
<FormLayout>
|
||||
<TextField
|
||||
label="Subject"
|
||||
value={supportSubject}
|
||||
onChange={handleSubjectChange}
|
||||
/>
|
||||
<TextField
|
||||
label="Message"
|
||||
value={supportMessage}
|
||||
onChange={handleMessageChange}
|
||||
multiline
|
||||
/>
|
||||
</FormLayout>
|
||||
</Modal.Section>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>TuistLab</title>
|
||||
</Helmet>
|
||||
<AppProvider
|
||||
theme={theme}
|
||||
i18n={{
|
||||
Polaris: {
|
||||
Avatar: {
|
||||
label: 'Avatar',
|
||||
labelWithInitials: 'Avatar with initials {initials}',
|
||||
},
|
||||
ContextualSaveBar: {
|
||||
save: 'Save',
|
||||
discard: 'Discard',
|
||||
},
|
||||
TextField: {
|
||||
characterCount: '{count} characters',
|
||||
},
|
||||
TopBar: {
|
||||
toggleMenuLabel: 'Toggle menu',
|
||||
|
||||
SearchField: {
|
||||
clearButtonLabel: 'Clear',
|
||||
search: 'Search',
|
||||
},
|
||||
},
|
||||
Modal: {
|
||||
iFrameTitle: 'body markup',
|
||||
},
|
||||
Frame: {
|
||||
skipToContent: 'Skip to content',
|
||||
navigationLabel: 'Navigation',
|
||||
Navigation: {
|
||||
closeMobileNavigationLabel: 'Close navigation',
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Frame
|
||||
topBar={topBarMarkup}
|
||||
navigation={navigationMarkup}
|
||||
showMobileNavigation={mobileNavigationActive}
|
||||
onNavigationDismiss={toggleMobileNavigationActive}
|
||||
skipToContentTarget={skipToContentRef.current}
|
||||
>
|
||||
{contextualSaveBarMarkup}
|
||||
{loadingMarkup}
|
||||
{pageMarkup}
|
||||
{toastMarkup}
|
||||
{modalMarkup}
|
||||
</Frame>
|
||||
</AppProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
FormLayout,
|
||||
Frame,
|
||||
Layout,
|
||||
Page,
|
||||
TopBar,
|
||||
TextField,
|
||||
Form,
|
||||
Button,
|
||||
} from '@shopify/polaris';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import TopBarUserMenu from '../polaris/TopBarUserMenu';
|
||||
// import { useOrganizationCreateMutation } from '../graphql/types';
|
||||
|
||||
type FormData = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
const OrganizationNewPage = () => {
|
||||
// const [
|
||||
// createOrganization,
|
||||
// { error: createOrganizationError, data: createOrganizationData },
|
||||
// ] = useOrganizationCreateMutation();
|
||||
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
handleSubmit,
|
||||
} = useForm<FormData>();
|
||||
const onSubmit = () => {
|
||||
// createOrganization({
|
||||
// variables: { organization: { name: data.name } },
|
||||
// });
|
||||
};
|
||||
|
||||
// Page Markup
|
||||
const pageMarkup = (
|
||||
<Page title="Create a new organization">
|
||||
<Layout>
|
||||
<Layout.AnnotatedSection
|
||||
title="Organization details"
|
||||
description="We'll need some basic information from your organization in order to create it."
|
||||
>
|
||||
<Card sectioned>
|
||||
<Form noValidate onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormLayout>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
defaultValue=""
|
||||
rules={{ required: true }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
label="Organization name"
|
||||
placeholder="Example: craftweg"
|
||||
onChange={field.onChange}
|
||||
value={field.value}
|
||||
error={
|
||||
errors.name?.type === 'required'
|
||||
? 'The name is required to create an organization.'
|
||||
: null
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button primary submit>
|
||||
Submit
|
||||
</Button>
|
||||
</FormLayout>
|
||||
</Form>
|
||||
</Card>
|
||||
</Layout.AnnotatedSection>
|
||||
</Layout>
|
||||
</Page>
|
||||
);
|
||||
|
||||
const topBarUserMenu = <TopBarUserMenu />;
|
||||
const topBarMarkup = (
|
||||
<TopBar showNavigationToggle userMenu={topBarUserMenu} />
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>New organization</title>
|
||||
</Helmet>
|
||||
<Frame topBar={topBarMarkup}>{pageMarkup}</Frame>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrganizationNewPage;
|
|
@ -0,0 +1,59 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import {
|
||||
Card,
|
||||
FormLayout,
|
||||
Frame,
|
||||
Layout,
|
||||
Page,
|
||||
Toast,
|
||||
TopBar,
|
||||
} from '@shopify/polaris';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import TopBarUserMenu from '../polaris/TopBarUserMenu';
|
||||
|
||||
const ProjectNewPage = () => {
|
||||
// Toast
|
||||
const [toastActive, setToastActive] = useState(false);
|
||||
const toggleToastActive = useCallback(
|
||||
() => setToastActive((_toastActive) => !_toastActive),
|
||||
[],
|
||||
);
|
||||
const toastMarkup = toastActive ? (
|
||||
<Toast onDismiss={toggleToastActive} content="Changes saved" />
|
||||
) : null;
|
||||
|
||||
// Page Markup
|
||||
const pageMarkup = (
|
||||
<Page title="Create a project">
|
||||
<Layout>
|
||||
<Layout.AnnotatedSection
|
||||
title="Project details"
|
||||
description="We'll need some basic information from your project in order to create it."
|
||||
>
|
||||
<Card sectioned>
|
||||
<FormLayout />
|
||||
</Card>
|
||||
</Layout.AnnotatedSection>
|
||||
</Layout>
|
||||
</Page>
|
||||
);
|
||||
|
||||
const topBarUserMenu = <TopBarUserMenu />;
|
||||
const topBarMarkup = (
|
||||
<TopBar showNavigationToggle userMenu={topBarUserMenu} />
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>New project</title>
|
||||
</Helmet>
|
||||
<Frame topBar={topBarMarkup}>
|
||||
{pageMarkup}
|
||||
{toastMarkup}
|
||||
</Frame>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectNewPage;
|
|
@ -0,0 +1,59 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import {
|
||||
Card,
|
||||
FormLayout,
|
||||
Frame,
|
||||
Layout,
|
||||
Page,
|
||||
Toast,
|
||||
TopBar,
|
||||
} from '@shopify/polaris';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import TopBarUserMenu from '../polaris/TopBarUserMenu';
|
||||
|
||||
const SettingsPage = () => {
|
||||
// Toast
|
||||
const [toastActive, setToastActive] = useState(false);
|
||||
const toggleToastActive = useCallback(
|
||||
() => setToastActive((_toastActive) => !_toastActive),
|
||||
[],
|
||||
);
|
||||
const toastMarkup = toastActive ? (
|
||||
<Toast onDismiss={toggleToastActive} content="Changes saved" />
|
||||
) : null;
|
||||
|
||||
// Page Markup
|
||||
const pageMarkup = (
|
||||
<Page title="Create a project">
|
||||
<Layout>
|
||||
<Layout.AnnotatedSection
|
||||
title="Project details"
|
||||
description="We'll need some basic information from your project in order to create it."
|
||||
>
|
||||
<Card sectioned>
|
||||
<FormLayout />
|
||||
</Card>
|
||||
</Layout.AnnotatedSection>
|
||||
</Layout>
|
||||
</Page>
|
||||
);
|
||||
|
||||
const topBarUserMenu = <TopBarUserMenu />;
|
||||
const topBarMarkup = (
|
||||
<TopBar showNavigationToggle userMenu={topBarUserMenu} />
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>Settings</title>
|
||||
</Helmet>
|
||||
<Frame topBar={topBarMarkup}>
|
||||
{pageMarkup}
|
||||
{toastMarkup}
|
||||
</Frame>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPage;
|
|
@ -0,0 +1,66 @@
|
|||
import React, { useState, useCallback } from 'react';
|
||||
import { TopBar } from '@shopify/polaris';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import {
|
||||
projectNewPath,
|
||||
organizationNewPath,
|
||||
settingsPath,
|
||||
} from '../utilities/routes';
|
||||
import signOut from '../networking/signOut';
|
||||
import { useEnvironment } from '../utilities/EnvironmentProvider';
|
||||
|
||||
const TopBarUserMenu = () => {
|
||||
const history = useHistory();
|
||||
const environment = useEnvironment();
|
||||
|
||||
const [userMenuActive, setUserMenuActive] = useState(false);
|
||||
const toggleUserMenuActive = useCallback(
|
||||
() => setUserMenuActive((_userMenuActive) => !_userMenuActive),
|
||||
[],
|
||||
);
|
||||
const userMenuActions = [
|
||||
{
|
||||
items: [
|
||||
{
|
||||
content: 'New project',
|
||||
accessibilityLabel: 'Create a new project',
|
||||
onAction: () => {
|
||||
history.push(projectNewPath);
|
||||
},
|
||||
},
|
||||
{
|
||||
content: 'New organization',
|
||||
accesibilityLabel: 'Create a new organization',
|
||||
onAction: () => {
|
||||
history.push(organizationNewPath);
|
||||
},
|
||||
},
|
||||
{
|
||||
content: 'Settings',
|
||||
accesibility: 'Check out and change the user settings',
|
||||
onAction: () => {
|
||||
history.push(settingsPath);
|
||||
},
|
||||
},
|
||||
{
|
||||
content: 'Sign out',
|
||||
onAction: signOut,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<TopBar.UserMenu
|
||||
actions={userMenuActions}
|
||||
name={environment.user.email}
|
||||
detail="Test"
|
||||
initials="S"
|
||||
avatar={environment.user.avatarUrl}
|
||||
open={userMenuActive}
|
||||
onToggle={toggleUserMenuActive}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopBarUserMenu;
|
|
@ -0,0 +1,13 @@
|
|||
import logo from '../images/logo-topbar.png';
|
||||
|
||||
const theme = {
|
||||
logo: {
|
||||
width: 120,
|
||||
topBarSource: logo,
|
||||
contextualSaveBarSource: logo,
|
||||
url: `${BASE_URL}`,
|
||||
accessibilityLabel: 'TuistLab',
|
||||
},
|
||||
};
|
||||
|
||||
export default theme;
|
|
@ -0,0 +1,3 @@
|
|||
@import "tailwindcss/base";
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
|
@ -0,0 +1,34 @@
|
|||
/* eslint-disable */
|
||||
// https://davidpiesse.github.io/tailwind-md-colours/
|
||||
module.exports = {
|
||||
// purge: ["app/views/*/*.html.erb"],
|
||||
darkMode: false, // or 'media' or 'class'
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'deep-orange': '#ff5722',
|
||||
'deep-orange-50': '#fbe9e7',
|
||||
'deep-orange-100': '#ffccbc',
|
||||
'deep-orange-200': '#ffab91',
|
||||
'deep-orange-300': '#ff8a65',
|
||||
'deep-orange-400': '#ff7043',
|
||||
'deep-orange-500': '#ff5722',
|
||||
'deep-orange-600': '#f4511e',
|
||||
'deep-orange-700': '#e64a19',
|
||||
'deep-orange-800': '#d84315',
|
||||
'deep-orange-900': '#bf360c',
|
||||
'deep-orange-100-accent': '#ff9e80',
|
||||
'deep-orange-200-accent': '#ff6e40',
|
||||
'deep-orange-400-accent': '#ff3d00',
|
||||
'deep-orange-700-accent': '#dd2c00',
|
||||
},
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/forms'),
|
||||
require('@tailwindcss/typography'),
|
||||
],
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
declare const BASE_URL;
|
||||
declare const ENVIRONMENT;
|
||||
declare const BUGSNAG_FRONTEND_API_KEY;
|
|
@ -0,0 +1,4 @@
|
|||
declare module '*.png' {
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
declare module '*.svg' {
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { Frame, Loading, EmptyState, Card } from '@shopify/polaris';
|
||||
import { useMeQuery } from '../graphql/types';
|
||||
import type { User } from '../graphql/types';
|
||||
|
||||
type Environment = {
|
||||
user: Pick<User, 'email' | 'avatarUrl'>;
|
||||
};
|
||||
|
||||
const EnvironmentContext = React.createContext<
|
||||
Environment | undefined
|
||||
>(undefined);
|
||||
|
||||
const EnvironmentProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const { data, loading } = useMeQuery();
|
||||
if (loading) {
|
||||
return (
|
||||
<Frame>
|
||||
<Loading />
|
||||
</Frame>
|
||||
);
|
||||
}
|
||||
if (data) {
|
||||
const environment = {
|
||||
user: data.me,
|
||||
};
|
||||
return (
|
||||
<EnvironmentContext.Provider value={environment}>
|
||||
{children}
|
||||
</EnvironmentContext.Provider>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Frame>
|
||||
<Card sectioned>
|
||||
<EmptyState
|
||||
heading="Manage your inventory transfers"
|
||||
action={{ content: 'Add transfer' }}
|
||||
secondaryAction={{
|
||||
content: 'Learn more',
|
||||
url: 'https://help.shopify.com',
|
||||
}}
|
||||
image="https://cdn.shopify.com/s/files/1/0262/4071/2726/files/emptystate-files.png"
|
||||
>
|
||||
<p>
|
||||
Track and receive your incoming inventory from suppliers.
|
||||
</p>
|
||||
</EmptyState>
|
||||
</Card>
|
||||
</Frame>
|
||||
);
|
||||
};
|
||||
|
||||
export const useEnvironment = (): Environment =>
|
||||
useContext(EnvironmentContext);
|
||||
|
||||
export default EnvironmentProvider;
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
import Bugsnag from '@bugsnag/js';
|
||||
|
||||
let BugsnagErrorBoundary;
|
||||
|
||||
if (ENVIRONMENT !== 'development') {
|
||||
BugsnagErrorBoundary =
|
||||
Bugsnag.getPlugin('react').createErrorBoundary(React);
|
||||
}
|
||||
|
||||
const ErrorBoundary = ({
|
||||
children,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
}) => {
|
||||
if (ENVIRONMENT === 'development') {
|
||||
return <>{children}</>;
|
||||
}
|
||||
return <BugsnagErrorBoundary>{children}</BugsnagErrorBoundary>;
|
||||
};
|
||||
|
||||
export default ErrorBoundary;
|
|
@ -0,0 +1,4 @@
|
|||
export const projectNewPath = '/projects/new';
|
||||
export const organizationNewPath = '/organizations/new';
|
||||
export const settingsPath = '/settings';
|
||||
export const signOutPath = '/users/sign_out';
|
|
@ -0,0 +1,10 @@
|
|||
# typed: ignore
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationJob < ActiveJob::Base
|
||||
# Automatically retry jobs that encountered a deadlock
|
||||
# retry_on ActiveRecord::Deadlocked
|
||||
|
||||
# Most jobs are safe to ignore if the underlying records are no longer available
|
||||
# discard_on ActiveJob::DeserializationError
|
||||
end
|
|
@ -0,0 +1,97 @@
|
|||
# frozen_string_literal: true
|
||||
require "octokit"
|
||||
require "jwt"
|
||||
require "digest"
|
||||
require "openssl"
|
||||
require "securecompare"
|
||||
require "base64"
|
||||
require "dig_bang"
|
||||
|
||||
module GitHub
|
||||
class App
|
||||
Error = Class.new(StandardError)
|
||||
NonExistingInstallationOnRepositoryError = Class.new(Error)
|
||||
|
||||
attr_reader :app_id, :app_name, :bot_login, :webhook_secret, :private_key, :oauth_id, :oauth_secret
|
||||
|
||||
def initialize(app_id:, app_name:, bot_login:, webhook_secret:, private_key:, oauth_id:, oauth_secret:)
|
||||
@app_id = app_id
|
||||
@app_name = app_name
|
||||
@bot_login = bot_login
|
||||
@webhook_secret = webhook_secret
|
||||
@private_key = private_key
|
||||
@oauth_id = oauth_id
|
||||
@oauth_secret = oauth_secret
|
||||
end
|
||||
|
||||
# Returns the URL the user can be forwarded to
|
||||
# @param [String] ID of the user or organization that is installing your GitHub App.
|
||||
# @param [Array<String>] The list of repositories the app should be installed into.
|
||||
# @return [String] The URL to redirect the user to.
|
||||
def install_url(target_id:, repository_ids: [])
|
||||
query = URI.encode_www_form(
|
||||
suggested_target_id: target_id,
|
||||
repository_ids: repository_ids
|
||||
)
|
||||
"https://github.com/apps/#{app_name}/installations/new/permissions?#{query}"
|
||||
end
|
||||
|
||||
def verify_webhook_signature(signature:, message:)
|
||||
algorithm, signature = signature.split("=", 2)
|
||||
return false unless algorithm == "sha1"
|
||||
SecureCompare.secure_compare(signature, OpenSSL::HMAC.hexdigest(algorithm, webhook_secret, message))
|
||||
end
|
||||
|
||||
def authenticated_client
|
||||
Octokit::Client.new(bearer_token: token)
|
||||
end
|
||||
|
||||
def authenticated_client_for_repository(repository_full_name)
|
||||
cache_key = [self, "repository_token", private_key, repository_full_name]
|
||||
return Octokit::Client.new(bearer_token: Rails.cache.fetch(cache_key)) if Rails.cache.exist?(cache_key)
|
||||
|
||||
installation_id = installation_for_repository(repository_full_name)[:id]
|
||||
response = authenticated_client.create_app_installation_access_token(installation_id,
|
||||
repositories: [repository_full_name.split("/").last])
|
||||
token = response[:token]
|
||||
expires_at = response[:expires_at]
|
||||
Rails.cache.write(cache_key, token, expires_in: expires_at)
|
||||
Octokit::Client.new(bearer_token: token)
|
||||
end
|
||||
|
||||
class << self
|
||||
def tuist_lab
|
||||
@tuist_lab ||= GitHub::App.new(
|
||||
app_id: Rails.application.credentials.devise.dig!(:omniauth, :github, :app_id),
|
||||
app_name: Rails.application.credentials.devise.dig!(:omniauth, :github, :app_name),
|
||||
bot_login: Rails.application.credentials.devise.dig!(:omniauth, :github, :bot_login),
|
||||
webhook_secret: Rails.application.credentials.devise.dig!(:omniauth, :github, :webhook_secret),
|
||||
private_key: Base64.decode64(Rails.application.credentials.devise.dig!(:omniauth, :github,
|
||||
:private_key_base_64)),
|
||||
oauth_id: Rails.application.credentials.devise.dig!(:omniauth, :github, :client_id),
|
||||
oauth_secret: Rails.application.credentials.devise.dig!(:omniauth, :github, :client_secret)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def installation_for_repository(repository_full_name)
|
||||
authenticated_client.find_repository_installation(repository_full_name)
|
||||
rescue Octokit::NotFound
|
||||
raise NonExistingInstallationOnRepositoryError
|
||||
end
|
||||
|
||||
def token
|
||||
Rails.cache.fetch([self, private_key, app_id], expires_in: 10.minutes) do
|
||||
private_key = OpenSSL::PKey::RSA.new(self.private_key)
|
||||
payload = {
|
||||
# issued at time, 60 seconds in the past to allow for clock drift
|
||||
iat: Time.now.to_i - 60,
|
||||
exp: Time.now.to_i + (10 * 60),
|
||||
iss: app_id,
|
||||
}
|
||||
JWT.encode(payload, private_key, "RS256")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
# typed: ignore
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationMailer < ActionMailer::Base
|
||||
default from: "noreply@tuist.io"
|
||||
layout "mailer"
|
||||
end
|
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
class ExampleMailer < ApplicationMailer
|
||||
def example_email
|
||||
mail(to: "pedro@ppinera.es", subject: "Welcome to My Awesome Site")
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
class Account < ApplicationRecord
|
||||
# Validations
|
||||
validates :name, presence: true, length: { maximum: 30, minimum: 5 }, uniqueness: true,
|
||||
format: { with: /\A[a-zA-Z\-\_]+\z/, message: "invalid account name" }
|
||||
validates :owner_id, uniqueness: { scope: :owner_type }
|
||||
|
||||
# Associations
|
||||
belongs_to :owner, inverse_of: :account, polymorphic: true, dependent: :destroy
|
||||
has_many :projects, dependent: :destroy
|
||||
end
|
|
@ -0,0 +1,6 @@
|
|||
# typed: ignore
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationRecord < ActiveRecord::Base
|
||||
self.abstract_class = true
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
class Authorization < ApplicationRecord
|
||||
# include Encryptable
|
||||
# attr_encrypted :token, :secret, :refresh_token
|
||||
|
||||
belongs_to :user, optional: true
|
||||
validates :uid, uniqueness: { scope: [:provider] }
|
||||
end
|
|
@ -0,0 +1,48 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module TokenAuthenticatable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_save :ensure_authentication_token
|
||||
end
|
||||
|
||||
class_methods do
|
||||
attr_reader :authentication_token_attribute
|
||||
|
||||
def attr_authentication_token(attribute)
|
||||
@authentication_token_attribute = attribute
|
||||
end
|
||||
end
|
||||
|
||||
def authentication_token
|
||||
send(self.class.authentication_token_attribute.to_s)
|
||||
end
|
||||
|
||||
def authentication_token=(token)
|
||||
send("#{self.class.authentication_token_attribute}=", token)
|
||||
end
|
||||
|
||||
def encoded_authentication_token
|
||||
ApiTokenStrategy::Token.new(
|
||||
self.class.name,
|
||||
id,
|
||||
authentication_token,
|
||||
).encode
|
||||
end
|
||||
|
||||
private
|
||||
def ensure_authentication_token
|
||||
if authentication_token.blank?
|
||||
self.authentication_token = generate_authentication_token
|
||||
end
|
||||
end
|
||||
|
||||
def generate_authentication_token
|
||||
loop do
|
||||
token = Devise.friendly_token(30)
|
||||
query = { self.class.authentication_token_attribute.to_sym => token }
|
||||
break token unless self.class.exists?(query)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
class Organization < ApplicationRecord
|
||||
resourcify
|
||||
|
||||
# Associations
|
||||
has_one :account, dependent: :destroy, inverse_of: :owner, foreign_key: :owner_id
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
class Project < ApplicationRecord
|
||||
# Concerns
|
||||
include TokenAuthenticatable
|
||||
|
||||
# TokenAuthenticatable
|
||||
attr_authentication_token :api_token
|
||||
|
||||
# Validations
|
||||
validates :name, presence: true, length: { maximum: 30, minimum: 5 }
|
||||
validates :repository_full_name,
|
||||
format: { with: %r{\A[\w.@\:-~]+/[\w.@\:-~]+\z}, message: "invalid organization/repo format" }
|
||||
|
||||
# Associations
|
||||
belongs_to :account
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
class Role < ApplicationRecord
|
||||
# rubocop:disable Rails/HasAndBelongsToMany
|
||||
has_and_belongs_to_many :users, join_table: :users_roles
|
||||
# rubocop:enable Rails/HasAndBelongsToMany
|
||||
|
||||
belongs_to :resource,
|
||||
polymorphic: true,
|
||||
optional: true
|
||||
|
||||
validates :resource_type,
|
||||
inclusion: { in: Rolify.resource_types },
|
||||
allow_nil: true
|
||||
|
||||
scopify
|
||||
end
|
|
@ -0,0 +1,29 @@
|
|||
# typed: ignore
|
||||
# frozen_string_literal: true
|
||||
|
||||
class User < ApplicationRecord
|
||||
# Roles
|
||||
rolify
|
||||
|
||||
# Devise
|
||||
devise :database_authenticatable,
|
||||
:registerable,
|
||||
:recoverable,
|
||||
:rememberable,
|
||||
:validatable,
|
||||
# :confirmable,
|
||||
:lockable,
|
||||
:timeoutable,
|
||||
:trackable,
|
||||
:omniauthable,
|
||||
omniauth_providers: [:github]
|
||||
|
||||
# Associations
|
||||
has_one :account, dependent: :destroy, inverse_of: :owner, foreign_key: :owner_id
|
||||
has_many :authorizations, dependent: :destroy
|
||||
|
||||
def avatar_url
|
||||
hash = Digest::MD5.hexdigest(email.downcase)
|
||||
"https://www.gravatar.com/avatar/#{hash}"
|
||||
end
|
||||
end
|
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
class AdminPolicy < ApplicationPolicy
|
||||
def access?
|
||||
user&.email == "pedro@ppinera.es" || Rails.env.development?
|
||||
end
|
||||
end
|
|
@ -0,0 +1,50 @@
|
|||
# frozen_string_literal: true
|
||||
class ApplicationPolicy
|
||||
attr_reader :user, :record
|
||||
|
||||
def initialize(user, record)
|
||||
@user = user
|
||||
@record = record
|
||||
end
|
||||
|
||||
def index?
|
||||
false
|
||||
end
|
||||
|
||||
def show?
|
||||
false
|
||||
end
|
||||
|
||||
def create?
|
||||
false
|
||||
end
|
||||
|
||||
def new?
|
||||
create?
|
||||
end
|
||||
|
||||
def update?
|
||||
false
|
||||
end
|
||||
|
||||
def edit?
|
||||
update?
|
||||
end
|
||||
|
||||
def destroy?
|
||||
false
|
||||
end
|
||||
|
||||
class Scope
|
||||
attr_reader :user, :scope
|
||||
|
||||
def initialize(user, scope)
|
||||
@user = user
|
||||
@scope = scope
|
||||
end
|
||||
|
||||
def resolve
|
||||
scope.all
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
class ApplicationPresenter
|
||||
def self.present(*args, &block)
|
||||
new(*args).present(&block)
|
||||
end
|
||||
|
||||
def present
|
||||
raise NotImplementedError
|
||||
end
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
class AccountCreateService < ApplicationService
|
||||
attr_reader :owner, :name
|
||||
|
||||
def initialize(owner:, name:)
|
||||
@owner = owner
|
||||
@name = name
|
||||
super()
|
||||
end
|
||||
|
||||
def call
|
||||
account = Account.new(owner: owner, name: name)
|
||||
account.save!
|
||||
account
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationService
|
||||
def self.call(*args, &block)
|
||||
new(*args).call(&block)
|
||||
end
|
||||
|
||||
def call
|
||||
raise NotImplementedError
|
||||
end
|
||||
end
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AuthorizationAddService < ApplicationService
|
||||
def initialize(user:, data:)
|
||||
@user = user
|
||||
@data = data
|
||||
super()
|
||||
end
|
||||
|
||||
def call
|
||||
@user
|
||||
.authorizations
|
||||
.build(
|
||||
{
|
||||
provider: @data["provider"],
|
||||
uid: @data["uid"],
|
||||
# token: data['credentials']['token'],
|
||||
# secret: data['credentials']['secret'],
|
||||
# refresh_token: data['credentials']['refresh_token'],
|
||||
# expires: data['credentials']['expires'],
|
||||
# expires_at: (Time.at(data['credentials']['expires_at']) rescue nil),
|
||||
# Human readable label if a user connects multiple Google accounts
|
||||
email: @data["info"]["email"],
|
||||
}
|
||||
)
|
||||
.save
|
||||
end
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
class OrganizationCreateService < ApplicationService
|
||||
attr_reader :name, :admin
|
||||
|
||||
def initialize(name:, admin:)
|
||||
@name = name
|
||||
@admin = admin
|
||||
super()
|
||||
end
|
||||
|
||||
def call
|
||||
ActiveRecord::Base.transaction do
|
||||
organization = Organization.new
|
||||
admin.add_role(:admin, organization)
|
||||
organization.save!
|
||||
AccountCreateService.call(owner: organization, name: name)
|
||||
organization
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
class ProjectCreateService < ApplicationService
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue