Re-implement the client

Not all website features are working, but this commit is mostly
focused on an architecture that can be used in future projects
This commit is contained in:
0x1eef 2024-07-31 22:48:00 -03:00
parent b8748509a4
commit 2bf57351ea
39 changed files with 5132 additions and 3635 deletions

View file

@ -17,14 +17,10 @@ AllCops:
- lib/**/*.rb - lib/**/*.rb
- libexec/* - libexec/*
- libexec/*/** - libexec/*/**
- nanoc/rules/*
- nanoc/lib/*
- bin/* - bin/*
- test/* - test/*
Exclude: Exclude:
- src/css/vendor/tail.css/bin/* - src/css/vendor/tail.css/bin/*
- src/tmp/*
- src/tmp/**/*
## ##
# Enabled # Enabled

View file

@ -4,3 +4,4 @@ gem "twenty-cli", path: "./cli"
gem "twenty-server", path: "./server" gem "twenty-server", path: "./server"
gem "twenty-client", path: "./client" gem "twenty-client", path: "./client"
gem "listen" gem "listen"
gem "server.rb", path: "../../libs/ruby/server.rb"

View file

@ -1,3 +1,10 @@
PATH
remote: ../../libs/ruby/server.rb
specs:
server.rb (0.2.2)
puma (~> 6.3)
rack (~> 3.0)
PATH PATH
remote: . remote: .
specs: specs:
@ -24,7 +31,6 @@ PATH
twenty-server (0.5.8) twenty-server (0.5.8)
graphql (~> 2.2) graphql (~> 2.2)
sequel (~> 5.78) sequel (~> 5.78)
server.rb (~> 0.2.2)
sqlite3 (~> 1.6) sqlite3 (~> 1.6)
GEM GEM
@ -43,11 +49,11 @@ GEM
rb-fsevent (~> 0.10, >= 0.10.3) rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10) rb-inotify (~> 0.9, >= 0.9.10)
mini_portile2 (2.8.6) mini_portile2 (2.8.6)
nio4r (2.7.1) nio4r (2.7.3)
paint (2.3.0) paint (2.3.0)
puma (6.4.2) puma (6.4.2)
nio4r (~> 2.0) nio4r (~> 2.0)
rack (3.0.10) rack (3.1.7)
rake (13.2.1) rake (13.2.1)
rb-fsevent (0.11.2) rb-fsevent (0.11.2)
rb-inotify (0.10.1) rb-inotify (0.10.1)
@ -55,9 +61,6 @@ GEM
ryo.rb (0.5.5) ryo.rb (0.5.5)
sequel (5.80.0) sequel (5.80.0)
bigdecimal bigdecimal
server.rb (0.2.2)
puma (~> 6.3)
rack (~> 3.0)
sqlite3 (1.7.3) sqlite3 (1.7.3)
mini_portile2 (~> 2.8.0) mini_portile2 (~> 2.8.0)
@ -68,6 +71,7 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
listen listen
rake (~> 13.2) rake (~> 13.2)
server.rb!
twenty! twenty!
twenty-cli! twenty-cli!
twenty-client! twenty-client!

View file

@ -5,26 +5,29 @@ purpose. But it's also a place where I can experiment with
a different stack for the development of [web] applications a different stack for the development of [web] applications
in Ruby. See **Design** for more info. in Ruby. See **Design** for more info.
## Features
* Provides a command-line utility to start / stop a web server
* Connect / disconnect a project from the command line
* Designed to work offline
* Lightweight stack
* Easy to install, easy to use
## Design ## Design
* The server is powered by [rack](https://github.com/rack/rack) and [puma](https://github.com/puma/puma): * The [server/](server/) is powered by Ruby
- Accepts GraphQL requests at `/graphql` - [rack](https://github.com/rack/rack#readmne),
- Serves client (HTML, JS, CSS) [graphql-ruby](https://github.com/rmosolgo/graphql-ruby#readme),
- Dependencies: Sequel, SQLite3, ruby-graphql and [puma](https://github.com/puma/puma#readme)
* The client is a statically compiled [nanoc](https://github.com/nanoc/nanoc) website: - The server provides the /graphql endpoint for client <-> server communication
- Dependencies: webpack, typescript, react - The server serves static files (HTML, JS, CSS, ...) via [puma (Ruby HTTP server)](https://github.com/puma/puma#readme)
* The CLI controls the web server: - The /graphql endpoint enters the [graphql-ruby](https://github.com/rmosolgo/graphql-ruby#readme) stack
* The [client/](client/) is powered by NodeJS
- [webpack](https://webpack.js.org/),
[typescript](https://www.typescriptlang.org/),
[react](https://react.dev/),
and [react-router](https://reactrouter.com/en/main)
- The client produces a build/ directory
- The client provides static files (HTML, JS, CSS, ...)
- The client provides routes via [react-router](https://reactrouter.com/en/main)
- The client communicates with the server via [@apollo/client (GraphQL client)](https://www.apollographql.com/docs/react/)
* The [cli](cli/) is powered by Ruby
- Start / stop web server - Start / stop web server
- Run database migrations - Run database migrations
- Run developer console - Run developer console
- Available as a RubyGem executable
* Each component (server, client, cli) are separate packages * Each component (server, client, cli) are separate packages
in a monorepo in a monorepo
* Easy to distribute as a RubyGem * Easy to distribute as a RubyGem

View file

@ -22,14 +22,10 @@ AllCops:
- lib/**/*.rb - lib/**/*.rb
- libexec/* - libexec/*
- libexec/*/** - libexec/*/**
- nanoc/rules/*
- nanoc/lib/*
- bin/* - bin/*
- test/* - test/*
Exclude: Exclude:
- src/css/vendor/tail.css/bin/* - src/css/vendor/tail.css/bin/*
- src/tmp/*
- src/tmp/**/*
## ##
# Enabled # Enabled

View file

@ -1,28 +0,0 @@
module.exports = {
extends: ["standard-with-typescript", "standard-jsx", "prettier"],
parserOptions: {
project: "./tsconfig.json",
},
rules: {
"@typescript-eslint/member-delimiter-style": 2,
"@typescript-eslint/semi": ["error", "always"],
"@typescript-eslint/no-extra-semi": "error",
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/strict-boolean-expressions": 0,
"@typescript-eslint/no-floating-promises": 0,
"@typescript-eslint/prefer-nullish-coalescing": 0,
"@typescript-eslint/restrict-template-expressions": 0,
"@typescript-eslint/promise-function-async": 0,
"@typescript-eslint/consistent-type-definitions": 0,
"@typescript-eslint/no-misused-promises": ["error", {"checksConditionals": false}],
"@typescript-eslint/no-redeclare": 0,
"@typescript-eslint/no-non-null-assertion": 0,
"@typescript-eslint/member-delimiter-style": 0,
"no-return-assign": 0,
"no-useless-return": 0,
"quotes": 0,
"object-curly-spacing": 2,
"n/no-callback-literal": 0,
"import/no-absolute-path": 0
},
};

View file

@ -22,14 +22,10 @@ AllCops:
- lib/**/*.rb - lib/**/*.rb
- libexec/* - libexec/*
- libexec/*/** - libexec/*/**
- nanoc/rules/*
- nanoc/lib/*
- bin/* - bin/*
- test/* - test/*
Exclude: Exclude:
- src/css/vendor/tail.css/bin/* - src/css/vendor/tail.css/bin/*
- src/tmp/*
- src/tmp/**/*
## ##
# Enabled # Enabled

View file

@ -1,7 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
source "https://rubygems.org" source "https://rubygems.org"
gemspec gemspec
require 'rbconfig' require 'rbconfig'
case RbConfig::CONFIG['target_os'] case RbConfig::CONFIG['target_os']
when /(?i-mx:bsd|dragonfly)/ when /(?i-mx:bsd|dragonfly)/

View file

@ -6,78 +6,16 @@ PATH
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2) ast (2.4.2)
colored (1.2)
concurrent-ruby (1.2.3)
cri (2.15.11)
ddmetrics (1.1.0)
ddplugin (1.0.3)
diff-lcs (1.5.1)
ffi (1.16.3)
immutable-ruby (0.1.0)
concurrent-ruby (~> 1.1)
sorted_set (~> 1.0)
json (2.7.1) json (2.7.1)
json_schema (0.21.0)
language_server-protocol (3.17.0.3) language_server-protocol (3.17.0.3)
lint_roller (1.1.0) lint_roller (1.1.0)
listen (3.8.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
memo_wise (1.8.0)
nanoc (4.12.19)
addressable (~> 2.5)
colored (~> 1.2)
nanoc-checking (~> 1.0, >= 1.0.2)
nanoc-cli (= 4.12.19)
nanoc-core (= 4.12.19)
nanoc-deploying (~> 1.0)
parallel (~> 1.12)
tty-command (~> 0.8)
tty-which (~> 0.4)
nanoc-checking (1.0.2)
nanoc-cli (~> 4.12, >= 4.12.4)
nanoc-core (~> 4.12, >= 4.12.4)
nanoc-cli (4.12.19)
cri (~> 2.15)
diff-lcs (~> 1.3)
nanoc-core (= 4.12.19)
zeitwerk (~> 2.1)
nanoc-core (4.12.19)
concurrent-ruby (~> 1.1)
ddmetrics (~> 1.0)
ddplugin (~> 1.0)
immutable-ruby (~> 0.1)
json_schema (~> 0.19)
memo_wise (~> 1.5)
psych (>= 4.0, < 6.0)
slow_enumerator_tools (~> 1.0)
tty-platform (~> 0.2)
zeitwerk (~> 2.1)
nanoc-deploying (1.0.2)
nanoc-checking (~> 1.0)
nanoc-cli (~> 4.11, >= 4.11.15)
nanoc-core (~> 4.11, >= 4.11.15)
nanoc-webpack.rb (0.10.6)
ryo.rb (~> 0.5)
test-cmd.rb (~> 0.12.4)
parallel (1.24.0) parallel (1.24.0)
parser (3.3.0.5) parser (3.3.0.5)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
pastel (0.8.0)
tty-color (~> 0.5)
psych (5.1.2)
stringio
public_suffix (5.0.4)
racc (1.7.3) racc (1.7.3)
rainbow (3.1.1) rainbow (3.1.1)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
rbtree (0.4.6)
regexp_parser (2.9.0) regexp_parser (2.9.0)
rexml (3.2.8) rexml (3.2.8)
strscan (>= 3.0.9) strscan (>= 3.0.9)
@ -98,12 +36,6 @@ GEM
rubocop (>= 1.48.1, < 2.0) rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.30.0, < 2.0) rubocop-ast (>= 1.30.0, < 2.0)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ryo.rb (0.5.5)
set (1.1.0)
slow_enumerator_tools (1.1.0)
sorted_set (1.0.3)
rbtree
set (~> 1.0)
standard (1.35.1) standard (1.35.1)
language_server-protocol (~> 3.17.0.2) language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.0) lint_roller (~> 1.0)
@ -116,16 +48,8 @@ GEM
standard-performance (1.3.1) standard-performance (1.3.1)
lint_roller (~> 1.1) lint_roller (~> 1.1)
rubocop-performance (~> 1.20.2) rubocop-performance (~> 1.20.2)
stringio (3.1.0)
strscan (3.1.0) strscan (3.1.0)
test-cmd.rb (0.12.4)
tty-color (0.6.0)
tty-command (0.10.1)
pastel (~> 0.8)
tty-platform (0.3.0)
tty-which (0.5.0)
unicode-display_width (2.5.0) unicode-display_width (2.5.0)
zeitwerk (2.6.13)
PLATFORMS PLATFORMS
amd64-freebsd-14 amd64-freebsd-14
@ -133,9 +57,6 @@ PLATFORMS
x86_64-openbsd x86_64-openbsd
DEPENDENCIES DEPENDENCIES
listen (~> 3.8)
nanoc (~> 4.12)
nanoc-webpack.rb (~> 0.10.6)
standard (~> 1.35) standard (~> 1.35)
twenty-client! twenty-client!

View file

@ -1,26 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require "nanoc-webpack"
def require_rules(rules, locals = {}, target = binding)
locals.each { target.local_variable_set(_1, _2) }
path = File.join(Dir.getwd, rules)
target.eval(
if File.readable?(path)
File.read(path)
elsif File.readable?("#{path}.rb")
File.read("#{path}.rb")
elsif File.readable?("#{path}.rules")
File.read("#{path}.rules")
else
raise LoadError, "#{path} is not readable"
end
)
end
require_rules "nanoc/rules/assets"
require_rules "nanoc/rules/react"
compile("/**/*") { write(nil) }
layout "/**/*", :erb

View file

@ -16,9 +16,6 @@ Gem::Specification.new do |gem|
gem.description = "#{gem.summary}. " \ gem.description = "#{gem.summary}. " \
"Static content (HTML, CSS, JS). " \ "Static content (HTML, CSS, JS). " \
"See https://rubygems.org/gems/twenty for context." "See https://rubygems.org/gems/twenty for context."
gem.add_development_dependency "nanoc", "~> 4.12"
gem.add_development_dependency "nanoc-webpack.rb", "~> 0.10.6"
gem.add_development_dependency "listen", "~> 3.8"
gem.add_development_dependency "standard", "~> 1.35" gem.add_development_dependency "standard", "~> 1.35"
gem.metadata = { "source_code_uri" => "https://github.com/0x1eef/twenty#readme" } gem.metadata = { "source_code_uri" => "https://github.com/0x1eef/twenty#readme" }
end end

View file

@ -16,9 +16,6 @@ Gem::Specification.new do |gem|
gem.description = "#{gem.summary}. " \ gem.description = "#{gem.summary}. " \
"Static content (HTML, CSS, JS). " \ "Static content (HTML, CSS, JS). " \
"See https://rubygems.org/gems/<%= parent %> for context." "See https://rubygems.org/gems/<%= parent %> for context."
gem.add_development_dependency "nanoc", "~> 4.12"
gem.add_development_dependency "nanoc-webpack.rb", "~> 0.10.6"
gem.add_development_dependency "listen", "~> 3.8"
gem.add_development_dependency "standard", "~> 1.35" gem.add_development_dependency "standard", "~> 1.35"
gem.metadata = { "source_code_uri" => "https://github.com/0x1eef/<%= parent %>#readme" } gem.metadata = { "source_code_uri" => "https://github.com/0x1eef/<%= parent %>#readme" }
end end

16
client/eslint.config.mjs Normal file
View file

@ -0,0 +1,16 @@
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import prettier from 'eslint-plugin-prettier/recommended';
export default tseslint.config(
{ignores: ["src/js/types/schema.ts"]},
eslint.configs.recommended,
...tseslint.configs.recommended,
prettier,
{
rules: {
'@typescript-eslint/no-require-imports': 0
},
}
)

View file

@ -1,23 +0,0 @@
# A list of file extensions that Nanoc will consider to be textual rather than
# binary. If an item with an extension not in this list is found, the file
# will be considered as binary.
text_extensions: [ 'adoc', 'asciidoc', 'atom', 'coffee', 'css', 'erb', 'haml', 'handlebars', 'hb', 'htm', 'html', 'js', 'less', 'markdown', 'md', 'ms', 'mustache', 'php', 'rb', 'rdoc', 'sass', 'scss', 'slim', 'tex', 'txt', 'xhtml', 'xml', 'ts', 'tsx' ]
prune:
auto_prune: true
lib_dirs: ['nanoc/lib']
output_dir: build/
data_sources:
- type: filesystem
encoding: utf-8
content_dir: src/
layouts_dir: src/layouts
server:
unix:
path: /tmp/github.com.0x1eef.twenty
tcp:
host: 127.0.0.1
port: 7777

View file

@ -1,10 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
compile("/fonts/*.ttf") do
write(item.identifier.to_s)
end
compile("/favicon.svg") do
write(item.identifier.to_s)
end

View file

@ -1,25 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
{
"Task" => ["tasks/new", "tasks/edit"],
"Tasks" => ["/", "tasks"],
"Projects" => ["projects"]
}.each do |component, paths|
compile "/html/react.html.erb", rep: component do
filter(:erb, locals: {component: "react-#{component.downcase}", src: "/js/main.js"})
paths.each do |path|
(path == "/") ? write("/index.html") : write("/#{path}/index.html")
end
end
end
compile "/js/main/main.tsx" do
buildenv = ENV["buildenv"] || "development"
filter(
:webpack,
argv: ["--mode", buildenv, "--config", "webpack.#{buildenv}.js"],
depend_on: %w[/js/components /js/hooks /js/types /js/lib /css]
)
write("/js/main.js")
end

8206
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,35 +1,41 @@
{ {
"name": "twenty", "name": "twenty",
"devDependencies": { "devDependencies": {
"@apollo/client": "^3.3.21", "@apollo/client": "^3.3",
"@graphql-codegen/cli": "^5.0.0", "@graphql-codegen/cli": "^5.0",
"@graphql-codegen/typescript": "^4.0.1", "@graphql-codegen/typescript": "^4.0",
"@graphql-codegen/typescript-resolvers": "^4.0.1", "@graphql-codegen/typescript-resolvers": "^4.0",
"@types/luxon": "^3.3.7", "@types/luxon": "^3.3",
"@types/react": "^18.0.18", "@types/react": "^18.0",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0",
"@types/showdown": "^2.0.6", "@types/showdown": "^2.0",
"classnames": "^2.3.2", "classnames": "^2.3",
"css-loader": "^7.1.2", "clean-webpack-plugin": "^4.0.0",
"esbuild-loader": "^4.1.0", "copy-webpack-plugin": "^12.0",
"eslint": "^8.26.0", "css-loader": "^7.1",
"eslint-config-prettier": "^8.5.0", "esbuild-loader": "^4.1",
"graphql": "^16.8.1", "eslint": "^9.8",
"luxon": "^3.4.4", "eslint-config-prettier": "^9.1",
"prettier": "^2.7.1", "eslint-plugin-prettier": "^5.2.1",
"react": "^18.2.0", "graphql": "^16.8",
"react-dom": "^18.2.0", "html-webpack-plugin": "^5.6",
"react-hook-form": "^7.49.2", "luxon": "^3.4",
"sass": "^1.77.8", "prettier": "^3.3",
"sass-loader": "^16.0.0", "react": "^18.2",
"showdown": "^2.1.0", "react-dom": "^18.2",
"style-loader": "^4.0.0", "react-hook-form": "^7.49",
"ts-standard": "^12.0.1", "react-router-dom": "^6.25.1",
"tslib": "^2.2.0", "sass": "^1.77",
"typescript": "^4.8.2", "sass-loader": "^16.0",
"webpack": "^5.91.0", "showdown": "^2.1",
"webpack-cli": "^4.10.0", "style-loader": "^4.0",
"webpack-merge": "^5.10.0" "tslib": "^2.2",
"typescript": "^5.5",
"typescript-eslint": "^8.0.0-alpha.58",
"webpack": "^5.93",
"webpack-cli": "^5.1",
"webpack-dev-server": "^5.0",
"webpack-merge": "^6.0"
}, },
"scripts": { "scripts": {
"eslint": "npm exec eslint -- --fix src/js/", "eslint": "npm exec eslint -- --fix src/js/",

View file

@ -6,7 +6,7 @@
<link rel="icon" href="/favicon.svg"/> <link rel="icon" href="/favicon.svg"/>
</head> </head>
<body> <body>
<div class="<%= component %> w-full wrapper font-sans"></div> <div class="w-full wrapper font-sans react-app"></div>
<script src="<%= src %>"></script> <script src="<%= mainjs %>"></script>
</body> </body>
</html> </html>

View file

@ -1,8 +1,12 @@
import { PropsWithChildren } from "react"; import { createBrowserRouter, RouterProvider } from "react-router-dom";
import React from "react";
import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client"; import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client";
import { loadErrorMessages, loadDevMessages } from "@apollo/client/dev";
import { AppContext } from "~/Context"; import { AppContext } from "~/Context";
import { Tasks } from "~/components/Tasks";
import { Task } from "~/components/Task";
export function App({ children }: PropsWithChildren<{}>) { export function App() {
const client = new ApolloClient({ const client = new ApolloClient({
uri: "/graphql", uri: "/graphql",
cache: new InMemoryCache(), cache: new InMemoryCache(),
@ -16,6 +20,24 @@ export function App({ children }: PropsWithChildren<{}>) {
const cookies = Object.fromEntries( const cookies = Object.fromEntries(
document.cookie.split(";").map(e => e.split("=")), document.cookie.split(";").map(e => e.split("=")),
); );
const router = createBrowserRouter([
{
path: "/",
element: <Tasks />,
},
{
path: "/tasks",
element: <Tasks />
},
{
path: "/tasks/new",
element: <Task />
},
{
path: "/tasks/edit",
element: <Task />
}
]);
/* allowlist: param keys acceptable as cookie keys */ /* allowlist: param keys acceptable as cookie keys */
const allowlist = ["projectId"]; const allowlist = ["projectId"];
Object.entries(params).forEach(([key, value]) => { Object.entries(params).forEach(([key, value]) => {
@ -27,9 +49,12 @@ export function App({ children }: PropsWithChildren<{}>) {
document.cookie = `${key}=${value}; path=/`; document.cookie = `${key}=${value}; path=/`;
} }
}); });
loadDevMessages();
loadErrorMessages();
return ( return (
<AppContext.Provider value={{ params, cookies }}> <ApolloProvider client={client}>
<ApolloProvider client={client}>{children}</ApolloProvider> <AppContext.Provider value={{ params, cookies }}/>
</AppContext.Provider> <RouterProvider router={router} />
</ApolloProvider>
); );
} }

View file

@ -1,3 +1,4 @@
import React from "react";
import type { Task } from "~/types/schema"; import type { Task } from "~/types/schema";
import classnames from "classnames"; import classnames from "classnames";
import { DateTime } from "luxon"; import { DateTime } from "luxon";

View file

@ -1,4 +1,4 @@
import { useContext } from "react"; import React, { useContext } from "react";
import { AppContext } from "~/Context"; import { AppContext } from "~/Context";
import { Maybe } from "~/types/schema"; import { Maybe } from "~/types/schema";
import { ProjectSelect } from "~/components/ProjectSelect"; import { ProjectSelect } from "~/components/ProjectSelect";

View file

@ -1,3 +1,4 @@
import React from "react";
import { useProjects } from "~/hooks/queries/useProjects"; import { useProjects } from "~/hooks/queries/useProjects";
import { Project, Maybe } from "~/types/schema"; import { Project, Maybe } from "~/types/schema";
import { Select, Option } from "~/components/Select"; import { Select, Option } from "~/components/Select";
@ -47,5 +48,4 @@ export function ProjectSelect({ onChange, selected }: Props) {
}} }}
/> />
); );
1;
} }

View file

@ -1,4 +1,4 @@
import { ReactNode, useState, useEffect } from "react"; import React, { ReactNode, useState, useEffect } from "react";
import { Filter } from "./Filter"; import { Filter } from "./Filter";
const LI_CLASSNAME = [ const LI_CLASSNAME = [

View file

@ -1,4 +1,4 @@
import { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import classnames from "classnames"; import classnames from "classnames";
export type Tab = { export type Tab = {

View file

@ -1,4 +1,4 @@
import { useEffect, useState, useContext } from "react"; import React, { useEffect, useState, useContext } from "react";
import { AppContext } from "~/Context"; import { AppContext } from "~/Context";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useCreateTask } from "~/hooks/mutations/useCreateTask"; import { useCreateTask } from "~/hooks/mutations/useCreateTask";
@ -35,7 +35,11 @@ export function Task() {
const res = await createTask({ variables: { input } }); const res = await createTask({ variables: { input } });
const payload = res?.data?.createTask; const payload = res?.data?.createTask;
const { errors } = payload; const { errors } = payload;
errors.length ? alert(errors) : (location.href = "/tasks"); if (errors.length) {
alert(errors);
} else {
location.href = "/tasks";
}
} }
}; };
@ -78,7 +82,11 @@ export function Task() {
labels={["Editor", "Preview"]} labels={["Editor", "Preview"]}
defaultLabel={taskId ? "preview" : "editor"} defaultLabel={taskId ? "preview" : "editor"}
onChange={(tab: Tab) => { onChange={(tab: Tab) => {
tab.id === "editor" ? setIsEditable(true) : setIsEditable(false); if (tab.id === "editor") {
setIsEditable(true);
} else {
setIsEditable(false);
}
}} }}
/> />
{isEditable ? ( {isEditable ? (

View file

@ -1,3 +1,4 @@
import React from "react";
import { Task, TaskStatus } from "~/types/schema"; import { Task, TaskStatus } from "~/types/schema";
import { useUpdateTask } from "~/hooks/mutations/useUpdateTask"; import { useUpdateTask } from "~/hooks/mutations/useUpdateTask";
import { GET_TASKS } from "~/hooks/queries/useTasks"; import { GET_TASKS } from "~/hooks/queries/useTasks";

View file

@ -1,4 +1,4 @@
import { useEffect, useContext } from "react"; import React, { useEffect, useContext } from "react";
import { AppContext } from "~/Context"; import { AppContext } from "~/Context";
import { NavBar } from "~/components/NavBar"; import { NavBar } from "~/components/NavBar";
import { Group } from "~/components/Group"; import { Group } from "~/components/Group";

View file

@ -1,36 +1,9 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import { App } from "~/components/App"; import { App } from "~/components/App";
import { Tasks } from "~/components/Tasks";
import { Projects } from "~/components/Projects";
import { Task } from "~/components/Task";
import "@css/main.scss"; import "@css/main.scss";
(function () { (function () {
const components = { const root = document.querySelector(".react-app");
Task: () => ( ReactDOM.createRoot(root).render(<App/>);
<App>
<Task />
</App>
),
Tasks: () => (
<App>
<Tasks />
</App>
),
Projects: () => (
<App>
<Projects />
</App>
),
};
const ents = Object.entries(components);
for (let i = 0; i < ents.length; i++) {
const [component, getJSX] = ents[i];
const root = document.querySelector(`.react-${component.toLowerCase()}`);
if (root) {
ReactDOM.createRoot(root).render(getJSX());
break;
}
}
})(); })();

View file

@ -1,22 +1,30 @@
const webpack = require('webpack'); const webpack = require("webpack");
const path = require('path'); const HtmlWebpackPlugin = require("html-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const path = require("path");
module.exports = { module.exports = {
entry: path.resolve(__dirname, "src/js/main/main.tsx"),
output: {
path: path.resolve(__dirname, "build"),
filename: "static/js/main.js"
},
resolve: { resolve: {
alias: { alias: {
'~': [path.resolve('src/js')], "~": [path.resolve(__dirname, "src/js")],
'@css': [path.resolve('src/css')] "@css": [path.resolve(__dirname, "src/css")]
}, },
extensions: ['.ts', '.tsx', '.scss'] extensions: [".ts", ".tsx", ".scss"]
}, },
module: { module: {
rules: [ rules: [
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
loader: 'esbuild-loader', loader: "esbuild-loader",
options: { options: {
loader: 'tsx', loader: "tsx",
target: 'es2015' target: "es2015"
} }
}, },
{ {
@ -31,8 +39,9 @@ module.exports = {
], ],
}, },
plugins: [ plugins: [
new webpack.ProvidePlugin({ new CleanWebpackPlugin(),
React: 'react', new CopyWebpackPlugin({patterns: [
}), {from: "./src/favicon.svg", to: "favicon.ico"}
]}),
], ],
} }

View file

@ -1,6 +1,18 @@
const { merge } = require('webpack-merge'); const { merge } = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const common = require('./webpack.common.js'); const common = require('./webpack.common.js');
const path = require('path');
module.exports = merge( module.exports = merge(
common, common,
{mode: "development"} { mode: "development", devServer: {static: './build'} },
{ plugins: [new HtmlWebpackPlugin({
inject: false,
templateParameters: {
mainjs: '/static/js/main.js'
}
})]},
) )
console.log(module.exports)

View file

@ -3,18 +3,17 @@
cwd = File.realpath File.join(__dir__, "..", "..", "..", "client") cwd = File.realpath File.join(__dir__, "..", "..", "..", "client")
desc "Start web server" desc "Start web server"
task :server, [:protocol] do |_t, args| task :server, [:protocol] do |_t, args|
nanoc = Ryo.from_yaml(path: File.join(cwd, "nanoc.yaml"))
h = args.to_h h = args.to_h
p = h[:protocol] || "tcp" p = h[:protocol] || "tcp"
n = File.basename File.dirname(cwd) n = File.basename File.dirname(cwd)
Process.setproctitle "rake server[#{p}] [#{n}]" Process.setproctitle "rake server[#{p}] [#{n}]"
if p == "unix" if p == "unix"
Twenty::Command::Up Twenty::Command::Up
.new(["-u", nanoc.server.unix.path]) .new(["-u", "/tmp/www/twenty.al-ridwan.home.network"])
.run .run
else else
Twenty::Command::Up Twenty::Command::Up
.new(["-b", nanoc.server.tcp.host, "-p", nanoc.server.tcp.port]) .new(["-b", "127.0.0.1", "-p", "2222"])
.run .run
end end
end end

View file

@ -22,14 +22,10 @@ AllCops:
- lib/**/*.rb - lib/**/*.rb
- libexec/* - libexec/*
- libexec/*/** - libexec/*/**
- nanoc/rules/*
- nanoc/lib/*
- bin/* - bin/*
- test/* - test/*
Exclude: Exclude:
- src/css/vendor/tail.css/bin/* - src/css/vendor/tail.css/bin/*
- src/tmp/*
- src/tmp/**/*
## ##
# Enabled # Enabled

View file

@ -1,3 +1,4 @@
# frozen_string_literal: true # frozen_string_literal: true
source "https://rubygems.org" source "https://rubygems.org"
gem 'server.rb', path: '../../../libs/ruby/server.rb'
gemspec gemspec

View file

@ -1,10 +1,16 @@
PATH
remote: ../../../libs/ruby/server.rb
specs:
server.rb (0.2.2)
puma (~> 6.3)
rack (~> 3.0)
PATH PATH
remote: . remote: .
specs: specs:
twenty-server (0.5.8) twenty-server (0.5.8)
graphql (~> 2.2) graphql (~> 2.2)
sequel (~> 5.78) sequel (~> 5.78)
server.rb (~> 0.2.2)
sqlite3 (~> 1.6) sqlite3 (~> 1.6)
GEM GEM
@ -52,9 +58,6 @@ GEM
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
sequel (5.78.0) sequel (5.78.0)
bigdecimal bigdecimal
server.rb (0.2.2)
puma (~> 6.3)
rack (~> 3.0)
sqlite3 (1.7.3) sqlite3 (1.7.3)
mini_portile2 (~> 2.8.0) mini_portile2 (~> 2.8.0)
sqlite3 (1.7.3-aarch64-linux) sqlite3 (1.7.3-aarch64-linux)
@ -91,6 +94,7 @@ PLATFORMS
x86_64-linux x86_64-linux
DEPENDENCIES DEPENDENCIES
server.rb!
standard (~> 1.35) standard (~> 1.35)
test-unit (~> 3.5.7) test-unit (~> 3.5.7)
twenty-server! twenty-server!

View file

@ -2,30 +2,54 @@
module Twenty::Rack module Twenty::Rack
module GraphQL module GraphQL
extend self
STATIC = [%r|/static|, %r|/favicon.ico|]
## ##
# Extends {Server::Dir Server::Dir} (a static file
# Rack application) with a /graphql endpoint
#
# @param [Hash] env # @param [Hash] env
# Environment hash # Environment hash
# # @return [[Integer, Hash, #each]
# @return [Array<Integer, Hash, #each>]
# Returns a response # Returns a response
def call(env) def call(env)
req = Rack::Request.new(env) req = Rack::Request.new(env)
if req.post? && if req.post?
req.path == "/graphql" graphql(req)
elsif req.get? || req.head?
file(req)
else
[404, {}, "".each_line]
end
end
private
def graphql(req)
if req.path =~ %r|/graphql|
params = JSON.parse(req.body.string) params = JSON.parse(req.body.string)
result = Twenty::GraphQL::Schema.execute( body = Twenty::GraphQL::Schema.execute(
params["query"], params["query"],
variables: params["variables"], variables: params["variables"],
context: {} context: {}
) ).to_json
[200, {"content-type" => "application/json"}, [result.to_json]] head = { "content-length" => body.bytesize, "content-type" => "application/json" }
[200, head, body.each_line]
else else
super(env) body = { errors: ["Request path was not found"] }.to_json
head = { "content-length" => body.bytesize, "content-type" => "application/json" }
[404, {}, body.each_line]
end
end
def file(req)
if STATIC.find { req.path =~ _1 }
dir = Server::Dir.new(Twenty.build)
dir.call(req.env)
else
path = File.join(Twenty.build, "index.html")
body = File.binread(path)
head = { "content-length" => body.bytesize, "content-type" => "text/html" }
[200, head, body.each_line]
end end
end end
Server::Dir.prepend(self)
end end
end end

View file

@ -7,9 +7,11 @@ module Twenty::Rack
## ##
# @param [Hash, #to_h] options # @param [Hash, #to_h] options
# Hash of server options # Hash of server options
#
# @return [Thread] # @return [Thread]
def self.server(options = {}) def self.server(options = {})
Server.dir(Twenty.build, options.to_h) Server.new Rack::Builder.app {
use Server::ETag
run Twenty::Rack::GraphQL
}, Server.prepare(options)
end end
end end

View file

@ -20,7 +20,7 @@ Gem::Specification.new do |gem|
gem.add_runtime_dependency "sequel", "~> 5.78" gem.add_runtime_dependency "sequel", "~> 5.78"
gem.add_runtime_dependency "sqlite3", "~> 1.6" gem.add_runtime_dependency "sqlite3", "~> 1.6"
gem.add_runtime_dependency "graphql", "~> 2.2" gem.add_runtime_dependency "graphql", "~> 2.2"
gem.add_runtime_dependency "server.rb", "~> 0.2.2" #gem.add_runtime_dependency "server.rb", "~> 0.2"
gem.add_development_dependency "test-unit", "~> 3.5.7" gem.add_development_dependency "test-unit", "~> 3.5.7"
gem.add_development_dependency "standard", "~> 1.35" gem.add_development_dependency "standard", "~> 1.35"
gem.metadata = { "source_code_uri" => "https://github.com/0x1eef/twenty#readme" } gem.metadata = { "source_code_uri" => "https://github.com/0x1eef/twenty#readme" }

View file

@ -20,7 +20,7 @@ Gem::Specification.new do |gem|
gem.add_runtime_dependency "sequel", "~> 5.78" gem.add_runtime_dependency "sequel", "~> 5.78"
gem.add_runtime_dependency "sqlite3", "~> 1.6" gem.add_runtime_dependency "sqlite3", "~> 1.6"
gem.add_runtime_dependency "graphql", "~> 2.2" gem.add_runtime_dependency "graphql", "~> 2.2"
gem.add_runtime_dependency "server.rb", "~> 0.2.2" #gem.add_runtime_dependency "server.rb", "~> 0.2"
gem.add_development_dependency "test-unit", "~> 3.5.7" gem.add_development_dependency "test-unit", "~> 3.5.7"
gem.add_development_dependency "standard", "~> 1.35" gem.add_development_dependency "standard", "~> 1.35"
gem.metadata = { "source_code_uri" => "https://github.com/0x1eef/<%= parent %>#readme" } gem.metadata = { "source_code_uri" => "https://github.com/0x1eef/<%= parent %>#readme" }