diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 7d9e042b..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,85 +0,0 @@ -# Ruby CircleCI 2.0 configuration file -# -# Check https://circleci.com/docs/2.0/language-ruby/ for more details -# -version: 2 -jobs: - build: - docker: - - image: circleci/ruby:2.6.3-node - environment: - PGHOST: localhost - PGUSER: retrospective - RAILS_ENV: test - - image: postgres:9.5 - environment: - POSTGRES_USER: retrospective - POSTGRES_DB: retrospective_test - POSTGRES_PASSWORD: "" - - working_directory: ~/retrospective - - steps: - - checkout - - run: - name: Check current version of node - command: node -v - - - restore_cache: - keys: - - v1-dependencies-{{ checksum "Gemfile.lock" }} - - v1-dependencies- - - - run: - name: install dependencies - command: | - gem install bundler && bundle install --jobs=4 --retry=3 --path vendor/bundle - - - save_cache: - paths: - - ./vendor/bundle - key: v1-dependencies-{{ checksum "Gemfile.lock" }} - - - restore_cache: - keys: - - v2-yarn-{{ checksum "yarn.lock" }} - - v2-yarn- - - - run: - name: Yarn Install - command: yarn install --cache-folder ~/.cache/yarn - - - save_cache: - key: v2-yarn-{{ checksum "yarn.lock" }} - paths: - - ~/.cache/yarn - - - run: - name: Yarn Install - command: yarn install --cache-folder ~/.cache/yarn - - - run: bundle exec rake db:migrate - - - run: - name: run rubocop - command: bundle exec rubocop - - - run: - name: run tests - command: | - mkdir /tmp/test-results - TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \ - circleci tests split --split-by=timings)" - - bundle exec rspec \ - --format progress \ - --format RspecJunitFormatter \ - --out /tmp/test-results/rspec.xml \ - --format progress \ - $TEST_FILES - - - store_test_results: - path: /tmp/test-results - - store_artifacts: - path: /tmp/test-results - destination: test-results diff --git a/.dockerdev/.psqlrc b/.dockerdev/.psqlrc new file mode 100644 index 00000000..c6fef144 --- /dev/null +++ b/.dockerdev/.psqlrc @@ -0,0 +1 @@ +\set HISTFILE `[[ -z $PSQL_HISTFILE ]] && echo $HOME/.psql_history || echo $PSQL_HISTFILE` diff --git a/.dockerdev/Aptfile b/.dockerdev/Aptfile new file mode 100644 index 00000000..f027e0d4 --- /dev/null +++ b/.dockerdev/Aptfile @@ -0,0 +1 @@ +vim diff --git a/.dockerdev/Dockerfile b/.dockerdev/Dockerfile new file mode 100644 index 00000000..be7d067b --- /dev/null +++ b/.dockerdev/Dockerfile @@ -0,0 +1,55 @@ +# Source: https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development + +ARG RUBY_VERSION + +FROM ruby:$RUBY_VERSION + +ARG NODE_MAJOR +ARG PG_MAJOR +ARG BUNDLER_VERSION +ARG YARN_VERSION + +# Add PostgreSQL to sources list +RUN curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - \ + && echo 'deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main' $PG_MAJOR > /etc/apt/sources.list.d/pgdg.list + +# Add NodeJS to sources list +RUN curl -sL https://deb.nodesource.com/setup_$NODE_MAJOR.x | bash - + +# Add Yarn to the sources list +RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ + && echo 'deb http://dl.yarnpkg.com/debian/ stable main' > /etc/apt/sources.list.d/yarn.list + +# Install dependencies +# We use an external Aptfile for that, stay tuned +COPY .dockerdev/Aptfile /tmp/Aptfile +RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \ + DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \ + build-essential \ + postgresql-client-$PG_MAJOR \ + nodejs \ + yarn=$YARN_VERSION-1 \ + $(cat /tmp/Aptfile | xargs) && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \ + truncate -s 0 /var/log/*log +RUN yarn + +# Configure bundler and PATH +ENV LANG=C.UTF-8 \ + GEM_HOME=/bundle \ + BUNDLE_JOBS=4 \ + BUNDLE_RETRY=3 +ENV BUNDLE_PATH $GEM_HOME +ENV BUNDLE_APP_CONFIG=$BUNDLE_PATH \ + BUNDLE_BIN=$BUNDLE_PATH/bin +ENV PATH /app/bin:$BUNDLE_BIN:$PATH + +# Upgrade RubyGems and install required Bundler version +RUN gem update --system && \ + gem install bundler:$BUNDLER_VERSION + +# Create a directory for the app code +RUN mkdir -p /app + +WORKDIR /app diff --git a/app/javascript/components/.keep b/.dockerdev/bundle/.gitkeep similarity index 100% rename from app/javascript/components/.keep rename to .dockerdev/bundle/.gitkeep diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..f68e97b0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +# http://editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.env.development.example b/.env.development.example index ac89e985..e460c4be 100644 --- a/.env.development.example +++ b/.env.development.example @@ -1,7 +1,15 @@ DATABASE_URL=postgres://user:password@postgresql:5432/retrospective_development?max_connections=5 -GITHUB_ID= -GITHUB_SECRET= +ALFRED_KEY= +ALFRED_SECRET= S3_BUCKET_NAME= S3_KEY= S3_SECRET= SENTRY_KEY= +SKIP_ALFRED= + +GOOGLE_KEY= +GOOGLE_SECRET= +FACEBOOK_KEY= +FACEBOOK_SECRET= +GITHUB_KEY= +GITHUB_SECRET= diff --git a/.env.test.example b/.env.test.example index c7c6aebe..8be3b774 100644 --- a/.env.test.example +++ b/.env.test.example @@ -1,7 +1,7 @@ DATABASE_URL=postgres://user:password@postgresql:5432/retrospective_development?max_connections=5 DATABASE_CLEANER_ALLOW_REMOTE_DATABASE_URL=true -GITHUB_ID= -GITHUB_SECRET= +ALFRED_KEY= +ALFRED_SECRET= S3_BUCKET_NAME= S3_KEY= S3_SECRET= diff --git a/.github/workflows/aws.yml b/.github/workflows/aws.yml new file mode 100644 index 00000000..eb80ae05 --- /dev/null +++ b/.github/workflows/aws.yml @@ -0,0 +1,49 @@ +on: + workflow_dispatch + +name: Deploy to Amazon ECS + +jobs: + deploy: + name: Deploy + runs-on: self-hosted + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: Build, tag, and push image to Amazon ECR + id: build-image + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + ECR_REPOSITORY: retro + IMAGE_TAG: ${{ github.sha }} + run: | + # Build a docker container and + # push it to ECR so that it can + # be deployed to ECS. + docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG --build-arg RUBY_VERSION=2.6.6 --build-arg NODE_MAJOR=12 --build-arg PG_MAJOR=11 --build-arg YARN_VERSION=1.19.1 --build-arg BUNDLER_VERSION=2.0.2 --build-arg SECRET_KEY_BASE=${{ secrets.SECRET_KEY_BASE }} . + docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG + echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" + - name: Execute SSH commmands on remote server + uses: JimCronqvist/action-ssh@master + id: deploy-ec2 + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + ECR_REPOSITORY: retro + IMAGE_TAG: ${{ github.sha }} + with: + hosts: 'ubuntu@${{ secrets.HOST }}' + privateKey: ${{ secrets.DEPLOY_KEY }} + command: ECR="$ECR_REGISTRY/$ECR_REPOSITORY" IMAGE="$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" ~/deploy/deploy.sh diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..99c995e8 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,59 @@ +env: + RUBY_VERSION: 2.6.6 + POSTGRESQL_USERNAME: postgres + POSTGRESQL_PASSWORD: postgres + POSTGRES_DB: postgres + +name: Rails tests +on: [push,pull_request] +jobs: + rubocop-test: + name: Rubocop + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: ruby/setup-ruby@v1 + - name: Install Rubocop + run: gem install rubocop -v 0.74.0 + - name: Check code + run: rubocop + rspec-test: + name: Rspec + needs: rubocop-test + runs-on: ubuntu-latest + services: + postgres: + image: postgres:latest + ports: + - 5432:5432 + env: + POSTGRES_USER: ${{ env.POSTGRESQL_USERNAME }} + POSTGRES_PASSWORD: ${{ env.POSTGRESQL_PASSWORD }} + steps: + - uses: actions/checkout@v1 + - uses: ruby/setup-ruby@v1 + - name: Install postgres client + run: sudo apt-get install libpq-dev + - uses: actions/cache@v2 + with: + path: vendor/bundle + key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + ${{ runner.os }}-gems- + - name: Install dependencies + run: | + gem install bundler + bundle config path vendor/bundle + bundle install --jobs 4 --retry 3 + - name: Create database + run: | + bundler exec rails db:create RAILS_ENV=test + bundler exec rails db:migrate RAILS_ENV=test + - name: Run tests + run: bundle exec rspec + - name: Upload coverage results + uses: actions/upload-artifact@master + if: always() + with: + name: coverage report + path: coverage diff --git a/.gitignore b/.gitignore index 1345a95b..b2bde509 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ # Ignore bundler config. /.bundle +/.dockerdev/bundle # Ignore all logfiles and tempfiles. /log/* @@ -22,6 +23,7 @@ /public/assets .byebug_history +.ruby-gemset # Ignore master key for decrypting credentials and more. /config/master.key @@ -29,7 +31,9 @@ /vendor/* .env - +.generators +.rakeTasks +.idea/ /public/packs /public/packs-test /node_modules @@ -38,3 +42,6 @@ yarn-debug.log* .yarn-integrity coverage **/uploads +**/.DS_Store + +/config/credentials/production.key diff --git a/.rubocop.yml b/.rubocop.yml index 58a66480..b8ae6c8d 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -16,6 +16,7 @@ Metrics/LineLength: Metrics/BlockLength: Exclude: - ./spec/**/*_spec.rb + - ./lib/tasks/*.rake Metrics/MethodLength: Exclude: @@ -28,5 +29,12 @@ Lint/AmbiguousBlockAssociation: Style/Documentation: Enabled: false +Style/IfUnlessModifier: + Exclude: + - ./db/seeds.rb + Bundler/OrderedGems: Enabled: false + +Metrics/AbcSize: + Max: 20 diff --git a/.ruby-version b/.ruby-version index b2eb3147..fe30cc14 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -ruby-2.6.3 \ No newline at end of file +ruby-2.6.6 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..cb19b264 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,70 @@ +# Source: https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development + +ARG RUBY_VERSION + +FROM ruby:$RUBY_VERSION + +ARG SECRET_KEY_BASE +ARG NODE_MAJOR +ARG PG_MAJOR +ARG BUNDLER_VERSION +ARG YARN_VERSION + +# Add PostgreSQL to sources list +RUN curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - \ + && echo 'deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main' $PG_MAJOR > /etc/apt/sources.list.d/pgdg.list + +# Add NodeJS to sources list +RUN curl -sL https://deb.nodesource.com/setup_$NODE_MAJOR.x | bash - + +# Add Yarn to the sources list +RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ + && echo 'deb http://dl.yarnpkg.com/debian/ stable main' > /etc/apt/sources.list.d/yarn.list + +# Install dependencies +# We use an external Aptfile for that, stay tuned +COPY .dockerdev/Aptfile /tmp/Aptfile +RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \ + DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \ + build-essential \ + postgresql-client-$PG_MAJOR \ + nodejs \ + yarn=$YARN_VERSION-1 \ + $(cat /tmp/Aptfile | xargs) && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \ + truncate -s 0 /var/log/*log + +# Configure bundler and PATH +ENV LANG=C.UTF-8 \ + GEM_HOME=/bundle \ + BUNDLE_JOBS=4 \ + BUNDLE_RETRY=3 +ENV BUNDLE_PATH $GEM_HOME +ENV BUNDLE_APP_CONFIG=$BUNDLE_PATH \ + BUNDLE_BIN=$BUNDLE_PATH/bin +ENV PATH /app/bin:$BUNDLE_BIN:$PATH + +# Upgrade RubyGems and install required Bundler version +RUN gem update --system && \ + gem install bundler:$BUNDLER_VERSION + +# Create a directory for the app code +RUN mkdir -p /app + +WORKDIR /app + +COPY . . + +ENV RAILS_ENV production + +RUN bundle config github.https true && \ + bundle install \ + --without development test \ + --jobs=4 \ + --deployment + +RUN yarn + +RUN SECRET_KEY_BASE=$SECRET_KEY_BASE RAILS_ENV=production bundle exec rails assets:precompile + diff --git a/Gemfile b/Gemfile index 5dc0309e..8c1e8073 100644 --- a/Gemfile +++ b/Gemfile @@ -3,29 +3,29 @@ source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } -ruby '2.6.3' +ruby '2.6.6' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '~> 6.0' +gem 'rails', '~> 6.1' # Use postgresql as the database for Active Record gem 'pg', '>= 0.18', '< 2.0' # Use Puma as the app server -gem 'puma', '~> 3.11' +gem 'puma', '~> 5.1' # Use SCSS for stylesheets -gem 'sass-rails', '~> 5.0' +gem 'sass-rails', '~> 6.0' # Use Uglifier as compressor for JavaScript assets gem 'uglifier', '>= 1.3.0' # See https://github.com/rails/execjs#readme for more supported runtimes # gem 'mini_racer', platforms: :ruby # Use CoffeeScript for .coffee assets and views -gem 'coffee-rails', '~> 4.2' +gem 'coffee-rails', '~> 5.0' # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks # gem 'turbolinks', '~> 5' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder gem 'jbuilder', '~> 2.5' # Use Redis adapter to run Action Cable in production -# gem 'redis', '~> 4.0' +gem 'redis', '~> 4.2' # Use ActiveModel has_secure_password # gem 'bcrypt', '~> 3.1.7' @@ -40,10 +40,17 @@ gem 'bootsnap', '>= 1.1.0', require: false gem 'dotenv-rails' gem 'sentry-raven' +# Authentication +gem 'devise' +gem 'omniauth-alfred', git: 'https://github.com/cybergizer-hq/omniauth-alfred', branch: 'master' +gem 'omniauth-google-oauth2' +gem 'omniauth-facebook' +gem 'omniauth-github' + group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console - gem 'rubocop', '~> 0.74.0', require: false - gem 'rspec-rails', '~> 4.0.0.beta2' + gem 'rubocop', '~> 1.6.1', require: false + gem 'rspec-rails', '~> 4.0.1' gem 'rspec_junit_formatter' gem 'faker', git: 'https://github.com/stympy/faker.git', branch: 'master' gem 'pry' @@ -53,8 +60,9 @@ end group :development do # Access an interactive console on exception pages or by calling 'console' anywhere in the code. gem 'web-console', '>= 3.3.0' - gem 'listen', '>= 3.0.5', '< 3.2' + gem 'listen', '>= 3.0.5', '< 3.4' gem 'rails-erd' + gem 'letter_opener' end group :test do @@ -75,15 +83,23 @@ end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] -gem 'devise' -gem 'omniauth-github' -gem 'webpacker', '~> 4.x' +gem 'webpacker', '~> 5' gem 'rails-assets-bulma', source: 'https://rails-assets.org' gem 'fog-aws', require: false gem 'carrierwave', '>= 2.0.0.rc', '< 3.0' gem 'nanoid' -gem 'action_policy', '~> 0.3.0' +gem 'action_policy', '~> 0.5.4' gem 'react-rails' -gem 'active_model_serializers', '~> 0.10.0' +gem 'active_model_serializers', '~> 0.10.12' gem 'dry-monads' gem 'aasm' +gem 'sidekiq' + +gem 'graphql', '~> 1.11' +gem 'action_policy-graphql', '~> 0.5' +gem 'graphiql-rails', group: :development +group :test, :development do + gem 'action-cable-testing' +end + +gem 'bullet', group: 'test' diff --git a/Gemfile.lock b/Gemfile.lock index a670cc17..2876681b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,94 +1,117 @@ +GIT + remote: https://github.com/cybergizer-hq/omniauth-alfred + revision: 9cbda091493b3a68fe7d5136d91199db32741ff5 + branch: master + specs: + omniauth-alfred (0.0.0) + omniauth (~> 1.0) + omniauth-oauth2 (~> 1.0) + GIT remote: https://github.com/stympy/faker.git - revision: 4e9144825fcc9ba5c83cc0fd037779ab82f3120b + revision: ec06d217644cf3d66bd0e658c9b168af8f0c38c7 branch: master specs: - faker (2.6.0) - i18n (>= 1.6, < 1.8) + faker (2.15.1) + i18n (>= 1.6, < 2) GEM remote: https://rubygems.org/ remote: https://rails-assets.org/ specs: - aasm (5.0.6) + aasm (5.1.1) concurrent-ruby (~> 1.0) - action_policy (0.3.2) - actioncable (6.0.0) - actionpack (= 6.0.0) + action-cable-testing (0.6.1) + actioncable (>= 5.0) + action_policy (0.5.4) + ruby-next-core (>= 0.10.3) + action_policy-graphql (0.5.2) + action_policy (>= 0.5.0) + graphql (>= 1.9.3) + ruby-next-core (>= 0.10.0) + actioncable (6.1.0) + actionpack (= 6.1.0) + activesupport (= 6.1.0) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.0.0) - actionpack (= 6.0.0) - activejob (= 6.0.0) - activerecord (= 6.0.0) - activestorage (= 6.0.0) - activesupport (= 6.0.0) + actionmailbox (6.1.0) + actionpack (= 6.1.0) + activejob (= 6.1.0) + activerecord (= 6.1.0) + activestorage (= 6.1.0) + activesupport (= 6.1.0) mail (>= 2.7.1) - actionmailer (6.0.0) - actionpack (= 6.0.0) - actionview (= 6.0.0) - activejob (= 6.0.0) + actionmailer (6.1.0) + actionpack (= 6.1.0) + actionview (= 6.1.0) + activejob (= 6.1.0) + activesupport (= 6.1.0) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.0.0) - actionview (= 6.0.0) - activesupport (= 6.0.0) - rack (~> 2.0) + actionpack (6.1.0) + actionview (= 6.1.0) + activesupport (= 6.1.0) + rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.0.0) - actionpack (= 6.0.0) - activerecord (= 6.0.0) - activestorage (= 6.0.0) - activesupport (= 6.0.0) + actiontext (6.1.0) + actionpack (= 6.1.0) + activerecord (= 6.1.0) + activestorage (= 6.1.0) + activesupport (= 6.1.0) nokogiri (>= 1.8.5) - actionview (6.0.0) - activesupport (= 6.0.0) + actionview (6.1.0) + activesupport (= 6.1.0) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - active_model_serializers (0.10.10) - actionpack (>= 4.1, < 6.1) - activemodel (>= 4.1, < 6.1) + active_model_serializers (0.10.12) + actionpack (>= 4.1, < 6.2) + activemodel (>= 4.1, < 6.2) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (6.0.0) - activesupport (= 6.0.0) + activejob (6.1.0) + activesupport (= 6.1.0) globalid (>= 0.3.6) - activemodel (6.0.0) - activesupport (= 6.0.0) - activerecord (6.0.0) - activemodel (= 6.0.0) - activesupport (= 6.0.0) - activestorage (6.0.0) - actionpack (= 6.0.0) - activejob (= 6.0.0) - activerecord (= 6.0.0) + activemodel (6.1.0) + activesupport (= 6.1.0) + activerecord (6.1.0) + activemodel (= 6.1.0) + activesupport (= 6.1.0) + activestorage (6.1.0) + actionpack (= 6.1.0) + activejob (= 6.1.0) + activerecord (= 6.1.0) + activesupport (= 6.1.0) marcel (~> 0.3.1) - activesupport (6.0.0) + mimemagic (~> 0.3.2) + activesupport (6.1.0) concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - zeitwerk (~> 2.1, >= 2.1.8) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) addressable (2.7.0) public_suffix (>= 2.0.2, < 5.0) archive-zip (0.12.0) io-like (~> 0.3.0) - ast (2.4.0) + ast (2.4.1) babel-source (5.8.35) babel-transpiler (0.7.0) babel-source (>= 4.0, < 6) execjs (~> 2.0) - bcrypt (3.1.13) + bcrypt (3.1.16) bindex (0.8.1) - bootsnap (1.4.5) + bootsnap (1.5.1) msgpack (~> 1.0) - builder (3.2.3) - byebug (11.0.1) - capybara (3.29.0) + builder (3.2.4) + bullet (6.1.2) + activesupport (>= 3.0.0) + uniform_notifier (~> 1.11) + byebug (11.1.3) + capybara (3.34.0) addressable mini_mime (>= 0.1.3) nokogiri (~> 1.8) @@ -96,7 +119,7 @@ GEM rack-test (>= 0.6.3) regexp_parser (~> 1.5) xpath (~> 3.2) - carrierwave (2.0.2) + carrierwave (2.1.0) activemodel (>= 5.0.0) activesupport (>= 5.0.0) addressable (~> 2.6) @@ -110,58 +133,58 @@ GEM chromedriver-helper (2.1.1) archive-zip (~> 0.10) nokogiri (~> 1.8) - coderay (1.1.2) - coffee-rails (4.2.2) + coderay (1.1.3) + coffee-rails (5.0.0) coffee-script (>= 2.2.0) - railties (>= 4.0.0) + railties (>= 5.2.0) coffee-script (2.4.1) coffee-script-source execjs coffee-script-source (1.12.2) - concurrent-ruby (1.1.5) - connection_pool (2.2.2) - crack (0.4.3) - safe_yaml (~> 1.0.0) - crass (1.0.4) - database_cleaner (1.7.0) - devise (4.7.1) + concurrent-ruby (1.1.7) + connection_pool (2.2.3) + crack (0.4.4) + crass (1.0.6) + database_cleaner (1.8.5) + devise (4.7.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) responders warden (~> 1.2.3) - diff-lcs (1.3) + diff-lcs (1.4.4) docile (1.3.2) - dotenv (2.7.5) - dotenv-rails (2.7.5) - dotenv (= 2.7.5) - railties (>= 3.2, < 6.1) - dry-core (0.4.9) + dotenv (2.7.6) + dotenv-rails (2.7.6) + dotenv (= 2.7.6) + railties (>= 3.2) + dry-core (0.5.0) concurrent-ruby (~> 1.0) - dry-equalizer (0.2.2) - dry-monads (1.3.1) + dry-equalizer (0.3.0) + dry-monads (1.3.5) concurrent-ruby (~> 1.0) dry-core (~> 0.4, >= 0.4.4) dry-equalizer - erubi (1.9.0) - excon (0.67.0) + erubi (1.10.0) + excon (0.78.1) execjs (2.7.0) - factory_bot (5.1.1) - activesupport (>= 4.2.0) - factory_bot_rails (5.1.1) - factory_bot (~> 5.1.0) - railties (>= 4.2.0) - faraday (0.17.0) + factory_bot (6.1.0) + activesupport (>= 5.0.0) + factory_bot_rails (6.1.0) + factory_bot (~> 6.1.0) + railties (>= 5.0.0) + faraday (1.1.0) multipart-post (>= 1.2, < 3) - ffi (1.11.1) - fog-aws (3.5.2) + ruby2_keywords + ffi (1.14.1) + fog-aws (3.7.0) fog-core (~> 2.1) fog-json (~> 1.1) fog-xml (~> 0.1) ipaddress (~> 0.8) - fog-core (2.1.2) + fog-core (2.2.3) builder - excon (~> 0.58) + excon (~> 0.71) formatador (~> 0.2) mime-types fog-json (1.2.0) @@ -173,29 +196,35 @@ GEM formatador (0.2.5) globalid (0.4.2) activesupport (>= 4.2.0) - hashdiff (1.0.0) - hashie (3.6.0) - i18n (1.7.0) + graphiql-rails (1.7.0) + railties + sprockets-rails + graphql (1.11.6) + hashdiff (1.0.1) + hashie (4.1.0) + i18n (1.8.5) concurrent-ruby (~> 1.0) - image_processing (1.9.3) + image_processing (1.12.1) mini_magick (>= 4.9.5, < 5) - ruby-vips (>= 2.0.13, < 3) + ruby-vips (>= 2.0.17, < 3) io-like (0.3.0) ipaddress (0.8.3) - jaro_winkler (1.5.3) jbuilder (2.9.1) activesupport (>= 4.2.0) - json (2.2.0) + json (2.3.1) json_matchers (0.11.1) json_schema json_schema (0.20.8) jsonapi-renderer (0.2.2) - jwt (2.2.1) - listen (3.1.5) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - ruby_dep (~> 1.2) - loofah (2.3.0) + jwt (2.2.2) + launchy (2.5.0) + addressable (~> 2.7) + letter_opener (1.7.0) + launchy (~> 2.2) + listen (3.3.3) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + loofah (2.8.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) @@ -203,69 +232,79 @@ GEM marcel (0.3.3) mimemagic (~> 0.3.2) method_source (0.9.2) - mime-types (3.3) + mime-types (3.3.1) mime-types-data (~> 3.2015) - mime-types-data (3.2019.1009) - mimemagic (0.3.3) - mini_magick (4.9.5) + mime-types-data (3.2020.1104) + mimemagic (0.3.10) + nokogiri (~> 1) + rake + mini_magick (4.11.0) mini_mime (1.0.2) mini_portile2 (2.4.0) - minitest (5.12.2) - msgpack (1.3.1) - multi_json (1.13.1) + minitest (5.14.2) + msgpack (1.3.3) + multi_json (1.15.0) multi_xml (0.6.0) multipart-post (2.1.1) nanoid (2.0.0) - nio4r (2.5.2) - nokogiri (1.10.4) + nio4r (2.5.4) + nokogiri (1.10.10) mini_portile2 (~> 2.4.0) - oauth2 (1.4.2) + oauth2 (1.4.4) faraday (>= 0.8, < 2.0) jwt (>= 1.0, < 3.0) multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 3) - omniauth (1.9.0) - hashie (>= 3.4.6, < 3.7.0) + omniauth (1.9.1) + hashie (>= 3.4.6) rack (>= 1.6.2, < 3) - omniauth-github (1.3.0) + omniauth-facebook (8.0.0) + omniauth-oauth2 (~> 1.2) + omniauth-github (1.4.0) omniauth (~> 1.5) omniauth-oauth2 (>= 1.4.0, < 2.0) - omniauth-oauth2 (1.6.0) + omniauth-google-oauth2 (0.8.1) + jwt (>= 2.0) oauth2 (~> 1.1) + omniauth (>= 1.1.1) + omniauth-oauth2 (>= 1.6) + omniauth-oauth2 (1.7.0) + oauth2 (~> 1.4) omniauth (~> 1.9) orm_adapter (0.5.0) - parallel (1.18.0) - parser (2.6.5.0) - ast (~> 2.4.0) - pg (1.1.4) + parallel (1.20.1) + parser (2.7.2.0) + ast (~> 2.4.1) + pg (1.2.3) pry (0.12.2) coderay (~> 1.1.0) method_source (~> 0.9.0) - pry-byebug (3.7.0) + pry-byebug (3.8.0) byebug (~> 11.0) pry (~> 0.10) - public_suffix (4.0.1) - puma (3.12.1) - rack (2.0.7) + public_suffix (4.0.6) + puma (5.1.1) + nio4r (~> 2.0) + rack (2.2.3) rack-proxy (0.6.5) rack rack-test (1.1.0) rack (>= 1.0, < 3) - rails (6.0.0) - actioncable (= 6.0.0) - actionmailbox (= 6.0.0) - actionmailer (= 6.0.0) - actionpack (= 6.0.0) - actiontext (= 6.0.0) - actionview (= 6.0.0) - activejob (= 6.0.0) - activemodel (= 6.0.0) - activerecord (= 6.0.0) - activestorage (= 6.0.0) - activesupport (= 6.0.0) - bundler (>= 1.3.0) - railties (= 6.0.0) + rails (6.1.0) + actioncable (= 6.1.0) + actionmailbox (= 6.1.0) + actionmailer (= 6.1.0) + actionpack (= 6.1.0) + actiontext (= 6.1.0) + actionview (= 6.1.0) + activejob (= 6.1.0) + activemodel (= 6.1.0) + activerecord (= 6.1.0) + activestorage (= 6.1.0) + activesupport (= 6.1.0) + bundler (>= 1.15.0) + railties (= 6.1.0) sprockets-rails (>= 2.0.0) rails-assets-bulma (0.7.4) rails-controller-testing (1.0.4) @@ -282,132 +321,146 @@ GEM ruby-graphviz (~> 1.2) rails-html-sanitizer (1.3.0) loofah (~> 2.3) - railties (6.0.0) - actionpack (= 6.0.0) - activesupport (= 6.0.0) + railties (6.1.0) + actionpack (= 6.1.0) + activesupport (= 6.1.0) method_source rake (>= 0.8.7) - thor (>= 0.20.3, < 2.0) + thor (~> 1.0) rainbow (3.0.0) - rake (13.0.0) - rb-fsevent (0.10.3) - rb-inotify (0.10.0) + rake (13.0.3) + rb-fsevent (0.10.4) + rb-inotify (0.10.1) ffi (~> 1.0) - react-rails (2.6.0) + react-rails (2.6.1) babel-transpiler (>= 0.7.0) connection_pool execjs railties (>= 3.2) tilt - regexp_parser (1.6.0) - responders (3.0.0) + redis (4.2.5) + regexp_parser (1.8.2) + responders (3.0.1) actionpack (>= 5.0) railties (>= 5.0) - rspec-core (3.9.0) - rspec-support (~> 3.9.0) - rspec-expectations (3.9.0) + rexml (3.2.4) + rspec-core (3.10.0) + rspec-support (~> 3.10.0) + rspec-expectations (3.10.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-mocks (3.9.0) + rspec-support (~> 3.10.0) + rspec-mocks (3.10.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-rails (4.0.0.beta2) + rspec-support (~> 3.10.0) + rspec-rails (4.0.1) actionpack (>= 4.2) activesupport (>= 4.2) railties (>= 4.2) - rspec-core (~> 3.8) - rspec-expectations (~> 3.8) - rspec-mocks (~> 3.8) - rspec-support (~> 3.8) - rspec-support (3.9.0) + rspec-core (~> 3.9) + rspec-expectations (~> 3.9) + rspec-mocks (~> 3.9) + rspec-support (~> 3.9) + rspec-support (3.10.0) rspec_junit_formatter (0.4.1) rspec-core (>= 2, < 4, != 2.12.0) - rubocop (0.74.0) - jaro_winkler (~> 1.5.1) + rubocop (1.6.1) parallel (~> 1.10) - parser (>= 2.6) + parser (>= 2.7.1.5) rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml + rubocop-ast (>= 1.2.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 1.7) + unicode-display_width (>= 1.4.0, < 2.0) + rubocop-ast (1.3.0) + parser (>= 2.7.1.5) ruby-graphviz (1.2.4) + ruby-next-core (0.10.5) ruby-progressbar (1.10.1) - ruby-vips (2.0.15) + ruby-vips (2.0.17) ffi (~> 1.9) - ruby_dep (1.5.0) - rubyzip (2.0.0) - safe_yaml (1.0.5) - sass (3.7.4) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - sass-rails (5.1.0) - railties (>= 5.2.0) - sass (~> 3.1) - sprockets (>= 2.8, < 4.0) - sprockets-rails (>= 2.0, < 4.0) - tilt (>= 1.1, < 3) - selenium-webdriver (3.142.6) + ruby2_keywords (0.0.2) + rubyzip (2.3.0) + 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 + selenium-webdriver (3.142.7) childprocess (>= 0.5, < 4.0) rubyzip (>= 1.2.2) - sentry-raven (2.11.3) - faraday (>= 0.7.6, < 1.0) + semantic_range (2.3.1) + sentry-raven (3.1.1) + faraday (>= 1.0) + sidekiq (6.1.2) + connection_pool (>= 2.2.2) + rack (~> 2.0) + redis (>= 4.2.0) simplecov (0.17.1) docile (~> 1.1) json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.2) - sprockets (3.7.2) + sprockets (4.0.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (3.2.1) + sprockets-rails (3.2.2) actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) test-prof (0.10.0) - thor (0.20.3) - thread_safe (0.3.6) + thor (1.0.1) tilt (2.0.10) - tzinfo (1.2.5) - thread_safe (~> 0.1) + tzinfo (2.0.4) + concurrent-ruby (~> 1.0) uglifier (4.2.0) execjs (>= 0.3.0, < 3) - unicode-display_width (1.6.0) - vcr (5.0.0) - warden (1.2.8) - rack (>= 2.0.6) - web-console (4.0.1) + unicode-display_width (1.7.0) + uniform_notifier (1.13.0) + vcr (6.0.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) - webmock (3.7.6) + webmock (3.10.0) addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webpacker (4.0.7) - activesupport (>= 4.2) + webpacker (5.2.1) + activesupport (>= 5.2) rack-proxy (>= 0.6.1) - railties (>= 4.2) - websocket-driver (0.7.1) + railties (>= 5.2) + semantic_range (>= 2.3.0) + websocket-driver (0.7.3) websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.4) + websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.2.0) + zeitwerk (2.4.2) PLATFORMS ruby DEPENDENCIES aasm - action_policy (~> 0.3.0) - active_model_serializers (~> 0.10.0) + action-cable-testing + action_policy (~> 0.5.4) + action_policy-graphql (~> 0.5) + active_model_serializers (~> 0.10.12) bootsnap (>= 1.1.0) + bullet capybara (>= 2.15) carrierwave (>= 2.0.0.rc, < 3.0) chromedriver-helper - coffee-rails (~> 4.2) + coffee-rails (~> 5.0) database_cleaner devise dotenv-rails @@ -415,26 +468,34 @@ DEPENDENCIES factory_bot_rails faker! fog-aws + graphiql-rails + graphql (~> 1.11) jbuilder (~> 2.5) json_matchers - listen (>= 3.0.5, < 3.2) + letter_opener + listen (>= 3.0.5, < 3.4) nanoid + omniauth-alfred! + omniauth-facebook omniauth-github + omniauth-google-oauth2 pg (>= 0.18, < 2.0) pry pry-byebug - puma (~> 3.11) - rails (~> 6.0) + puma (~> 5.1) + rails (~> 6.1) rails-assets-bulma! rails-controller-testing rails-erd react-rails - rspec-rails (~> 4.0.0.beta2) + redis (~> 4.2) + rspec-rails (~> 4.0.1) rspec_junit_formatter - rubocop (~> 0.74.0) - sass-rails (~> 5.0) + rubocop (~> 1.6.1) + sass-rails (~> 6.0) selenium-webdriver sentry-raven + sidekiq simplecov test-prof tzinfo-data @@ -442,10 +503,10 @@ DEPENDENCIES vcr web-console (>= 3.3.0) webmock - webpacker (~> 4.x) + webpacker (~> 5) RUBY VERSION - ruby 2.6.3p62 + ruby 2.6.6p146 BUNDLED WITH - 2.0.2 + 2.1.4 diff --git a/README.md b/README.md index 7db80e4c..aa963bc2 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,34 @@ Things you may want to cover: * Services (job queues, cache servers, search engines, etc.) -* Deployment instructions - * ... + +## Development instructions + +1. Install docker: [docker engine install](https://docs.docker.com/engine/install/ "docker engine install") + + +2. Install docker-compose: [docker compose install](https://docs.docker.com/compose/install/ "docker compose install") + + +3. Clone the project: https://github.com/cybergizer-hq/retrospective + + +4. Create and setup postgres db: +``` +docker-compose run rake db:create db:setup +``` + + +5. Run the containers: +``` +docker-compose up -d rails +``` + +run Rails console if needed: +``` +docker-compose run runner +``` + +6. In order to skip Alfred login and login with the first seed user + put `SKIP_ALFRED=true` in your .env file diff --git a/app/assets/images/chevron-down.svg b/app/assets/images/chevron-down.svg new file mode 100644 index 00000000..cff15593 --- /dev/null +++ b/app/assets/images/chevron-down.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/assets/images/default_avatar.jpg b/app/assets/images/default_avatar.jpg deleted file mode 100644 index 8f553cee..00000000 Binary files a/app/assets/images/default_avatar.jpg and /dev/null differ diff --git a/app/assets/images/icon-calendar.png b/app/assets/images/icon-calendar.png new file mode 100644 index 00000000..c7a041cd Binary files /dev/null and b/app/assets/images/icon-calendar.png differ diff --git a/app/assets/images/icon-cards.png b/app/assets/images/icon-cards.png new file mode 100644 index 00000000..fbb75490 Binary files /dev/null and b/app/assets/images/icon-cards.png differ diff --git a/app/assets/images/icon-done.svg b/app/assets/images/icon-done.svg new file mode 100644 index 00000000..9a6f4031 --- /dev/null +++ b/app/assets/images/icon-done.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/assets/images/icon-members.svg b/app/assets/images/icon-members.svg new file mode 100644 index 00000000..8efab22d --- /dev/null +++ b/app/assets/images/icon-members.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/assets/images/icon-plus.svg b/app/assets/images/icon-plus.svg new file mode 100644 index 00000000..6b2d6fbf --- /dev/null +++ b/app/assets/images/icon-plus.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/assets/images/right-arrow.svg b/app/assets/images/right-arrow.svg new file mode 100644 index 00000000..f30e9d54 --- /dev/null +++ b/app/assets/images/right-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/images/setting.svg b/app/assets/images/setting.svg new file mode 100644 index 00000000..cd21378c --- /dev/null +++ b/app/assets/images/setting.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/stylesheets/board.scss b/app/assets/stylesheets/board.scss new file mode 100644 index 00000000..53304c57 --- /dev/null +++ b/app/assets/stylesheets/board.scss @@ -0,0 +1,97 @@ +// board's style + +// .board-button{ +// font-weight: 500; +// cursor: pointer; +// -webkit-user-select: none; +// -moz-user-select: none; +// -ms-user-select: none; +// user-select: none; +// border: 1px solid; +// border-radius: 6px; +// -webkit-appearance: none; +// -moz-appearance: none; +// color: var(--color-text-primary); +// background-color: var(--color-btn-bg); +// border-color: var(--color-btn-border); +// padding: 3px 12px; +// font-size: 12px; +// line-height: 20px; +// margin: 5px; +// } + +// .board-title{ +// font-size: 30px; +// font-weight: 400; +// } + +// .link-as-button{ +// display: block; +// } + +// .board-button.is-success{ +// background-color: #23d160; +// } + +// .cyber-logo-link:hover{ +// text-decoration: underline; +// color: #0366d6; +// } + + +// .board-subtitle { +// font-size: 14px!important; +// font-weight: 600; +// padding-bottom: 10px; +// } + +// .board-users { +// float: left; +// padding-right: 10px; +// margin-top: 13px; +// } + +// .outer-circle { +// background: #385a94; +// border-radius: 50%; +// height: 50px; +// width: 50px; +// margin-left: 10px; +// } + +// .is-success { +// background-color: #23d160; +// color: #fff; +// } + +// .is-info { +// background-color: #209cee; +// color: #fff; +// } + +// .board-column-title { +// font-size: 14px!important; +// font-weight: 600; +// padding-bottom: 10px; +// height: 30px; +// } + +// .float_left { +// float: left; +// } + +// .float_right { +// float: right; +// } + +// .card-buttons { +// padding-top: 5px; +// width: 100%; +// height: 40px; +// } + + + + + + diff --git a/app/assets/stylesheets/common.scss b/app/assets/stylesheets/common.scss new file mode 100644 index 00000000..c750bf02 --- /dev/null +++ b/app/assets/stylesheets/common.scss @@ -0,0 +1,183 @@ +/* +* Prefixed by https://autoprefixer.github.io +* PostCSS: v7.0.29, +* Autoprefixer: v9.7.6 +* Browsers: last 4 version +*/ + +.animated-board { + max-height: 0; + opacity: 0; + -webkit-transition: 0.6s; + -o-transition: 0.6s; + transition: 0.6s; + -webkit-transform: translate(0, -100%); + -ms-transform: translate(0, -100%); + transform: translate(0, -100%); + visibility: hidden; + &.active { + opacity: 1; + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + transform: translate(0, 0); + max-height: none; + visibility: visible; + } +} + +.board-card { + padding: 1.25rem; + min-height: 215px; + background: #ffffff; + -webkit-box-shadow: 0px 0px 40px rgba(0, 0, 0, 0.25); + box-shadow: 0px 0px 40px rgba(0, 0, 0, 0.25); + position: relative; + .board-title { + font-family: Roboto; + font-style: normal; + font-weight: bold; + font-size: 1.125rem; + text-decoration: none; + a { + color: black; + } + } + .board-stats-table { + font-family: Roboto; + font-style: normal; + font-weight: normal; + margin-top: 20px; + display: -ms-grid; + display: grid; + -ms-grid-columns: 20px 10px 1fr; + grid-template-columns: 20px 1fr; + -ms-grid-rows: 30px 0px 30px 0px 30px; + grid-template-rows: 30px 30px 30px; + grid-gap: 0px 10px; + .icon-container { + text-align: right; + img { + padding: 0; + } + } + } + .board-stats-table > *:nth-child(1) { + -ms-grid-row: 1; + -ms-grid-column: 1; + } + .board-stats-table > *:nth-child(2) { + -ms-grid-row: 1; + -ms-grid-column: 3; + } + .board-stats-table > *:nth-child(3) { + -ms-grid-row: 3; + -ms-grid-column: 1; + } + .board-stats-table > *:nth-child(4) { + -ms-grid-row: 3; + -ms-grid-column: 3; + } + .board-stats-table > *:nth-child(5) { + -ms-grid-row: 5; + -ms-grid-column: 1; + } + .board-stats-table > *:nth-child(6) { + -ms-grid-row: 5; + -ms-grid-column: 3; + } + .button-grid { + position: absolute; + right: 0; + bottom: 0; + display: -ms-grid; + display: grid; + -ms-grid-columns: 90px 5px 90px; + grid-template-columns: 90px 90px; + grid-gap: 5px; + .board-button { + height: 20px; + background-color: #c4c4c4; + font-family: Roboto; + font-style: normal; + font-weight: normal; + padding: 10px 30px; + &:last-child { + -ms-grid-column: 2; + grid-column: 2; + } + } + } +} + +.board-date { + font-family: Roboto; + font-style: normal; + font-weight: normal; + font-size: 1.5rem; + margin-bottom: 40px; + &.animated-board:not(.active) { + margin-bottom: 0px; + } +} + +.dropdown_boards_index { + position: relative; + display: inline-block; +} + +.dropdown-content_boards_index { + padding: 10px; + display: none; + position: absolute; + background-color: #f1f1f1; + -webkit-box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); + box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); + z-index: 1; +} + +.dropdown_boards_index:hover .dropdown-content_boards_index { + display: block; +} + +.inline-icon { + display: inline-block; + padding-right: 10px; +} + +.load-more-boards-container { + margin-top: 90px; + margin-left: auto; + margin-right: auto; + text-align: center; + z-index: 100; + position: relative; + .load-more-boards-button { + width: 71.5px; + height: auto; + -webkit-transition: -webkit-transform 0.45s; + transition: -webkit-transform 0.45s; + -o-transition: transform 0.45s; + transition: transform 0.45s; + transition: transform 0.45s, -webkit-transform 0.45s; + &.clicked { + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); + } + } +} + +.new_board_button { + max-width: 180px; + height: 40px; + margin-top: 40px; + background: #92b5fa; + font-size: 16px; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + padding-right: 10px; +} diff --git a/app/assets/stylesheets/users.scss b/app/assets/stylesheets/users.scss new file mode 100644 index 00000000..cd793241 --- /dev/null +++ b/app/assets/stylesheets/users.scss @@ -0,0 +1,18 @@ +// Place all the styles related to the Users controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ + +// .avatar-upload > input { +// display: none; +// } +// .image-upload > label { +// cursor:pointer; +// } + +// .delete-button { +// display: none; +// } + +// .avatar-delete > label { +// cursor: pointer; +// } diff --git a/app/channels/action_items_channel.rb b/app/channels/action_items_channel.rb new file mode 100644 index 00000000..b89de20c --- /dev/null +++ b/app/channels/action_items_channel.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class ActionItemsChannel < ApplicationCable::Channel + def subscribed + params.deep_transform_keys!(&:underscore) + board = Board.find_by(slug: params[:board_slug]) + + return reject unless board + + stream_for board + end + + def unsubscribed + stop_all_streams + end +end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index 8d6c2a1b..630f7658 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -2,5 +2,22 @@ module ApplicationCable class Connection < ActionCable::Connection::Base + identified_by :current_user + + def connect + self.current_user = find_verified_user + logger.add_tags 'ActionCable', current_user.id + end + + protected + + def find_verified_user + verified_user = User.find_by(id: cookies.signed['user.id']) + if verified_user && cookies.signed['user.expires_at'] > Time.now + verified_user + else + reject_unauthorized_connection + end + end end end diff --git a/app/channels/cards_channel.rb b/app/channels/cards_channel.rb new file mode 100644 index 00000000..465fba65 --- /dev/null +++ b/app/channels/cards_channel.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class CardsChannel < ApplicationCable::Channel + def subscribed + params.deep_transform_keys!(&:underscore) + board = Board.find_by(slug: params[:board_slug]) + + return reject unless board + + stream_for board + end + + def unsubscribed + stop_all_streams + end +end diff --git a/app/channels/comments_channel.rb b/app/channels/comments_channel.rb new file mode 100644 index 00000000..a546ec0a --- /dev/null +++ b/app/channels/comments_channel.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class CommentsChannel < ApplicationCable::Channel + def subscribed + params.deep_transform_keys!(&:underscore) + board = Board.find_by(slug: params[:board_slug]) + + return reject unless board + + stream_for board + end + + def unsubscribed + stop_all_streams + end +end diff --git a/app/channels/graphql_channel.rb b/app/channels/graphql_channel.rb new file mode 100644 index 00000000..500bcf1c --- /dev/null +++ b/app/channels/graphql_channel.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +class GraphqlChannel < ApplicationCable::Channel + def subscribed + @subscription_ids = [] + end + + def execute(data) + result = execute_query(data) + + payload = { + result: result.subscription? ? { data: nil } : result.to_h, + more: result.subscription? + } + + @subscription_ids << context[:subscription_id] if result.context[:subscription_id] + + transmit(payload) + end + + def unsubscribed + @subscription_ids.each { |sid| RetrospectiveSchema.subscriptions.delete_subscription(sid) } + end + + private + + def execute_query(data) + RetrospectiveSchema.execute( + query: data['query'], + context: context, + variables: data['variables'], + operation_name: data['operationName'] + ) + end + + def context + { + current_user_id: current_user&.id, + current_user: current_user, + channel: self + } + end +end diff --git a/app/channels/memberships_channel.rb b/app/channels/memberships_channel.rb new file mode 100644 index 00000000..25300165 --- /dev/null +++ b/app/channels/memberships_channel.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class MembershipsChannel < ApplicationCable::Channel + def subscribed + params.deep_transform_keys!(&:underscore) + board = Board.find_by(slug: params[:board_slug]) + + return reject unless board + + stream_for board + end + + def unsubscribed + stop_all_streams + end +end diff --git a/app/controllers/action_items_controller.rb b/app/controllers/action_items_controller.rb index 936f77e3..478307cd 100644 --- a/app/controllers/action_items_controller.rb +++ b/app/controllers/action_items_controller.rb @@ -9,12 +9,14 @@ class ActionItemsController < ApplicationController skip_verify_authorized only: :index - rescue_from ActionPolicy::Unauthorized do |ex| - redirect_to action_items_path, alert: ex.result.message - end - def index - @action_items = ActionItem.eager_load(:board).order(created_at: :asc) + @action_items = user_action_items.where(status: 'pending') + .eager_load(:board) + .order(times_moved: :desc, created_at: :asc) + + @action_items_resolved = user_action_items.where.not(status: 'pending') + .eager_load(:board) + .order(updated_at: :desc) end def close @@ -35,9 +37,15 @@ def complete def reopen if @action_item.reopen! - redirect_to action_items_path, notice: 'Action Item was successfully reopend' + redirect_to action_items_path, notice: 'Action Item was successfully reopened' else redirect_to action_items_path, alert: @action_item.errors.full_messages.join(', ') end end + + private + + def user_action_items + current_user&.action_items + end end diff --git a/app/controllers/api/action_items_controller.rb b/app/controllers/api/action_items_controller.rb deleted file mode 100644 index 3c55ac20..00000000 --- a/app/controllers/api/action_items_controller.rb +++ /dev/null @@ -1,68 +0,0 @@ -# frozen_string_literal: true - -module API - class ActionItemsController < API::ApplicationController - before_action :set_board, :set_action_item - before_action do - authorize! @action_item, context: { board: @board } - end - - def update - if @action_item.update(body: params.permit(:edited_body)[:edited_body]) - render json: { updated_body: @action_item.body }, status: :ok - else - render json: { error: @action_item.errors }, status: :bad_request - end - end - - def destroy - if @action_item.destroy - head :no_content - else - render json: { error: @action_item.errors.full_messages.join(',') }, status: :bad_request - end - end - - def move - if @action_item.move!(@board) - head :ok - else - render json: { error: @action_item.errors.full_messages.join(',') }, status: :bad_request - end - end - - def close - if @action_item.close! - head :ok - else - render json: { error: @action_item.errors.full_messages.join(',') }, status: :bad_request - end - end - - def complete - if @action_item.complete! - head :ok - else - render json: { error: @action_item.errors.full_messages.join(',') }, status: :bad_request - end - end - - def reopen - if @action_item.reopen! - head :ok - else - render json: { error: @action_item.errors.full_messages.join(',') }, status: :bad_request - end - end - - private - - def set_board - @board = Board.find_by!(slug: params[:board_slug]) - end - - def set_action_item - @action_item = ActionItem.find(params[:id]) - end - end -end diff --git a/app/controllers/api/application_controller.rb b/app/controllers/api/application_controller.rb deleted file mode 100644 index e8d1963e..00000000 --- a/app/controllers/api/application_controller.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module API - class ApplicationController < ActionController::Base - before_action :authenticate_user! - authorize :user, through: :current_or_guest_user - verify_authorized - - rescue_from ActionPolicy::Unauthorized do |ex| - render json: { error: ex.result.message }, status: :unauthorized - end - - private - - def current_or_guest_user - current_user || User.new - end - end -end diff --git a/app/controllers/api/boards_controller.rb b/app/controllers/api/boards_controller.rb deleted file mode 100644 index 8a9a61b4..00000000 --- a/app/controllers/api/boards_controller.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -module API - class BoardsController < API::ApplicationController - before_action :set_board - before_action do - authorize! @board - end - - def invite - users = Boards::FindUsersToInvite.new(board_params[:email], @board).call - if users.any? - result = Boards::InviteUsers.new(@board, users).call - render json: result.value!, each_serializer: MembershipSerializer - else - render json: { error: 'User was not found' }, status: 400 - end - end - - def suggestions - result = Boards::Suggestions.new(params[:autocomplete]).call - render json: result - end - - private - - def board_params - params.require(:board).permit(:email) - end - - def set_board - @board = Board.find_by!(slug: params[:slug]) - end - end -end diff --git a/app/controllers/api/cards_controller.rb b/app/controllers/api/cards_controller.rb deleted file mode 100644 index 461efbb7..00000000 --- a/app/controllers/api/cards_controller.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -module API - class CardsController < API::ApplicationController - before_action :set_board, :set_card - before_action do - authorize! @card - end - - def update - if @card.update(body: params.permit(:edited_body)[:edited_body]) - render json: { updated_body: @card.body }, status: :ok - else - render json: { error: @card.errors }, status: :bad_request - end - end - - def destroy - if @card.destroy - head :no_content - else - render json: { error: @card.errors.full_messages.join(',') }, status: :bad_request - end - end - - def like - if @card.like! - render json: { likes: @card.likes }, status: :ok - else - render json: { error: @card.errors.full_messages.join(',') }, status: :bad_request - end - end - - private - - def set_board - @board = Board.find_by!(slug: params[:board_slug]) - end - - def set_card - @card = Card.find(params[:id]) - end - end -end diff --git a/app/controllers/api/memberships_controller.rb b/app/controllers/api/memberships_controller.rb deleted file mode 100644 index ef1aa9dd..00000000 --- a/app/controllers/api/memberships_controller.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -module API - class MembershipsController < API::ApplicationController - before_action :set_board, :set_membership - authorize :membership, through: :current_membership - - before_action except: :destroy do - authorize! - end - - def index - members = @board.memberships - render json: members, each_serializer: MembershipSerializer - end - - def destroy - member = Membership.find(params[:id]) - authorize! member - if member.destroy - head :no_content - else - render json: { error: member.errors }, status: :bad_request - end - end - - def ready_status - render json: @membership.ready - end - - def ready_toggle - @membership.update(ready: !@membership.ready) - render json: @membership.ready - end - - private - - def membership_params - params.require(:membership).permit(:email) - end - - def set_board - @board = Board.find_by!(slug: params[:board_slug]) - end - - def set_membership - @membership = Membership.find_by(board_id: @board.id, user_id: current_user.id) - end - - def current_membership - @membership - end - end -end diff --git a/app/controllers/api/v1/action_items_controller.rb b/app/controllers/api/v1/action_items_controller.rb new file mode 100644 index 00000000..3d8ed6f3 --- /dev/null +++ b/app/controllers/api/v1/action_items_controller.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +module API + module V1 + class ActionItemsController < API::V1::BaseController + before_action only: %i[create update] do + params.deep_transform_keys!(&:underscore) + end + + before_action except: %i[update destroy] do + @board = Board.find_by!(slug: params[:board_slug]) + end + + before_action except: :create do + @action_item = ActionItem.find(params[:id]) + end + + # app/graphql/mutations/add_action_item_mutation.rb + def create + action_item = ActionItem.new(action_item_params.merge!(board: @board, + author: current_user)) + + authorize! action_item, context: { user: current_user, board: @board } + + if action_item.save + prepare_and_make_response(action_item, @board) + else + render_json_error(action_item.errors.full_messages) + end + end + + # app/graphql/mutations/update_action_item_mutation.rb + def update + authorize! @action_item, context: { user: current_user, board: @action_item.board } + + if @action_item.update(action_item_params) + prepare_and_make_response(@action_item, @action_item.board) + else + render_json_error(@action_item.errors.full_messages) + end + end + + # app/graphql/mutations/destroy_action_item_mutation.rb + def destroy + authorize! @action_item, context: { user: current_user, board: @action_item.board } + + if @action_item.destroy + prepare_and_make_response(@action_item, @action_item.board) + else + render_json_error(@action_item.errors.full_messages) + end + end + + # app/graphql/mutations/close_action_item_mutation.rb + def close + authorize! @action_item, context: { user: current_user, board: @board } + + if @action_item.close! + prepare_and_make_response(@action_item, @board) + else + render_json_error(@action_item.errors.full_messages) + end + end + + # app/graphql/mutations/complete_action_item_mutation.rb + def complete + authorize! @action_item, context: { user: current_user, board: @board } + + if @action_item.complete! + prepare_and_make_response(@action_item, @board) + else + render_json_error(@action_item.errors.full_messages) + end + end + + # app/graphql/mutations/reopen_action_item_mutation.rb + def reopen + authorize! @action_item, context: { user: current_user, board: @board } + + if @action_item.reopen! + prepare_and_make_response(@action_item, @board) + else + render_json_error(@action_item.errors.full_messages) + end + end + + # app/graphql/mutations/move_action_item_mutation.rb + def move + authorize! @action_item, context: { user: current_user, board: @board } + + if @action_item.move!(@board) + prepare_and_make_response(@action_item, @board) + else + render_json_error(@action_item.errors.full_messages) + end + end + + private + + def action_item_params + params.permit(:body, :assignee_id) + end + + def prepare_and_make_response(action_item, board) + payload = serialize_resource(action_item) + + ActionItemsChannel.broadcast_to(board, payload) + render json: payload + end + end + end +end diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb new file mode 100644 index 00000000..974c6a43 --- /dev/null +++ b/app/controllers/api/v1/base_controller.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module API + module V1 + class BaseController < ApplicationController + respond_to :json + + rescue_from ActiveRecord::RecordNotFound, with: :not_found + + def serialize_resource(resource, adapter = :json, key_transform = :camel_lower) + { + data: + ActiveModelSerializers::SerializableResource.new( + resource, adapter: adapter, key_transform: key_transform + ).as_json + }.to_json + end + + def render_json_error(message, status = :unprocessable_entity) + render json: { errors: { fullMessages: message } }, + status: status + end + + private + + def not_found + render json: { + errors: { + status: '404', + fullMessages: 'Not Found' + } + }, status: :not_found + end + end + end +end diff --git a/app/controllers/api/v1/boards_controller.rb b/app/controllers/api/v1/boards_controller.rb new file mode 100644 index 00000000..d9411187 --- /dev/null +++ b/app/controllers/api/v1/boards_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module API + module V1 + class BoardsController < API::V1::BaseController + # app/graphql/queries/boards.rb + def index + authorize! + + boards = Board.includes(:cards).limit(10) + + render json: serialize_resource(boards) + end + + # app/graphql/queries/board.rb + def show + board = Board.find(params[:id]) + + authorize! board + + render json: serialize_resource(board) + end + end + end +end diff --git a/app/controllers/api/v1/cards_controller.rb b/app/controllers/api/v1/cards_controller.rb new file mode 100644 index 00000000..286a5443 --- /dev/null +++ b/app/controllers/api/v1/cards_controller.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module API + module V1 + class CardsController < API::V1::BaseController + before_action only: %i[create update] do + params.deep_transform_keys!(&:underscore) + end + + before_action except: :create do + @card = Card.find(params[:id]) + authorize! @card + end + + # app/graphql/mutations/add_card_mutation.rb + def create + board = Board.find_by!(slug: params[:board_slug]) + authorize! board, to: :create_cards? + card = Card.new(card_params.merge!(board: board, author: current_user)) + + if card.save + prepare_and_make_response(card) + else + render_json_error(card.errors.full_messages) + end + end + + # app/graphql/mutations/update_card_mutation.rb + def update + if @card.update(card_params) + prepare_and_make_response(@card) + else + render_json_error(@card.errors.full_messages) + end + end + + # app/graphql/mutations/destroy_card_mutation.rb + def destroy + if @card.destroy + prepare_and_make_response(@card) + else + render_json_error(@card.errors.full_messages) + end + end + + # app/graphql/mutations/like_card_mutation.rb + def like + if @card.like! + prepare_and_make_response(@card) + else + render_json_error(@card.errors.full_messages) + end + end + + private + + def card_params + params.permit(:kind, :body) + end + + def prepare_and_make_response(card) + payload = serialize_resource(card) + + CardsChannel.broadcast_to(card.board, payload) + render json: payload + end + end + end +end diff --git a/app/controllers/api/v1/comments_controller.rb b/app/controllers/api/v1/comments_controller.rb new file mode 100644 index 00000000..3cd565cf --- /dev/null +++ b/app/controllers/api/v1/comments_controller.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module API + module V1 + class CommentsController < API::V1::BaseController + before_action only: %i[create update] do + params.deep_transform_keys!(&:underscore) + end + + before_action except: :create do + @comment = Comment.find(params[:id]) + authorize! @comment + end + + # app/graphql/mutations/add_comment_mutation.rb + def create + board = Board.find_by!(params[:id]) + authorize! board, to: :create_comments? + comment = Comment.new(comment_params.merge!(author: current_user)) + + if comment.save + prepare_and_make_response(comment) + else + render_json_error(@card.errors.full_messages) + end + end + + # app/graphql/mutations/update_comment_mutation.rb + def update + if @comment.update(comment_params) + prepare_and_make_response(@comment) + else + render_json_error(@card.errors.full_messages) + end + end + + # app/graphql/mutations/destroy_comment_mutation.rb + def destroy + if @comment.destroy + prepare_and_make_response(@comment) + else + render_json_error(@card.errors.full_messages) + end + end + + # app/graphql/mutations/like_comment_mutation.rb + def like + if @comment.like! + prepare_and_make_response(@comment) + else + render_json_error(@card.errors.full_messages) + end + end + + private + + def comment_params + params.permit(:card_id, :content) + end + + def prepare_and_make_response(comment) + payload = serialize_resource(comment) + + CommentsChannel.broadcast_to(comment.card.board, payload) + render json: payload + end + end + end +end diff --git a/app/controllers/api/v1/memberships_controller.rb b/app/controllers/api/v1/memberships_controller.rb new file mode 100644 index 00000000..0cddd234 --- /dev/null +++ b/app/controllers/api/v1/memberships_controller.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +module API + module V1 + class MembershipsController < API::V1::BaseController + before_action only: %i[index current create] do + @board = Board.find_by!(slug: params[:board_slug]) + end + + before_action only: %i[index current] do + @current_membership = Membership.find_by!(user_id: current_user.id, board_id: @board.id) + end + + before_action only: %i[destroy toggle_ready_status] do + @membership = Membership.find(params[:id]) + end + + skip_verify_authorized only: %i[index current] + + # app/graphql/queries/memberships.rb + def index + memberships = @board.memberships.includes(:user) + render json: serialize_resource(memberships) + end + + # app/graphql/queries/membership.rb + def current + membership = @board.memberships.find_by(user: current_user) + render json: serialize_resource(membership) + end + + # app/graphql/mutations/invite_members_mutation.rb + def create + authorize! @board, to: :invite?, context: { user: current_user } + + users = Boards::FindUsersToInvite.new(params[:email], @board).call + + if users.any? + result = Boards::InviteUsers.new(@board, users).call + prepare_and_make_response(result.value!, @board) + else + render_json_error('User was not found', :not_found) + end + end + + # app/graphql/mutations/destroy_membership_mutation.rb + # rubocop:disable Metrics/MethodLength + def destroy + authorize! @membership, + context: { membership: Membership.find_by(user: current_user, + board: @membership.board) } + + @membership.transaction do + @membership.board.board_permissions_users.where(user: @membership.user).destroy_all + @membership.destroy + end + + if !@membership.persisted? + prepare_and_make_response(@membership, @membership.board) + else + render_json_error(@membership.errors.full_messages) + end + end + # rubocop:enable Metrics/MethodLength + + # app/graphql/mutations/toggle_ready_status_mutation.rb + def toggle_ready_status + authorize! @membership, to: :ready_toggle?, + context: { membership: @membership } + + if @membership.update(ready: !@membership.ready) + prepare_and_make_response(@membership, @membership.board) + else + render_json_error(@membership.errors.full_messages) + end + end + + private + + def prepare_and_make_response(membership, board) + payload = serialize_resource(membership) + + MembershipsChannel.broadcast_to(board, payload) + render json: payload + end + end + end +end diff --git a/app/controllers/api/v1/users_controller.rb b/app/controllers/api/v1/users_controller.rb new file mode 100644 index 00000000..e3fb84ed --- /dev/null +++ b/app/controllers/api/v1/users_controller.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module API + module V1 + class UsersController < API::V1::BaseController + skip_verify_authorized only: :suggestions + + # app/graphql/queries/suggestions.rb + def suggestions + autocomplete = params[:autocomplete] + @suggestions = Boards::Suggestions.new(autocomplete).call + + render json: { + data: { + suggestions: @suggestions + } + } + end + end + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5e5e09c6..8e001ec7 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -2,10 +2,15 @@ class ApplicationController < ActionController::Base before_action :set_raven_context + before_action :store_user_location!, if: :storable_location? before_action :authenticate_user!, except: %i[sign_in sign_up] authorize :user, through: :current_or_guest_user verify_authorized unless: [:devise_controller?] + rescue_from ActionPolicy::Unauthorized do |ex| + redirect_to root_path, alert: ex.result.message + end + private def set_raven_context @@ -16,4 +21,18 @@ def set_raven_context def current_or_guest_user current_user || User.new end + + # Its important that the location is NOT stored if: + # - The request method is not GET (non idempotent) + # - The request is handled by a Devise controller such as Devise::SessionsController + # as that could cause an infinite redirect loop. + # - The request is an Ajax request as this can lead to very unexpected behaviour. + def storable_location? + request.get? && is_navigational_format? && !devise_controller? && !request.xhr? + end + + def store_user_location! + # :user is the scope we are authenticating + store_location_for(:user, request.fullpath) + end end diff --git a/app/controllers/boards/action_items_controller.rb b/app/controllers/boards/action_items_controller.rb deleted file mode 100644 index 20f1e694..00000000 --- a/app/controllers/boards/action_items_controller.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -module Boards - class ActionItemsController < Boards::ApplicationController - authorize :board, through: :current_board - - def create - action_item = @board.action_items.build(action_item_params) - authorize! action_item - if action_item.save - redirect_to @board, notice: 'Action Item was successfully saved' - else - redirect_to @board, alert: action_item.errors.full_messages.join(', ') - end - end - - private - - def current_board - @board - end - - def action_item_params - params.require(:action_item).permit(:status, :body) - end - end -end diff --git a/app/controllers/boards/application_controller.rb b/app/controllers/boards/application_controller.rb deleted file mode 100644 index 2d32f90b..00000000 --- a/app/controllers/boards/application_controller.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module Boards - class ApplicationController < ::ApplicationController - before_action :set_board - - rescue_from ActionPolicy::Unauthorized do |ex| - redirect_to @board, alert: ex.result.message - end - - private - - def set_board - @board = Board.find_by!(slug: params[:board_slug]) - end - end -end diff --git a/app/controllers/boards/cards_controller.rb b/app/controllers/boards/cards_controller.rb deleted file mode 100644 index 35e47935..00000000 --- a/app/controllers/boards/cards_controller.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module Boards - class CardsController < Boards::ApplicationController - def create - card = @board.cards.build(card_params.merge(author_id: current_user.id)) - authorize! card - card.save! - redirect_to @board - end - - private - - def card_params - params.require(:card).permit(:kind, :body) - end - end -end diff --git a/app/controllers/boards/memberships_controller.rb b/app/controllers/boards/memberships_controller.rb deleted file mode 100644 index c9b2e226..00000000 --- a/app/controllers/boards/memberships_controller.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -module Boards - class MembershipsController < Boards::ApplicationController - skip_verify_authorized - - def create - @membership = @board.memberships.build(user_id: current_user.id, role: :member) - if @membership.save - redirect_to @board, notice: "#{current_user.email.split('@').first} has joined the board!" - else - redirect_to @board, alert: @membership.errors.full_messages.join(', ') - end - end - end -end diff --git a/app/controllers/boards_controller.rb b/app/controllers/boards_controller.rb index a054a0c4..56b1bedb 100644 --- a/app/controllers/boards_controller.rb +++ b/app/controllers/boards_controller.rb @@ -1,34 +1,53 @@ # frozen_string_literal: true class BoardsController < ApplicationController - before_action :set_board, only: %i[show continue edit update destroy] + layout 'board', only: :show + + before_action :set_board, only: %i[show continue edit update destroy history] skip_before_action :authenticate_user!, only: :show skip_verify_authorized only: :show - rescue_from ActionPolicy::Unauthorized do |ex| - redirect_to boards_path, alert: ex.result.message + def my + authorize! + + @boards_by_date = boards_by_role(:creator) end - def index + def participating authorize! - @boards = current_user.boards.order(created_at: :desc) + + @boards_by_date = boards_by_role(:member) end - # rubocop: disable Metrics/AbcSize + def history + authorize! + + boards = Boards::GetHistoryOfBoard.new(@board.id).call + @boards_by_date = boards.order(created_at: :desc) + .group_by { |record| record.created_at.strftime('%B, %Y') } + end + + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/MethodLength + # rubocop:disable Metrics/LineLength def show - @cards_by_type = { - mad: @board.cards.mad.includes(:author), - sad: @board.cards.sad.includes(:author), - glad: @board.cards.glad.includes(:author) - } - @action_items = @board.action_items + authorize! @board + @cards_by_type = @board.column_names.map do |column| + [[column, ActiveModelSerializers::SerializableResource.new(@board.cards.where(kind: column) + .includes(:author, comments: [:author]).order(created_at: :desc)).as_json]].to_h + end.reduce({}, :merge).as_json + @action_items = ActiveModelSerializers::SerializableResource.new(@board.action_items.order(created_at: :desc)).as_json @action_item = ActionItem.new(board_id: @board.id) - + @board_creators = @board.memberships.where(role: 'creator').pluck(:user_id) @previous_action_items = if @board.previous_board&.action_items&.any? - @board.previous_board.action_items + ActiveModelSerializers::SerializableResource.new(@board.previous_board.action_items).as_json end + @users = ActiveModelSerializers::SerializableResource.new(User.find(@board.memberships.pluck(:user_id))).as_json + # @data = RetrospectiveSchema.execute(INITIAL__QUERY) end - # rubocop: enable Metrics/AbcSize + # rubocop:enable Metrics/LineLength + # rubocop:enable Metrics/MethodLength + # rubocop:enable Metrics/AbcSize def new authorize! @@ -44,17 +63,24 @@ def create @board = Board.new(board_params) @board.memberships.build(user_id: current_user.id, role: 'creator') - if @board.save + result = Boards::BuildPermissions.new(@board, current_user).call(identifiers_scope: 'creator') + + if result.success? && @board.save redirect_to @board, notice: 'Board was successfully created.' else + flash[:alert] = result.failure render :new end end def update authorize! @board + old_column_names = @board.column_names if @board.update(board_params) - redirect_to boards_path, notice: 'Board was successfully updated.' + result = Boards::RenameColumns.new(@board).call(old_column_names, @board.column_names) + if result.success? + redirect_to edit_board_path(@board), notice: 'Board was successfully updated.' + end else render :edit end @@ -63,9 +89,9 @@ def update def destroy authorize! @board if @board.destroy - redirect_to boards_path, notice: 'Board was successfully deleted.' + redirect_to my_boards_path, notice: 'Board was successfully deleted.' else - redirect_to boards_path, alert: @board.errors.full_messages.join(', ') + redirect_to my_boards_path, alert: @board.errors.full_messages.join(', ') end end @@ -75,17 +101,25 @@ def continue if result.success? redirect_to result.value!, notice: 'Board was successfully created.' else - redirect_to boards_path, alert: result.failure + redirect_to my_boards_path, alert: result.failure end end private def board_params - params.require(:board).permit(:title, :team_id, :email) + params.require(:board).permit(:title, :team_id, :email, :private, column_names: []) end def set_board @board = Board.find_by!(slug: params[:slug]) end + + def boards_by_role(role) + Board.where.not(id: Board.select(:previous_board_id).where.not(previous_board_id: nil)) + .joins(:memberships).where(memberships: { user_id: current_user.id, role: role }) + .includes(:users, :cards, :action_items) + .order(created_at: :desc) + .group_by { |record| record.created_at.strftime('%B, %Y') } + end end diff --git a/app/controllers/boardsql_controller.rb b/app/controllers/boardsql_controller.rb new file mode 100644 index 00000000..0ee804eb --- /dev/null +++ b/app/controllers/boardsql_controller.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class BoardsqlController < ApplicationController + skip_verify_authorized only: :show + + def show; end +end diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb new file mode 100644 index 00000000..e53bfeda --- /dev/null +++ b/app/controllers/graphql_controller.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +class GraphqlController < ActionController::Base + # 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, if: proc { |c| c.request.format == 'application/json' } + + # rubocop:disable Metrics/LineLength, Metrics/MethodLength + def execute + variables = ensure_hash(params[:variables]) + query = params[:query] + operation_name = params[:operationName] + context = { + current_user: current_user + } + result = RetrospectiveSchema.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 + # rubocop:enable Metrics/LineLength, Metrics/MethodLength + + private + + # Handle form data, JSON body, or a blank value + # rubocop:disable Metrics/MethodLength + def ensure_hash(ambiguous_param) + case ambiguous_param + when String + if ambiguous_param.present? + ensure_hash(JSON.parse(ambiguous_param)) + else + {} + end + when Hash, ActionController::Parameters + ambiguous_param + when nil + {} + else + raise ArgumentError, "Unexpected parameter: #{ambiguous_param}" + end + end + # rubocop:enable Metrics/MethodLength + + # rubocop:disable Metrics/LineLength + def handle_error_in_development(err) + logger.error err.message + logger.error err.backtrace.join("\n") + + render json: { error: { message: err.message, backtrace: err.backtrace }, data: {} }, status: 500 + end + # rubocop:enable Metrics/LineLength +end diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 7df82476..880a301f 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -2,19 +2,69 @@ module Users class OmniauthCallbacksController < Devise::OmniauthCallbacksController + def alfred + @user = User.from_omniauth(auth.provider, auth.uid, auth.info) + + if @user.valid? + sign_in_and_redirect @user, event: :authentication + set_flash_message(:notice, :success, kind: 'Alfred') if is_navigational_format? + else + session['devise.alfred_data'] = auth.except('extra') + redirect_to new_user_session_path, alert: @user.errors.full_messages.join("\n") + end + end + + def google + @user = User.from_omniauth(auth.provider, auth.uid, auth.info) + + if @user.valid? + sign_in_and_redirect @user, event: :authentication + set_flash_message(:notice, :success, kind: 'Google') if is_navigational_format? + else + session['devise.google_data'] = auth.except('extra') + redirect_to new_user_session_path, alert: @user.errors.full_messages.join("\n") + end + end + + def facebook + @user = User.from_omniauth(auth.provider, auth.uid, auth.info) + + if @user.valid? + sign_in_and_redirect @user, event: :authentication + set_flash_message(:notice, :success, kind: 'Facebook') if is_navigational_format? + else + session['devise.facebook_data'] = auth.except('extra') + redirect_to new_user_session_path, alert: @user.errors.full_messages.join("\n") + end + end + def github - @user = User.from_omniauth(request.env['omniauth.auth']) - if @user.persisted? + @user = User.from_omniauth(auth.provider, auth.uid, auth.info) + + if @user.valid? sign_in_and_redirect @user, event: :authentication - set_flash_message(:notice, :success, kind: 'GitHub') if is_navigational_format? + set_flash_message(:notice, :success, kind: 'Github') if is_navigational_format? else - session['devise.github_data'] = request.env['omniauth.auth'] - redirect_to new_user_registration_url + session['devise.github_data'] = auth.except('extra') + redirect_to new_user_session_path, alert: @user.errors.full_messages.join("\n") end end + def developer + @user = User.first + + sign_in_and_redirect @user, event: :authentication + set_flash_message(:notice, :success, kind: 'Developer') if is_navigational_format? + end + def failure - redirect_to root_path + redirect_to new_user_session_path + end + + private + + def auth + request.env['omniauth.auth'] end end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb new file mode 100644 index 00000000..b581c5cc --- /dev/null +++ b/app/controllers/users_controller.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class UsersController < ApplicationController + before_action :set_user + before_action do + authorize! @user + end + + def edit; end + + def update + @user.update(user_params) + render :edit + end + + def avatar_destroy + @user.save if @user.remove_avatar! + render :edit + end + + private + + def set_user + @user = User.find(params[:id]) + end + + def user_params + params.require(:user).permit(:nickname, :first_name, :last_name, :avatar) + end +end diff --git a/app/graphql/mutations/.keep b/app/graphql/mutations/.keep new file mode 100644 index 00000000..e69de29b diff --git a/app/graphql/mutations/add_action_item_mutation.rb b/app/graphql/mutations/add_action_item_mutation.rb new file mode 100644 index 00000000..0deebd7e --- /dev/null +++ b/app/graphql/mutations/add_action_item_mutation.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Mutations + class AddActionItemMutation < Mutations::BaseMutation + argument :attributes, Types::ActionItemAttributes, required: true + + field :action_item, Types::ActionItemType, null: true + + # rubocop:disable Metrics/MethodLength + def resolve(attributes:) + params = attributes.to_h + board = Board.find_by!(slug: params.delete(:board_slug)) + action_item = ActionItem.new(item_params(params, board)) + authorize! action_item, to: :create?, context: { user: context[:current_user], board: board } + if action_item.save + RetrospectiveSchema.subscriptions.trigger('action_item_added', { board_slug: board.slug }, + action_item) + { action_item: action_item } + else + { errors: { full_messages: action_item.errors.full_messages } } + end + end + # rubocop:enable Metrics/MethodLength + + def item_params(params, board) + params.merge(board: board, author: context[:current_user]) + end + end +end diff --git a/app/graphql/mutations/add_card_mutation.rb b/app/graphql/mutations/add_card_mutation.rb new file mode 100644 index 00000000..8dd17cc6 --- /dev/null +++ b/app/graphql/mutations/add_card_mutation.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Mutations + class AddCardMutation < Mutations::BaseMutation + argument :attributes, Types::CardAttributes, required: true + + field :card, Types::CardType, null: true + + # rubocop:disable Metrics/MethodLength + def resolve(attributes:) + params = attributes.to_h + board = Board.find_by!(slug: params.delete(:board_slug)) + authorize! board, to: :create_cards?, context: { user: context[:current_user], board: board } + + result = Boards::Cards::Create.new(current_user).call(card_params(params, board)) + + if result.success? + card = result.value! + RetrospectiveSchema.subscriptions.trigger('card_added', { board_slug: board.slug }, card) + { card: card } + else + { errors: { full_messages: result.failure.message } } + end + end + # rubocop:enable Metrics/MethodLength + + def card_params(params, board) + params.merge(board: board, author: context[:current_user]) + end + end +end diff --git a/app/graphql/mutations/add_comment_mutation.rb b/app/graphql/mutations/add_comment_mutation.rb new file mode 100644 index 00000000..5be13ec4 --- /dev/null +++ b/app/graphql/mutations/add_comment_mutation.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Mutations + class AddCommentMutation < Mutations::BaseMutation + argument :attributes, Types::CommentAttributes, required: true + + field :comment, Types::CommentType, null: true + + # rubocop:disable Metrics/MethodLength + # rubocop:disable Metrics/AbcSize + def resolve(attributes:) + params = attributes.to_h + card = Card.find(params[:card_id]) + authorize! card.board, to: :create_comments?, context: { user: context[:current_user] } + + result = Boards::Cards::Comments::Create.new(current_user).call(comment_params(params)) + + if result.success? + comment = result.value! + card = comment.card + RetrospectiveSchema.subscriptions.trigger('card_updated', + { board_slug: card.board.slug }, card) + { comment: comment } + else + { errors: { full_messages: result.failure.message } } + end + end + # rubocop:enable Metrics/MethodLength + # rubocop:enable Metrics/AbcSize + + def comment_params(params) + params.merge(author: context[:current_user]) + end + end +end diff --git a/app/graphql/mutations/base_mutation.rb b/app/graphql/mutations/base_mutation.rb new file mode 100644 index 00000000..412ba927 --- /dev/null +++ b/app/graphql/mutations/base_mutation.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Mutations + class BaseMutation < GraphQL::Schema::RelayClassicMutation + include ActionPolicy::GraphQL::Behaviour + argument_class Types::BaseArgument + field_class Types::BaseField + input_object_class Types::BaseInputObject + object_class Types::BaseObject + + field :errors, Types::ValidationErrorsType, null: true + end +end diff --git a/app/graphql/mutations/close_action_item_mutation.rb b/app/graphql/mutations/close_action_item_mutation.rb new file mode 100644 index 00000000..0097ce2c --- /dev/null +++ b/app/graphql/mutations/close_action_item_mutation.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Mutations + class CloseActionItemMutation < Mutations::BaseMutation + argument :id, ID, required: true + argument :board_slug, String, required: true + + field :action_item, Types::ActionItemType, null: true + + # rubocop:disable Metrics/MethodLength + def resolve(id:, board_slug:) + action_item = ActionItem.find(id) + board = Board.find_by!(slug: board_slug) + authorize! action_item, to: :close?, + context: { user: context[:current_user], + board: board } + + if action_item.close! + RetrospectiveSchema.subscriptions.trigger('action_item_updated', { board_slug: board.slug }, + action_item) + { action_item: action_item } + else + { errors: { full_messages: action_item.errors.full_messages } } + end + end + # rubocop:enable Metrics/MethodLength + end +end diff --git a/app/graphql/mutations/complete_action_item_mutation.rb b/app/graphql/mutations/complete_action_item_mutation.rb new file mode 100644 index 00000000..20f8ea0e --- /dev/null +++ b/app/graphql/mutations/complete_action_item_mutation.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Mutations + class CompleteActionItemMutation < Mutations::BaseMutation + argument :id, ID, required: true + argument :board_slug, String, required: true + + field :action_item, Types::ActionItemType, null: true + + # rubocop:disable Metrics/MethodLength + def resolve(id:, board_slug:) + action_item = ActionItem.find(id) + board = Board.find_by!(slug: board_slug) + authorize! action_item, to: :complete?, + context: { user: context[:current_user], board: board } + + if action_item.complete! + RetrospectiveSchema.subscriptions.trigger('action_item_updated', { board_slug: board.slug }, + action_item) + { action_item: action_item } + else + { errors: { full_messages: action_item.errors.full_messages } } + end + end + # rubocop:enable Metrics/MethodLength + end +end diff --git a/app/graphql/mutations/destroy_action_item_mutation.rb b/app/graphql/mutations/destroy_action_item_mutation.rb new file mode 100644 index 00000000..44f77739 --- /dev/null +++ b/app/graphql/mutations/destroy_action_item_mutation.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Mutations + class DestroyActionItemMutation < Mutations::BaseMutation + argument :id, ID, required: true + + field :id, Int, null: true + + # rubocop:disable Metrics/MethodLength + def resolve(id:) + action_item = ActionItem.find(id) + authorize! action_item, to: :destroy?, context: { user: context[:current_user], + board: action_item.board } + + if action_item.destroy + RetrospectiveSchema.subscriptions.trigger('action_item_destroyed', + { board_slug: action_item.board.slug }, + id: id) + { id: id } + else + { errors: { full_messages: action_item.errors.full_messages } } + end + end + # rubocop:enable Metrics/MethodLength + end +end diff --git a/app/graphql/mutations/destroy_card_mutation.rb b/app/graphql/mutations/destroy_card_mutation.rb new file mode 100644 index 00000000..7afca25f --- /dev/null +++ b/app/graphql/mutations/destroy_card_mutation.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Mutations + class DestroyCardMutation < Mutations::BaseMutation + argument :id, ID, required: true + + field :id, Int, null: false + def resolve(id:) + card = Card.find(id) + authorize! card, to: :destroy?, context: { user: context[:current_user] } + + if card.destroy + RetrospectiveSchema.subscriptions.trigger('card_destroyed', + { board_slug: card.board.slug }, + id: id, kind: card.kind) + { id: id } + else + { errors: { full_messages: card.errors.full_messages } } + end + end + end +end diff --git a/app/graphql/mutations/destroy_comment_mutation.rb b/app/graphql/mutations/destroy_comment_mutation.rb new file mode 100644 index 00000000..891bf9e6 --- /dev/null +++ b/app/graphql/mutations/destroy_comment_mutation.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Mutations + class DestroyCommentMutation < Mutations::BaseMutation + argument :id, ID, required: true + + field :id, Int, null: false + def resolve(id:) + comment = Comment.find(id) + authorize! comment, to: :destroy?, context: { user: context[:current_user] } + card = comment.card + if comment.destroy + RetrospectiveSchema.subscriptions.trigger('card_updated', + { board_slug: card.board.slug }, card) + { id: id } + else + { errors: { full_messages: comment.errors.full_messages } } + end + end + end +end diff --git a/app/graphql/mutations/destroy_membership_mutation.rb b/app/graphql/mutations/destroy_membership_mutation.rb new file mode 100644 index 00000000..7dba9994 --- /dev/null +++ b/app/graphql/mutations/destroy_membership_mutation.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Mutations + class DestroyMembershipMutation < Mutations::BaseMutation + argument :id, ID, required: true + + field :id, Int, null: true + + # rubocop:disable Metrics/MethodLength + # rubocop:disable Metrics/AbcSize + def resolve(id:) + membership = Membership.find(id) + authorize! membership, to: :destroy?, + context: { membership: Membership.find_by(user: context[:current_user], + board: membership.board) } + + membership.transaction do + membership.board.board_permissions_users.where(user: membership.user).destroy_all + membership.destroy + end + + if !membership.persisted? + RetrospectiveSchema.subscriptions.trigger('membership_destroyed', + { board_slug: membership.board.slug }, + id: id) + { id: id } + else + { errors: { full_messages: membership.errors.full_messages } } + end + end + # rubocop:enable Metrics/MethodLength + # rubocop:enable Metrics/AbcSize + end +end diff --git a/app/graphql/mutations/invite_members_mutation.rb b/app/graphql/mutations/invite_members_mutation.rb new file mode 100644 index 00000000..4918fbd4 --- /dev/null +++ b/app/graphql/mutations/invite_members_mutation.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Mutations + class InviteMembersMutation < Mutations::BaseMutation + argument :email, String, required: true + argument :board_slug, String, required: true + + field :memberships, [Types::MembershipType], null: true + + # rubocop:disable Metrics/MethodLength + def resolve(email:, board_slug:) + board = Board.find_by!(slug: board_slug) + authorize! board, to: :invite?, context: { user: context[:current_user] } + + users = Boards::FindUsersToInvite.new(email, board).call + if users.any? + result = Boards::InviteUsers.new(board, users).call + memberships = result.value! + RetrospectiveSchema.subscriptions.trigger('membership_list_updated', + { board_slug: board.slug }, + memberships) + { memberships: memberships } + else + { errors: + { full_messages: ['User was not found'] } } + end + end + # rubocop:enable Metrics/MethodLength + end +end diff --git a/app/graphql/mutations/like_card_mutation.rb b/app/graphql/mutations/like_card_mutation.rb new file mode 100644 index 00000000..17262d3c --- /dev/null +++ b/app/graphql/mutations/like_card_mutation.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Mutations + class LikeCardMutation < Mutations::BaseMutation + argument :id, ID, required: true + + field :card, Types::CardType, null: true + def resolve(id:) + card = Card.find(id) + authorize! card, to: :like?, context: { user: context[:current_user] } + + if card.like! + RetrospectiveSchema.subscriptions.trigger('card_updated', + { board_slug: card.board.slug }, + card) + { card: card } + else + { errors: { full_messages: card.errors.full_messages } } + end + end + end +end diff --git a/app/graphql/mutations/like_comment_mutation.rb b/app/graphql/mutations/like_comment_mutation.rb new file mode 100644 index 00000000..97e445e2 --- /dev/null +++ b/app/graphql/mutations/like_comment_mutation.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Mutations + class LikeCommentMutation < Mutations::BaseMutation + argument :id, ID, required: true + + field :comment, Types::CommentType, null: true + + def resolve(id:) + comment = Comment.find(id) + authorize! comment, to: :like?, context: { user: context[:current_user] } + if comment.like! + RetrospectiveSchema.subscriptions.trigger('card_updated', + { board_slug: comment.card.board.slug }, + comment.card) + { comment: comment } + else + { errors: { full_messages: comment.errors.full_messages } } + end + end + end +end diff --git a/app/graphql/mutations/move_action_item_mutation.rb b/app/graphql/mutations/move_action_item_mutation.rb new file mode 100644 index 00000000..d9ffbddb --- /dev/null +++ b/app/graphql/mutations/move_action_item_mutation.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Mutations + class MoveActionItemMutation < Mutations::BaseMutation + argument :id, ID, required: true + argument :board_slug, String, required: true + + field :action_item, Types::ActionItemType, null: true + + # rubocop:disable Metrics/MethodLength + def resolve(id:, board_slug:) + action_item = ActionItem.find(id) + board = Board.find_by!(slug: board_slug) + authorize! action_item, to: :move?, context: { user: context[:current_user], + board: board } + + if action_item.move!(board) + RetrospectiveSchema.subscriptions.trigger('action_item_moved', { board_slug: board.slug }, + action_item) + { action_item: action_item } + else + { errors: { full_messages: action_item.errors.full_messages } } + end + end + # rubocop:enable Metrics/MethodLength + end +end diff --git a/app/graphql/mutations/reopen_action_item_mutation.rb b/app/graphql/mutations/reopen_action_item_mutation.rb new file mode 100644 index 00000000..2088b936 --- /dev/null +++ b/app/graphql/mutations/reopen_action_item_mutation.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Mutations + class ReopenActionItemMutation < Mutations::BaseMutation + argument :id, ID, required: true + argument :board_slug, String, required: true + + field :action_item, Types::ActionItemType, null: true + + # rubocop:disable Metrics/MethodLength + def resolve(id:, board_slug:) + action_item = ActionItem.find(id) + board = Board.find_by!(slug: board_slug) + authorize! action_item, to: :reopen?, context: { user: context[:current_user], + board: board } + + if action_item.reopen! + RetrospectiveSchema.subscriptions.trigger('action_item_updated', { board_slug: board.slug }, + action_item) + { action_item: action_item } + else + { errors: { full_messages: action_item.errors.full_messages } } + end + end + # rubocop:enable Metrics/MethodLength + end +end diff --git a/app/graphql/mutations/toggle_ready_status_mutation.rb b/app/graphql/mutations/toggle_ready_status_mutation.rb new file mode 100644 index 00000000..4a9f1fd7 --- /dev/null +++ b/app/graphql/mutations/toggle_ready_status_mutation.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Mutations + class ToggleReadyStatusMutation < Mutations::BaseMutation + argument :id, ID, required: true + + field :membership, Types::MembershipType, null: true + # rubocop:disable Metrics/MethodLength + def resolve(id:) + membership = Membership.find(id) + authorize! membership, to: :ready_toggle?, + context: { membership: membership } + + if membership.update(ready: !membership.ready) + RetrospectiveSchema.subscriptions.trigger('membership_updated', + { board_slug: membership.board.slug }, + membership) + { membership: membership } + else + { errors: { full_messages: membership.errors.full_messages } } + end + end + # rubocop:enable Metrics/MethodLength + end +end diff --git a/app/graphql/mutations/update_action_item_mutation.rb b/app/graphql/mutations/update_action_item_mutation.rb new file mode 100644 index 00000000..8eae4e88 --- /dev/null +++ b/app/graphql/mutations/update_action_item_mutation.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Mutations + class UpdateActionItemMutation < Mutations::BaseMutation + argument :id, ID, required: true + argument :attributes, Types::ActionItemAttributes, required: true + + field :action_item, Types::ActionItemType, null: true + + # rubocop:disable Metrics/MethodLength + def resolve(id:, attributes:) + action_item = ActionItem.find(id) + + authorize! action_item, to: :update?, context: { user: context[:current_user], + board: action_item.board } + + if action_item.update(attributes.to_h) + RetrospectiveSchema.subscriptions.trigger('action_item_updated', + { board_slug: action_item.board.slug }, + action_item) + { action_item: action_item } + else + { errors: action_item.errors } + end + end + # rubocop:enable Metrics/MethodLength + end +end diff --git a/app/graphql/mutations/update_card_mutation.rb b/app/graphql/mutations/update_card_mutation.rb new file mode 100644 index 00000000..2aafc2e8 --- /dev/null +++ b/app/graphql/mutations/update_card_mutation.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Mutations + class UpdateCardMutation < Mutations::BaseMutation + argument :id, ID, required: true + argument :attributes, Types::CardAttributes, required: true + + field :card, Types::CardType, null: true + def resolve(id:, attributes:) + card = Card.find(id) + authorize! card, to: :update?, context: { user: context[:current_user] } + + if card.update(attributes.to_h) + RetrospectiveSchema.subscriptions.trigger('card_updated', + { board_slug: card.board.slug }, card) + { card: card } + else + { errors: { full_messages: card.errors.full_messages } } + end + end + end +end diff --git a/app/graphql/mutations/update_comment_mutation.rb b/app/graphql/mutations/update_comment_mutation.rb new file mode 100644 index 00000000..e81a658c --- /dev/null +++ b/app/graphql/mutations/update_comment_mutation.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Mutations + class UpdateCommentMutation < Mutations::BaseMutation + argument :id, ID, required: true + argument :attributes, Types::CommentAttributes, required: true + + field :comment, Types::CommentType, null: true + def resolve(id:, attributes:) + comment = Comment.find(id) + + authorize! comment, to: :update?, context: { user: context[:current_user] } + + if comment.update(attributes.to_h) + card = comment.card + RetrospectiveSchema.subscriptions.trigger('card_updated', + { board_slug: card.board.slug }, card) + { comment: comment } + else + { errors: { full_messages: comment.errors.full_messages } } + end + end + end +end diff --git a/app/graphql/queries/base_query.rb b/app/graphql/queries/base_query.rb new file mode 100644 index 00000000..2bda3c86 --- /dev/null +++ b/app/graphql/queries/base_query.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Queries + class BaseQuery < GraphQL::Schema::Resolver + include ActionPolicy::GraphQL::Behaviour + end +end diff --git a/app/graphql/queries/board.rb b/app/graphql/queries/board.rb new file mode 100644 index 00000000..c49f03f6 --- /dev/null +++ b/app/graphql/queries/board.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Queries + class Board < Queries::BaseQuery + description 'Returns board by id' + + argument :id, ID, required: true + + type Types::BoardType, null: false + + def resolve(id:) + ::Board.find(id) + end + end +end diff --git a/app/graphql/queries/boards.rb b/app/graphql/queries/boards.rb new file mode 100644 index 00000000..ea1e51e1 --- /dev/null +++ b/app/graphql/queries/boards.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Queries + class Boards < Queries::BaseQuery + description 'Returns a list of boards' + + type [Types::BoardType], null: false + + def resolve + ::Board.limit(10) + end + end +end diff --git a/app/graphql/queries/membership.rb b/app/graphql/queries/membership.rb new file mode 100644 index 00000000..caa88ebd --- /dev/null +++ b/app/graphql/queries/membership.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Queries + class Membership < Queries::BaseQuery + description 'Returns membership by id' + + argument :board_slug, String, required: true + + type Types::MembershipType, null: false + + def resolve(board_slug:) + ::Board.find_by!(slug: board_slug).memberships.find_by(user: context[:current_user]) + end + end +end diff --git a/app/graphql/queries/memberships.rb b/app/graphql/queries/memberships.rb new file mode 100644 index 00000000..3087207f --- /dev/null +++ b/app/graphql/queries/memberships.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Queries + class Memberships < Queries::BaseQuery + description 'Returns memberships by board slug' + + argument :board_slug, String, required: true + + type [Types::MembershipType], null: false + + def resolve(board_slug:) + ::Board.find_by!(slug: board_slug).memberships.includes([:user]) + end + end +end diff --git a/app/graphql/queries/suggestions.rb b/app/graphql/queries/suggestions.rb new file mode 100644 index 00000000..ba8eea56 --- /dev/null +++ b/app/graphql/queries/suggestions.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Queries + class Suggestions < Queries::BaseQuery + description 'Returns suggestions for given input' + + argument :autocomplete, String, required: true + + type Types::SuggestionType, null: false + + def resolve(autocomplete:) + ::Boards::Suggestions.new(autocomplete).call + end + end +end diff --git a/app/graphql/retrospective_schema.rb b/app/graphql/retrospective_schema.rb new file mode 100644 index 00000000..114329fb --- /dev/null +++ b/app/graphql/retrospective_schema.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class RetrospectiveSchema < GraphQL::Schema + use GraphQL::Subscriptions::ActionCableSubscriptions, redis: Redis.new + + mutation(Types::MutationType) + query(Types::QueryType) + subscription(Types::SubscriptionType) + + rescue_from(ActionPolicy::Unauthorized) do |exp| + raise GraphQL::ExecutionError.new( + # use result.message (backed by i18n) as an error message + exp.result.message, + # use GraphQL error extensions to provide more context + extensions: { + code: :unauthorized, + fullMessages: exp.result.reasons.full_messages, + details: exp.result.reasons.details + } + ) + end +end diff --git a/app/graphql/types/.keep b/app/graphql/types/.keep new file mode 100644 index 00000000..e69de29b diff --git a/app/graphql/types/action_item_attributes.rb b/app/graphql/types/action_item_attributes.rb new file mode 100644 index 00000000..c7eab3eb --- /dev/null +++ b/app/graphql/types/action_item_attributes.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Types + class ActionItemAttributes < Types::BaseInputObject + description 'Attributes for creating or updating action item' + + argument :status, String, required: false + argument :body, String, required: false + argument :assignee_id, ID, required: false + argument :board_slug, String, required: false + end +end diff --git a/app/graphql/types/action_item_type.rb b/app/graphql/types/action_item_type.rb new file mode 100644 index 00000000..d7eace63 --- /dev/null +++ b/app/graphql/types/action_item_type.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Types + class ActionItemType < Types::BaseObject + field :id, Int, null: false + field :body, String, null: false + field :times_moved, Int, null: false, camelize: false + field :assignee, Types::UserType, null: true + field :assignee_avatar_url, String, null: true, camelize: false + field :status, String, null: true + field :author, Types::UserType, null: false + + def assignee_avatar_url + object.assignee.avatar.thumb.url if object.assignee + end + end +end diff --git a/app/graphql/types/avatar_type.rb b/app/graphql/types/avatar_type.rb new file mode 100644 index 00000000..bd79572e --- /dev/null +++ b/app/graphql/types/avatar_type.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Types + class AvatarType < Types::BaseObject + field :thumb, Types::ThumbType, null: true + end +end diff --git a/app/graphql/types/base_argument.rb b/app/graphql/types/base_argument.rb new file mode 100644 index 00000000..2e2278c5 --- /dev/null +++ b/app/graphql/types/base_argument.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module Types + class BaseArgument < GraphQL::Schema::Argument + end +end diff --git a/app/graphql/types/base_enum.rb b/app/graphql/types/base_enum.rb new file mode 100644 index 00000000..cf43fea4 --- /dev/null +++ b/app/graphql/types/base_enum.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module Types + class BaseEnum < GraphQL::Schema::Enum + end +end diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb new file mode 100644 index 00000000..611eb056 --- /dev/null +++ b/app/graphql/types/base_field.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Types + class BaseField < GraphQL::Schema::Field + argument_class Types::BaseArgument + end +end diff --git a/app/graphql/types/base_input_object.rb b/app/graphql/types/base_input_object.rb new file mode 100644 index 00000000..27951132 --- /dev/null +++ b/app/graphql/types/base_input_object.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Types + class BaseInputObject < GraphQL::Schema::InputObject + argument_class Types::BaseArgument + end +end diff --git a/app/graphql/types/base_interface.rb b/app/graphql/types/base_interface.rb new file mode 100644 index 00000000..256779bd --- /dev/null +++ b/app/graphql/types/base_interface.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Types + module BaseInterface + include GraphQL::Schema::Interface + + field_class Types::BaseField + end +end diff --git a/app/graphql/types/base_object.rb b/app/graphql/types/base_object.rb new file mode 100644 index 00000000..421cabc6 --- /dev/null +++ b/app/graphql/types/base_object.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Types + class BaseObject < GraphQL::Schema::Object + include ActionPolicy::GraphQL::Behaviour + field_class Types::BaseField + end +end diff --git a/app/graphql/types/base_scalar.rb b/app/graphql/types/base_scalar.rb new file mode 100644 index 00000000..719bc808 --- /dev/null +++ b/app/graphql/types/base_scalar.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module Types + class BaseScalar < GraphQL::Schema::Scalar + end +end diff --git a/app/graphql/types/base_union.rb b/app/graphql/types/base_union.rb new file mode 100644 index 00000000..30a5668c --- /dev/null +++ b/app/graphql/types/base_union.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module Types + class BaseUnion < GraphQL::Schema::Union + end +end diff --git a/app/graphql/types/board_type.rb b/app/graphql/types/board_type.rb new file mode 100644 index 00000000..00a320bf --- /dev/null +++ b/app/graphql/types/board_type.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Types + class BoardType < Types::BaseObject + field :id, ID, null: false + field :title, String, null: false + field :slug, String, null: false + field :cards, [Types::CardType], null: false + field :created_at, GraphQL::Types::ISO8601DateTime, null: false + field :updated_at, GraphQL::Types::ISO8601DateTime, null: false + end +end diff --git a/app/graphql/types/card_attributes.rb b/app/graphql/types/card_attributes.rb new file mode 100644 index 00000000..5695e61a --- /dev/null +++ b/app/graphql/types/card_attributes.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + class CardAttributes < Types::BaseInputObject + description 'Attributes for creating or updating card' + + argument :kind, String, required: false + argument :body, String, required: false + argument :board_slug, String, required: false + end +end diff --git a/app/graphql/types/card_type.rb b/app/graphql/types/card_type.rb new file mode 100644 index 00000000..56c5ed7f --- /dev/null +++ b/app/graphql/types/card_type.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Types + class CardType < Types::BaseObject + field :id, Int, null: false + field :kind, String, null: false + field :body, String, null: false + field :likes, Int, null: false + field :author_id, ID, null: false + field :author, Types::UserType, null: false + field :board_id, ID, null: false + field :created_at, GraphQL::Types::ISO8601DateTime, null: false + field :updated_at, GraphQL::Types::ISO8601DateTime, null: false + field :comments, [Types::CommentType], null: false + end +end diff --git a/app/graphql/types/comment_attributes.rb b/app/graphql/types/comment_attributes.rb new file mode 100644 index 00000000..205ae1fb --- /dev/null +++ b/app/graphql/types/comment_attributes.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Types + class CommentAttributes < Types::BaseInputObject + description 'Attributes for creating or updating comment' + + argument :content, String, required: false + argument :card_id, ID, required: false + end +end diff --git a/app/graphql/types/comment_type.rb b/app/graphql/types/comment_type.rb new file mode 100644 index 00000000..9cd1dc05 --- /dev/null +++ b/app/graphql/types/comment_type.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + class CommentType < Types::BaseObject + field :id, Int, null: false + field :author, Types::UserType, null: false + field :card_id, Int, null: false + field :content, String, null: false + field :likes, Int, null: false + end +end diff --git a/app/graphql/types/deleted_card_type.rb b/app/graphql/types/deleted_card_type.rb new file mode 100644 index 00000000..ddec2da1 --- /dev/null +++ b/app/graphql/types/deleted_card_type.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Types + class DeletedCardType < Types::BaseObject + field :id, Int, null: false + field :kind, String, null: false + end +end diff --git a/app/graphql/types/membership_type.rb b/app/graphql/types/membership_type.rb new file mode 100644 index 00000000..44c3ef6e --- /dev/null +++ b/app/graphql/types/membership_type.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + class MembershipType < Types::BaseObject + field :id, Int, null: false + field :role, String, null: false + field :ready, Boolean, null: false + field :user, Types::UserType, null: false + field :board, Types::BoardType, null: false + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb new file mode 100644 index 00000000..6ef90cf5 --- /dev/null +++ b/app/graphql/types/mutation_type.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Types + class MutationType < Types::BaseObject + field :add_action_item, mutation: Mutations::AddActionItemMutation + field :add_card, mutation: Mutations::AddCardMutation + field :add_comment, mutation: Mutations::AddCommentMutation + field :close_action_item, mutation: Mutations::CloseActionItemMutation + field :complete_action_item, mutation: Mutations::CompleteActionItemMutation + field :destroy_action_item, mutation: Mutations::DestroyActionItemMutation + field :destroy_card, mutation: Mutations::DestroyCardMutation + field :destroy_membership, mutation: Mutations::DestroyMembershipMutation + field :destroy_comment, mutation: Mutations::DestroyCommentMutation + field :invite_members, mutation: Mutations::InviteMembersMutation + field :like_card, mutation: Mutations::LikeCardMutation + field :like_comment, mutation: Mutations::LikeCommentMutation + field :move_action_item, mutation: Mutations::MoveActionItemMutation + field :reopen_action_item, mutation: Mutations::ReopenActionItemMutation + field :toggle_ready_status, mutation: Mutations::ToggleReadyStatusMutation + field :update_action_item, mutation: Mutations::UpdateActionItemMutation + field :update_card, mutation: Mutations::UpdateCardMutation + field :update_comment, mutation: Mutations::UpdateCommentMutation + end +end diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb new file mode 100644 index 00000000..672ac8d8 --- /dev/null +++ b/app/graphql/types/query_type.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + class QueryType < Types::BaseObject + field :boards, resolver: Queries::Boards + field :board, resolver: Queries::Board + field :membership, resolver: Queries::Membership + field :memberships, resolver: Queries::Memberships + field :suggestions, resolver: Queries::Suggestions + end +end diff --git a/app/graphql/types/subscription_type.rb b/app/graphql/types/subscription_type.rb new file mode 100644 index 00000000..1c0ec25b --- /dev/null +++ b/app/graphql/types/subscription_type.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Types + class SubscriptionType < BaseObject + # rubocop:disable Metrics/LineLength + field :action_item_added, Types::ActionItemType, null: false, description: 'An action item was added' do + argument :boardSlug, String, required: true + end + + field :action_item_moved, Types::ActionItemType, null: false, description: 'An action item was moved' do + argument :boardSlug, String, required: true + end + + field :action_item_destroyed, Types::ActionItemType, null: false, description: 'An action item was destroyed' do + argument :boardSlug, String, required: true + end + + field :action_item_updated, Types::ActionItemType, null: false, description: 'An action item was updated' do + argument :boardSlug, String, required: true + end + + field :card_added, Types::CardType, null: false, description: 'A card was added' do + argument :boardSlug, String, required: true + end + field :card_updated, Types::CardType, null: false, description: 'A card wad updated' do + argument :boardSlug, String, required: true + end + + field :card_destroyed, Types::DeletedCardType, null: false, description: 'A card wad destroyed' do + argument :boardSlug, String, required: true + end + + field :membership_destroyed, Types::MembershipType, null: false, description: 'A membership record was destroyed' do + argument :boardSlug, String, required: true + end + + field :membership_list_updated, [Types::MembershipType], null: false, description: 'List of board members was updated' do + argument :boardSlug, String, required: true + end + + field :membership_updated, Types::MembershipType, null: false, description: 'A membership record was updated' do + argument :boardSlug, String, required: true + end + + # rubocop:enable Metrics/LineLength + + def action_item_added(*); end + + def action_item_moved(*); end + + def action_item_destroyed(*); end + + def action_item_updated(*); end + + def card_added(*); end + + def card_updated(*); end + + def card_destroyed(*); end + + def membership_destroyed(*); end + + def membership_list_updated(*); end + + def membership_updated(*); end + end +end diff --git a/app/graphql/types/suggestion_type.rb b/app/graphql/types/suggestion_type.rb new file mode 100644 index 00000000..430ee4ab --- /dev/null +++ b/app/graphql/types/suggestion_type.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Types + class SuggestionType < Types::BaseObject + field :users, [String], null: false + field :teams, [String], null: false + end +end diff --git a/app/graphql/types/thumb_type.rb b/app/graphql/types/thumb_type.rb new file mode 100644 index 00000000..0996ed34 --- /dev/null +++ b/app/graphql/types/thumb_type.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Types + class ThumbType < Types::BaseObject + field :url, String, null: true + end +end diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb new file mode 100644 index 00000000..3adea17d --- /dev/null +++ b/app/graphql/types/user_type.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Types + class UserType < Types::BaseObject + field :id, ID, null: false + field :email, String, null: false + field :nickname, String, null: true + field :last_name, String, null: true + field :first_name, String, null: true + field :avatar, Types::AvatarType, null: true + field :created_at, GraphQL::Types::ISO8601DateTime, null: false + field :updated_at, GraphQL::Types::ISO8601DateTime, null: false + end +end diff --git a/app/graphql/types/validation_errors_type.rb b/app/graphql/types/validation_errors_type.rb new file mode 100644 index 00000000..937ea530 --- /dev/null +++ b/app/graphql/types/validation_errors_type.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Types + class ValidationErrorsType < Types::BaseObject + field :details, String, null: false + field :full_messages, [String], null: false + + def details + object.details.to_json + end + end +end diff --git a/app/helpers/authorization_helper.rb b/app/helpers/authorization_helper.rb new file mode 100644 index 00000000..776ccb18 --- /dev/null +++ b/app/helpers/authorization_helper.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module AuthorizationHelper + def providers + names = %w[alfred facebook google github] + + names.select { |name| ENV["#{name.upcase}_KEY"].present? } + end +end diff --git a/app/javascript/channels/consumer.js b/app/javascript/channels/consumer.js new file mode 100644 index 00000000..3557bff6 --- /dev/null +++ b/app/javascript/channels/consumer.js @@ -0,0 +1,6 @@ +// Action Cable provides the framework to deal with WebSockets in Rails. +// You can generate new channels where WebSocket features live using the `rails generate channel` command. + +import {createConsumer} from '@rails/actioncable'; + +export default createConsumer(); diff --git a/app/javascript/channels/index.js b/app/javascript/channels/index.js new file mode 100644 index 00000000..298a304d --- /dev/null +++ b/app/javascript/channels/index.js @@ -0,0 +1,5 @@ +// Load all the channels within this directory and all subdirectories. +// Channel files must be named *_channel.js. + +const channels = require.context('.', true, /_channel\.js$/); +channels.keys().forEach(channels); diff --git a/app/javascript/components/ActionItem/ActionItem.css b/app/javascript/components/ActionItem/ActionItem.css deleted file mode 100644 index 10687090..00000000 --- a/app/javascript/components/ActionItem/ActionItem.css +++ /dev/null @@ -1,9 +0,0 @@ -.box { margin-bottom: 1.5rem; } - -.green_font {color: hsl(141, 71%, 48%)} -.yellow_font {color: hsl(48, 100%, 67%)} -.red_font {color: hsl(348, 100%, 61%)} - -.green_bg {background-color: hsl(141, 71%, 48%)} -.yellow_bg {background-color: hsl(48, 100%, 67%)} -.red_bg {background-color: hsl(348, 100%, 61%)} diff --git a/app/javascript/components/ActionItem/ActionItemBody/ActionItemBody.css b/app/javascript/components/ActionItem/ActionItemBody/ActionItemBody.css deleted file mode 100644 index 350b138e..00000000 --- a/app/javascript/components/ActionItem/ActionItemBody/ActionItemBody.css +++ /dev/null @@ -1,11 +0,0 @@ -textarea { - overflow: hidden; - resize: none; - width: 100%; - font-size: 1rem; - font-weight: 400; - line-height: 1.5; - color: hsl(0, 0%, 29%); -} - -.text { overflow-wrap: break-word; } diff --git a/app/javascript/components/ActionItem/ActionItemBody/index.js b/app/javascript/components/ActionItem/ActionItemBody/index.js deleted file mode 100644 index eef92357..00000000 --- a/app/javascript/components/ActionItem/ActionItemBody/index.js +++ /dev/null @@ -1,80 +0,0 @@ -import React from "react" -import Textarea from "react-textarea-autosize" -import "./ActionItemBody.css" - -class ActionItemBody extends React.Component { - constructor(props) { - super(props); - this.state = { - dbValue: this.props.body, - inputValue: this.props.body, - editMode : false - }; - } - - editModeToggle = () => { - this.setState({editMode: !this.state.editMode}); - } - - handleChange = (e) => { - this.setState({inputValue: e.target.value}); - } - - handleKeyPress = (e) => { - if(e.key === 'Enter'){ - this.editModeToggle() - this.submitRequest() - e.preventDefault() - } - } - - submitRequest() { - fetch(`/api/${window.location.pathname}/action_items/${this.props.id}`, { - method: 'PATCH', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'X-CSRF-Token': document.querySelector("meta[name='csrf-token']").getAttribute('content') - }, - body: JSON.stringify({ - edited_body: this.state.inputValue - }) - }).then((result) => { - if (result.status == 200) { - result.json() - .then((resultHash) => { - this.setState({dbValue: resultHash.updated_body}); - }) - } - else { - this.setState({inputValue: this.state.dbValue}); - throw result - } - }).catch((error) => { - error.json() - .then((errorHash) => { - console.log(errorHash.error) - }) - }); - } - - render () { - const { inputValue, editMode } = this.state; - const { editable } = this.props; - - return ( -
- - -