-
Notifications
You must be signed in to change notification settings - Fork 92
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: ✨ Added Onboarding Customisation
* feat: 🚀 Add backend for onboarding pages * refactor: 📦 Cleanup code for darkmode * feat: ✨ Add onboarding page configurator * feat: 🎉 Add wizarr "theme" to markdown editor * feat: 🎊 Cleanup toolbar for markdown editor * refactor: 📦 Cleanup code for OnboardingSection * feat: 🎉 Add custom onboarding markdown to help page * refactor: 📦 Cleanup imports for md-editor types * feat: 🎊 Add tooltips for onboarding page manage buttons * feat: 🎉 Allow translations for markdown editor buttons, etc * fix: 🩹 Fix error with height in Carousel * fix: 🩹 Update message and doc for onboarding api * feat: 🎉 Create migration file for onboarding table * feat: 🚀 Allow uploading images for onboarding pages * fix: 🩹 md-editor-v3 as frontend dependency * fix: 🐛 Fix error in .gitignore * fix: 🚑 Resolve change request for PR * feat: 🎊 Add finish button to help page
- Loading branch information
1 parent
c8b8d36
commit e6e9d68
Showing
22 changed files
with
2,252 additions
and
392 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
85 changes: 85 additions & 0 deletions
85
apps/wizarr-backend/wizarr_backend/api/routes/image_api.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import os | ||
from json import dumps, loads | ||
from uuid import uuid4 | ||
from flask import send_from_directory, current_app, request | ||
from flask_jwt_extended import jwt_required | ||
from flask_restx import Namespace, Resource, reqparse | ||
from werkzeug.utils import secure_filename | ||
from werkzeug.datastructures import FileStorage | ||
|
||
api = Namespace("Image", description="Image related operations", path="/image") | ||
|
||
# Define the file upload parser | ||
file_upload_parser = reqparse.RequestParser() | ||
file_upload_parser.add_argument('file', location='files', | ||
type=FileStorage, required=True, | ||
help='Image file') | ||
|
||
@api.route("") | ||
class ImageListApi(Resource): | ||
"""API resource for all images""" | ||
|
||
@jwt_required() | ||
@api.doc(security="jwt") | ||
@api.expect(file_upload_parser) | ||
def post(self): | ||
"""Upload image""" | ||
# Check if the post request has the file part | ||
if 'file' not in request.files: | ||
return {"message": "No file part"}, 400 | ||
file = request.files['file'] | ||
# If the user does not select a file, the browser submits an | ||
# empty file without a filename. | ||
if file.filename == '': | ||
return {"message": "No selected file"}, 400 | ||
if file: | ||
# Extract the file extension | ||
file_extension = os.path.splitext(secure_filename(file.filename))[1].lower() | ||
if file_extension not in ['.png', '.jpg', '.jpeg']: | ||
return {"message": "Unsupported file format"}, 400 | ||
|
||
upload_folder = current_app.config['UPLOAD_FOLDER'] | ||
if not os.path.exists(upload_folder): | ||
os.makedirs(upload_folder) | ||
# Generate a unique filename using UUID | ||
filename = f"{uuid4()}{file_extension}" | ||
|
||
# Check if the file exists and generate a new UUID if it does | ||
while os.path.exists(os.path.join(upload_folder, filename)): | ||
filename = f"{uuid4()}{file_extension}" | ||
file_path = os.path.join(upload_folder, filename) | ||
file.save(file_path) | ||
return {"message": f"File {filename} uploaded successfully", "filename": filename}, 201 | ||
|
||
|
||
@api.route("/<filename>") | ||
class ImageAPI(Resource): | ||
"""API resource for a single image""" | ||
|
||
@api.response(404, "Image not found") | ||
@api.response(500, "Internal server error") | ||
def get(self, filename): | ||
"""Get image""" | ||
# Assuming images are stored in a directory specified by UPLOAD_FOLDER config | ||
upload_folder = current_app.config['UPLOAD_FOLDER'] | ||
image_path = os.path.join(upload_folder, filename) | ||
if os.path.exists(image_path): | ||
Check failure Code scanning / CodeQL Uncontrolled data used in path expression High
This path depends on a
user-provided value Error loading related location Loading |
||
return send_from_directory(upload_folder, filename) | ||
else: | ||
return {"message": "Image not found"}, 404 | ||
|
||
@jwt_required() | ||
@api.doc(description="Delete a single image") | ||
@api.response(404, "Image not found") | ||
@api.response(500, "Internal server error") | ||
def delete(self, filename): | ||
"""Delete image""" | ||
upload_folder = current_app.config['UPLOAD_FOLDER'] | ||
image_path = os.path.join(upload_folder, filename) | ||
|
||
# Check if the file exists | ||
if not os.path.exists(image_path): | ||
Check failure Code scanning / CodeQL Uncontrolled data used in path expression High
This path depends on a
user-provided value Error loading related location Loading |
||
return {"message": "Image not found"}, 404 | ||
|
||
os.remove(image_path) | ||
Check failure Code scanning / CodeQL Uncontrolled data used in path expression High
This path depends on a
user-provided value Error loading related location Loading |
||
return {"message": "Image deleted successfully"}, 200 |
106 changes: 106 additions & 0 deletions
106
apps/wizarr-backend/wizarr_backend/api/routes/onboarding_api.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
from json import dumps, loads | ||
from flask import request | ||
from flask_jwt_extended import jwt_required | ||
from flask_restx import Namespace, Resource | ||
from playhouse.shortcuts import model_to_dict | ||
from peewee import fn | ||
from app.models.database import db | ||
|
||
from app.models.database.onboarding import Onboarding as OnboardingDB | ||
|
||
api = Namespace("Onboarding", description="Onboarding related operations", path="/onboarding") | ||
|
||
@api.route("") | ||
class OnboardingListApi(Resource): | ||
"""API resource for all onboarding pages""" | ||
|
||
@api.doc(security="jwt") | ||
def get(self): | ||
"""Get onboarding pages""" | ||
response = list(OnboardingDB.select().order_by(OnboardingDB.order).dicts()) | ||
return loads(dumps(response, indent=4, sort_keys=True, default=str)), 200 | ||
|
||
@api.doc(security="jwt") | ||
@jwt_required() | ||
def post(self): | ||
"""Create onboarding page""" | ||
value = request.form.get("value") | ||
enabled = request.form.get("enabled") in ["true", "True", "1"] | ||
max_order = OnboardingDB.select(fn.MAX(OnboardingDB.order)).scalar() or 0 | ||
new_order = max_order + 1 | ||
onboarding_page = OnboardingDB.create(order=new_order, value=value, enabled=enabled) | ||
onboarding_page.save() | ||
return { "message": "Onboarding page created", "page": model_to_dict(onboarding_page) }, 200 | ||
|
||
|
||
@api.route("/<int:onboarding_id>") | ||
class OnboardingAPI(Resource): | ||
"""API resource for a single onboarding page""" | ||
|
||
method_decorators = [jwt_required()] | ||
|
||
@api.doc(description="Updates a single onboarding page") | ||
@api.response(404, "Onboarding page not found") | ||
@api.response(500, "Internal server error") | ||
def put(self, onboarding_id: int): | ||
value = request.form.get("value") | ||
enabled = request.form.get("enabled") | ||
order = request.form.get("order", type=int) | ||
|
||
with db.atomic() as transaction: | ||
page = OnboardingDB.get_or_none(OnboardingDB.id == onboarding_id) | ||
if not page: | ||
return {"error": "Onboarding page not found"}, 404 | ||
|
||
if(value is not None): | ||
page.value = value | ||
if(enabled is not None): | ||
page.enabled = enabled in ["true", "True", "1"] | ||
|
||
if order is not None and page.order != order: | ||
step = 1 if page.order > order else -1 | ||
start, end = sorted([page.order, order]) | ||
|
||
# Update orders of affected pages | ||
affected_pages = OnboardingDB.select().where( | ||
OnboardingDB.id != onboarding_id, | ||
OnboardingDB.order >= start, | ||
OnboardingDB.order <= end, | ||
) | ||
|
||
for p in affected_pages: | ||
p.order += step | ||
p.save() # Save each affected page | ||
|
||
# Update the target page | ||
page.order = order | ||
page.save() # Save the target page | ||
|
||
try: | ||
transaction.commit() # Commit the transaction | ||
except Exception as e: | ||
transaction.rollback() # Rollback in case of error | ||
return {"error": str(e)}, 500 | ||
return loads(dumps(model_to_dict(page), indent=4, sort_keys=True, default=str)), 200 | ||
|
||
@api.doc(description="Delete a single onboarding page") | ||
@api.response(404, "Invite not found") | ||
@api.response(500, "Internal server error") | ||
def delete(self, onboarding_id): | ||
"""Delete onboarding page""" | ||
# Select the invite from the database | ||
onboarding_page = OnboardingDB.get_or_none(OnboardingDB.id == onboarding_id) | ||
|
||
# Check if the invite exists | ||
if not onboarding_page: | ||
return {"message": "Onboarding page not found"}, 404 | ||
|
||
onboarding_page.delete_instance() | ||
|
||
# Update order of subsequent pages | ||
subsequent_pages = OnboardingDB.select().where(OnboardingDB.order > onboarding_page.order) | ||
for page in subsequent_pages: | ||
page.order -= 1 | ||
page.save() | ||
|
||
return { "message": "Onboarding page deleted successfully" }, 200 |
40 changes: 40 additions & 0 deletions
40
apps/wizarr-backend/wizarr_backend/app/migrator/migrations/2024-07-30_11-02-04.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# | ||
# CREATED ON VERSION: V4.1.1 | ||
# MIGRATION: 2024-07-30_11-02-04 | ||
# CREATED: Tue Jul 30 2024 | ||
# | ||
|
||
from peewee import * | ||
from playhouse.migrate import * | ||
|
||
from app import db | ||
|
||
# Do not change the name of this file, | ||
# migrations are run in order of their filenames date and time | ||
|
||
def run(): | ||
# Use migrator to perform actions on the database | ||
migrator = SqliteMigrator(db) | ||
|
||
# Create new table 'onboarding' | ||
with db.transaction(): | ||
# Check if the table exists | ||
cursor = db.cursor() | ||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='onboarding';") | ||
table_exists = cursor.fetchone() | ||
|
||
if not table_exists: | ||
db.execute_sql(""" | ||
CREATE TABLE "onboarding" ( | ||
"id" INTEGER NOT NULL UNIQUE, | ||
"value" TEXT NOT NULL, | ||
"order" INTEGER NOT NULL UNIQUE, | ||
"enabled" INTEGER NOT NULL DEFAULT 1, | ||
PRIMARY KEY("id") | ||
) | ||
""") | ||
print("Table 'onboarding' created successfully") | ||
else: | ||
print("Table 'onboarding' already exists") | ||
|
||
print("Migration 2024-07-30_11-02-04 complete") |
8 changes: 8 additions & 0 deletions
8
apps/wizarr-backend/wizarr_backend/app/models/database/onboarding.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
from peewee import BooleanField, CharField, IntegerField | ||
from app.models.database.base import BaseModel | ||
|
||
class Onboarding(BaseModel): | ||
id = IntegerField(primary_key=True, unique=True) | ||
value = CharField(null=False) | ||
order = IntegerField(null=False, unique=True) | ||
enabled = BooleanField(default=False) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
.md-editor-preview { | ||
--md-theme-quote-border: 5px solid rgb(208, 49, 67) !important; | ||
--md-theme-link-color: rgb(208, 49, 67) !important; | ||
--md-theme-link-hover-color: rgb(208, 49, 67) !important; | ||
} | ||
|
||
.md-editor-preview { | ||
word-break: normal !important; | ||
|
||
h1, h2, h3, h4, h5, h6 { | ||
word-break: normal !important; | ||
} | ||
h1, h2, h3, h4, h5, h6 { | ||
&:first-child { | ||
margin-top: 0; | ||
} | ||
} | ||
} | ||
|
||
.md-editor.md-editor-dark, .md-editor-modal-container[data-theme='dark'] { | ||
--md-color: #fff; | ||
} | ||
|
||
.md-editor.md-editor-previewOnly { | ||
background-color: inherit !important; | ||
} | ||
|
||
.md-editor.md-editor-previewOnly .md-editor-preview-wrapper { | ||
padding: inherit !important; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.