Make project table associated with the user

This commit is contained in:
Takeshi Kimata
2024-05-22 22:33:21 +09:00
parent 704f20239e
commit 7a0bb63e7b
17 changed files with 174 additions and 24 deletions

View File

@@ -17,6 +17,20 @@ module.exports = {
type: Sequelize.STRING, type: Sequelize.STRING,
allowNull: true, allowNull: true,
}, },
isPublic: {
type: Sequelize.BOOLEAN,
allowNull: false,
},
userId: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: 'users',
key: 'id',
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
},
createdAt: { createdAt: {
type: Sequelize.DATE, type: Sequelize.DATE,
allowNull: false, allowNull: false,

View File

@@ -8,6 +8,19 @@ function defineProject(sequelize, DataTypes) {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true, allowNull: true,
}, },
isPublic: {
type: DataTypes.BOOLEAN,
allowNull: false,
},
userId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'user',
key: 'id',
},
onDelete: 'CASCADE',
},
}); });
Project.associate = (models) => { Project.associate = (models) => {

View File

@@ -22,9 +22,9 @@ function defineUser(sequelize, DataTypes) {
}, },
}); });
// User.associate = (models) => { User.associate = (models) => {
// User.hasMany(models.Project, { foreignKey: "userId" }); User.hasMany(models.Project, { foreignKey: 'userId' });
// }; };
return User; return User;
} }

View File

@@ -8,7 +8,7 @@ module.exports = function (sequelize) {
router.put('/:projectId', async (req, res) => { router.put('/:projectId', async (req, res) => {
const projectId = req.params.projectId; const projectId = req.params.projectId;
const { name, detail } = req.body; const { name, detail, isPublic } = req.body;
try { try {
const project = await Project.findByPk(projectId); const project = await Project.findByPk(projectId);
if (!project) { if (!project) {
@@ -17,6 +17,7 @@ module.exports = function (sequelize) {
await project.update({ await project.update({
name, name,
detail, detail,
isPublic,
}); });
res.json(project); res.json(project);
} catch (error) { } catch (error) {

View File

@@ -8,10 +8,12 @@ module.exports = function (sequelize) {
router.post('/', async (req, res) => { router.post('/', async (req, res) => {
try { try {
const { name, detail } = req.body; const { name, detail, isPublic, userId } = req.body;
const newProject = await Project.create({ const newProject = await Project.create({
name, name,
detail, detail,
isPublic,
userId,
}); });
res.json(newProject); res.json(newProject);
} catch (error) { } catch (error) {

View File

@@ -1,20 +1,39 @@
'use strict'; 'use strict';
const path = require('path');
const fs = require('fs'); const fs = require('fs');
const bcrypt = require('bcrypt');
module.exports = { module.exports = {
up: async (queryInterface, Sequelize) => { up: async (queryInterface, Sequelize) => {
const hashedPassword = await bcrypt.hash('password', 10);
// Add projects table records
await queryInterface.bulkInsert('Users', [
{
email: 'admin@testplat.com',
password: hashedPassword,
username: 'Admin',
role: 1,
avatarPath: null,
createdAt: new Date(),
updatedAt: new Date(),
},
]);
// Add projects table records // Add projects table records
await queryInterface.bulkInsert('Projects', [ await queryInterface.bulkInsert('Projects', [
{ {
name: 'TestPlat Test', name: 'TestPlat Test',
detail: "Test Plat's Manual test", detail: "Test Plat's Manual test",
userId: 1,
isPublic: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}, },
{ {
name: 'Iron Muscle App(筋トレアプリ)', name: 'Iron Muscle App(筋トレアプリ)',
detail: 'リリース前総合評価', detail: 'リリース前総合評価',
userId: 1,
isPublic: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}, },

View File

@@ -62,11 +62,15 @@
"actions": "Actions", "actions": "Actions",
"project_name": "Project Name", "project_name": "Project Name",
"project_detail": "Project Detail", "project_detail": "Project Detail",
"public": "Public",
"if_you_make_public": "If you make project public, everyone who has access to this site will be able to view it.",
"close": "Close", "close": "Close",
"create": "Create", "create": "Create",
"update": "Update", "update": "Update",
"please_enter": "Please enter project name", "please_enter": "Please enter project name",
"no_projects_found": "No projects found" "no_projects_found": "No projects found",
"you_need_signed_in": "You need signed in",
"sign_in": "Sign in"
}, },
"Project": { "Project": {
"home": "Home", "home": "Home",

View File

@@ -61,11 +61,15 @@
"actions": "アクション", "actions": "アクション",
"project_name": "プロジェクト名", "project_name": "プロジェクト名",
"project_detail": "プロジェクト詳細", "project_detail": "プロジェクト詳細",
"public": "パブリック",
"if_you_make_public": "プロジェクトをパブリックにすると、このサイトにアクセスできるすべてのユーザーがプロジェクトを見ることができます。",
"close": "閉じる", "close": "閉じる",
"create": "作成", "create": "作成",
"update": "更新", "update": "更新",
"please_enter": "プロジェクト名を入力してください", "please_enter": "プロジェクト名を入力してください",
"no_projects_found": "プロジェクトがありません" "no_projects_found": "プロジェクトがありません",
"you_need_signed_in": "サインインが必要です",
"sign_in": "サインイン"
}, },
"Project": { "Project": {
"home": "ホーム", "home": "ホーム",

View File

@@ -5,6 +5,7 @@ import { useContext } from 'react';
import { TokenContext } from './TokenProvider'; import { TokenContext } from './TokenProvider';
import { useRouter } from '@/src/navigation'; import { useRouter } from '@/src/navigation';
import { AccountDropDownMessages } from '@/types/user'; import { AccountDropDownMessages } from '@/types/user';
import Avatar from 'boring-avatars';
type Props = { type Props = {
messages: AccountDropDownMessages; messages: AccountDropDownMessages;
@@ -22,11 +23,23 @@ export default function DropdownAccount({ messages, locale, onItemPress }: Props
router.push(`/`, { locale: locale }); router.push(`/`, { locale: locale });
}; };
let userAvatar =
context.token && context.token.user ? (
<Avatar
size={16}
name={context.token.user.username}
variant="beam"
colors={['#0A0310', '#49007E', '#FF005B', '#FF7D10', '#FFB238']}
/>
) : (
<User size={16} />
);
const signinItems = [ const signinItems = [
{ {
uid: 'account', uid: 'account',
title: messages.account, title: messages.account,
icon: <User size={16} />, icon: userAvatar,
onPress: () => { onPress: () => {
router.push('/account', { locale: locale }); router.push('/account', { locale: locale });
onItemPress(); onItemPress();
@@ -67,7 +80,7 @@ export default function DropdownAccount({ messages, locale, onItemPress }: Props
return ( return (
<Dropdown> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
<Button size="sm" variant="light" startContent={<User size={16} />} endContent={<ChevronDown size={16} />}> <Button size="sm" variant="light" startContent={userAvatar} endContent={<ChevronDown size={16} />}>
{context.token && context.token.user ? context.token.user.username : messages.signIn} {context.token && context.token.user ? context.token.user.username : messages.signIn}
</Button> </Button>
</DropdownTrigger> </DropdownTrigger>

View File

@@ -6,7 +6,7 @@ import { Link } from '@/src/navigation';
import { ChevronRight, Eye, EyeOff } from 'lucide-react'; import { ChevronRight, Eye, EyeOff } from 'lucide-react';
import { UserType, AuthMessages } from '@/types/user'; import { UserType, AuthMessages } from '@/types/user';
import { roles } from '@/config/selection'; import { roles } from '@/config/selection';
import { signUp, signIn, getRandomAvatarPath } from './authControl'; import { signUp, signIn } from './authControl';
import { isValidEmail, isValidPassword } from './validate'; import { isValidEmail, isValidPassword } from './validate';
import { TokenContext } from '../TokenProvider'; import { TokenContext } from '../TokenProvider';
import { useRouter } from '@/src/navigation'; import { useRouter } from '@/src/navigation';

View File

@@ -0,0 +1,31 @@
'use client';
import { ProjectsMessages } from '@/types/project';
import { Link } from '@/src/navigation';
import { Button, Modal, ModalContent, ModalHeader, ModalFooter } from '@nextui-org/react';
type Props = {
isOpen: boolean;
onCancel: () => void;
messages: ProjectsMessages;
locale: string;
};
export default function NeedSignedInDialog({ isOpen, onCancel, messages, locale }: Props) {
return (
<Modal
isOpen={isOpen}
onOpenChange={() => {
onCancel();
}}
>
<ModalContent>
<ModalHeader className="flex flex-col gap-1">{messages.needSignedIn}</ModalHeader>
<ModalFooter>
<Link href={'/account/signin'} locale={locale}>
<Button color="primary">{messages.signIn}</Button>
</Link>
</ModalFooter>
</ModalContent>
</Modal>
);
}

View File

@@ -1,14 +1,24 @@
'use client'; 'use client';
import React from 'react'; import React from 'react';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Button, Input, Textarea, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from '@nextui-org/react'; import {
Button,
Input,
Textarea,
Checkbox,
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
} from '@nextui-org/react';
import { ProjectType, ProjectsMessages } from '@/types/project'; import { ProjectType, ProjectsMessages } from '@/types/project';
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
editingProject: ProjectType; editingProject: ProjectType;
onCancel: () => void; onCancel: () => void;
onSubmit: (name: string, detail: string) => void; onSubmit: (name: string, detail: string, isPublic: boolean) => void;
messages: ProjectsMessages; messages: ProjectsMessages;
}; };
@@ -25,6 +35,8 @@ export default function ProjectDialog({ isOpen, editingProject, onCancel, onSubm
errorMessage: '', errorMessage: '',
}); });
const [isProjectPublic, setIsProjectPublic] = useState(editingProject ? editingProject.isPublic : true);
useEffect(() => { useEffect(() => {
if (editingProject) { if (editingProject) {
setProjectName({ setProjectName({
@@ -36,6 +48,8 @@ export default function ProjectDialog({ isOpen, editingProject, onCancel, onSubm
...projectDetail, ...projectDetail,
text: editingProject.detail ? editingProject.detail : '', text: editingProject.detail ? editingProject.detail : '',
}); });
setIsProjectPublic(editingProject.isPublic);
} else { } else {
setProjectName({ setProjectName({
...projectName, ...projectName,
@@ -46,6 +60,8 @@ export default function ProjectDialog({ isOpen, editingProject, onCancel, onSubm
...projectDetail, ...projectDetail,
text: '', text: '',
}); });
setIsProjectPublic(true);
} }
}, [editingProject]); }, [editingProject]);
@@ -73,7 +89,7 @@ export default function ProjectDialog({ isOpen, editingProject, onCancel, onSubm
return; return;
} }
onSubmit(projectName.text, projectDetail.text); onSubmit(projectName.text, projectDetail.text, isProjectPublic);
clear(); clear();
}; };
@@ -112,6 +128,10 @@ export default function ProjectDialog({ isOpen, editingProject, onCancel, onSubm
}); });
}} }}
/> />
<Checkbox isSelected={isProjectPublic} onValueChange={setIsProjectPublic}>
{messages.public}
</Checkbox>
<div className="text-small text-default-500">{messages.ifYouMakePublic}</div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button variant="light" onPress={onCancel}> <Button variant="light" onPress={onCancel}>

View File

@@ -1,10 +1,12 @@
'use client'; 'use client';
import { useEffect, useState } from 'react'; import { useEffect, useState, useContext } from 'react';
import { Button } from '@nextui-org/react'; import { Button } from '@nextui-org/react';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
import { TokenContext } from '@/src/app/[locale]/TokenProvider';
import { ProjectType, ProjectsMessages } from '@/types/project'; import { ProjectType, ProjectsMessages } from '@/types/project';
import ProjectsTable from './ProjectsTable'; import ProjectsTable from './ProjectsTable';
import ProjectDialog from './ProjectDialog'; import ProjectDialog from './ProjectDialog';
import NeedSignedInDialog from './NeedSignedInDialog';
import { fetchProjects, createProject, updateProject, deleteProject } from './projectsControl'; import { fetchProjects, createProject, updateProject, deleteProject } from './projectsControl';
export type Props = { export type Props = {
@@ -13,6 +15,7 @@ export type Props = {
}; };
export default function ProjectsPage({ messages, locale }: Props) { export default function ProjectsPage({ messages, locale }: Props) {
const context = useContext(TokenContext);
const [projects, setProjects] = useState([]); const [projects, setProjects] = useState([]);
useEffect(() => { useEffect(() => {
@@ -29,9 +32,15 @@ export default function ProjectsPage({ messages, locale }: Props) {
}, []); }, []);
// dialog // dialog
const [isNeedSignedInDialogOpen, setIsNeedSignedInDialogOpen] = useState(false);
const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false); const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false);
const [editingProject, setEditingProject] = useState<ProjectType | null>(null); const [editingProject, setEditingProject] = useState<ProjectType | null>(null);
const openDialogForCreate = () => { const openDialogForCreate = () => {
if (!context.token || !context.token.user) {
setIsNeedSignedInDialogOpen(true);
return;
}
setIsProjectDialogOpen(true); setIsProjectDialogOpen(true);
setEditingProject(null); setEditingProject(null);
}; };
@@ -41,13 +50,13 @@ export default function ProjectsPage({ messages, locale }: Props) {
setEditingProject(null); setEditingProject(null);
}; };
const onSubmit = async (name: string, detail: string) => { const onSubmit = async (name: string, detail: string, isPublic: boolean) => {
if (editingProject) { if (editingProject) {
const updatedProject = await updateProject(editingProject.id, name, detail); const updatedProject = await updateProject(editingProject.id, name, detail, isPublic);
const updatedProjects = projects.map((project) => (project.id === updatedProject.id ? updatedProject : project)); const updatedProjects = projects.map((project) => (project.id === updatedProject.id ? updatedProject : project));
setProjects(updatedProjects); setProjects(updatedProjects);
} else { } else {
const newProject = await createProject(name, detail); const newProject = await createProject(name, detail, isPublic, context.token.user.id);
setProjects([...projects, newProject]); setProjects([...projects, newProject]);
} }
closeDialog(); closeDialog();
@@ -93,6 +102,13 @@ export default function ProjectsPage({ messages, locale }: Props) {
onSubmit={onSubmit} onSubmit={onSubmit}
messages={messages} messages={messages}
/> />
<NeedSignedInDialog
isOpen={isNeedSignedInDialogOpen}
onCancel={() => setIsNeedSignedInDialogOpen(false)}
messages={messages}
locale={locale}
/>
</div> </div>
); );
} }

View File

@@ -16,11 +16,15 @@ export default function Page(params: { locale: string }) {
actions: t('actions'), actions: t('actions'),
projectName: t('project_name'), projectName: t('project_name'),
projectDetail: t('project_detail'), projectDetail: t('project_detail'),
public: t('public'),
ifYouMakePublic: t('if_you_make_public'),
close: t('close'), close: t('close'),
create: t('create'), create: t('create'),
update: t('update'), update: t('update'),
pleaseEnter: t('please_enter'), pleaseEnter: t('please_enter'),
noProjectsFound: t('no_projects_found'), noProjectsFound: t('no_projects_found'),
needSignedIn: t('you_need_signed_in'),
signIn: t('sign_in'),
}; };
return ( return (
<> <>

View File

@@ -29,10 +29,12 @@ async function fetchProjects() {
/** /**
* Create project * Create project
*/ */
async function createProject(name: string, detail: string) { async function createProject(name: string, detail: string, isPublic: boolean, userId: number) {
const newProjectData = { const newProjectData = {
name: name, name,
detail: detail, detail,
isPublic,
userId,
}; };
const fetchOptions = { const fetchOptions = {
@@ -61,10 +63,11 @@ async function createProject(name: string, detail: string) {
/** /**
* Update project * Update project
*/ */
async function updateProject(projectId: number, name: string, detail: string) { async function updateProject(projectId: number, name: string, detail: string, isPublic: boolean) {
const updatedProjectData = { const updatedProjectData = {
name: name, name,
detail: detail, detail,
isPublic,
}; };
const fetchOptions = { const fetchOptions = {

View File

@@ -5,6 +5,8 @@ export type ProjectType = {
id: number; id: number;
name: string; name: string;
detail: string; detail: string;
isPublic: string;
userId: number;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
Folders: FolderType[]; // additional property Folders: FolderType[]; // additional property
@@ -23,11 +25,15 @@ export type ProjectsMessages = {
actions: string; actions: string;
projectName: string; projectName: string;
projectDetail: string; projectDetail: string;
public: string;
ifYouMakePublic: string;
close: string; close: string;
create: string; create: string;
update: string; update: string;
pleaseEnter: string; pleaseEnter: string;
noProjectsFound: string; noProjectsFound: string;
needSignedIn: string;
signIn: string;
}; };
export type ProjectMessages = { export type ProjectMessages = {