Sharing tables readonly with other users
- Added sharing feature for table owners to share their tables with other registered users. - Fixed a bug where the wrong entries would be deleted or modified when searching or filtering.
This commit is contained in:
parent
c6cb3a48bc
commit
61708e5199
|
|
@ -24,4 +24,5 @@ file = "src/schema.rs"
|
|||
custom_type_derives = ["diesel::query_builder::QueryId", "Clone"]
|
||||
|
||||
[migrations_directory]
|
||||
dir = "/opt/inventur/inventur/inventur_db/migrations"
|
||||
dir = "/home/johannes/code/inventur/inventur_db/migrations"
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE shares;
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
CREATE TABLE shares (
|
||||
id INTEGER AUTO_INCREMENT UNIQUE NOT NULL,
|
||||
PRIMARY KEY(id),
|
||||
sharee_id INTEGER NOT NULL,
|
||||
FOREIGN KEY (sharee_id) REFERENCES users(id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE,
|
||||
tblid INTEGER NOT NULL,
|
||||
FOREIGN KEY (tblid) REFERENCES jrtables(id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE,
|
||||
readonly BOOLEAN NOT NULL DEFAULT true
|
||||
);
|
||||
|
|
@ -26,6 +26,7 @@ mod jrcolumns;
|
|||
mod jrentries;
|
||||
mod jrcells;
|
||||
mod users;
|
||||
mod shares;
|
||||
|
||||
use dotenvy::dotenv;
|
||||
use std::env;
|
||||
|
|
@ -38,11 +39,12 @@ use diesel::deserialize;
|
|||
use diesel::backend::Backend;
|
||||
use diesel::sql_types::Integer;
|
||||
use diesel::expression::Expression;
|
||||
use models::Share;
|
||||
|
||||
/// Every column has a type.
|
||||
/// The default is text, currently number (isize) exists too.
|
||||
/// Types are mainly used for sorting and can be helpful in client applications.
|
||||
#[derive(PartialEq, Clone, Copy, diesel::FromSqlRow)]
|
||||
#[derive(PartialEq, Clone, Copy, diesel::FromSqlRow, Debug)]
|
||||
#[repr(i32)]
|
||||
pub enum FIELDTYPE {
|
||||
TEXT = 0,
|
||||
|
|
@ -75,6 +77,7 @@ impl From<isize> for FIELDTYPE {
|
|||
|
||||
/// represents and summarised all relevant data of a table.
|
||||
/// Standard return type if whole tables should be returned
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Tbl {
|
||||
/// table id of the represented object
|
||||
pub tblid: i32,
|
||||
|
|
@ -109,6 +112,14 @@ pub struct User {
|
|||
pub email: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ShareCard {
|
||||
pub tblid: i32,
|
||||
pub sharee_id: i32,
|
||||
pub sharee_name: String,
|
||||
pub readonly: bool,
|
||||
}
|
||||
|
||||
/// Connect to database.
|
||||
/// Return value must be passed to most of the crate's functions.
|
||||
pub fn establish_connection() -> MysqlConnection {
|
||||
|
|
@ -130,14 +141,21 @@ pub fn establish_connection() -> MysqlConnection {
|
|||
/// if there has been another error
|
||||
///
|
||||
pub fn get_table(conn: &mut MysqlConnection, tblid: i32, uid: i32) -> Option<Tbl> {
|
||||
let tbl = fetch_table(conn, tblid);
|
||||
if tbl.is_none() ||
|
||||
tbl.clone().unwrap().1 != uid
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(tbl.unwrap().0)
|
||||
}
|
||||
|
||||
fn fetch_table(conn: &mut MysqlConnection, tblid: i32) -> Option<(Tbl, i32)>{
|
||||
let tbl = jrtables::get_tbl(conn, tblid);
|
||||
if tbl.is_err() {
|
||||
return None;
|
||||
}
|
||||
let tbl = tbl.unwrap();
|
||||
if tbl.owner_id != uid {
|
||||
return None;
|
||||
}
|
||||
let clmns = jrcolumns::get_clmns_of(conn, tblid);
|
||||
if clmns.is_err() {
|
||||
return None;
|
||||
|
|
@ -172,7 +190,20 @@ pub fn get_table(conn: &mut MysqlConnection, tblid: i32, uid: i32) -> Option<Tbl
|
|||
let data = data;
|
||||
rows.push(TRow { row_pos: rowid.row_pos, cells: data });
|
||||
}
|
||||
Some(Tbl { tblid: tbl.id, name: tbl.name, column_names: clmn_nms, column_types: clmn_tps,rows: rows })
|
||||
Some((Tbl { tblid: tbl.id, name: tbl.name, column_names: clmn_nms, column_types: clmn_tps,rows: rows }, tbl.owner_id))
|
||||
}
|
||||
|
||||
pub fn get_table_shared(conn: &mut MysqlConnection, tblid: i32, uid: i32) -> Option<Tbl> {
|
||||
let tbl = fetch_table(conn, tblid);
|
||||
let share = shares::is_shared(conn, tblid, uid);
|
||||
if !share {
|
||||
return None;
|
||||
}
|
||||
let tbl = tbl;
|
||||
if tbl.is_none() {
|
||||
return None;
|
||||
}
|
||||
Some(tbl.unwrap().0)
|
||||
}
|
||||
|
||||
/// Take a Tbl object, sort it according to parameters and return a sorted Tbl object.
|
||||
|
|
@ -468,3 +499,83 @@ pub fn edit_column(conn: &mut MysqlConnection, tblid: i32, column_pos: i32, new_
|
|||
}
|
||||
Some(true)
|
||||
}
|
||||
|
||||
|
||||
pub fn share(conn: &mut MysqlConnection, tblid: i32, sharee_id: &i32, readonly: &bool, uid: i32) -> Option<bool> {
|
||||
let owner = jrtables::get_owner_id(conn, tblid);
|
||||
println!(":)");
|
||||
if owner.is_err() ||
|
||||
owner.unwrap() != uid ||
|
||||
shares::edit_or_create(conn, tblid, sharee_id, readonly).is_err()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(true)
|
||||
}
|
||||
|
||||
pub fn unshare(conn: &mut MysqlConnection, tblid: i32, sharee_id: &i32, uid: i32) -> Option<bool> {
|
||||
let owner = jrtables::get_owner_id(conn, tblid);
|
||||
if owner.is_err() ||
|
||||
owner.unwrap() != uid ||
|
||||
shares::delete(conn, tblid, sharee_id).is_err()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(true)
|
||||
}
|
||||
|
||||
pub fn get_sharees(conn: &mut MysqlConnection, tblid: i32, uid: i32) -> Option<Vec<ShareCard>> {
|
||||
let owner = jrtables::get_owner_id(conn, tblid);
|
||||
if owner.is_err() ||
|
||||
owner.unwrap() != uid
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let result = shares::get_shares_tbl(conn, tblid);
|
||||
if result.is_err() {
|
||||
return None;
|
||||
}
|
||||
let result = result.unwrap();
|
||||
let mut sharecards = Vec::new();
|
||||
for share in result {
|
||||
let uname = users::get_uname(conn, uid);
|
||||
if uname.is_err() {
|
||||
continue;
|
||||
}
|
||||
sharecards.push(ShareCard { tblid: tblid, sharee_id: share.sharee_id, sharee_name: uname.unwrap(), readonly: share.readonly });
|
||||
}
|
||||
return Some(sharecards)
|
||||
}
|
||||
|
||||
pub fn get_shared_tbls(conn: &mut MysqlConnection, uid: i32) -> Option<Vec<ShareCard>> {
|
||||
let result = shares::get_shares_usr(conn, uid);
|
||||
if result.is_err() {
|
||||
return None;
|
||||
}
|
||||
let result = result.unwrap();
|
||||
let uname = users::get_uname(conn, uid);
|
||||
if uname.is_err() {
|
||||
return None;
|
||||
}
|
||||
let uname = uname.unwrap();
|
||||
let mut sharecards = Vec::new();
|
||||
for share in result {
|
||||
sharecards.push(ShareCard { tblid: share.tblid, sharee_id: uid, sharee_name: uname.clone(), readonly: share.readonly});
|
||||
}
|
||||
Some(sharecards)
|
||||
}
|
||||
|
||||
pub fn query_users(conn: &mut MysqlConnection, query: String, uid: i32) -> Option<Vec<User>> {
|
||||
let result = users::query_users(conn, query);
|
||||
if users::get_uname(conn, uid).is_err() ||
|
||||
result.is_err()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let result = result.unwrap();
|
||||
let mut pubusers: Vec<User> = Vec::new();
|
||||
for user in result {
|
||||
pubusers.push( User { uid: user.id, uname: user.username, email: user.email } );
|
||||
}
|
||||
Some(pubusers)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
*/
|
||||
|
||||
use diesel::prelude::*;
|
||||
use crate::schema::{jrtables, jrcolumns, jrentries, jrcells, users};
|
||||
use crate::schema::{jrtables, jrcolumns, jrentries, jrcells, users, shares};
|
||||
|
||||
#[derive(Queryable, Selectable, Identifiable, Associations)]
|
||||
#[diesel(table_name = crate::schema::jrtables)]
|
||||
|
|
@ -114,3 +114,23 @@ pub struct NewUser {
|
|||
pub email: String,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[diesel(table_name = shares)]
|
||||
#[diesel(check_for_backend(diesel::mysql::Mysql))]
|
||||
pub struct NewShare {
|
||||
pub sharee_id: i32,
|
||||
pub readonly: bool,
|
||||
pub tblid: i32,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Selectable, Identifiable, Associations)]
|
||||
#[diesel(table_name = shares)]
|
||||
#[diesel(belongs_to(Jrtable, foreign_key = tblid))]
|
||||
#[diesel(belongs_to(User, foreign_key = sharee_id))]
|
||||
#[diesel(check_for_backend(diesel::mysql::Mysql))]
|
||||
pub struct Share {
|
||||
pub id: i32,
|
||||
pub sharee_id: i32,
|
||||
pub tblid: i32,
|
||||
pub readonly: bool,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,3 @@
|
|||
/* Simple web app using rocket to help maintain inventory data.
|
||||
* Copyright (C) 2024 Johannes Randerath
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// @generated automatically by Diesel CLI.
|
||||
|
||||
diesel::table! {
|
||||
|
|
@ -55,6 +38,15 @@ diesel::table! {
|
|||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
shares (id) {
|
||||
id -> Integer,
|
||||
sharee_id -> Integer,
|
||||
tblid -> Integer,
|
||||
readonly -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
users (id) {
|
||||
id -> Integer,
|
||||
|
|
@ -70,11 +62,14 @@ diesel::joinable!(jrcells -> jrentries (jrentry_id));
|
|||
diesel::joinable!(jrcolumns -> jrtables (jrtable_id));
|
||||
diesel::joinable!(jrentries -> jrtables (jrtable_id));
|
||||
diesel::joinable!(jrtables -> users (owner_id));
|
||||
diesel::joinable!(shares -> jrtables (tblid));
|
||||
diesel::joinable!(shares -> users (sharee_id));
|
||||
|
||||
diesel::allow_tables_to_appear_in_same_query!(
|
||||
jrcells,
|
||||
jrcolumns,
|
||||
jrentries,
|
||||
jrtables,
|
||||
shares,
|
||||
users,
|
||||
);
|
||||
|
|
|
|||
53
inventur_db/src/shares.rs
Normal file
53
inventur_db/src/shares.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
use crate::models;
|
||||
use crate::schema;
|
||||
|
||||
use models::{NewShare, Share};
|
||||
use schema::shares::dsl::{id, shares, tblid, sharee_id, readonly};
|
||||
use diesel::mysql::MysqlConnection;
|
||||
use diesel::prelude::*;
|
||||
|
||||
|
||||
pub fn edit_or_create(conn: &mut MysqlConnection, tableid: i32, sharee: &i32, ro: &bool) -> Result<usize, diesel::result::Error> {
|
||||
let share = shares.filter(tblid.eq(tableid))
|
||||
.filter(sharee_id.eq(sharee))
|
||||
.select(id)
|
||||
.first::<i32>(conn);
|
||||
if share.is_err() {
|
||||
return diesel::insert_into(schema::shares::table)
|
||||
.values(&(NewShare { tblid: tableid, sharee_id: *sharee, readonly: *ro }))
|
||||
.execute(conn);
|
||||
}
|
||||
return diesel::update(shares.find(share.unwrap()))
|
||||
.set(readonly.eq(ro))
|
||||
.execute(conn);
|
||||
}
|
||||
|
||||
pub fn delete(conn: &mut MysqlConnection, tableid: i32, sharee: &i32) -> Result<usize, diesel::result::Error> {
|
||||
diesel::delete(shares.filter(tblid.eq(tableid)).filter(sharee_id.eq(*sharee))).execute(conn)
|
||||
}
|
||||
|
||||
pub fn get_shares_tbl(conn: &mut MysqlConnection, tableid: i32) -> Result<Vec<Share>, diesel::result::Error> {
|
||||
shares
|
||||
.filter(tblid.eq(tableid))
|
||||
.select(Share::as_select())
|
||||
.load::<Share>(conn)
|
||||
}
|
||||
|
||||
pub fn get_shares_usr(conn: &mut MysqlConnection, uid: i32) -> Result<Vec<Share>, diesel::result::Error> {
|
||||
shares
|
||||
.filter(sharee_id.eq(uid))
|
||||
.select(Share::as_select())
|
||||
.load::<Share>(conn)
|
||||
}
|
||||
|
||||
pub fn is_shared(conn: &mut MysqlConnection, tableid: i32, uid: i32) -> bool {
|
||||
let share = shares
|
||||
.filter(sharee_id.eq(uid))
|
||||
.filter(tblid.eq(tableid))
|
||||
.execute(conn);
|
||||
if share.is_err() {
|
||||
return false;
|
||||
}
|
||||
println!("{share:?}");
|
||||
share.unwrap() >= 1
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ use crate::models;
|
|||
use crate::schema;
|
||||
|
||||
use schema::users::dsl::{users, id, username, email};
|
||||
use models::NewUser;
|
||||
use models::{ User, NewUser };
|
||||
|
||||
use diesel::prelude::*;
|
||||
use diesel::mysql::MysqlConnection;
|
||||
|
|
@ -58,4 +58,11 @@ pub fn get_uname(conn: &mut MysqlConnection, uid: i32) -> Result<String, diesel:
|
|||
.first::<String>(conn)
|
||||
}
|
||||
|
||||
pub fn query_users(conn: &mut MysqlConnection, query: String) -> Result<Vec<User>, diesel::result::Error> {
|
||||
users
|
||||
.filter(username.like(format!("{query}%")))
|
||||
.select(User::as_select())
|
||||
.load::<User>(conn)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
89
src/main.rs
89
src/main.rs
|
|
@ -38,7 +38,11 @@ use std::env;
|
|||
use dotenvy::dotenv;
|
||||
use rocket::Config;
|
||||
use rocket::figment::providers::Env;
|
||||
use rocket::http::CookieJar;
|
||||
use rocket::http::{CookieJar, Status};
|
||||
use rocket::form::Form;
|
||||
use rocket::serde::json::Json;
|
||||
|
||||
use table::forms::{SearchString, UserQueryResult};
|
||||
|
||||
/// Database connection using diesel and rocket_sync_db_pools
|
||||
#[database("inventur")]
|
||||
|
|
@ -47,13 +51,17 @@ pub struct Db(diesel::MysqlConnection);
|
|||
/// Home page featuring a preview view of all the user's tables
|
||||
/// Needs an authenticated user.
|
||||
#[get("/")]
|
||||
async fn home(conn: Db, user: AuthUser) -> Template {
|
||||
async fn home(conn: Db, user: AuthUser) -> Option<Template> {
|
||||
let uid = user.uid;
|
||||
let (tids, tnames) = table::get_tids(&conn, uid).await;
|
||||
let mut cols = Vec::new();
|
||||
let mut rows = Vec::new();
|
||||
for tblid in tids.clone() {
|
||||
let tbl = conn.run(move |c| inventur_db::get_table(c, tblid, uid)).await.unwrap();
|
||||
let tbl = conn.run(move |c| inventur_db::get_table(c, tblid, uid)).await;
|
||||
if tbl.is_none() {
|
||||
return None;
|
||||
}
|
||||
let tbl = tbl.unwrap();
|
||||
cols.push(tbl.column_names.clone());
|
||||
let mut rws : Vec<Vec<String>> = Vec::new();
|
||||
for row in tbl.rows {
|
||||
|
|
@ -61,15 +69,51 @@ async fn home(conn: Db, user: AuthUser) -> Template {
|
|||
}
|
||||
rows.push(rws.clone());
|
||||
}
|
||||
Template::render("home",
|
||||
context!{
|
||||
tids: tids,
|
||||
tnames: tnames,
|
||||
columns: cols,
|
||||
rows: rows,
|
||||
username: user.uname,
|
||||
email: user.email,
|
||||
}
|
||||
let mut shares = conn.run(move |c| inventur_db::get_shared_tbls(c, user.uid)).await;
|
||||
if shares.is_none() {
|
||||
shares = Some(Vec::new());
|
||||
}
|
||||
let shares = shares.unwrap();
|
||||
let mut sharetblids = Vec::new();
|
||||
for tbl in shares {
|
||||
sharetblids.push(tbl.tblid);
|
||||
}
|
||||
let sharetblids = sharetblids;
|
||||
let mut sharetbls = Vec::new();
|
||||
for tblid in sharetblids.clone() {
|
||||
let sharetbl = conn.run(move |c| inventur_db::get_table_shared(c, tblid, uid)).await;
|
||||
if sharetbl.is_some() {
|
||||
sharetbls.push(sharetbl.unwrap());
|
||||
}
|
||||
}
|
||||
let sharetbls = sharetbls;
|
||||
let mut sharedcols = Vec::new();
|
||||
let mut sharedrows = Vec::new();
|
||||
let mut sharednms = Vec::new();
|
||||
for sharetbl in sharetbls {
|
||||
sharedcols.push(sharetbl.column_names.clone());
|
||||
let mut rws: Vec<Vec<String>> = Vec::new();
|
||||
for row in sharetbl.rows {
|
||||
rws.push(row.cells);
|
||||
}
|
||||
sharednms.push(sharetbl.name);
|
||||
sharedrows.push(rws.clone());
|
||||
}
|
||||
Some(
|
||||
Template::render("home",
|
||||
context!{
|
||||
tids: tids,
|
||||
tnames: tnames,
|
||||
columns: cols,
|
||||
rows: rows,
|
||||
username: user.uname,
|
||||
email: user.email,
|
||||
sharedtblids: sharetblids,
|
||||
sharednms: sharednms,
|
||||
sharedcols: sharedcols,
|
||||
sharedrows: sharedrows,
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +137,21 @@ async fn favicon() -> Redirect {
|
|||
Redirect::to(uri!("/img/favicon.ico"))
|
||||
}
|
||||
|
||||
/// Setup app for launch:
|
||||
#[post("/users", data="<data>")]
|
||||
async fn search_users(conn: Db, data: Form<SearchString>, user: AuthUser) -> Result<Json<Vec<UserQueryResult>>, Status> {
|
||||
let result = conn.run(move |c| inventur_db::query_users(c, data.query.clone(), user.uid)).await;
|
||||
if result.is_none() {
|
||||
return Err(Status::Forbidden);
|
||||
}
|
||||
let result = result.unwrap();
|
||||
let mut queryresults: Vec<UserQueryResult> = Vec::new();
|
||||
for u in result {
|
||||
queryresults.push( UserQueryResult { id: u.uid, username: u.uname } );
|
||||
}
|
||||
Ok(rocket::serde::json::Json(queryresults))
|
||||
}
|
||||
|
||||
/// Set app up for launch:
|
||||
/// Load configuration from a file called .env in the project's root.
|
||||
/// Use tera templates, connect to mysql db, setup oauth
|
||||
/// Serve everything related to ...
|
||||
|
|
@ -114,10 +172,11 @@ async fn rocket() -> _ {
|
|||
.attach(Template::fairing())
|
||||
.attach(Db::fairing())
|
||||
.attach(OAuth2::<auth::RanderathIdentity>::fairing("oauth"))
|
||||
.mount("/", routes![auth::oauth_login, auth::oauth_callback, home, login_home, favicon, logout])
|
||||
.mount("/table", routes![table::table, table::table_sec, table::edit_tname, table::create_table, table::import_table, table::delete_table])
|
||||
.mount("/", routes![auth::oauth_login, auth::oauth_callback, home, login_home, favicon, logout, search_users])
|
||||
.mount("/table", routes![table::table, table::table_sec, table::edit_tname, table::create_table, table::import_table, table::delete_table, table::edit_share])
|
||||
.mount("/row", routes![table::new_entry, table::edit_entry, table::delete_entry])
|
||||
.mount("/column", routes![table::delete_column, table::edit_column, table::create_column])
|
||||
.mount("/table/readonly", routes![table::table_readonly])
|
||||
.register("/", catchers![auth::redirect_to_login_401, auth::redirect_to_login_403])
|
||||
.mount("/img", FileServer::from(relative!("static/img")))
|
||||
.mount("/css", FileServer::from(relative!("static/css")))
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@
|
|||
|
||||
//! Submodule holding structs relevant to handle form data.
|
||||
|
||||
use rocket::serde::Deserialize;
|
||||
use rocket::serde::{Serialize, Deserialize};
|
||||
//use inventur_db::ShareCard;
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct DeleteColumn {
|
||||
|
|
@ -87,4 +88,38 @@ pub struct DeleteTable {
|
|||
pub tblid: i32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct Sharee {
|
||||
id: i32,
|
||||
username: String,
|
||||
readonly: bool,
|
||||
}
|
||||
|
||||
impl From<inventur_db::ShareCard> for Sharee {
|
||||
fn from(sharecard: inventur_db::ShareCard) -> Self {
|
||||
Sharee { id: sharecard.sharee_id, username: sharecard.sharee_name, readonly: sharecard.readonly}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct EditShare {
|
||||
pub sharees: Vec<i32>,
|
||||
pub readonly: Vec<bool>,
|
||||
pub delete: Vec<bool>,
|
||||
pub tblid: i32,
|
||||
pub new_user: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct UserQueryResult {
|
||||
pub id: i32,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct SearchString {
|
||||
pub query: String,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ use rocket::serde::json::Json;
|
|||
|
||||
use inventur_db::FIELDTYPE;
|
||||
|
||||
use self::forms::{NewTable, DeleteTable, ImportTable, EditTname};
|
||||
use self::forms::{NewTable, DeleteTable, ImportTable, EditTname, EditShare};
|
||||
use self::table_view::rocket_uri_macro_table_sec;
|
||||
|
||||
/// Delete a table.
|
||||
|
|
@ -88,4 +88,34 @@ pub async fn edit_tname(conn: Db, data: Form<EditTname>, user: AuthUser) -> Resu
|
|||
Ok(Redirect::to(uri!("/table", table_sec(tblid))))
|
||||
}
|
||||
|
||||
/// Edit or create the sharees of a table
|
||||
#[post("/share", data="<data>")]
|
||||
pub async fn edit_share(conn: Db, data: Form<EditShare>, user: AuthUser) -> Result<Redirect, Status> {
|
||||
let uid = user.uid;
|
||||
let tblid = data.tblid;
|
||||
let ro = data.readonly.clone();
|
||||
let mut sharees = data.sharees.clone();
|
||||
if data.new_user.is_some() {
|
||||
sharees.push(data.new_user.unwrap());
|
||||
}
|
||||
let sharees = sharees;
|
||||
for (i, sharee) in sharees.iter().enumerate() {
|
||||
let sharee = sharee.clone();
|
||||
let reado = ro[i].clone();
|
||||
if conn.run(move |c| inventur_db::share(c, tblid, &sharee, &reado, uid)).await.is_none() {
|
||||
return Err(Status::Forbidden);
|
||||
}
|
||||
}
|
||||
let del = data.delete.clone();
|
||||
for (i, todel) in del.iter().enumerate() {
|
||||
if *todel {
|
||||
let sharee = sharees[i].clone();
|
||||
if conn.run(move |c| inventur_db::unshare(c, tblid, &sharee, uid)).await.is_none() {
|
||||
return Err(Status::Forbidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Redirect::to(uri!("/table", table_sec(tblid))))
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,29 +20,36 @@
|
|||
|
||||
use crate::auth;
|
||||
use crate::Db;
|
||||
use crate::table::forms;
|
||||
use inventur_db;
|
||||
|
||||
use auth::AuthUser;
|
||||
use rocket_dyn_templates::{Template, context};
|
||||
use rocket::response::Redirect;
|
||||
use forms::Sharee;
|
||||
|
||||
/// View an authenticated user sees visiting the /table/<table id> page
|
||||
/// Optional get parametes are the field to sort by, the direction of the sort, the fields to search in and the value to search for.
|
||||
/// Returns None if the table does not exist, could not be fetched or if the user is not the table's owner.
|
||||
#[get("/<tid>?<sort_dir>&<sort_field>&<search_fields>&<search_value>")]
|
||||
pub async fn table(conn: Db, tid: i32, sort_dir: Option<u8>, sort_field: Option<usize>, search_fields: Option<Vec<i32>>, search_value: Option<String>, user: AuthUser) -> Option<Template> {
|
||||
let uid = user.uid;
|
||||
let table = conn.run(move |c| inventur_db::get_table(c, tid, uid)).await;
|
||||
if table.is_none() {
|
||||
return None;
|
||||
}
|
||||
let mut table = table.unwrap();
|
||||
use inventur_db::Tbl;
|
||||
|
||||
struct PassTbl {
|
||||
tbl: Tbl,
|
||||
searchvalue: String,
|
||||
searchfields: Vec<i32>,
|
||||
sortdir: u8,
|
||||
sortfield: usize,
|
||||
tname: String,
|
||||
column_names: Vec<String>,
|
||||
column_types: Vec<i32>,
|
||||
rows: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
async fn sort_and_search(tbl: Tbl, sort_dir: Option<u8>, sort_field: Option<usize>, search_fields: Option<Vec<i32>>, search_value: Option<String>) -> PassTbl {
|
||||
let searchvalue;
|
||||
let searchfields;
|
||||
let mut tbl = tbl;
|
||||
if search_value.is_some() && !search_value.clone().unwrap().eq_ignore_ascii_case("") && search_fields.is_some() {
|
||||
searchvalue = search_value.unwrap();
|
||||
searchfields = search_fields.unwrap();
|
||||
table = inventur_db::search_table(table, searchfields.clone(), searchvalue.clone());
|
||||
tbl = inventur_db::search_table(tbl, searchfields.clone(), searchvalue.clone());
|
||||
}else {
|
||||
if search_value.is_none() {
|
||||
searchvalue = String::new();
|
||||
|
|
@ -50,7 +57,7 @@ pub async fn table(conn: Db, tid: i32, sort_dir: Option<u8>, sort_field: Option<
|
|||
searchvalue = search_value.unwrap();
|
||||
}
|
||||
if search_fields.is_none() {
|
||||
searchfields = (0i32..(table.column_names.len() as i32)).collect();
|
||||
searchfields = (0i32..(tbl.column_names.len() as i32)).collect();
|
||||
|
||||
} else {
|
||||
searchfields = search_fields.unwrap();
|
||||
|
|
@ -69,33 +76,97 @@ pub async fn table(conn: Db, tid: i32, sort_dir: Option<u8>, sort_field: Option<
|
|||
}else {
|
||||
sortfield = sort_field.unwrap();
|
||||
}
|
||||
let table = inventur_db::sort_table(table, sortfield, sortdir);
|
||||
|
||||
let column_names = tbl.column_names.clone();
|
||||
let column_types = tbl.column_types.clone().iter().map(|x: &inventur_db::FIELDTYPE| *x as i32).collect::<Vec<i32>>();
|
||||
let rows : Vec<Vec<String>>= tbl.rows.iter().map(|v| {let mut r = v.cells.clone(); r.insert(0, v.row_pos.to_string()); r}).collect();
|
||||
|
||||
PassTbl { tbl: inventur_db::sort_table(tbl.clone(), sortfield, sortdir), searchvalue, searchfields, sortdir, sortfield, tname: tbl.name, column_names, column_types, rows }
|
||||
|
||||
}
|
||||
|
||||
/// View an authenticated user sees visiting the /table/<table id> page
|
||||
/// Optional get parametes are the field to sort by, the direction of the sort, the fields to search in and the value to search for.
|
||||
/// Returns None if the table does not exist, could not be fetched or if the user is not the table's owner.
|
||||
#[get("/<tid>?<sort_dir>&<sort_field>&<search_fields>&<search_value>")]
|
||||
pub async fn table(conn: Db, tid: i32, sort_dir: Option<u8>, sort_field: Option<usize>, search_fields: Option<Vec<i32>>, search_value: Option<String>, user: AuthUser) -> Option<Template> {
|
||||
let uid = user.uid;
|
||||
let table = conn.run(move |c| inventur_db::get_table(c, tid, uid)).await;
|
||||
if table.is_none() {
|
||||
return None;
|
||||
}
|
||||
let passtable = sort_and_search(table.unwrap(), sort_dir, sort_field, search_fields, search_value).await;
|
||||
let table = passtable.tbl;
|
||||
let tname = table.name;
|
||||
let column_names = table.column_names;
|
||||
let column_types = table.column_types.iter().map(|x: &inventur_db::FIELDTYPE| *x as i32).collect::<Vec<i32>>();
|
||||
let rows : Vec<Vec<String>>= table.rows.iter().map(|v| {let mut r = v.cells.clone(); r.insert(0, v.row_pos.to_string()); r}).collect();
|
||||
|
||||
let (tids, tnames) = get_tids(&conn, uid).await;
|
||||
|
||||
let sharecards = conn.run(move |c| inventur_db::get_sharees(c, tid, uid)).await;
|
||||
let mut sharees : Vec<Sharee> = Vec::new();
|
||||
if sharecards.is_some() {
|
||||
sharees = sharecards.unwrap().iter().map(|s| Sharee::from((*s).clone())).collect();
|
||||
}
|
||||
let sharees = sharees;
|
||||
|
||||
Some(
|
||||
Template::render("table",
|
||||
Template::render("table_owned",
|
||||
context!{
|
||||
search_value: searchvalue,
|
||||
search_fields: searchfields,
|
||||
sort_field: sortfield,
|
||||
sort_dir: sortdir,
|
||||
base_url: "/table",
|
||||
search_value: passtable.searchvalue,
|
||||
search_fields: passtable.searchfields,
|
||||
sort_field: passtable.sortfield,
|
||||
sort_dir: passtable.sortdir,
|
||||
tblid: tid,
|
||||
tblname: tname,
|
||||
tblname: passtable.tname,
|
||||
tids: tids,
|
||||
tnames: tnames,
|
||||
column_names: column_names,
|
||||
column_types: column_types,
|
||||
rows: rows,
|
||||
column_names: passtable.column_names,
|
||||
column_types: passtable.column_types,
|
||||
rows: passtable.rows,
|
||||
username: user.uname,
|
||||
email: user.email,
|
||||
shared: sharees,
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/<tid>?<sort_dir>&<sort_field>&<search_fields>&<search_value>")]
|
||||
pub async fn table_readonly(conn: Db, tid: i32, sort_dir: Option<u8>, sort_field: Option<usize>, search_fields: Option<Vec<i32>>, search_value: Option<String>, user: AuthUser) -> Option<Template> {
|
||||
let uid = user.uid;
|
||||
let table = conn.run(move |c| inventur_db::get_table_shared(c, tid, uid)).await;
|
||||
if table.is_none() {
|
||||
return None;
|
||||
}
|
||||
let passtable = sort_and_search(table.unwrap(), sort_dir, sort_field, search_fields, search_value).await;
|
||||
let table = passtable.tbl;
|
||||
|
||||
let (tids, tnames) = get_tids(&conn, uid).await;
|
||||
|
||||
Some(
|
||||
Template::render("table_readonly",
|
||||
context!{
|
||||
base_url: "/table/readonly",
|
||||
search_value: passtable.searchvalue,
|
||||
search_fields: passtable.searchfields,
|
||||
sort_field: passtable.sortfield,
|
||||
sort_dir: passtable.sortdir,
|
||||
tblid: tid,
|
||||
tblname: passtable.tname,
|
||||
tids: tids,
|
||||
tnames: tnames,
|
||||
column_names: passtable.column_names,
|
||||
column_types: passtable.column_types,
|
||||
rows: passtable.rows,
|
||||
username: user.uname,
|
||||
email: user.email,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
/// View to redirect a post request handled to manipulate a table or its display representation back to the (new) table view.
|
||||
|
|
|
|||
|
|
@ -18,64 +18,30 @@
|
|||
|
||||
let edit_mode = false;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", (event) => {
|
||||
let rows = document.getElementById("content_table").getElementsByTagName('tbody')[0].rows;
|
||||
for (let i = 0; i < rows.length; i++)
|
||||
{
|
||||
let row = rows[i];
|
||||
let createClickHandler = function(r)
|
||||
{
|
||||
return function()
|
||||
{
|
||||
$('#edit_entry_modal').modal('show');
|
||||
document.getElementById('modal_caller').innerHTML = i+1;
|
||||
document.getElementById('form_edit_entry_rowpos').value = i+1;
|
||||
document.getElementById('form_delete_entry_rowpos').value = i+1;
|
||||
let cells = row.cells;
|
||||
for (let j = 1; j < cells.length; j++) {
|
||||
document.getElementById(`form_edit_entry_${j}`).value = cells[j].innerHTML;
|
||||
}
|
||||
};
|
||||
};
|
||||
row.onclick = createClickHandler(row);
|
||||
}
|
||||
});
|
||||
document.addEventListener("DOMContentLoaded", (event) => {
|
||||
let rows = document.getElementById("content_table").getElementsByTagName('tbody')[0].rows;
|
||||
for (let i = 0; i < rows.length; i++)
|
||||
{
|
||||
let row = rows[i];
|
||||
let createClickHandler = function(r)
|
||||
{
|
||||
return function()
|
||||
{
|
||||
$('#edit_entry_modal').modal('show');
|
||||
let row_pos = $(this).find('td').first().html();
|
||||
$('#modal_caller').html(row_pos);
|
||||
$('#form_edit_entry_rowpos').val(row_pos);
|
||||
$('#form_delete_entry_rowpos').val(row_pos);
|
||||
let cells = row.cells;
|
||||
for (let j = 1; j < cells.length; j++) {
|
||||
$(`#form_edit_entry_${j}`).val(cells[j].innerHTML);
|
||||
}
|
||||
};
|
||||
};
|
||||
row.onclick = createClickHandler(row);
|
||||
}
|
||||
});
|
||||
|
||||
function toggle_edit_tname() {
|
||||
let span = document.getElementById('tname');
|
||||
if (!edit_mode) {
|
||||
span.innerHTML = `
|
||||
<div class='row'>
|
||||
<div class='col-auto ml-1 p-2'>
|
||||
<form id='form_edit_table' action='/table/name/edit' method='post'>
|
||||
<input name='tblid' value='${tblid}' hidden />
|
||||
<input type='text' class='form-control' id='tname_edit' name='new_name' value='${tblname}' />
|
||||
</form>
|
||||
</div>
|
||||
<div class='col-auto mt-0 pt-0 pr-1'>
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-secondary" onclick="toggle_edit_tname();" type="button"><i class="bi bi-x"></i></button>
|
||||
<button class='btn btn-success' type='button' onclick='edit_tname()'>
|
||||
<i class='bi bi-check'></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class='btn btn-danger' type="button" onclick='confirm_delete("table");'><i class="bi bi-trash3-fill"></i></button>
|
||||
</div>
|
||||
<form id="form_delete_table" action="/table/delete" method="post">
|
||||
<input value="${tblid}" name="tblid" hidden />
|
||||
</form>`;
|
||||
edit_mode = true;
|
||||
document.getElementById('pencil_button_edit_tname').hidden = true;
|
||||
} else {
|
||||
document.getElementById('pencil_button_edit_tname').hidden = false;
|
||||
span.innerHTML = `${tblname}`;
|
||||
edit_mode = false;
|
||||
}
|
||||
}
|
||||
function edit_tname() {
|
||||
document.getElementById('form_edit_table').submit();
|
||||
toggle_edit_tname();
|
||||
}
|
||||
function edit_column(clmn_index) {
|
||||
document.getElementById('form_edit_column_name').value = column_names[clmn_index];
|
||||
document.getElementById('form_edit_column_type').value = column_types[clmn_index];
|
||||
|
|
|
|||
71
static/js/table_owned.js
Normal file
71
static/js/table_owned.js
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
|
||||
$(function() {
|
||||
$('.edit_column_btn').attr('hidden', false);
|
||||
});
|
||||
|
||||
|
||||
function query_users(querystr) {
|
||||
let req = new XMLHttpRequest();
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
let sel = $('#form_share_table_select_new_user');
|
||||
sel.empty();
|
||||
let resp = $.parseJSON(this.responseText);
|
||||
for (user of resp.slice(0, 5)) {
|
||||
sel
|
||||
.append($('<li>')
|
||||
.attr('class', "list-group-item align-content-center")
|
||||
.text(user.username)
|
||||
.on('click', function() {
|
||||
$('#form_share_table_new_user_input')
|
||||
.val(user.username)
|
||||
.attr('disabled', true);
|
||||
$('#form_share_table_new_user_id')
|
||||
.val(user.id)
|
||||
.attr('disabled', false);
|
||||
}));
|
||||
console.log(sel);
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
req.open("POST", "/users", true);
|
||||
req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||||
req.send("query=" + querystr);
|
||||
}
|
||||
|
||||
function toggle_edit_tname() {
|
||||
let span = document.getElementById('tname');
|
||||
if (!edit_mode) {
|
||||
span.innerHTML = `
|
||||
<div class='row'>
|
||||
<div class='col-auto ml-1 p-2'>
|
||||
<form id='form_edit_table' action='/table/name/edit' method='post'>
|
||||
<input name='tblid' value='${tblid}' hidden />
|
||||
<input type='text' class='form-control' id='tname_edit' name='new_name' value='${tblname}' />
|
||||
</form>
|
||||
</div>
|
||||
<div class='col-auto mt-0 pt-0 pr-1'>
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-secondary" onclick="toggle_edit_tname();" type="button"><i class="bi bi-x"></i></button>
|
||||
<button class='btn btn-success' type='button' onclick='edit_tname()'>
|
||||
<i class='bi bi-check'></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class='btn btn-danger' type="button" onclick='confirm_delete("table");'><i class="bi bi-trash3-fill"></i></button>
|
||||
</div>
|
||||
<form id="form_delete_table" action="/table/delete" method="post">
|
||||
<input value="${tblid}" name="tblid" hidden />
|
||||
</form>`;
|
||||
edit_mode = true;
|
||||
document.getElementById('pencil_button_edit_tname').hidden = true;
|
||||
} else {
|
||||
document.getElementById('pencil_button_edit_tname').hidden = false;
|
||||
span.innerHTML = `${tblname}`;
|
||||
edit_mode = false;
|
||||
}
|
||||
}
|
||||
function edit_tname() {
|
||||
document.getElementById('form_edit_table').submit();
|
||||
toggle_edit_tname();
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5">New table</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="$('#form_create_table).trigger('reset');"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="form_create_table" method="post" action="/table/create">
|
||||
|
|
@ -77,7 +77,7 @@
|
|||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5">Import new table</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="$('#form_import_table').trigger('reset');"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form action="/table/import" method="post" id="form_import_table">
|
||||
|
|
|
|||
|
|
@ -64,5 +64,51 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
{% if sharednms |length > 1 %}
|
||||
<div class="accordion" id="shared_tables">
|
||||
{% for tname in sharednms %}
|
||||
{% set clms = sharedcols[loop.index0] %}
|
||||
{% set rws = sharedrows[loop.index0] %}
|
||||
{% set tid = sharedtblids[loop.index0] %}
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#acc_shared_{{ tid }}" aria-expanded="false" aria-controls="acc_shared_{{ tid }}">
|
||||
{{ tname }}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="acc_shared_{{ tid }}" class="accordion-collapse collapse" data-bs-parent="#shared_tables">
|
||||
<div class="accordion-body">
|
||||
<div class="row justify-content-between">
|
||||
<div class="col-auto"><h3>{{ tname }}</h3></div><div class="col-auto"><a class="btn btn-primary" href="/table/{{ tid }}">Open</a></div>
|
||||
</div>
|
||||
<!-- Preview -->
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
{% for clm in clms %}
|
||||
<th>{{ clm }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in rws %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
{% for clm in clms %}
|
||||
<td>{{ row[loop.index0] }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock body %}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,25 +25,30 @@
|
|||
|
||||
<!-- Table header and editing -->
|
||||
<div class="row justify-content-between">
|
||||
<div class="col-auto">
|
||||
<div class="col-2 me-0 pe-0 pt-0 mt-0">
|
||||
<h1>
|
||||
<div class='input-group'>
|
||||
<span id="tname">{{ tblname }}</span>
|
||||
<button class="btn" type="button" onclick="toggle_edit_tname();" id="pencil_button_edit_tname">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class='input-group mt-0'>
|
||||
<span id="tname">{{ tblname }}</span>
|
||||
{% block edit_tname %}
|
||||
{% endblock edit_tname %}
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="col-1 align-self-start align-content-start ms-0 ps-0 mt-0 pt-0">
|
||||
{% block share %}
|
||||
{% endblock share %}
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Search bar -->
|
||||
<div class="col-auto">
|
||||
<div class="btn-toolbar mt-2" role="toolbar">
|
||||
<div class="btn-group me-2">
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#create_entry_modal">
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-3 align-self-end align-content-end pt-0 mt-0">
|
||||
<div class="btn-toolbar mt-0 pt-0" role="toolbar">
|
||||
{% block new_entry %}
|
||||
{% endblock new_entry %}
|
||||
<form method="get" action="/table/{{ tblid }}">
|
||||
<div class="input-group">
|
||||
<div class="input-group-text dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
|
|
@ -81,7 +86,8 @@
|
|||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="btn-toolbar">
|
||||
<button class="btn" data-bs-toggle="modal" data-bs-target="#create_column_modal" type="button"><i class="bi bi-bookmark-plus"></i></button>
|
||||
{% block new_column %}
|
||||
{% endblock new_column %}
|
||||
<form method="get" action="/table/{{ tblid }}">
|
||||
<input value="0" name="sort_field" hidden />
|
||||
<input value="{% if sort_field == 0 %}{{ (sort_dir + 1) % 2}}{% else %}0{% endif %}" name="sort_dir" hidden />
|
||||
|
|
@ -105,7 +111,7 @@
|
|||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="btn-toolbar">
|
||||
<button class="btn p-0 me-2" type="button" data-bs-toggle="modal" data-bs-target="#edit_column_modal" onclick="edit_column({{ loop.index0 }});"><i class="bi bi-pencil"></i></button>
|
||||
<button class="btn p-0 me-2 edit_column_btn" type="button" data-bs-toggle="modal" data-bs-target="#edit_column_modal" onclick="edit_column({{ loop.index0 }});" hidden><i class="bi bi-pencil"></i></button>
|
||||
<form method="GET" action="/table/{{ tblid }}">
|
||||
<input value="{{ loop.index }}" name="sort_field" hidden />
|
||||
<input value="{% if sort_field == loop.index %}{{ (sort_dir + 1) % 2}}{% else %}0{% endif %}" name="sort_dir" hidden />
|
||||
|
|
@ -136,20 +142,5 @@
|
|||
</tbody>
|
||||
</table>
|
||||
{% endblock body %}
|
||||
{% block modals %}
|
||||
<!-- Table specific modals -->
|
||||
{% include "table_modals" %}
|
||||
{% endblock modals %}
|
||||
|
||||
{% block script %}
|
||||
<!-- table specific values -->
|
||||
<script type="text/javascript">
|
||||
const column_names = [{% for col in column_names %} '{{ col }}', {% endfor %}];
|
||||
const column_types = [{% for col in column_types %} {{ col }}, {% endfor %}];
|
||||
const tblid = {{ tblid }};
|
||||
const tblname = '{{ tblname }}';
|
||||
</script>
|
||||
<!-- Table specific functionality -->
|
||||
<script type="text/javascript" src="/js/table.js"></script>
|
||||
{% endblock script %}
|
||||
|
||||
|
|
|
|||
25
templates/table_owned.html.tera
Normal file
25
templates/table_owned.html.tera
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{% extends "table_write" %}
|
||||
|
||||
{% block edit_tname %}
|
||||
<button class="btn" type="button" onclick="toggle_edit_tname();" id="pencil_button_edit_tname">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
{% endblock edit_tname %}
|
||||
|
||||
{% block share %}
|
||||
<div class="button-group mt-0">
|
||||
<button class="btn btn-outline-dark mt-0" data-bs-toggle="modal" data-bs-target="#share_table_modal"><i class="bi bi-send-arrow-up"></i></button>
|
||||
</div>
|
||||
{% endblock share %}
|
||||
|
||||
{% block more_modals %}
|
||||
{{ super() }}
|
||||
<!-- Table owned specific modals -->
|
||||
{% include "table_owned_modals" %}
|
||||
{% endblock more_modals %}
|
||||
|
||||
{% block script %}
|
||||
<!-- Table owned specific functionality -->
|
||||
<script type="text/javascript" src="/js/table_owned.js"></script>
|
||||
{% endblock script %}
|
||||
|
||||
65
templates/table_owned_modals.html.tera
Normal file
65
templates/table_owned_modals.html.tera
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
|
||||
{% block modal_share_table %}
|
||||
<!-- Share table -->
|
||||
<div class="modal fade" id="share_table_modal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5">
|
||||
Share table
|
||||
</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="$( '#form_share_table' ).trigger('reset');"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form action="/table/share" method="post" id="form_share_table" onreset="$('.2benabled').prop('disabled', false);">
|
||||
<input name="tblid" value="{{ tblid }}" hidden>
|
||||
{% for user in shared %}
|
||||
<div class="row py-1">
|
||||
<div class="col-auto">
|
||||
<input value="{{ user.username }}" readonly id="form_share_table_user_{{ user.id }}" class="form-control 2benabled" />
|
||||
<input value="{{ user.id }}" name="sharees" hidden />
|
||||
</div>
|
||||
<div class="col-auto form-check form-check-inline pt-2">
|
||||
<label for="form_share_table_ro_{{ user.id }}" class="form-check-label"> Read only </label>
|
||||
<input id="form_share_table_ro_{{ user.id }}" type="checkbox" class="form-check-input 2benabled" {% if user.readonly %}checked{% endif %} onchange="$('#form_share_table_action_ro_{{ user.id }}').val($(this).is(':checked'));" />
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<input id="form_share_table_action_ro_{{ user.id }}" name="readonly" value="{{ user.readonly }}" hidden>
|
||||
<input id="form_share_table_action_del_{{ user.id }}" name="delete" value="false" hidden>
|
||||
<button id="form_share_table_delete_button_{{ user.id }}" class="btn btn-outline-danger 2benabled" type="button" onclick="$('#form_share_table_action_del_{{ user.id }}').val(true); $('#form_share_table_ro_{{ user.id }}').prop('disabled', true); $('#form_share_table_user_{{ user.id }}').prop('disabled', true); $('#form_share_table_delete_button_{{ user.id }}').prop('disabled', true);"><i class="bi bi-trash3"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="row py-1">
|
||||
<div class="col-auto">
|
||||
<input id="form_share_table_new_user_input" placeholder="User" class="form-control" onchange="query_users($(this).val());" autofocus autocomplete="off" />
|
||||
<input id="form_share_table_new_user_id" name="new_user" hidden>
|
||||
<div class="card text-bg-light mt-2">
|
||||
<ul class="list-group list-group-flush" id="form_share_table_select_new_user">
|
||||
<!-- users to select from -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto form-check form-check-inline pt-2">
|
||||
<label for="form_share_table_new_ro">Read-only</label>
|
||||
<input name="readonly" type="checkbox" class="form-check-input" checked />
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="row justify-content-betwen">
|
||||
<div class="col-auto">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-secondary" data-bs-dismiss="modal" onclick="$('#form_share_table').trigger('reset');"><i class="bi bi-x"></i></button>
|
||||
<button class="btn btn-primary" onclick="$('#form_share_table').trigger('submit').trigger('reset');"><i class="bi bi-check-lg"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock modal_share_table %}
|
||||
1
templates/table_readonly.html.tera
Normal file
1
templates/table_readonly.html.tera
Normal file
|
|
@ -0,0 +1 @@
|
|||
{% extends "table" %}
|
||||
38
templates/table_write.html.tera
Normal file
38
templates/table_write.html.tera
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
{% extends "table" %}
|
||||
|
||||
{% block new_entry %}
|
||||
<div class="btn-group me-2 mt-0">
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#create_entry_modal">
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endblock new_entry %}
|
||||
|
||||
{% block new_column %}
|
||||
<button class="btn" data-bs-toggle="modal" data-bs-target="#create_column_modal" type="button"><i class="bi bi-bookmark-plus"></i></button>
|
||||
{% endblock new_column %}
|
||||
|
||||
|
||||
{% block edit_column %}
|
||||
|
||||
{% endblock edit_column %}
|
||||
|
||||
|
||||
{% block modals %}
|
||||
<!-- Table write specific modals -->
|
||||
{% include "table_write_modals" %}
|
||||
{% endblock modals %}
|
||||
|
||||
{% block script %}
|
||||
<!-- table specific values -->
|
||||
<script type="text/javascript">
|
||||
const column_names = [{% for col in column_names %} '{{ col }}', {% endfor %}];
|
||||
const column_types = [{% for col in column_types %} {{ col }}, {% endfor %}];
|
||||
const tblid = {{ tblid }};
|
||||
const tblname = '{{ tblname }}';
|
||||
</script>
|
||||
<!-- Table write specific functionality -->
|
||||
<script type="text/javascript" src="/js/table_write.js"></script>
|
||||
{% endblock script %}
|
||||
|
||||
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5">Add entry</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="document.getElementById('form_create_entry').reset();"></button>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="$('#form_create_entry').trigger('reset');"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h5>Cell values:</h5>
|
||||
|
|
@ -49,8 +49,8 @@
|
|||
<div class="col-auto">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-secondary" data-bs-dismiss="modal" type="button" onclick="document.getElementById('form_create_entry').reset();"><i class="bi bi-x"></i></button>
|
||||
<button class="btn btn-primary" type="button" onclick="document.getElementById('form_create_entry').submit().reset();"><i class="bi bi-check-lg"></i></button>
|
||||
<button class="btn btn-secondary" data-bs-dismiss="modal" type="button" onclick="$('#form_create_entry').trigger('reset');"><i class="bi bi-x"></i></button>
|
||||
<button class="btn btn-primary" type="button" onclick="$('#form_create_entry').trigger('submit').trigger('reset');"><i class="bi bi-check-lg"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -66,7 +66,7 @@
|
|||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5"><div class="row"><div class="col-auto">Row <span id="modal_caller"></span></div><div class="col-auto"><i class="bi bi-pencil-fill"></i></div></div></h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="document.getElementById('form_edit_entry').reset();"></button>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="$('#form_edit_entry').trigger('reset');"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form action="/row/edit" method="post" id="form_edit_entry">
|
||||
|
|
@ -96,8 +96,8 @@
|
|||
</form>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-secondary" data-bs-dismiss="modal" type="button" onclick="document.getElementById('form_edit_entry').reset();"><i class="bi bi-x"></i></button>
|
||||
<button class="btn btn-primary" type="button" onclick="submit_and_reset('form_edit_entry');" class="bi bi-check-lg"></i></button>
|
||||
<button class="btn btn-secondary" data-bs-dismiss="modal" type="button" onclick="$('#form_edit_entry').trigger('reset');"><i class="bi bi-x"></i></button>
|
||||
<button class="btn btn-primary" type="button" onclick="$('#form_edit_entry').trigger('submit').trigger('reset');"><i class="bi bi-check-lg"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -108,7 +108,7 @@
|
|||
|
||||
{% block modal_new_column %}
|
||||
<!-- Add column -->
|
||||
<div class="modal fade" id="create_column_modal" tabindex="-1" aria-hidden="true" onclick="document.getElementById('form_create_column').reset();">
|
||||
<div class="modal fade" id="create_column_modal" tabindex="-1" aria-hidden="true" onclick="$('#form_create_column').trigger('reset');">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
|
|
@ -137,8 +137,8 @@
|
|||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-secondary" data-bs-dismiss="modal" type="button" onclick="document.getElementById('form_create_column').reset();"><i class="bi bi-x"></i></button>
|
||||
<button class="btn btn-primary" type="button" onclick="submit_and_reset('form_create_column');"><i class="bi bi-check-lg"></i></button>
|
||||
<button class="btn btn-secondary" data-bs-dismiss="modal" type="button" onclick="$'#form_create_column').trigger('reset');"><i class="bi bi-x"></i></button>
|
||||
<button class="btn btn-primary" type="button" onclick="$('#form_create_column').trigger('submit').trigger('reset');"><i class="bi bi-check-lg"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -153,7 +153,7 @@
|
|||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5"><div class="row"><div class="col-auto"><span id="modal_caller"></span></div><div class="col-auto"><i class="bi bi-pencil-fill"></i></div></div></h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="$('#form_edit_column').trigger('reset');"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form action="/column/edit" method="post" id="form_edit_column">
|
||||
|
|
@ -186,8 +186,8 @@
|
|||
</form>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-secondary" data-bs-dismiss="modal" type="button" onclick="document.getElementById('form_edit_column').reset();"><i class="bi bi-x"></i></button>
|
||||
<button class="btn btn-primary" type="button" onclick="submit_and_reset('form_edit_column');"><i class="bi bi-check-lg"></i></button>
|
||||
<button class="btn btn-secondary" data-bs-dismiss="modal" type="button" onclick="$('#form_edit_column').trigger('reset');"><i class="bi bi-x"></i></button>
|
||||
<button class="btn btn-primary" type="button" onclick="$('#form_edit_column').trigger('submit').trigger('reset');"><i class="bi bi-check-lg"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Loading…
Reference in New Issue
Block a user