Skip to content

Commit

Permalink
Build endpoints and UI for Moment Templates
Browse files Browse the repository at this point in the history
  • Loading branch information
julianguyen committed Apr 13, 2021
1 parent 4c9987b commit 8fc802b
Show file tree
Hide file tree
Showing 28 changed files with 836 additions and 31 deletions.
4 changes: 4 additions & 0 deletions app/assets/stylesheets/dashboard/dashboard_section.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
&Right {
margin-left: auto;
padding-left: $size-20;

button {
font-weight: bold;
}
}

@media screen and (max-width: $small) {
Expand Down
18 changes: 18 additions & 0 deletions app/controllers/concerns/moment_templates_concern.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true
module MomentTemplatesConcern
extend ActiveSupport::Concern

def create_response_object(moment_template)
return unless moment_template.save!

{ id: moment_template.id,
name: moment_template.name, description: moment_template.description }
end

def update_response_object(moment_template)
return unless moment_template.update!(moment_template_params)

{ id: moment_template.id,
name: moment_template.name, description: moment_template.description }
end
end
32 changes: 32 additions & 0 deletions app/controllers/moment_templates_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true
class MomentTemplatesController < ApplicationController
include MomentTemplatesConcern

def index
@templates = current_user.moment_templates.order('LOWER(name)')
end

def create
moment_template = MomentTemplate.new(
moment_template_params.merge(user_id: current_user.id)
)
render json: create_response_object(moment_template)
end

def update
moment_template = MomentTemplate.find_by(id: params[:id])
render json: update_response_object(moment_template)
end

def destroy
moment_template = MomentTemplate.find_by(id: params[:id])
moment_template&.destroy
redirect_to_path(moment_templates_path)
end

private

def moment_template_params
params.require(:moment_template).permit(:name, :description)
end
end
1 change: 1 addition & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class User < ApplicationRecord
has_many :moments
has_many :categories
has_many :care_plan_contacts
has_many :moment_templates
# rubocop:disable Layout/LineLength
has_many :data_requests, class_name: 'Users::DataRequest', foreign_key: :user_id
# rubocop:enable Layout/LineLength
Expand Down
3 changes: 3 additions & 0 deletions app/views/moment_templates/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<%= react_component('MomentTemplates', html_options: {}, props: {
templates: @templates
}) %>
60 changes: 40 additions & 20 deletions app/views/shared/_page_title.html.erb
Original file line number Diff line number Diff line change
@@ -1,37 +1,57 @@
<% if !not_signed_in_root_path? %>
<div class="pageTitle">
<% if sign_in_path? %>
<div class="pageTitle">
<%= t('layouts.application.signin') %>
</div>
<% elsif join_path? %>
<%= t('layouts.application.create_account') %>
<div class="pageTitle">
<%= t('layouts.application.create_account') %>
</div>
<% elsif forgot_password_path? %>
<%= t('account.forgot_password') %>
<div class="pageTitle">
<%= t('account.forgot_password') %>
</div>
<% elsif update_account_path? %>
<%= t('layouts.application.update_account') %>
<div class="pageTitle">
<%= t('layouts.application.update_account') %>
</div>
<% elsif send_ally_invitation_path? %>
<%= t('devise.invitations.new.header') %>
<div class="pageTitle">
<%= t('devise.invitations.new.header') %>
</div>
<% elsif ally_accept_invitation_path? %>
<%= t('devise.invitations.edit.header') %>
<div class="pageTitle">
<%= t('devise.invitations.edit.header') %>
</div>
<% elsif reset_password_path? %>
<%= t('layouts.title.reset_password') %>
<div class="pageTitle">
<%= t('layouts.title.reset_password') %>
</div>
<% elsif new_user_confirmation_path? %>
<%= t('devise.confirmations.resend_confirmation') %>
<% elsif yield(:page_new).present? && @page_new %>
<div class="pageTitleLeft">
<%= yield(:title) %>
<div class="pageTitle">
<%= t('devise.confirmations.resend_confirmation') %>
</div>
<div class="pageTitleRight">
<%= link_to @page_new, yield(:page_new), class: 'buttonM' %>
<% elsif yield(:page_new).present? && @page_new %>
<div class="pageTitle">
<div>
<%= yield(:title) %>
</div>
<div class="pageTitleRight">
<%= link_to @page_new, yield(:page_new), class: 'buttonM' %>
</div>
</div>
<% elsif @page_author.present? %>
<div class="pageTitleLeft">
<%= yield(:title) %>
<div class="pageTitle">
<div>
<%= yield(:title) %>
</div>
<div class="pageTitleRight">
<%= render partial: '/shared/page_author', locals: { author: @page_author } %>
</div>
</div>
<div class="pageTitleRight">
<%= render partial: '/shared/page_author', locals: { author: @page_author } %>
<% elsif yield(:title).present? %>
<div class="pageTitle">
<%= yield(:title) %>
</div>
<% else %>
<%= yield(:title) %>
<% end %>
</div>
<% end %>
1 change: 1 addition & 0 deletions client/.flowconfig
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module.name_mapper='^components\(.*\)$' -> '<PROJECT_ROOT>/app/components\1'
module.name_mapper='^utils\(.*\)$' -> '<PROJECT_ROOT>/app/utils\1'
module.name_mapper='^mocks\(.*\)$' -> '<PROJECT_ROOT>/app/mocks\1'
module.name_mapper='^widgets\(.*\)$' -> '<PROJECT_ROOT>/app/widgets\1'
module.name_mapper='^pages\(.*\)$' -> '<PROJECT_ROOT>/app/pages\1'

module.name_mapper='.*\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)' -> '<PROJECT_ROOT>/flow/media.js'
module.name_mapper='.*\.\(css\|sass\|scss\)' -> '<PROJECT_ROOT>/flow/stylesheets.js'
Expand Down
48 changes: 48 additions & 0 deletions client/app/components/PageTitle/__tests__/PageTitle.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// @flow
import React from 'react';
import { render, screen } from '@testing-library/react';
import { PageTitle } from 'components/PageTitle';

const TITLE = 'title';
const SUBTITLE = 'subtitle';
const CTA_TEXT = 'cta';
const INSTRUCTIONS = 'instructions';

describe('PageTitle', () => {
it('renders with title and subtitle', () => {
render(<PageTitle title={TITLE} subtitle={SUBTITLE} />);
expect(screen.getByText(TITLE)).toBeInTheDocument();
expect(screen.getByText(SUBTITLE)).toBeInTheDocument();
expect(window.document.title).toEqual(`if-me.org | ${TITLE}`);
});

it('renders with title, subtitle, and cta', () => {
render(
<PageTitle
title={TITLE}
subtitle={SUBTITLE}
cta={<button type="button">{CTA_TEXT}</button>}
/>,
);
expect(screen.getByText(TITLE)).toBeInTheDocument();
expect(screen.getByText(SUBTITLE)).toBeInTheDocument();
expect(window.document.title).toEqual(`if-me.org | ${TITLE}`);
expect(screen.getByText(CTA_TEXT)).toBeInTheDocument();
});

it('renders with title, subtitle, cta, and instructions', () => {
render(
<PageTitle
title={TITLE}
subtitle={SUBTITLE}
cta={<button type="button">{CTA_TEXT}</button>}
instructions={INSTRUCTIONS}
/>,
);
expect(screen.getByText(TITLE)).toBeInTheDocument();
expect(screen.getByText(SUBTITLE)).toBeInTheDocument();
expect(window.document.title).toEqual(`if-me.org | ${TITLE}`);
expect(screen.getByText(CTA_TEXT)).toBeInTheDocument();
expect(screen.getByText(INSTRUCTIONS)).toBeInTheDocument();
});
});
30 changes: 30 additions & 0 deletions client/app/components/PageTitle/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// @flow
import React, { useEffect } from 'react';
import { I18n } from 'libs/i18n';
import globalCSS from 'styles/_global.scss';

type Props = {
title: string,
subtitle: string,
cta?: any,
instructions?: any,
};

export const PageTitle = ({
title, subtitle, cta, instructions,
}: Props) => {
useEffect(() => {
window.document.title = `${I18n.t('app_name')} | ${title}`;
});

return (
<>
<div className={globalCSS.pageTitle}>
<div>{title}</div>
{cta && <div className={globalCSS.pageTitleRight}>{cta}</div>}
</div>
<div className={globalCSS.subtitle}>{subtitle}</div>
{instructions}
</>
);
};
15 changes: 15 additions & 0 deletions client/app/pages/MomentTemplates/MomentTemplates.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@import '~styles/_global.scss';

.newTemplate {
@media screen and (max-width: $small) {
width: 100%;
}

button {
font-weight: bold;

@media screen and (max-width: $small) {
width: inherit;
}
}
}
6 changes: 6 additions & 0 deletions client/app/pages/MomentTemplates/MomentTemplatesContext.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// @flow
import { createContext } from 'react';

const TemplatesContext = createContext<Object>({});

export default TemplatesContext;
107 changes: 107 additions & 0 deletions client/app/pages/MomentTemplates/MomentTemplatesForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// @flow
import React, { useState } from 'react';
import { I18n } from 'libs/i18n';
import { Utils } from 'utils';
import DynamicForm from 'components/Form/DynamicForm';
import TemplatesContext from './MomentTemplatesContext';
import css from './MomentTemplates.scss';

export type Template = {
id: number,
name: string,
description: string,
};

type Response = {
error?: string,
data?: Template,
};

export type Props = {
template?: Template,
};

export const MomentTemplatesForm = ({ template }: Props) => {
const [error, setError] = useState();
const action = `/moment_templates/${
template ? `update?id=${template.id}` : 'create'
}`;

const onSubmit = (response: Response, context: Object) => {
const {
templates,
setTemplates,
setEditableTemplate,
setOpenModal,
setModalKey,
} = context;
if (response.error) {
setError(response.error);
} else {
let newTemplates = [...templates];
if (template) {
newTemplates = newTemplates.filter((c) => c.id !== template.id);
}
newTemplates.push(response.data);
setTemplates(newTemplates.sort((a, b) => a.name.localeCompare(b.name)));
setOpenModal(false);
setModalKey(Utils.randomString());
setEditableTemplate(null);
}
};

return (
<TemplatesContext.Consumer>
{(context) => (
<>
<DynamicForm
type={template ? 'patch' : 'post'}
formProps={{
action,
inputs: [
{
id: 'moment_template_name',
name: 'moment_template[name]',
type: 'text',
label: I18n.t('common.name'),
dark: true,
required: true,
value: template && template.name,
},
{
id: 'moment_template_description',
name: 'moment_template[description]',
type: 'textarea',
label: I18n.t('common.form.description'),
dark: true,
required: true,
value: template && template.description,
},
{
dark: true,
id: 'submit',
name: 'commit',
type: 'submit',
value: I18n.t('common.actions.submit'),
},
],
}}
onSubmit={(response) => onSubmit(response, context)}
/>
{error && (
<div
className={`${css.errorField} ${css.smallMarginTop}`}
role="alert"
>
{error}
</div>
)}
</>
)}
</TemplatesContext.Consumer>
);
};

export default ({ template }: Props) => (
<MomentTemplatesForm template={template} />
);
Loading

0 comments on commit 8fc802b

Please sign in to comment.