add: finish forge stuff
This commit is contained in:
parent
53fb4d5778
commit
68071b96c8
21 changed files with 329 additions and 18 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -3288,7 +3288,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tetratto"
|
||||
version = "5.0.0"
|
||||
version = "6.0.0"
|
||||
dependencies = [
|
||||
"ammonia",
|
||||
"async-stripe",
|
||||
|
@ -3319,7 +3319,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tetratto-core"
|
||||
version = "5.0.0"
|
||||
version = "6.0.0"
|
||||
dependencies = [
|
||||
"async-recursion",
|
||||
"base16ct",
|
||||
|
@ -3341,7 +3341,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tetratto-l10n"
|
||||
version = "5.0.0"
|
||||
version = "6.0.0"
|
||||
dependencies = [
|
||||
"pathbufd",
|
||||
"serde",
|
||||
|
@ -3350,7 +3350,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tetratto-shared"
|
||||
version = "5.0.0"
|
||||
version = "6.0.0"
|
||||
dependencies = [
|
||||
"ammonia",
|
||||
"chrono",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "tetratto"
|
||||
version = "5.0.0"
|
||||
version = "6.0.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
|
|
|
@ -119,6 +119,7 @@ pub const STACKS_MANAGE: &str = include_str!("./public/html/stacks/manage.lisp")
|
|||
pub const FORGE_HOME: &str = include_str!("./public/html/forge/home.lisp");
|
||||
pub const FORGE_BASE: &str = include_str!("./public/html/forge/base.lisp");
|
||||
pub const FORGE_INFO: &str = include_str!("./public/html/forge/info.lisp");
|
||||
pub const FORGE_TICKETS: &str = include_str!("./public/html/forge/tickets.lisp");
|
||||
|
||||
// langs
|
||||
pub const LANG_EN_US: &str = include_str!("./langs/en-US.toml");
|
||||
|
@ -392,6 +393,7 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD {
|
|||
write_template!(html_path->"forge/home.html"(crate::assets::FORGE_HOME) -d "forge" --config=config --lisp plugins);
|
||||
write_template!(html_path->"forge/base.html"(crate::assets::FORGE_BASE) --config=config --lisp plugins);
|
||||
write_template!(html_path->"forge/info.html"(crate::assets::FORGE_INFO) --config=config --lisp plugins);
|
||||
write_template!(html_path->"forge/tickets.html"(crate::assets::FORGE_TICKETS) --config=config --lisp plugins);
|
||||
|
||||
html_path
|
||||
}
|
||||
|
|
|
@ -208,3 +208,5 @@ version = "1.0.0"
|
|||
"forge:label.create_new" = "Create new forge"
|
||||
"forge:tab.info" = "Info"
|
||||
"forge:tab.tickets" = "Tickets"
|
||||
"forge:action.reopen" = "Reopen"
|
||||
"forge:action.close" = "Close"
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
--color-red: hsl(0, 84%, 40%);
|
||||
--color-green: hsl(100, 84%, 20%);
|
||||
--color-yellow: hsl(41, 63%, 75%);
|
||||
--color-purple: hsl(284, 84%, 20%);
|
||||
--radius: 6px;
|
||||
--circle: 360px;
|
||||
--shadow-x-offset: 0;
|
||||
|
@ -63,6 +64,7 @@
|
|||
--color-red: hsl(0, 94%, 82%);
|
||||
--color-green: hsl(100, 94%, 82%);
|
||||
--color-yellow: hsl(41, 63%, 65%);
|
||||
--color-purple: hsl(284, 94%, 82%);
|
||||
}
|
||||
|
||||
* {
|
||||
|
|
|
@ -179,6 +179,14 @@
|
|||
color: var(--color-green) !important;
|
||||
}
|
||||
|
||||
.yellow {
|
||||
color: var(--color-yellow) !important;
|
||||
}
|
||||
|
||||
.purple {
|
||||
color: var(--color-purple) !important;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
|
|
@ -115,7 +115,6 @@
|
|||
(div
|
||||
("style" "display: contents")
|
||||
(text "{{ self::post(post=post, owner=owner, secondary=secondary, community=community, show_community=show_community, can_manage_post=can_manage_post, repost=repost, expect_repost=true) }}"))
|
||||
|
||||
(text "{%- endmacro %} {% macro post(post, owner, question=false, secondary=false, community=false, show_community=true, can_manage_post=false, repost=false, expect_repost=false, poll=false, dont_show_title=false) -%} {% if community and show_community and community.id != config.town_square or question %}")
|
||||
(div
|
||||
("class" "card-nest")
|
||||
|
@ -174,7 +173,19 @@
|
|||
("class" "flex items-center")
|
||||
("style" "color: var(--color-primary)")
|
||||
(text "{{ icon \"square-asterisk\" }}"))
|
||||
(text "{%- endif %} {% if post.context.repost and post.context.repost.reposting %}")
|
||||
(text "{%- endif %}")
|
||||
(text "{% if community and community.is_forge -%} {% if post.is_open -%}")
|
||||
(span
|
||||
("title" "Open")
|
||||
("class" "flex items-center green")
|
||||
(text "{{ icon \"circle-dot\" }}"))
|
||||
(text "{% else %}")
|
||||
(span
|
||||
("title" "Closed")
|
||||
("class" "flex items-center purple")
|
||||
(text "{{ icon \"circle-check\" }}"))
|
||||
(text "{%- endif %} {%- endif %}")
|
||||
(text "{% if post.context.repost and post.context.repost.reposting %}")
|
||||
(span
|
||||
("title" "Repost")
|
||||
("class" "flex items-center")
|
||||
|
@ -215,7 +226,7 @@
|
|||
; title
|
||||
(text "{% if post.title and community and community.context.enable_titles -%}")
|
||||
(h2 (text "{{ post.title }}"))
|
||||
(hr ("class" "margin"))
|
||||
(hr ("class" "margin") ("style" "margin-top: var(--pad-2)"))
|
||||
(text "{%- endif %}")
|
||||
|
||||
; content
|
||||
|
@ -323,7 +334,23 @@
|
|||
(text "{{ icon \"quote\" }}")
|
||||
(span
|
||||
(text "{{ text \"communities:label.quote_post\" }}")))
|
||||
(text "{%- endif %} {% if user.id != post.owner -%}")
|
||||
(text "{%- endif %}")
|
||||
(text "{% if community and community.is_forge -%} {% if post.is_open -%}")
|
||||
(button
|
||||
("class" "green")
|
||||
("onclick" "trigger('me::update_open', ['{{ post.id }}', false])")
|
||||
(text "{{ icon \"circle-check\" }}")
|
||||
(span
|
||||
(text "{{ text \"forge:action.close\" }}")))
|
||||
(text "{% else %}")
|
||||
(button
|
||||
("class" "purple")
|
||||
("onclick" "trigger('me::update_open', ['{{ post.id }}', true])")
|
||||
(text "{{ icon \"refresh-ccw-dot\" }}")
|
||||
(span
|
||||
(text "{{ text \"forge:action.reopen\" }}")))
|
||||
(text "{%- endif %} {%- endif %}")
|
||||
(text "{% if user.id != post.owner -%}")
|
||||
(b
|
||||
("class" "title")
|
||||
(text "{{ text \"general:label.safety\" }}"))
|
||||
|
@ -1746,3 +1773,42 @@
|
|||
(text "{%- endif %}"))
|
||||
(text "{%- endif %}")
|
||||
(text "{%- endmacro %}")
|
||||
|
||||
(text "{% macro ticket(post, owner) -%}")
|
||||
(div
|
||||
("href" "/post/{{ post.id }}")
|
||||
("class" "card secondary w-fill flex flex-col gap-2")
|
||||
(div
|
||||
("class" "flex gap-2 items-center")
|
||||
; user info
|
||||
(a
|
||||
("href" "/@{{ owner.username }}")
|
||||
(text "{{ self::avatar(username=owner.username, size=\"24px\", selector_type=\"username\") }}"))
|
||||
(span
|
||||
("class" "name")
|
||||
(text "{{ self::full_username(user=owner) }}"))
|
||||
|
||||
; timestamp
|
||||
(span ("class" "date") (text "{{ post.created }}")))
|
||||
|
||||
; post title
|
||||
(a
|
||||
("href" "/post/{{ post.id }}")
|
||||
("class" "flush flex gap-2 items-center")
|
||||
; open/closed icon
|
||||
(text "{% if community and community.is_forge -%} {% if post.is_open -%}")
|
||||
(span
|
||||
("title" "Open")
|
||||
("class" "flex items-center green")
|
||||
(text "{{ icon \"circle-dot\" }}"))
|
||||
(text "{% else %}")
|
||||
(span
|
||||
("title" "Closed")
|
||||
("class" "flex items-center purple")
|
||||
(text "{{ icon \"circle-check\" }}"))
|
||||
(text "{%- endif %} {%- endif %}")
|
||||
|
||||
(h4
|
||||
("class" "no_p_margin")
|
||||
(text "{{ post.title|markdown|safe }}"))))
|
||||
(text "{%- endmacro %}")
|
||||
|
|
|
@ -3,4 +3,5 @@
|
|||
("class" "flex flex-col gap-4 w-full")
|
||||
(text "{{ macros::forge_nav(community=community, selected=\"info\") }}")
|
||||
(text "{{ components::community_info(community=community) }}"))
|
||||
; (text "{{ components::community_banner(id=community.id, community=community) }}")
|
||||
(text "{% endblock %}")
|
||||
|
|
18
crates/app/src/public/html/forge/tickets.lisp
Normal file
18
crates/app/src/public/html/forge/tickets.lisp
Normal file
|
@ -0,0 +1,18 @@
|
|||
(text "{% extends \"forge/base.html\" %} {% block content %}")
|
||||
(div
|
||||
("class" "flex flex-col gap-4 w-full")
|
||||
(text "{{ macros::forge_nav(community=community, selected=\"tickets\") }}")
|
||||
(div
|
||||
("class" "card-nest")
|
||||
(div
|
||||
("class" "card small flex items-center gap-2")
|
||||
(icon (text "circle-dot"))
|
||||
(str (text "forge:tab.tickets")))
|
||||
|
||||
(div
|
||||
("class" "card flex flex-col gap-2")
|
||||
(text "{% for post in feed -%}")
|
||||
(text "{{ components::ticket(post=post[0], owner=post[1]) }}")
|
||||
(text "{%- endfor %}")
|
||||
(text "{{ components::pagination(page=page, items=feed|length) }}"))))
|
||||
(text "{% endblock %}")
|
|
@ -135,6 +135,31 @@
|
|||
});
|
||||
});
|
||||
|
||||
self.define("update_open", async (_, id, status) => {
|
||||
if (
|
||||
!(await trigger("atto::confirm", [
|
||||
"Are you sure you want to do this?",
|
||||
]))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/v1/posts/${id}/open`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ open: status }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger("atto::toast", [
|
||||
res.ok ? "success" : "error",
|
||||
res.message,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
self.define("react", async (_, element, asset, asset_type, is_like) => {
|
||||
await trigger("atto::debounce", ["reactions::toggle"]);
|
||||
fetch("/api/v1/reactions", {
|
||||
|
|
|
@ -15,7 +15,10 @@ use tetratto_core::model::{
|
|||
use crate::{
|
||||
get_user_from_token,
|
||||
image::{save_webp_buffer, JsonMultipart},
|
||||
routes::api::v1::{CreatePost, CreateRepost, UpdatePostContent, UpdatePostContext, VoteInPoll},
|
||||
routes::api::v1::{
|
||||
CreatePost, CreateRepost, UpdatePostContent, UpdatePostContext, UpdatePostIsOpen,
|
||||
VoteInPoll,
|
||||
},
|
||||
State,
|
||||
};
|
||||
|
||||
|
@ -383,3 +386,25 @@ pub async fn vote_request(
|
|||
Err(e) => Json(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_is_open_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
Path(id): Path<usize>,
|
||||
Json(req): Json<UpdatePostIsOpen>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await).0;
|
||||
let user = match get_user_from_token!(jar, data) {
|
||||
Some(ua) => ua,
|
||||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
match data.update_post_is_open(id, user, req.open).await {
|
||||
Ok(_) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Post updated".to_string(),
|
||||
payload: (),
|
||||
}),
|
||||
Err(e) => Json(e.into()),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -121,6 +121,10 @@ pub fn routes() -> Router {
|
|||
"/posts/{id}/poll_vote",
|
||||
post(communities::posts::vote_request),
|
||||
)
|
||||
.route(
|
||||
"/posts/{id}/open",
|
||||
post(communities::posts::update_is_open_request),
|
||||
)
|
||||
// drafts
|
||||
.route("/drafts", post(communities::drafts::create_request))
|
||||
.route("/drafts/{id}", delete(communities::drafts::delete_request))
|
||||
|
@ -639,3 +643,8 @@ pub struct VoteInPoll {
|
|||
pub struct AppendAssociations {
|
||||
pub tokens: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdatePostIsOpen {
|
||||
pub open: bool,
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
use super::{communities::community_context, render_error};
|
||||
use super::{communities::community_context, render_error, PaginatedQuery};
|
||||
use crate::{
|
||||
assets::initial_context, check_community_permissions, community_context_bools, get_lang,
|
||||
get_user_from_token, State,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Path, Query},
|
||||
response::{Html, IntoResponse},
|
||||
extract::Path,
|
||||
Extension,
|
||||
};
|
||||
use axum_extra::extract::CookieJar;
|
||||
|
@ -124,3 +124,93 @@ pub async fn info_request(
|
|||
// return
|
||||
Ok(Html(data.1.render("forge/info.html", &context).unwrap()))
|
||||
}
|
||||
|
||||
/// `/forge/{title}/tickets`
|
||||
pub async fn tickets_request(
|
||||
jar: CookieJar,
|
||||
Path(title): Path<String>,
|
||||
Query(props): Query<PaginatedQuery>,
|
||||
Extension(data): Extension<State>,
|
||||
) -> impl IntoResponse {
|
||||
let data = data.read().await;
|
||||
let user = get_user_from_token!(jar, data.0);
|
||||
|
||||
let community = match data.0.get_community_by_title(&title.to_lowercase()).await {
|
||||
Ok(ua) => ua,
|
||||
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
||||
};
|
||||
|
||||
if community.id == 0 {
|
||||
// don't show page for void community
|
||||
return Err(Html(
|
||||
render_error(
|
||||
Error::GeneralNotFound("community".to_string()),
|
||||
&jar,
|
||||
&data,
|
||||
&user,
|
||||
)
|
||||
.await,
|
||||
));
|
||||
}
|
||||
|
||||
// check permissions
|
||||
let (can_read, _) = check_community_permissions!(community, jar, data, user);
|
||||
|
||||
// ...
|
||||
let ignore_users = crate::ignore_users_gen!(user, data);
|
||||
|
||||
let feed = match data
|
||||
.0
|
||||
.get_posts_by_community(community.id, 12, props.page)
|
||||
.await
|
||||
{
|
||||
Ok(p) => match data.0.fill_posts(p, &ignore_users, &user).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
||||
},
|
||||
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
||||
};
|
||||
|
||||
// let pinned = match data.0.get_pinned_posts_by_community(community.id).await {
|
||||
// Ok(p) => match data.0.fill_posts(p, &ignore_users, &user).await {
|
||||
// Ok(p) => p,
|
||||
// Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
||||
// },
|
||||
// Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
||||
// };
|
||||
|
||||
// init context
|
||||
let lang = get_lang!(jar, data.0);
|
||||
let mut context = initial_context(&data.0.0.0, lang, &user).await;
|
||||
|
||||
let (
|
||||
is_owner,
|
||||
is_joined,
|
||||
is_pending,
|
||||
can_post,
|
||||
can_manage_posts,
|
||||
can_manage_community,
|
||||
can_manage_roles,
|
||||
can_manage_questions,
|
||||
) = community_context_bools!(data, user, community);
|
||||
|
||||
context.insert("feed", &feed);
|
||||
// context.insert("pinned", &pinned);
|
||||
context.insert("page", &props.page);
|
||||
community_context(
|
||||
&mut context,
|
||||
&community,
|
||||
is_owner,
|
||||
is_joined,
|
||||
is_pending,
|
||||
can_post,
|
||||
can_read,
|
||||
can_manage_posts,
|
||||
can_manage_community,
|
||||
can_manage_roles,
|
||||
can_manage_questions,
|
||||
);
|
||||
|
||||
// return
|
||||
Ok(Html(data.1.render("forge/tickets.html", &context).unwrap()))
|
||||
}
|
||||
|
|
|
@ -114,6 +114,7 @@ pub fn routes() -> Router {
|
|||
// forge
|
||||
.route("/forges", get(forge::home_request))
|
||||
.route("/forge/{title}", get(forge::info_request))
|
||||
.route("/forge/{title}/tickets", get(forge::tickets_request))
|
||||
.route("/forge/{title}/members", get(communities::members_request))
|
||||
// stacks
|
||||
.route("/stacks", get(stacks::list_request))
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "tetratto-core"
|
||||
version = "5.0.0"
|
||||
version = "6.0.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
|
|
|
@ -15,5 +15,7 @@ CREATE TABLE IF NOT EXISTS posts (
|
|||
uploads TEXT NOT NULL,
|
||||
is_deleted INT NOT NULL,
|
||||
tsvector_content tsvector GENERATED ALWAYS AS (to_tsvector ('english', coalesce(content, ''))) STORED,
|
||||
poll_id BIGINT NOT NULL
|
||||
poll_id BIGINT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
is_open INT NOT NULL DEFAULT 1
|
||||
)
|
||||
|
|
|
@ -124,6 +124,7 @@ impl DataManager {
|
|||
// SKIP tsvector (12)
|
||||
poll_id: get!(x->13(i64)) as usize,
|
||||
title: get!(x->14(String)),
|
||||
is_open: get!(x->15(i32)) as i8 == 1,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1564,7 +1565,7 @@ impl DataManager {
|
|||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"INSERT INTO posts VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, DEFAULT, $13, $14)",
|
||||
"INSERT INTO posts VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, DEFAULT, $13, $14, $15)",
|
||||
params![
|
||||
&(data.id as i64),
|
||||
&(data.created as i64),
|
||||
|
@ -1583,7 +1584,8 @@ impl DataManager {
|
|||
&serde_json::to_string(&data.uploads).unwrap(),
|
||||
&{ if data.is_deleted { 1 } else { 0 } },
|
||||
&(data.poll_id as i64),
|
||||
&data.title
|
||||
&data.title,
|
||||
&{ if data.is_open { 1 } else { 0 } },
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -1803,6 +1805,59 @@ impl DataManager {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_post_is_open(&self, id: usize, user: User, is_open: bool) -> Result<()> {
|
||||
let y = self.get_post_by_id(id).await?;
|
||||
|
||||
// make sure this is a forge community
|
||||
let community = self.get_community_by_id(y.community).await?;
|
||||
|
||||
if !community.is_forge {
|
||||
return Err(Error::MiscError(
|
||||
"This community does not support this".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// check permissions
|
||||
let user_membership = self
|
||||
.get_membership_by_owner_community(user.id, y.community)
|
||||
.await?;
|
||||
|
||||
if (user.id != y.owner)
|
||||
&& !user_membership
|
||||
.role
|
||||
.check(CommunityPermission::MANAGE_POSTS)
|
||||
{
|
||||
if !user.permissions.check(FinePermission::MANAGE_POSTS) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_audit_log_entry(AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `update_post_is_open` with x value `{id}`"),
|
||||
))
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
let conn = match self.0.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"UPDATE posts SET is_open = $1 WHERE id = $2",
|
||||
params![if is_open { 1 } else { 0 }, &(id as i64)]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
self.0.1.remove(format!("atto.post:{}", id)).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_post_context(
|
||||
&self,
|
||||
id: usize,
|
||||
|
|
|
@ -258,6 +258,8 @@ pub struct Post {
|
|||
pub poll_id: usize,
|
||||
/// The title of the post (in communities where titles are enabled).
|
||||
pub title: String,
|
||||
/// If the post is "open". Posts can act as tickets in a forge community.
|
||||
pub is_open: bool,
|
||||
}
|
||||
|
||||
impl Post {
|
||||
|
@ -284,6 +286,7 @@ impl Post {
|
|||
is_deleted: false,
|
||||
poll_id,
|
||||
title: String::new(),
|
||||
is_open: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "tetratto-l10n"
|
||||
version = "5.0.0"
|
||||
version = "6.0.0"
|
||||
edition = "2024"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "tetratto-shared"
|
||||
version = "5.0.0"
|
||||
version = "6.0.0"
|
||||
edition = "2024"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
|
|
2
sql_changes/posts_is_open.sql
Normal file
2
sql_changes/posts_is_open.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE posts
|
||||
ADD COLUMN is_open INT NOT NULL DEFAULT 1;
|
Loading…
Add table
Add a link
Reference in a new issue