zenstack 0.1.42 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/extensions.json +7 -0
- package/.vscode/launch.json +49 -0
- package/.vscode/settings.json +4 -0
- package/README.md +1 -0
- package/package.json +8 -90
- package/packages/internal/jest.config.ts +32 -0
- package/packages/internal/package.json +42 -0
- package/packages/internal/src/constants.ts +1 -0
- package/packages/internal/src/handler/data/guard-utils.ts +7 -0
- package/packages/internal/src/handler/data/handler.ts +415 -0
- package/packages/internal/src/handler/data/query-processor.ts +504 -0
- package/packages/internal/src/handler/index.ts +1 -0
- package/packages/internal/src/handler/types.ts +20 -0
- package/packages/internal/src/index.ts +3 -0
- package/packages/internal/src/request-handler.ts +27 -0
- package/packages/internal/src/request.ts +101 -0
- package/packages/internal/src/types.ts +40 -0
- package/packages/internal/tests/query-processor.test.ts +172 -0
- package/{out/cli/tsconfig.template.json → packages/internal/tsconfig.json} +7 -3
- package/packages/runtime/auth.d.ts +1 -0
- package/packages/runtime/auth.js +3 -0
- package/packages/runtime/hooks.d.ts +10 -0
- package/packages/runtime/hooks.js +3 -0
- package/packages/runtime/index.d.ts +3 -0
- package/packages/runtime/index.js +1 -0
- package/packages/runtime/package-lock.json +512 -0
- package/packages/runtime/package.json +16 -0
- package/packages/runtime/server.d.ts +1 -0
- package/packages/runtime/server.js +3 -0
- package/packages/runtime/types.d.ts +1 -0
- package/packages/runtime/types.js +3 -0
- package/packages/schema/.eslintrc.json +13 -0
- package/packages/schema/.vscodeignore +4 -0
- package/packages/schema/asset/logo-dark.png +0 -0
- package/packages/schema/asset/logo-light.png +0 -0
- package/{bin → packages/schema/bin}/cli +0 -0
- package/packages/schema/jest.config.ts +32 -0
- package/packages/schema/langium-config.json +14 -0
- package/packages/schema/langium-quickstart.md +41 -0
- package/packages/schema/language-configuration.json +30 -0
- package/packages/schema/package.json +96 -0
- package/packages/schema/src/cli/cli-util.ts +80 -0
- package/packages/schema/src/cli/index.ts +64 -0
- package/packages/schema/src/extension.ts +76 -0
- package/packages/schema/src/generator/constants.ts +5 -0
- package/packages/schema/src/generator/index.ts +92 -0
- package/{out/generator/next-auth/index.js → packages/schema/src/generator/next-auth/index.ts} +46 -58
- package/{out → packages/schema/src}/generator/package.template.json +0 -0
- package/packages/schema/src/generator/prisma/expression-writer.ts +352 -0
- package/packages/schema/src/generator/prisma/index.ts +32 -0
- package/packages/schema/src/generator/prisma/plain-expression-builder.ts +91 -0
- package/packages/schema/src/generator/prisma/prisma-builder.ts +366 -0
- package/packages/schema/src/generator/prisma/query-gard-generator.ts +208 -0
- package/packages/schema/src/generator/prisma/schema-generator.ts +300 -0
- package/packages/schema/src/generator/react-hooks/index.ts +181 -0
- package/packages/schema/src/generator/service/index.ts +107 -0
- package/{out → packages/schema/src}/generator/tsconfig.template.json +0 -0
- package/packages/schema/src/generator/types.ts +17 -0
- package/packages/schema/src/generator/utils.ts +9 -0
- package/packages/schema/src/language-server/generated/ast.ts +603 -0
- package/{out/language-server/generated/grammar.js → packages/schema/src/language-server/generated/grammar.ts} +5 -8
- package/packages/schema/src/language-server/generated/module.ts +24 -0
- package/packages/schema/src/language-server/main.ts +12 -0
- package/{out → packages/schema/src}/language-server/stdlib.zmodel +0 -0
- package/packages/schema/src/language-server/types.ts +9 -0
- package/packages/schema/src/language-server/zmodel-index.ts +33 -0
- package/packages/schema/src/language-server/zmodel-linker.ts +409 -0
- package/packages/schema/src/language-server/zmodel-module.ts +90 -0
- package/packages/schema/src/language-server/zmodel-scope.ts +21 -0
- package/packages/schema/src/language-server/zmodel-validator.ts +35 -0
- package/packages/schema/src/language-server/zmodel.langium +186 -0
- package/packages/schema/src/utils/exec-utils.ts +5 -0
- package/packages/schema/src/utils/indent-string.ts +6 -0
- package/packages/schema/syntaxes/zmodel.json +57 -0
- package/packages/schema/syntaxes/zmodel.tmLanguage.json +57 -0
- package/packages/schema/tests/generator/expression-writer.test.ts +676 -0
- package/packages/schema/tests/generator/prisma-builder.test.ts +138 -0
- package/packages/schema/tests/schema/parser.test.ts +423 -0
- package/packages/schema/tests/schema/sample-todo.test.ts +14 -0
- package/packages/schema/tests/utils.ts +38 -0
- package/packages/schema/tsconfig.json +23 -0
- package/pnpm-workspace.yaml +3 -0
- package/samples/todo/.env +2 -0
- package/samples/todo/.eslintrc.json +3 -0
- package/samples/todo/.vscode/launch.json +11 -0
- package/samples/todo/README.md +34 -0
- package/samples/todo/components/AuthGuard.tsx +17 -0
- package/samples/todo/components/Avatar.tsx +22 -0
- package/samples/todo/components/BreadCrumb.tsx +44 -0
- package/samples/todo/components/ManageMembers.tsx +134 -0
- package/samples/todo/components/NavBar.tsx +57 -0
- package/samples/todo/components/SpaceMembers.tsx +76 -0
- package/samples/todo/components/Spaces.tsx +28 -0
- package/samples/todo/components/TimeInfo.tsx +17 -0
- package/samples/todo/components/Todo.tsx +72 -0
- package/samples/todo/components/TodoList.tsx +77 -0
- package/samples/todo/lib/context.ts +31 -0
- package/samples/todo/next.config.js +10 -0
- package/samples/todo/package-lock.json +7527 -0
- package/samples/todo/package.json +45 -0
- package/samples/todo/pages/_app.tsx +50 -0
- package/samples/todo/pages/api/auth/[...nextauth].ts +83 -0
- package/samples/todo/pages/api/zenstack/[...path].ts +16 -0
- package/samples/todo/pages/create-space.tsx +114 -0
- package/samples/todo/pages/index.tsx +32 -0
- package/samples/todo/pages/space/[slug]/[listId]/index.tsx +88 -0
- package/samples/todo/pages/space/[slug]/index.tsx +169 -0
- package/samples/todo/postcss.config.js +6 -0
- package/samples/todo/public/avatar.jpg +0 -0
- package/samples/todo/public/favicon.ico +0 -0
- package/samples/todo/public/logo.png +0 -0
- package/samples/todo/public/vercel.svg +4 -0
- package/samples/todo/styles/globals.css +7 -0
- package/samples/todo/tailwind.config.js +11 -0
- package/samples/todo/tsconfig.json +28 -0
- package/samples/todo/types/next-auth.d.ts +14 -0
- package/samples/todo/types/next.d.ts +16 -0
- package/samples/todo/zenstack/migrations/20221014084317_init/migration.sql +153 -0
- package/samples/todo/zenstack/migrations/20221020094651_upate_cli/migration.sql +23 -0
- package/samples/todo/zenstack/migrations/migration_lock.toml +3 -0
- package/samples/todo/zenstack/schema.prisma +126 -0
- package/samples/todo/zenstack/schema.zmodel +161 -0
- package/tests/integration/jest.config.ts +16 -0
- package/tests/integration/package-lock.json +1081 -0
- package/tests/integration/package.json +27 -0
- package/tests/integration/tests/operation-coverate.test.ts +563 -0
- package/tests/integration/tests/operations.zmodel +69 -0
- package/tests/integration/tests/todo-e2e.test.ts +577 -0
- package/tests/integration/tests/todo.zmodel +123 -0
- package/tests/integration/tests/tsconfig.template.json +10 -0
- package/tests/integration/tests/utils.ts +133 -0
- package/tests/integration/tsconfig.json +10 -0
- package/out/cli/cli-util.js +0 -64
- package/out/cli/cli-util.js.map +0 -1
- package/out/cli/generator.js +0 -1
- package/out/cli/generator.js.map +0 -1
- package/out/cli/index.js +0 -46
- package/out/cli/index.js.map +0 -1
- package/out/cli/package.template.json +0 -10
- package/out/extension.js +0 -81
- package/out/extension.js.map +0 -1
- package/out/generator/constants.js +0 -9
- package/out/generator/constants.js.map +0 -1
- package/out/generator/data-server/index.js +0 -1
- package/out/generator/data-server/index.js.map +0 -1
- package/out/generator/index.js +0 -98
- package/out/generator/index.js.map +0 -1
- package/out/generator/next-auth/index.js.map +0 -1
- package/out/generator/prisma/expression-writer.js +0 -287
- package/out/generator/prisma/expression-writer.js.map +0 -1
- package/out/generator/prisma/index.js +0 -38
- package/out/generator/prisma/index.js.map +0 -1
- package/out/generator/prisma/plain-expression-builder.js +0 -69
- package/out/generator/prisma/plain-expression-builder.js.map +0 -1
- package/out/generator/prisma/prisma-builder.js +0 -307
- package/out/generator/prisma/prisma-builder.js.map +0 -1
- package/out/generator/prisma/query-gard-generator.js +0 -159
- package/out/generator/prisma/query-gard-generator.js.map +0 -1
- package/out/generator/prisma/schema-generator.js +0 -201
- package/out/generator/prisma/schema-generator.js.map +0 -1
- package/out/generator/query-guard/index.js +0 -2
- package/out/generator/query-guard/index.js.map +0 -1
- package/out/generator/react-hooks/index.js +0 -179
- package/out/generator/react-hooks/index.js.map +0 -1
- package/out/generator/server/data/data-generator.js +0 -376
- package/out/generator/server/data/data-generator.js.map +0 -1
- package/out/generator/server/data/expression-writer.js +0 -287
- package/out/generator/server/data/expression-writer.js.map +0 -1
- package/out/generator/server/data/plain-expression-builder.js +0 -69
- package/out/generator/server/data/plain-expression-builder.js.map +0 -1
- package/out/generator/server/data-generator.js +0 -82
- package/out/generator/server/data-generator.js.map +0 -1
- package/out/generator/server/expression-writer.js +0 -1
- package/out/generator/server/expression-writer.js.map +0 -1
- package/out/generator/server/function/function-generator.js +0 -50
- package/out/generator/server/function/function-generator.js.map +0 -1
- package/out/generator/server/function-generator.js +0 -13
- package/out/generator/server/function-generator.js.map +0 -1
- package/out/generator/server/index.js +0 -88
- package/out/generator/server/index.js.map +0 -1
- package/out/generator/server/js-expression-builder.js +0 -1
- package/out/generator/server/js-expression-builder.js.map +0 -1
- package/out/generator/server/plain-expression-builder.js +0 -1
- package/out/generator/server/plain-expression-builder.js.map +0 -1
- package/out/generator/server/server-code-generator.js +0 -3
- package/out/generator/server/server-code-generator.js.map +0 -1
- package/out/generator/server/server-code-writer.js +0 -1
- package/out/generator/server/server-code-writer.js.map +0 -1
- package/out/generator/service/index.js +0 -133
- package/out/generator/service/index.js.map +0 -1
- package/out/generator/types.js +0 -10
- package/out/generator/types.js.map +0 -1
- package/out/generator/utils.js +0 -10
- package/out/generator/utils.js.map +0 -1
- package/out/language-server/generated/ast.js +0 -386
- package/out/language-server/generated/ast.js.map +0 -1
- package/out/language-server/generated/grammar.js.map +0 -1
- package/out/language-server/generated/module.js +0 -23
- package/out/language-server/generated/module.js.map +0 -1
- package/out/language-server/main.js +0 -12
- package/out/language-server/main.js.map +0 -1
- package/out/language-server/types.js +0 -3
- package/out/language-server/types.js.map +0 -1
- package/out/language-server/zmodel-index.js +0 -38
- package/out/language-server/zmodel-index.js.map +0 -1
- package/out/language-server/zmodel-linker.js +0 -241
- package/out/language-server/zmodel-linker.js.map +0 -1
- package/out/language-server/zmodel-module.js +0 -51
- package/out/language-server/zmodel-module.js.map +0 -1
- package/out/language-server/zmodel-scope.js +0 -30
- package/out/language-server/zmodel-scope.js.map +0 -1
- package/out/language-server/zmodel-validator.js +0 -25
- package/out/language-server/zmodel-validator.js.map +0 -1
- package/out/utils/indent-string.js +0 -9
- package/out/utils/indent-string.js.map +0 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { signIn, useSession } from 'next-auth/react';
|
|
2
|
+
|
|
3
|
+
type Props = {
|
|
4
|
+
children: JSX.Element | JSX.Element[];
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export default function AuthGuard({ children }: Props) {
|
|
8
|
+
const { status } = useSession();
|
|
9
|
+
if (status === 'loading') {
|
|
10
|
+
return <p>Loading...</p>;
|
|
11
|
+
} else if (status === 'unauthenticated') {
|
|
12
|
+
signIn();
|
|
13
|
+
return <></>;
|
|
14
|
+
} else {
|
|
15
|
+
return <>{children}</>;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { UserIcon } from '@heroicons/react/24/outline';
|
|
2
|
+
import { User } from 'next-auth';
|
|
3
|
+
import Image from 'next/image';
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
user: User;
|
|
7
|
+
size?: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function Avatar({ user, size }: Props) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="tooltip" data-tip={user.name || user.email}>
|
|
13
|
+
<Image
|
|
14
|
+
src={user.image || '/avatar.jpg'}
|
|
15
|
+
alt="avatar"
|
|
16
|
+
width={size || 32}
|
|
17
|
+
height={size || 32}
|
|
18
|
+
className="rounded-full"
|
|
19
|
+
/>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useList } from '@zenstackhq/runtime/hooks';
|
|
2
|
+
import Link from 'next/link';
|
|
3
|
+
import { useRouter } from 'next/router';
|
|
4
|
+
import { useCurrentSpace } from '@lib/context';
|
|
5
|
+
|
|
6
|
+
export default function BreadCrumb() {
|
|
7
|
+
const router = useRouter();
|
|
8
|
+
const space = useCurrentSpace();
|
|
9
|
+
const { get: getList } = useList();
|
|
10
|
+
|
|
11
|
+
const parts = router.asPath.split('/').filter((p) => p);
|
|
12
|
+
|
|
13
|
+
const [base, slug, listId] = parts;
|
|
14
|
+
if (base !== 'space') {
|
|
15
|
+
return <></>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const items: Array<{ text: string; link: string }> = [];
|
|
19
|
+
|
|
20
|
+
items.push({ text: 'Home', link: '/' });
|
|
21
|
+
items.push({ text: space?.name || '', link: `/space/${slug}` });
|
|
22
|
+
|
|
23
|
+
if (listId) {
|
|
24
|
+
const { data } = getList(listId);
|
|
25
|
+
items.push({
|
|
26
|
+
text: data?.title || '',
|
|
27
|
+
link: `/space/${slug}/${listId}`,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="text-sm text-gray-600 breadcrumbs">
|
|
33
|
+
<ul>
|
|
34
|
+
{items.map((item, i) => (
|
|
35
|
+
<li key={i}>
|
|
36
|
+
<Link href={item.link}>
|
|
37
|
+
<a>{item.text}</a>
|
|
38
|
+
</Link>
|
|
39
|
+
</li>
|
|
40
|
+
))}
|
|
41
|
+
</ul>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { PlusIcon, TrashIcon } from '@heroicons/react/24/outline';
|
|
2
|
+
import { useCurrentUser } from '@lib/context';
|
|
3
|
+
import { ServerErrorCode } from '@zenstackhq/internal';
|
|
4
|
+
import { HooksError, useSpaceUser } from '@zenstackhq/runtime/hooks';
|
|
5
|
+
import { Space, SpaceUserRole } from '@zenstackhq/runtime/types';
|
|
6
|
+
import { ChangeEvent, KeyboardEvent, useState } from 'react';
|
|
7
|
+
import { toast } from 'react-toastify';
|
|
8
|
+
import Avatar from './Avatar';
|
|
9
|
+
|
|
10
|
+
type Props = {
|
|
11
|
+
space: Space;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default function ManageMembers({ space }: Props) {
|
|
15
|
+
const [email, setEmail] = useState('');
|
|
16
|
+
const [role, setRole] = useState<SpaceUserRole>(SpaceUserRole.USER);
|
|
17
|
+
const user = useCurrentUser();
|
|
18
|
+
|
|
19
|
+
const { find, create: addMember, del: delMember } = useSpaceUser();
|
|
20
|
+
const { data: members } = find({
|
|
21
|
+
where: {
|
|
22
|
+
spaceId: space.id,
|
|
23
|
+
},
|
|
24
|
+
include: {
|
|
25
|
+
user: true,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const inviteUser = async () => {
|
|
30
|
+
try {
|
|
31
|
+
const r = await addMember({
|
|
32
|
+
data: {
|
|
33
|
+
user: {
|
|
34
|
+
connect: {
|
|
35
|
+
email,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
space: {
|
|
39
|
+
connect: {
|
|
40
|
+
id: space.id,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
role,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
console.log('SpaceUser created:', r);
|
|
47
|
+
} catch (err: any) {
|
|
48
|
+
console.error(JSON.stringify(err));
|
|
49
|
+
if (err.info?.code) {
|
|
50
|
+
const { info } = err as HooksError;
|
|
51
|
+
if (info.code === ServerErrorCode.UNIQUE_CONSTRAINT_VIOLATION) {
|
|
52
|
+
toast.error('User is already a member of the space');
|
|
53
|
+
} else if (
|
|
54
|
+
info.code === ServerErrorCode.REFERENCE_CONSTRAINT_VIOLATION
|
|
55
|
+
) {
|
|
56
|
+
toast.error('User is not found for this email');
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
toast.error(`Error occurred: ${err}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const removeMember = async (id: string) => {
|
|
65
|
+
if (confirm(`Are you sure to remove this member from space?`)) {
|
|
66
|
+
await delMember(id);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div>
|
|
72
|
+
<div className="flex flex-wrap gap-2 items-center mb-8 w-full">
|
|
73
|
+
<input
|
|
74
|
+
type="text"
|
|
75
|
+
placeholder="Type user email and enter to invite"
|
|
76
|
+
className="input input-sm input-bordered flex-grow mr-2"
|
|
77
|
+
value={email}
|
|
78
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
|
79
|
+
setEmail(e.currentTarget.value);
|
|
80
|
+
}}
|
|
81
|
+
onKeyUp={(e: KeyboardEvent<HTMLInputElement>) => {
|
|
82
|
+
if (e.key === 'Enter') {
|
|
83
|
+
inviteUser();
|
|
84
|
+
}
|
|
85
|
+
}}
|
|
86
|
+
/>
|
|
87
|
+
|
|
88
|
+
<select
|
|
89
|
+
className="select select-sm mr-2"
|
|
90
|
+
value={role}
|
|
91
|
+
onChange={(e: ChangeEvent<HTMLSelectElement>) => {
|
|
92
|
+
setRole(e.currentTarget.value as SpaceUserRole);
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
<option value={SpaceUserRole.USER}>USER</option>
|
|
96
|
+
<option value={SpaceUserRole.ADMIN}>ADMIN</option>
|
|
97
|
+
</select>
|
|
98
|
+
|
|
99
|
+
<button>
|
|
100
|
+
<PlusIcon className="w-6 h-6 text-gray-500" />
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<ul className="space-y-2">
|
|
105
|
+
{members?.map((member) => (
|
|
106
|
+
<li
|
|
107
|
+
key={member.id}
|
|
108
|
+
className="flex flex-wrap w-full justify-between"
|
|
109
|
+
>
|
|
110
|
+
<div className="flex items-center">
|
|
111
|
+
<div className="hidden md:block mr-2">
|
|
112
|
+
<Avatar user={member.user} size={32} />
|
|
113
|
+
</div>
|
|
114
|
+
<p className="w-36 md:w-48 line-clamp-1 mr-2">
|
|
115
|
+
{member.user.name || member.user.email}
|
|
116
|
+
</p>
|
|
117
|
+
<p>{member.role}</p>
|
|
118
|
+
</div>
|
|
119
|
+
<div className="flex items-center">
|
|
120
|
+
{user?.id !== member.user.id && (
|
|
121
|
+
<TrashIcon
|
|
122
|
+
className="w-4 h-4 text-gray-500"
|
|
123
|
+
onClick={() => {
|
|
124
|
+
removeMember(member.id);
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
</li>
|
|
130
|
+
))}
|
|
131
|
+
</ul>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import Image from 'next/image';
|
|
2
|
+
import Avatar from './Avatar';
|
|
3
|
+
import { signOut } from 'next-auth/react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { Space } from '@zenstackhq/runtime/types';
|
|
6
|
+
import { User } from 'next-auth';
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
space: Space | undefined;
|
|
10
|
+
user: User | undefined;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default function NavBar({ user, space }: Props) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="navbar bg-base-100 px-8 py-2 border-b">
|
|
16
|
+
<div className="flex-1">
|
|
17
|
+
<Link href="/">
|
|
18
|
+
<a className="flex items-center">
|
|
19
|
+
<Image
|
|
20
|
+
src="/logo.png"
|
|
21
|
+
alt="Logo"
|
|
22
|
+
width={32}
|
|
23
|
+
height={32}
|
|
24
|
+
/>
|
|
25
|
+
<div className="text-xl font-semibold ml-2 text-slate-700 hidden md:inline-block">
|
|
26
|
+
{space?.name || 'Welcome Todo App'}
|
|
27
|
+
</div>
|
|
28
|
+
<p className="text-xs ml-2 text-gray-500 self-end">
|
|
29
|
+
Powered by ZenStack
|
|
30
|
+
</p>
|
|
31
|
+
</a>
|
|
32
|
+
</Link>
|
|
33
|
+
</div>
|
|
34
|
+
<div className="flex-none">
|
|
35
|
+
<div className="dropdown dropdown-end">
|
|
36
|
+
<label
|
|
37
|
+
tabIndex={0}
|
|
38
|
+
className="btn btn-ghost btn-circle avatar"
|
|
39
|
+
>
|
|
40
|
+
{user && <Avatar user={user!} />}
|
|
41
|
+
</label>
|
|
42
|
+
<ul
|
|
43
|
+
tabIndex={0}
|
|
44
|
+
className="menu menu-compact dropdown-content mt-3 p-2 shadow bg-base-100 rounded-box w-52"
|
|
45
|
+
>
|
|
46
|
+
<li className="border-b border-gray-200">
|
|
47
|
+
{user && <div>{user.name || user.email}</div>}
|
|
48
|
+
</li>
|
|
49
|
+
<li>
|
|
50
|
+
<a onClick={() => signOut()}>Logout</a>
|
|
51
|
+
</li>
|
|
52
|
+
</ul>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useSpaceUser } from '@zenstackhq/runtime/hooks';
|
|
2
|
+
import { useCurrentSpace } from '@lib/context';
|
|
3
|
+
import { PlusIcon } from '@heroicons/react/24/outline';
|
|
4
|
+
import Avatar from './Avatar';
|
|
5
|
+
import ManageMembers from './ManageMembers';
|
|
6
|
+
import { Space } from '@zenstackhq/runtime/types';
|
|
7
|
+
|
|
8
|
+
function ManagementDialog(space?: Space) {
|
|
9
|
+
if (!space) return undefined;
|
|
10
|
+
return (
|
|
11
|
+
<>
|
|
12
|
+
<label htmlFor="management-modal" className="modal-button">
|
|
13
|
+
<PlusIcon className="w-6 h-6 text-gray-500 cursor-pointer mr-1" />
|
|
14
|
+
</label>
|
|
15
|
+
|
|
16
|
+
<input
|
|
17
|
+
type="checkbox"
|
|
18
|
+
id="management-modal"
|
|
19
|
+
className="modal-toggle"
|
|
20
|
+
/>
|
|
21
|
+
<div className="modal">
|
|
22
|
+
<div className="modal-box">
|
|
23
|
+
<h3 className="font-bold text-base md:text-lg">
|
|
24
|
+
Manage Members of {space.name}
|
|
25
|
+
</h3>
|
|
26
|
+
|
|
27
|
+
<div className="p-4 mt-4">
|
|
28
|
+
<ManageMembers space={space} />
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div className="modal-action">
|
|
32
|
+
<label
|
|
33
|
+
htmlFor="management-modal"
|
|
34
|
+
className="btn btn-outline"
|
|
35
|
+
>
|
|
36
|
+
Close
|
|
37
|
+
</label>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default function SpaceMembers() {
|
|
46
|
+
const space = useCurrentSpace();
|
|
47
|
+
|
|
48
|
+
const { find: findMembers } = useSpaceUser();
|
|
49
|
+
const { data: members } = findMembers({
|
|
50
|
+
where: {
|
|
51
|
+
spaceId: space?.id,
|
|
52
|
+
},
|
|
53
|
+
include: {
|
|
54
|
+
user: true,
|
|
55
|
+
},
|
|
56
|
+
orderBy: {
|
|
57
|
+
role: 'desc',
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="flex items-center">
|
|
63
|
+
{ManagementDialog(space)}
|
|
64
|
+
{members && (
|
|
65
|
+
<label
|
|
66
|
+
className="mr-1 modal-button cursor-pointer"
|
|
67
|
+
htmlFor="management-modal"
|
|
68
|
+
>
|
|
69
|
+
{members?.map((member) => (
|
|
70
|
+
<Avatar key={member.id} user={member.user} size={24} />
|
|
71
|
+
))}
|
|
72
|
+
</label>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useSpace } from '@zenstackhq/runtime/hooks';
|
|
2
|
+
import Link from 'next/link';
|
|
3
|
+
|
|
4
|
+
export default function Spaces() {
|
|
5
|
+
const { find } = useSpace();
|
|
6
|
+
const spaces = find();
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<ul className="flex flex-wrap gap-4">
|
|
10
|
+
{spaces.data?.map((space) => (
|
|
11
|
+
<li
|
|
12
|
+
className="card w-80 h-32 shadow-xl text-gray-600 cursor-pointer hover:bg-gray-50 border"
|
|
13
|
+
key={space.id}
|
|
14
|
+
>
|
|
15
|
+
<Link href={`/space/${space.slug}`}>
|
|
16
|
+
<a>
|
|
17
|
+
<div className="card-body" title={space.name}>
|
|
18
|
+
<h2 className="card-title line-clamp-1">
|
|
19
|
+
{space.name}
|
|
20
|
+
</h2>
|
|
21
|
+
</div>
|
|
22
|
+
</a>
|
|
23
|
+
</Link>
|
|
24
|
+
</li>
|
|
25
|
+
))}
|
|
26
|
+
</ul>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import moment from 'moment';
|
|
2
|
+
|
|
3
|
+
type Props = {
|
|
4
|
+
value: { createdAt: Date; updatedAt: Date; completedAt?: Date | null };
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export default function TimeInfo({ value }: Props) {
|
|
8
|
+
return (
|
|
9
|
+
<p className="text-sm text-gray-500">
|
|
10
|
+
{value.completedAt
|
|
11
|
+
? `Completed ${moment(value.completedAt).fromNow()}`
|
|
12
|
+
: value.createdAt === value.updatedAt
|
|
13
|
+
? `Created ${moment(value.createdAt).fromNow()}`
|
|
14
|
+
: `Updated ${moment(value.updatedAt).fromNow()}`}
|
|
15
|
+
</p>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { TrashIcon } from '@heroicons/react/24/outline';
|
|
2
|
+
import { useTodo } from '@zenstackhq/runtime/hooks';
|
|
3
|
+
import { Todo, User } from '@zenstackhq/runtime/types';
|
|
4
|
+
import { ChangeEvent, useEffect, useState } from 'react';
|
|
5
|
+
import Avatar from './Avatar';
|
|
6
|
+
import TimeInfo from './TimeInfo';
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
value: Todo & { owner: User };
|
|
10
|
+
updated?: (value: Todo) => any;
|
|
11
|
+
deleted?: (value: Todo) => any;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default function Component({ value, updated, deleted }: Props) {
|
|
15
|
+
const [completed, setCompleted] = useState(!!value.completedAt);
|
|
16
|
+
const { update, del } = useTodo();
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!!value.completedAt !== completed) {
|
|
20
|
+
update(value.id, {
|
|
21
|
+
data: { completedAt: completed ? new Date() : null },
|
|
22
|
+
}).then((newValue) => {
|
|
23
|
+
if (updated) {
|
|
24
|
+
updated(newValue);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const deleteTodo = async () => {
|
|
31
|
+
await del(value.id);
|
|
32
|
+
if (deleted) {
|
|
33
|
+
deleted(value);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="border rounded-lg px-8 py-4 shadow-lg flex flex-col items-center w-full lg:w-[480px]">
|
|
39
|
+
<div className="flex justify-between w-full mb-4">
|
|
40
|
+
<h3
|
|
41
|
+
className={`text-xl line-clamp-1 ${
|
|
42
|
+
value.completedAt
|
|
43
|
+
? 'line-through text-gray-400 italic'
|
|
44
|
+
: 'text-gray-700'
|
|
45
|
+
}`}
|
|
46
|
+
>
|
|
47
|
+
{value.title}
|
|
48
|
+
</h3>
|
|
49
|
+
<div className="flex">
|
|
50
|
+
<input
|
|
51
|
+
type="checkbox"
|
|
52
|
+
className="checkbox mr-2"
|
|
53
|
+
checked={completed}
|
|
54
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
55
|
+
setCompleted(e.currentTarget.checked)
|
|
56
|
+
}
|
|
57
|
+
/>
|
|
58
|
+
<TrashIcon
|
|
59
|
+
className="w-6 h-6 text-gray-500 cursor-pointer"
|
|
60
|
+
onClick={() => {
|
|
61
|
+
deleteTodo();
|
|
62
|
+
}}
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
<div className="flex justify-end w-full space-x-2">
|
|
67
|
+
<TimeInfo value={value} />
|
|
68
|
+
<Avatar user={value.owner} size={18} />
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import Image from 'next/image';
|
|
2
|
+
import { List } from '@zenstackhq/runtime/types';
|
|
3
|
+
import { customAlphabet } from 'nanoid';
|
|
4
|
+
import { LockClosedIcon, TrashIcon } from '@heroicons/react/24/outline';
|
|
5
|
+
import { User } from 'next-auth';
|
|
6
|
+
import Avatar from './Avatar';
|
|
7
|
+
import Link from 'next/link';
|
|
8
|
+
import { useRouter } from 'next/router';
|
|
9
|
+
import { useList } from '@zenstackhq/runtime/hooks';
|
|
10
|
+
import TimeInfo from './TimeInfo';
|
|
11
|
+
|
|
12
|
+
type Props = {
|
|
13
|
+
value: List & { owner: User };
|
|
14
|
+
deleted?: (value: List) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default function TodoList({ value, deleted }: Props) {
|
|
18
|
+
const router = useRouter();
|
|
19
|
+
|
|
20
|
+
const { del } = useList();
|
|
21
|
+
|
|
22
|
+
const deleteList = async () => {
|
|
23
|
+
if (confirm('Are you sure to delete this list?')) {
|
|
24
|
+
await del(value.id);
|
|
25
|
+
if (deleted) {
|
|
26
|
+
deleted(value);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="card w-80 bg-base-100 shadow-xl cursor-pointer hover:bg-gray-50">
|
|
33
|
+
<Link href={`${router.asPath}/${value.id}`}>
|
|
34
|
+
<a>
|
|
35
|
+
<figure>
|
|
36
|
+
<Image
|
|
37
|
+
src={`https://picsum.photos/300/200?r=${customAlphabet(
|
|
38
|
+
'0123456789'
|
|
39
|
+
)(4)}`}
|
|
40
|
+
width={320}
|
|
41
|
+
height={200}
|
|
42
|
+
alt="Cover"
|
|
43
|
+
/>
|
|
44
|
+
</figure>
|
|
45
|
+
</a>
|
|
46
|
+
</Link>
|
|
47
|
+
<div className="card-body">
|
|
48
|
+
<Link href={`${router.asPath}/${value.id}`}>
|
|
49
|
+
<a>
|
|
50
|
+
<h2 className="card-title line-clamp-1">
|
|
51
|
+
{value.title || 'Missing Title'}
|
|
52
|
+
</h2>
|
|
53
|
+
</a>
|
|
54
|
+
</Link>
|
|
55
|
+
<div className="card-actions flex w-full justify-between">
|
|
56
|
+
<div>
|
|
57
|
+
<TimeInfo value={value} />
|
|
58
|
+
</div>
|
|
59
|
+
<div className="flex space-x-2">
|
|
60
|
+
<Avatar user={value.owner} size={18} />
|
|
61
|
+
{value.private && (
|
|
62
|
+
<div className="tooltip" data-tip="Private">
|
|
63
|
+
<LockClosedIcon className="w-4 h-4 text-gray-500" />
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
<TrashIcon
|
|
67
|
+
className="w-4 h-4 text-gray-500 cursor-pointer"
|
|
68
|
+
onClick={() => {
|
|
69
|
+
deleteList();
|
|
70
|
+
}}
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useSpace } from '@zenstackhq/runtime/hooks';
|
|
2
|
+
import { Space } from '@zenstackhq/runtime/types';
|
|
3
|
+
import { User } from 'next-auth';
|
|
4
|
+
import { useSession } from 'next-auth/react';
|
|
5
|
+
import { useRouter } from 'next/router';
|
|
6
|
+
import { createContext } from 'react';
|
|
7
|
+
|
|
8
|
+
export const UserContext = createContext<User | undefined>(undefined);
|
|
9
|
+
|
|
10
|
+
export function useCurrentUser() {
|
|
11
|
+
const { data: session } = useSession();
|
|
12
|
+
return session?.user;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const SpaceContext = createContext<Space | undefined>(undefined);
|
|
16
|
+
|
|
17
|
+
export function useCurrentSpace() {
|
|
18
|
+
const router = useRouter();
|
|
19
|
+
const { find } = useSpace();
|
|
20
|
+
const spaces = find({
|
|
21
|
+
where: {
|
|
22
|
+
slug: router.query.slug as string,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (!router.query.slug) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return spaces.data?.[0];
|
|
31
|
+
}
|