Merge branch 'frappe:develop' into use-other-wording-for-quick-entry

This commit is contained in:
Corentin Flr 2023-11-17 12:01:25 +01:00 committed by GitHub
commit ecda12402c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
176 changed files with 3519 additions and 2328 deletions

View file

@ -35,7 +35,7 @@ repos:
rev: v2.7.1
hooks:
- id: prettier
types_or: [javascript]
types_or: [javascript, vue, scss]
# Ignore any files that might contain jinja / bundles
exclude: |
(?x)^(
@ -44,7 +44,8 @@ repos:
.*boilerplate.*|
frappe/www/website_script.js|
frappe/templates/includes/.*|
frappe/public/js/lib/.*
frappe/public/js/lib/.*|
frappe/website/doctype/website_theme/website_theme_template.scss
)$

View file

@ -10,6 +10,12 @@ export default {
fieldtype: "Data",
label: "Data 3",
},
{
fieldname: "gender",
fieldtype: "Link",
label: "Gender",
options: "Gender",
},
{
fieldname: "tab",
fieldtype: "Tab Break",

View file

@ -32,10 +32,13 @@ context("Control Float", () => {
cy.wait(200);
cy.fill_field("float_number", d.input, "Float").blur();
cy.get_field("float_number", "Float").should("have.value", d.blur_expected);
cy.wait(100);
cy.get_field("float_number", "Float").focus();
cy.wait(100);
cy.get_field("float_number", "Float").blur();
cy.wait(100);
cy.get_field("float_number", "Float").focus();
cy.wait(100);
cy.get_field("float_number", "Float").should("have.value", d.focus_expected);
});
});
@ -49,17 +52,17 @@ context("Control Float", () => {
{
input: "364.87,334",
blur_expected: "36.487,334",
focus_expected: "36487.334",
focus_expected: "36.487,334",
},
{
input: "36487,334",
blur_expected: "36.487,334",
focus_expected: "36487.334",
input: "36487,335",
blur_expected: "36.487,335",
focus_expected: "36.487,335",
},
{
input: "100",
blur_expected: "100,000",
focus_expected: "100",
input: "2*(2+47)+1,5+1",
blur_expected: "100,500",
focus_expected: "100,500",
},
],
},
@ -67,19 +70,19 @@ context("Control Float", () => {
number_format: "#,###.##",
values: [
{
input: "364,87.334",
blur_expected: "36,487.334",
focus_expected: "36487.334",
input: "464,87.334",
blur_expected: "46,487.334",
focus_expected: "46,487.334",
},
{
input: "36487.334",
blur_expected: "36,487.334",
focus_expected: "36487.334",
input: "46487.335",
blur_expected: "46,487.335",
focus_expected: "46,487.335",
},
{
input: "100",
blur_expected: "100.000",
focus_expected: "100",
input: "3*(2+47)+1.5+1",
blur_expected: "149.500",
focus_expected: "149.500",
},
],
},
@ -90,13 +93,13 @@ context("Control Float", () => {
{
input: "12.345",
blur_expected: "12.345,000",
focus_expected: "12345",
focus_expected: "12.345,000",
},
{
// parseFloat would reduce 12,340 to 12,34 if this string was ever to be parsed
input: "12.340",
blur_expected: "12.340,000",
focus_expected: "12340",
focus_expected: "12.340,000",
},
],
},

View file

@ -5,6 +5,8 @@ context("Customize Form", () => {
});
it("Changing to naming rule should update autoname", () => {
cy.fill_field("doc_type", "ToDo", "Link").blur();
cy.wait(2000);
cy.findByRole("tab", { name: "Details" }).click();
cy.click_form_section("Naming");
const naming_rule_default_autoname_map = {
"Set by user": "prompt",

View file

@ -35,6 +35,40 @@ context("Form Builder", () => {
cy.get(".title-area .indicator-pill.orange").should("have.text", "Not Saved");
});
it("Check if Filters are applied to the link field", () => {
// Visit the Form Builder
cy.visit(`/app/doctype/${doctype_name}`);
cy.findByRole("tab", { name: "Form" }).click();
cy.get("[data-fieldname='gender']").click();
// click on filter action button
cy.get('[data-fieldname="gender"] .field-actions button:first').click();
// add filter
cy.get(".modal-body .clear-filters").click();
cy.get(".modal-body .filter-action-buttons .add-filter").click();
cy.wait(100);
cy.get(".modal-body .filter-box .list_filter .filter-field .link-field input").type(
"Male"
);
cy.get(".btn-modal-primary").click();
// Save the document
cy.click_doc_primary_button("Save");
// Open a new Form
cy.new_form(doctype_name);
// Click on the "salutation" field
cy.get_field("gender").clear().click();
cy.intercept("POST", "/api/method/frappe.desk.search.search_link").as("search_link");
cy.wait("@search_link").then((data) => {
expect(data.response.body.message.length).to.eq(1);
expect(data.response.body.message[0].value).to.eq("Male");
});
});
it("Add empty section and save", () => {
cy.visit(`/app/doctype/${doctype_name}`);
cy.findByRole("tab", { name: "Form" }).click();
@ -43,7 +77,8 @@ context("Form Builder", () => {
// add new section
cy.get(first_section).click(15, 10);
cy.get(first_section).find(".section-actions button:first").click();
cy.get(first_section).find(".dropdown-btn:first").click();
cy.get(".dropdown-options:visible .dropdown-item:first").click();
// save
cy.click_doc_primary_button("Save");
@ -184,12 +219,14 @@ context("Form Builder", () => {
// add new section
cy.get(first_section).click(15, 10);
cy.get(first_section).find(".section-actions button:first").click();
cy.get(first_section).find(".dropdown-btn:first").click();
cy.get(".dropdown-options:visible .dropdown-item:first").click();
cy.get(".tab-content.active .form-section-container").should("have.length", 2);
// add new column
cy.get(first_section).find(".column:first").click(15, 10);
cy.get(first_section).find(".column:first .column-actions button:first").click();
cy.get(first_section).click(15, 10);
cy.get(first_section).find(".dropdown-btn:first").click();
cy.get(".dropdown-options:visible .dropdown-item:last").click();
cy.get(first_section).find(".column").should("have.length", 2);
});
@ -197,13 +234,15 @@ context("Form Builder", () => {
let first_section = ".tab-content.active .form-section-container:first";
// remove column
cy.get(first_section).find(".column:first").click(15, 10);
cy.get(first_section).find(".column:first .column-actions button:last").click();
cy.get(first_section).click(15, 10);
cy.get(first_section).find(".dropdown-btn:first").click();
cy.get(".dropdown-options:visible .dropdown-item:last").click();
cy.get(first_section).find(".column").should("have.length", 1);
// remove section
cy.get(first_section).click(15, 10);
cy.get(first_section).find(".section-actions button:last").click();
cy.get(first_section).find(".dropdown-btn:first").click();
cy.get(".dropdown-options:visible .dropdown-item").eq(1).click();
cy.get(".tab-content.active .form-section-container").should("have.length", 1);
// remove tab

View file

@ -4,7 +4,7 @@ context("Grid Configuration", () => {
cy.visit("/app/doctype/User");
});
it("Set user wise grid settings", () => {
cy.findByRole("tab", { name: "Form" }).click();
cy.findByRole("tab", { name: "Settings" }).click();
cy.get('.form-section[data-fieldname="fields_section"]').click();
cy.wait(100);
cy.get('.frappe-control[data-fieldname="fields"]').as("table");

View file

@ -156,6 +156,7 @@ context("Web Form", () => {
cy.findByRole("tab", { name: "Customization" }).click();
cy.fill_field("breadcrumbs", '[{"label": _("Notes"), "route":"note"}]', "Code");
cy.wait(2000);
cy.get(".form-tabs .nav-item .nav-link").contains("Customization").click();
cy.save();

View file

@ -20,7 +20,6 @@ context("Workspace 2.0", () => {
cy.get(".codex-editor__redactor .ce-block");
cy.get('.custom-actions button[data-label="Create%20Workspace"]').click();
cy.fill_field("title", "Test Private Page", "Data");
cy.fill_field("icon", "edit", "Icon");
cy.get_open_dialog().find(".modal-header").click();
cy.get_open_dialog().find(".btn-primary").click();
@ -52,7 +51,6 @@ context("Workspace 2.0", () => {
cy.get('.custom-actions button[data-label="Create%20Workspace"]').click();
cy.fill_field("title", "Test Child Page", "Data");
cy.fill_field("parent", "Test Private Page", "Select");
cy.fill_field("icon", "edit", "Icon");
cy.get_open_dialog().find(".modal-header").click();
cy.get_open_dialog().find(".btn-primary").click();

View file

@ -20,7 +20,6 @@ context("Workspace Blocks", () => {
cy.get(".codex-editor__redactor .ce-block");
cy.get('.custom-actions button[data-label="Create%20Workspace"]').click();
cy.fill_field("title", "Test Block Page", "Data");
cy.fill_field("icon", "edit", "Icon");
cy.get_open_dialog().find(".modal-header").click();
cy.get_open_dialog().find(".btn-primary").click();

View file

@ -37,22 +37,16 @@ Cypress.Commands.add("login", (email, password) => {
// cy.session clears all localStorage on new login, so we need to retain the last route
const session_last_route = window.localStorage.getItem("session_last_route");
return cy
.session(
[email, password] || "",
() => {
return cy.request({
url: "/api/method/login",
method: "POST",
body: {
usr: email,
pwd: password,
},
});
},
{
cacheAcrossSpecs: true,
}
)
.session([email, password] || "", () => {
return cy.request({
url: "/api/method/login",
method: "POST",
body: {
usr: email,
pwd: password,
},
});
})
.then(() => {
if (session_last_route) {
window.localStorage.setItem("session_last_route", session_last_route);

View file

@ -48,6 +48,11 @@ from frappe.exceptions import SiteNotSpecifiedError
@click.option(
"--set-default", is_flag=True, default=False, help="Set the new site as default site"
)
@click.option(
"--setup-db/--no-setup-db",
default=True,
help="Create user and database in mariadb/postgres; only bootstrap if false",
)
def new_site(
site,
db_root_username=None,
@ -64,6 +69,7 @@ def new_site(
db_host=None,
db_port=None,
set_default=False,
setup_db=True,
):
"Create a new site"
from frappe.installer import _new_site, extract_sql_from_archive
@ -88,6 +94,7 @@ def new_site(
db_type=db_type,
db_host=db_host,
db_port=db_port,
setup_db=setup_db,
)
if set_default:

View file

@ -162,8 +162,6 @@ class Communication(Document, CommunicationEmailMixin):
self.seen = 1
self.sent_or_received = "Sent"
self.set_status()
validate_email(self)
if self.communication_medium == "Email":
@ -173,6 +171,10 @@ class Communication(Document, CommunicationEmailMixin):
self.set_sender_full_name()
if self.is_new():
self.set_status()
self.mark_email_as_spam()
def validate_reference(self):
if self.reference_doctype and self.reference_name:
if not self.reference_owner:
@ -333,9 +335,6 @@ class Communication(Document, CommunicationEmailMixin):
)
def set_status(self):
if not self.is_new():
return
if self.reference_doctype and self.reference_name:
self.status = "Linked"
elif self.communication_type == "Communication":
@ -343,15 +342,13 @@ class Communication(Document, CommunicationEmailMixin):
else:
self.status = "Closed"
# set email status to spam
email_rule = frappe.db.get_value("Email Rule", {"email_id": self.sender, "is_spam": 1})
def mark_email_as_spam(self):
if (
self.communication_type == "Communication"
and self.communication_medium == "Email"
and self.sent_or_received == "Sent"
and email_rule
and self.sent_or_received == "Received"
and frappe.db.exists("Email Rule", {"email_id": self.sender, "is_spam": 1})
):
self.email_status = "Spam"
@classmethod
@ -433,7 +430,18 @@ class Communication(Document, CommunicationEmailMixin):
frappe.db.commit()
def parse_email_for_timeline_links(self):
parse_email(self, [self.recipients, self.cc, self.bcc])
if not frappe.db.get_value("Email Account", self.email_account, "enable_automatic_linking"):
return
for doctype, docname in parse_email([self.recipients, self.cc, self.bcc]):
if not frappe.db.get_value(doctype, docname, ignore=True):
continue
self.add_link(doctype, docname)
if not self.reference_doctype:
self.reference_doctype = doctype
self.reference_name = docname
# Timeline Links
def set_timeline_links(self):
@ -452,20 +460,13 @@ class Communication(Document, CommunicationEmailMixin):
add_contact_links_to_communication(self, contact_name)
def deduplicate_timeline_links(self):
if self.timeline_links:
links, duplicate = [], False
if not self.timeline_links:
return
for l in self.timeline_links:
t = (l.link_doctype, l.link_name)
if not t in links:
links.append(t)
else:
duplicate = True
if duplicate:
self.timeline_links.clear()
for l in links:
self.add_link(link_doctype=l[0], link_name=l[1])
unique_links = {(link.link_doctype, link.link_name) for link in self.timeline_links}
self.timeline_links = []
for doctype, name in unique_links:
self.add_link(doctype, name)
def add_link(self, link_doctype, link_name, autosave=False):
self.append("timeline_links", {"link_doctype": link_doctype, "link_name": link_name})
@ -574,36 +575,35 @@ def add_contact_links_to_communication(communication, contact_name):
communication.add_link(contact_link.link_doctype, contact_link.link_name)
def parse_email(communication, email_strings):
def parse_email(email_strings):
"""
Parse email to add timeline links.
When automatic email linking is enabled, an email from email_strings can contain
a doctype and docname ie in the format `admin+doctype+docname@example.com` or `admin+doctype=docname@example.com`,
the email is parsed and doctype and docname is extracted and timeline link is added.
the email is parsed and doctype and docname is extracted.
"""
if not frappe.db.get_value("Email Account", filters={"enable_automatic_linking": 1}):
return
for email_string in email_strings:
if email_string:
for email in email_string.split(","):
email_username = email.split("@", 1)[0]
email_local_parts = email_username.split("+")
docname = doctype = None
if len(email_local_parts) == 3:
doctype = unquote(email_local_parts[1])
docname = unquote(email_local_parts[2])
if not email_string:
continue
elif len(email_local_parts) == 2:
document_parts = email_local_parts[1].split("=", 1)
if len(document_parts) != 2:
continue
for email in email_string.split(","):
email_username = email.split("@", 1)[0]
email_local_parts = email_username.split("+")
docname = doctype = None
if len(email_local_parts) == 3:
doctype = unquote(email_local_parts[1])
docname = unquote(email_local_parts[2])
doctype = unquote(document_parts[0])
docname = unquote(document_parts[1])
elif len(email_local_parts) == 2:
document_parts = email_local_parts[1].split("=", 1)
if len(document_parts) != 2:
continue
if doctype and docname and frappe.db.get_value(doctype, docname, ignore=True):
communication.add_link(doctype, docname)
doctype = unquote(document_parts[0])
docname = unquote(document_parts[1])
if doctype and docname:
yield doctype, docname
def get_email_without_link(email):

View file

@ -1,10 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
from typing import TYPE_CHECKING
from urllib.parse import quote
import frappe
from frappe.core.doctype.communication.communication import Communication, get_emails
from frappe.core.doctype.communication.communication import Communication, get_emails, parse_email
from frappe.core.doctype.communication.email import add_attachments
from frappe.email.doctype.email_queue.email_queue import EmailQueue
from frappe.tests.utils import FrappeTestCase
@ -219,36 +218,25 @@ class TestCommunication(FrappeTestCase):
self.assertIn(comm_note_1.name, data)
self.assertIn(comm_note_2.name, data)
def test_link_in_email(self):
create_email_account()
def test_parse_email(self):
to = "Jon Doe <jon.doe@example.org>"
cc = """=?UTF-8?Q?Max_Mu=C3=9F?= <max.muss@examle.org>,
erp+Customer+that%20company@example.org"""
bcc = ""
notes = {}
for i in range(2):
frappe.delete_doc_if_exists("Note", f"test document link in email {i}")
notes[i] = frappe.get_doc(
{
"doctype": "Note",
"title": f"test document link in email {i}",
}
).insert(ignore_permissions=True)
results = list(parse_email([to, cc, bcc]))
self.assertEqual([("Customer", "that company")], results)
comm = frappe.get_doc(
{
"doctype": "Communication",
"communication_medium": "Email",
"subject": "Document Link in Email",
"sender": "comm_sender@example.com",
"recipients": f'comm_recipient+{quote("Note")}+{quote(notes[0].name)}@example.com,comm_recipient+{quote("Note")}={quote(notes[1].name)}@example.com',
}
).insert(ignore_permissions=True)
results = list(parse_email([to, bcc]))
self.assertEqual(results, [])
doc_links = [
(timeline_link.link_doctype, timeline_link.link_name) for timeline_link in comm.timeline_links
]
self.assertIn(("Note", notes[0].name), doc_links)
self.assertIn(("Note", notes[1].name), doc_links)
to = "jane.doe+A+Test@example.org"
cc = ""
bcc = "=?UTF-8?Q?Max_Mu=C3=9F?= <max.muss+Note=Very%20important@examle.org>"
results = list(parse_email([to, cc, bcc]))
self.assertEqual([("A", "Test"), ("Note", "Very important")], results)
def test_parse_emails(self):
def test_get_emails(self):
emails = get_emails(
[
"comm_recipient+DocType+DocName@example.com",
@ -293,6 +281,40 @@ class TestCommunication(FrappeTestCase):
self.assertEqual(comm_with_signature.content.count(signature), 1)
self.assertEqual(comm_without_signature.content.count(signature), 1)
def test_mark_as_spam(self):
frappe.get_doc(
{
"doctype": "Email Rule",
"email_id": "spammer@example.com",
"is_spam": 1,
}
).insert(ignore_permissions=True)
spam_comm: Communication = frappe.get_doc(
{
"doctype": "Communication",
"communication_medium": "Email",
"subject": "This is spam",
"sender": "spammer@example.com",
"recipients": "comm_recipient@example.com",
"sent_or_received": "Received",
}
).insert(ignore_permissions=True)
self.assertEqual(spam_comm.email_status, "Spam")
normal_comm: Communication = frappe.get_doc(
{
"doctype": "Communication",
"communication_medium": "Email",
"subject": "This is spam",
"sender": "friendlyhuman@example.com",
"recipients": "comm_recipient@example.com",
"sent_or_received": "Received",
}
).insert(ignore_permissions=True)
self.assertNotEqual(normal_comm.email_status, "Spam")
class TestCommunicationEmailMixin(FrappeTestCase):
def new_communication(self, recipients=None, cc=None, bcc=None) -> Communication:

View file

@ -23,6 +23,7 @@
"options",
"sort_options",
"show_dashboard",
"link_filters",
"defaults_section",
"default",
"column_break_6",
@ -560,13 +561,18 @@
"fieldname": "sort_options",
"fieldtype": "Check",
"label": "Sort Options"
},
{
"fieldname": "link_filters",
"fieldtype": "JSON",
"label": "Link Filters"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-10-25 06:53:45.194081",
"modified": "2023-11-13 11:48:51.502812",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",

View file

@ -87,6 +87,7 @@ class DocField(Document):
is_virtual: DF.Check
label: DF.Data | None
length: DF.Int
link_filters: DF.JSON | None
mandatory_depends_on: DF.Code | None
max_height: DF.Data | None
no_copy: DF.Check

View file

@ -136,6 +136,7 @@ class DocType(Document):
is_virtual: DF.Check
issingle: DF.Check
istable: DF.Check
link_filters: DF.JSON
links: DF.Table[DocTypeLink]
make_attachments_public: DF.Check
max_attachments: DF.Int

View file

@ -101,6 +101,7 @@ frappe.listview_settings["DocType"] = {
role: "System Manager",
share: 1,
write: 1,
submit: values.is_submittable ? 1 : 0,
},
],
fields: [{ fieldtype: "Section Break" }],

View file

@ -376,7 +376,7 @@ def relink_files(doc, fieldname, temp_doc_name):
"attached_to_field": fieldname,
"creation": (
"between",
[now_datetime() - add_to_date(date=now_datetime(), minutes=-60), now_datetime()],
[add_to_date(date=now_datetime(), minutes=-60), now_datetime()],
),
},
)

View file

@ -87,7 +87,9 @@ class RQJob(Document):
matched_job_ids = RQJob.get_matching_job_ids(args)[start : start + page_length]
conn = get_redis_conn()
jobs = [serialize_job(job) for job in Job.fetch_many(job_ids=matched_job_ids, connection=conn)]
jobs = [
serialize_job(job) for job in Job.fetch_many(job_ids=matched_job_ids, connection=conn) if job
]
return sorted(jobs, key=lambda j: j.modified, reverse=order_desc)

View file

@ -15,6 +15,7 @@
"fieldname",
"insert_after",
"length",
"link_filters",
"column_break_6",
"fieldtype",
"precision",
@ -444,6 +445,12 @@
"fieldname": "sort_options",
"fieldtype": "Check",
"label": "Sort Options"
},
{
"fieldname": "link_filters",
"fieldtype": "JSON",
"hidden": 1,
"label": "Link Filters"
}
],
"icon": "fa fa-glass",

View file

@ -94,6 +94,7 @@ class CustomField(Document):
is_virtual: DF.Check
label: DF.Data | None
length: DF.Int
link_filters: DF.JSON | None
mandatory_depends_on: DF.Code | None
module: DF.Link | None
no_copy: DF.Check

View file

@ -149,6 +149,7 @@ frappe.ui.form.on("Customize Form", {
);
render_form_builder(frm);
frm.get_field("form_builder").tab.set_active();
});
}

View file

@ -7,10 +7,12 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"details_tab",
"doc_type",
"properties",
"label",
"search_fields",
"link_filters",
"column_break_5",
"istable",
"is_calendar_and_gantt",
@ -24,17 +26,6 @@
"naming_section",
"naming_rule",
"autoname",
"document_actions_section",
"actions",
"document_links_section",
"links",
"document_states_section",
"states",
"form_tab",
"form_builder",
"fields_section_break",
"fields",
"settings_tab",
"form_settings_section",
"image_field",
"max_attachments",
@ -59,7 +50,17 @@
"section_break_8",
"sort_field",
"column_break_10",
"sort_order"
"sort_order",
"document_actions_section",
"actions",
"document_links_section",
"links",
"document_states_section",
"states",
"fields_section_break",
"fields",
"form_tab",
"form_builder"
],
"fields": [
{
@ -180,7 +181,6 @@
"depends_on": "doc_type",
"fieldname": "fields_section_break",
"fieldtype": "Section Break",
"hidden": 1,
"label": "Fields"
},
{
@ -372,11 +372,6 @@
"fieldtype": "Check",
"label": "Is Calendar and Gantt"
},
{
"fieldname": "settings_tab",
"fieldtype": "Tab Break",
"label": "Settings"
},
{
"fieldname": "form_builder",
"fieldtype": "HTML",
@ -386,6 +381,17 @@
"fieldname": "form_tab",
"fieldtype": "Tab Break",
"label": "Form"
},
{
"fieldname": "link_filters",
"fieldtype": "JSON",
"hidden": 1,
"label": "Link Filters"
},
{
"fieldname": "details_tab",
"fieldtype": "Tab Break",
"label": "Details"
}
],
"hide_toolbar": 1,
@ -394,7 +400,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-10-31 02:04:25.955931",
"modified": "2023-11-16 11:23:06.427432",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form",

View file

@ -54,6 +54,7 @@ class CustomizeForm(Document):
is_calendar_and_gantt: DF.Check
istable: DF.Check
label: DF.Data | None
link_filters: DF.JSON | None
links: DF.Table[DocTypeLink]
make_attachments_public: DF.Check
max_attachments: DF.Int
@ -681,6 +682,17 @@ def is_standard_or_system_generated_field(df):
return not df.get("is_custom_field") or df.get("is_system_generated")
@frappe.whitelist()
def get_link_filters_from_doc_without_customisations(doctype, fieldname):
"""Get the filters of a link field from a doc without customisations
In backend the customisations are not applied.
Customisations are applied in the client side.
"""
doc = frappe.get_doc("DocType", doctype)
field = list(filter(lambda x: x.fieldname == fieldname, doc.fields))
return field[0].link_filters
doctype_properties = {
"search_fields": "Data",
"title_field": "Data",
@ -761,6 +773,7 @@ docfield_properties = {
"hide_days": "Check",
"hide_seconds": "Check",
"is_virtual": "Check",
"link_filters": "JSON",
}
doctype_link_properties = {

View file

@ -24,6 +24,7 @@
"no_copy",
"allow_in_quick_entry",
"translatable",
"link_filters",
"column_break_7",
"default",
"precision",
@ -471,13 +472,18 @@
"fieldname": "sort_options",
"fieldtype": "Check",
"label": "Sort Options"
},
{
"fieldname": "link_filters",
"fieldtype": "JSON",
"label": "Link Filters"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-10-25 06:55:50.718441",
"modified": "2023-11-07 13:17:21.373626",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",

View file

@ -86,6 +86,7 @@ class CustomizeFormField(Document):
is_virtual: DF.Check
label: DF.Data | None
length: DF.Int
link_filters: DF.JSON | None
mandatory_depends_on: DF.Code | None
no_copy: DF.Check
non_negative: DF.Check

View file

@ -7,21 +7,34 @@
from frappe.database.database import savepoint
def setup_database(force, source_sql=None, verbose=None, no_mariadb_socket=False):
def setup_database(force, verbose=None, no_mariadb_socket=False):
import frappe
if frappe.conf.db_type == "postgres":
import frappe.database.postgres.setup_db
return frappe.database.postgres.setup_db.setup_database(force, source_sql, verbose)
return frappe.database.postgres.setup_db.setup_database()
else:
import frappe.database.mariadb.setup_db
return frappe.database.mariadb.setup_db.setup_database(
force, source_sql, verbose, no_mariadb_socket=no_mariadb_socket
force, verbose, no_mariadb_socket=no_mariadb_socket
)
def bootstrap_database(db_name, verbose=None, source_sql=None):
import frappe
if frappe.conf.db_type == "postgres":
import frappe.database.postgres.setup_db
return frappe.database.postgres.setup_db.bootstrap_database(db_name, verbose, source_sql)
else:
import frappe.database.mariadb.setup_db
return frappe.database.mariadb.setup_db.bootstrap_database(db_name, verbose, source_sql)
def drop_user_and_database(db_name, root_login=None, root_password=None):
import frappe

View file

@ -23,7 +23,7 @@ def get_mariadb_version(version_string: str = ""):
return version.rsplit(".", 1)
def setup_database(force, source_sql, verbose, no_mariadb_socket=False):
def setup_database(force, verbose, no_mariadb_socket=False):
frappe.local.session = frappe._dict({"user": "Administrator"})
db_name = frappe.local.conf.db_name
@ -55,8 +55,6 @@ def setup_database(force, source_sql, verbose, no_mariadb_socket=False):
# close root connection
root_conn.close()
bootstrap_database(db_name, verbose, source_sql)
def drop_user_and_database(db_name, root_login, root_password):
frappe.local.db = get_root_connection(root_login, root_password)
@ -75,8 +73,8 @@ def bootstrap_database(db_name, verbose, source_sql=None):
sys.exit(1)
import_db_from_sql(source_sql, verbose)
frappe.connect(db_name=db_name)
if "tabDefaultValue" not in frappe.db.get_tables(cached=False):
from click import secho

View file

@ -160,11 +160,16 @@ class PostgresDatabase(PostgresExceptionUtil, Database):
return LazyDecode(self._cursor.query)
def get_connection(self):
conn = psycopg2.connect(
"host='{}' dbname='{}' user='{}' password='{}' port={}".format(
self.host, self.user, self.user, self.password, self.port
)
)
conn_settings = {
"user": self.user,
"dbname": self.user,
"host": self.host,
"password": self.password,
}
if self.port:
conn_settings["port"] = self.port
conn = psycopg2.connect(**conn_settings)
conn.set_isolation_level(ISOLATION_LEVEL_REPEATABLE_READ)
return conn

View file

@ -3,7 +3,7 @@ import os
import frappe
def setup_database(force, source_sql=None, verbose=False):
def setup_database():
root_conn = get_root_connection(frappe.flags.root_login, frappe.flags.root_password)
root_conn.commit()
root_conn.sql("end")
@ -14,9 +14,6 @@ def setup_database(force, source_sql=None, verbose=False):
root_conn.sql("GRANT ALL PRIVILEGES ON DATABASE `{0}` TO {0}".format(frappe.conf.db_name))
root_conn.close()
bootstrap_database(frappe.conf.db_name, verbose, source_sql=source_sql)
frappe.connect()
def bootstrap_database(db_name, verbose, source_sql=None):
frappe.connect(db_name=db_name)

View file

@ -215,12 +215,12 @@ def clean_params(data):
def parse_json(data):
if isinstance(data.get("filters"), str):
data["filters"] = json.loads(data["filters"])
if isinstance(data.get("or_filters"), str):
data["or_filters"] = json.loads(data["or_filters"])
if isinstance(data.get("fields"), str):
data["fields"] = ["*"] if data["fields"] == "*" else json.loads(data["fields"])
if (filters := data.get("filters")) and isinstance(filters, str):
data["filters"] = json.loads(filters)
if (or_filters := data.get("or_filters")) and isinstance(or_filters, str):
data["or_filters"] = json.loads(or_filters)
if (fields := data.get("fields")) and isinstance(fields, str):
data["fields"] = ["*"] if fields == "*" else json.loads(fields)
if isinstance(data.get("docstatus"), str):
data["docstatus"] = json.loads(data["docstatus"])
if isinstance(data.get("save_user_settings"), str):

View file

@ -123,6 +123,5 @@ def send_welcome_email(welcome_email, email, email_group):
return
args = dict(email=email, email_group=email_group)
email_message = welcome_email.response or welcome_email.response_html
message = frappe.render_template(email_message, args)
message = frappe.render_template(welcome_email.response_, args)
frappe.sendmail(email, subject=welcome_email.subject, message=message)

View file

@ -2,11 +2,11 @@ frappe.listview_settings["Newsletter"] = {
add_fields: ["subject", "email_sent", "schedule_sending"],
get_indicator: function (doc) {
if (doc.email_sent) {
return [__("Sent"), "green", "email_sent,=,Yes"];
return [__("Sent"), "green", "email_sent,=,1"];
} else if (doc.schedule_sending) {
return [__("Scheduled"), "purple", "email_sent,=,No|schedule_sending,=,Yes"];
return [__("Scheduled"), "purple", "email_sent,=,0|schedule_sending,=,1"];
} else {
return [__("Not Sent"), "gray", "email_sent,=,No"];
return [__("Not Sent"), "gray", "email_sent,=,0"];
}
},
};

View file

@ -220,6 +220,9 @@ class EmailServer:
).where(Communication.email_account == self.settings.email_account).run()
if self.settings.use_imap:
# Remove {"} quotes that are added to handle spaces in IMAP Folder names
if folder[0] == folder[-1] == '"':
folder = folder[1:-2]
# new update for the IMAP Folder DocType
IMAPFolder = frappe.qb.DocType("IMAP Folder")
frappe.qb.update(IMAPFolder).set(IMAPFolder.uidvalidity, current_uid_validity).set(

View file

@ -48,6 +48,7 @@ def _new_site(
db_type=None,
db_host=None,
db_port=None,
setup_db=True,
):
"""Install a new Frappe site"""
@ -91,6 +92,7 @@ def _new_site(
db_host=db_host,
db_port=db_port,
no_mariadb_socket=no_mariadb_socket,
setup=setup_db,
)
apps_to_install = (
@ -128,9 +130,10 @@ def install_db(
db_host=None,
db_port=None,
no_mariadb_socket=False,
setup=True,
):
import frappe.database
from frappe.database import setup_database
from frappe.database import bootstrap_database, setup_database
if not db_type:
db_type = frappe.conf.db_type
@ -152,7 +155,15 @@ def install_db(
frappe.flags.root_login = root_login
frappe.flags.root_password = root_password
setup_database(force, source_sql, verbose, no_mariadb_socket)
if setup:
setup_database(force, verbose, no_mariadb_socket)
bootstrap_database(
db_name=frappe.conf.db_name,
verbose=verbose,
source_sql=source_sql,
)
frappe.conf.admin_password = frappe.conf.admin_password or admin_password

View file

@ -4,7 +4,7 @@
import json
import frappe
from frappe.integrations.utils import json_handler
from frappe.integrations.utils import get_json, json_handler
from frappe.model.document import Document
@ -45,7 +45,7 @@ class IntegrationRequest(Document):
data = json.loads(self.data)
data.update(params)
self.data = json.dumps(data)
self.data = get_json(data)
self.status = status
self.save(ignore_permissions=True)
frappe.db.commit()

View file

@ -69,9 +69,25 @@ def get_mapped_doc(
# main
if not target_doc:
target_doc = frappe.new_doc(table_maps[from_doctype]["doctype"])
target_doctype = table_maps[from_doctype]["doctype"]
if table_maps[from_doctype].get("on_parent"):
target_parent = table_maps[from_doctype].get("on_parent")
if isinstance(target_parent, str):
target_parent = frappe.get_doc(json.loads(target_parent))
target_parentfield = target_parent.get_parentfield_of_doctype(target_doctype)
target_doc = frappe.new_doc(
target_doctype, parent_doc=target_parent, parentfield=target_parentfield
)
target_parent.append(target_parentfield, target_doc)
ret_doc = target_parent
else:
target_doc = frappe.new_doc(target_doctype)
ret_doc = target_doc
elif isinstance(target_doc, str):
target_doc = frappe.get_doc(json.loads(target_doc))
ret_doc = target_doc
else:
ret_doc = target_doc
if (
not apply_strict_user_permissions
@ -147,16 +163,14 @@ def get_mapped_doc(
if postprocess:
postprocess(source_doc, target_doc)
target_doc.set_onload("load_after_mapping", True)
ret_doc.set_onload("load_after_mapping", True)
if (
apply_strict_user_permissions
and not ignore_permissions
and not target_doc.has_permission("create")
apply_strict_user_permissions and not ignore_permissions and not ret_doc.has_permission("create")
):
target_doc.raise_no_permission_to("create")
ret_doc.raise_no_permission_to("create")
return target_doc
return ret_doc
def map_doc(source_doc, target_doc, table_map, source_parent=None):

View file

@ -201,8 +201,9 @@ def rename_doc(
# call after_rename
new_doc = frappe.get_doc(doctype, new)
# copy any flags if required
new_doc._local = getattr(old_doc, "_local", None)
if validate:
# copy any flags if required
new_doc._local = getattr(old_doc, "_local", None)
new_doc.run_method("after_rename", old, new, merge)

View file

@ -8,19 +8,21 @@ from frappe.model.utils.user_settings import sync_user_settings, update_user_set
from frappe.utils.password import rename_password_field
def rename_field(doctype, old_fieldname, new_fieldname):
def rename_field(doctype, old_fieldname, new_fieldname, validate=True):
"""This functions assumes that doctype is already synced"""
meta = frappe.get_meta(doctype, cached=False)
new_field = meta.get_field(new_fieldname)
if not new_field:
print("rename_field: " + (new_fieldname) + " not found in " + doctype)
return
if not meta.issingle and not frappe.db.has_column(doctype, old_fieldname):
print("rename_field: " + (old_fieldname) + " not found in table for: " + doctype)
# never had the field?
return
if validate:
if not new_field:
print("rename_field: " + (new_fieldname) + " not found in " + doctype)
return
if not meta.issingle and not frappe.db.has_column(doctype, old_fieldname):
print("rename_field: " + (old_fieldname) + " not found in table for: " + doctype)
# never had the field?
return
if new_field.fieldtype in table_fields:
# change parentfield of table mentioned in options

View file

@ -1,164 +1,167 @@
// TODO instead of making copy of inter.css find a way to import it.
// workaround for css import as it fails for custom website_theme_template
@font-face {
font-family: 'Inter V';
font-family: "Inter V";
font-weight: 100 900;
font-display: swap;
font-style: normal;
src: url('/assets/frappe/css/fonts/inter/Inter.var.woff2?v=3.19') format('woff2-variations'),
url('/assets/frappe/css/fonts/inter/Inter.var.woff2?v=3.19') format('woff2');
src: url('/assets/frappe/css/fonts/inter/Inter.var.woff2?v=3.19') format('woff2') tech('variations');
}
@font-face {
font-family: 'Inter V';
src: url("/assets/frappe/css/fonts/inter/Inter.var.woff2?v=3.19") format("woff2-variations"),
url("/assets/frappe/css/fonts/inter/Inter.var.woff2?v=3.19") format("woff2");
src: url("/assets/frappe/css/fonts/inter/Inter.var.woff2?v=3.19") format("woff2")
tech("variations");
}
@font-face {
font-family: "Inter V";
font-weight: 100 900;
font-display: swap;
font-style: italic;
src: url('/assets/frappe/css/fonts/inter/Inter-Italic.var.woff2?v=3.19') format('woff2-variations'),
url('/assets/frappe/css/fonts/inter/Inter-Italic.var.woff2?v=3.19') format('woff2');
src: url('/assets/frappe/css/fonts/inter/Inter-Italic.var.woff2?v=3.19') format('woff2') tech('variations');
}
src: url("/assets/frappe/css/fonts/inter/Inter-Italic.var.woff2?v=3.19")
format("woff2-variations"),
url("/assets/frappe/css/fonts/inter/Inter-Italic.var.woff2?v=3.19") format("woff2");
src: url("/assets/frappe/css/fonts/inter/Inter-Italic.var.woff2?v=3.19") format("woff2")
tech("variations");
}
@font-face {
font-family: 'Inter';
font-family: "Inter";
font-display: swap;
font-style: italic;
font-style: italic;
font-weight: 100;
src: url("/assets/frappe/css/fonts/inter/inter_thinitalic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_thinitalic.woff") format("woff");
url("/assets/frappe/css/fonts/inter/inter_thinitalic.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-family: "Inter";
font-display: swap;
font-style: normal;
font-style: normal;
font-weight: 200;
src: url("/assets/frappe/css/fonts/inter/inter_extralight.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_extralight.woff") format("woff");
url("/assets/frappe/css/fonts/inter/inter_extralight.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-family: "Inter";
font-display: swap;
font-style: italic;
font-style: italic;
font-weight: 200;
src: url("/assets/frappe/css/fonts/inter/inter_extralightitalic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_extralightitalic.woff") format("woff");
url("/assets/frappe/css/fonts/inter/inter_extralightitalic.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-family: "Inter";
font-display: swap;
font-style: normal;
font-style: normal;
font-weight: 300;
src: url("/assets/frappe/css/fonts/inter/inter_light.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_light.woff") format("woff");
url("/assets/frappe/css/fonts/inter/inter_light.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-family: "Inter";
font-display: swap;
font-style: italic;
font-style: italic;
font-weight: 300;
src: url("/assets/frappe/css/fonts/inter/inter_lightitalic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_lightitalic.woff") format("woff");
url("/assets/frappe/css/fonts/inter/inter_lightitalic.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-family: "Inter";
font-display: swap;
font-style: normal;
font-style: normal;
font-weight: 400;
src: url("/assets/frappe/css/fonts/inter/inter_regular.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_regular.woff") format("woff");
url("/assets/frappe/css/fonts/inter/inter_regular.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-family: "Inter";
font-display: swap;
font-style: italic;
font-style: italic;
font-weight: 400;
src: url("/assets/frappe/css/fonts/inter/inter_italic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_italic.woff") format("woff");
url("/assets/frappe/css/fonts/inter/inter_italic.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-family: "Inter";
font-display: swap;
font-style: normal;
font-style: normal;
font-weight: 500;
src: url("/assets/frappe/css/fonts/inter/inter_medium.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_medium.woff") format("woff");
url("/assets/frappe/css/fonts/inter/inter_medium.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-family: "Inter";
font-display: swap;
font-style: italic;
font-style: italic;
font-weight: 500;
src: url("/assets/frappe/css/fonts/inter/inter_mediumitalic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_mediumitalic.woff") format("woff");
url("/assets/frappe/css/fonts/inter/inter_mediumitalic.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-family: "Inter";
font-display: swap;
font-style: normal;
font-style: normal;
font-weight: 600;
src: url("/assets/frappe/css/fonts/inter/inter_semibold.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_semibold.woff") format("woff");
url("/assets/frappe/css/fonts/inter/inter_semibold.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-family: "Inter";
font-display: swap;
font-style: italic;
font-style: italic;
font-weight: 600;
src: url("/assets/frappe/css/fonts/inter/inter_semibolditalic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_semibolditalic.woff") format("woff");
url("/assets/frappe/css/fonts/inter/inter_semibolditalic.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-family: "Inter";
font-display: swap;
font-style: normal;
font-style: normal;
font-weight: 700;
src: url("/assets/frappe/css/fonts/inter/inter_bold.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_bold.woff") format("woff");
url("/assets/frappe/css/fonts/inter/inter_bold.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-family: "Inter";
font-display: swap;
font-style: italic;
font-style: italic;
font-weight: 700;
src: url("/assets/frappe/css/fonts/inter/inter_bolditalic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_bolditalic.woff") format("woff");
url("/assets/frappe/css/fonts/inter/inter_bolditalic.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-family: "Inter";
font-display: swap;
font-style: normal;
font-style: normal;
font-weight: 800;
src: url("/assets/frappe/css/fonts/inter/inter_extrabold.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_extrabold.woff") format("woff");
url("/assets/frappe/css/fonts/inter/inter_extrabold.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-family: "Inter";
font-display: swap;
font-style: italic;
font-style: italic;
font-weight: 800;
src: url("/assets/frappe/css/fonts/inter/inter_extrabolditalic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_extrabolditalic.woff") format("woff");
url("/assets/frappe/css/fonts/inter/inter_extrabolditalic.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-family: "Inter";
font-display: swap;
font-style: normal;
font-style: normal;
font-weight: 900;
src: url("/assets/frappe/css/fonts/inter/inter_black.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_black.woff") format("woff");
url("/assets/frappe/css/fonts/inter/inter_black.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-family: "Inter";
font-display: swap;
font-style: italic;
font-style: italic;
font-weight: 900;
src: url("/assets/frappe/css/fonts/inter/inter_blackitalic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_blackitalic.woff") format("woff");
url("/assets/frappe/css/fonts/inter/inter_blackitalic.woff") format("woff");
}

View file

@ -1,220 +1,548 @@
$octicons-font-path: "." !default;
$octicons-version: "396334ee3da78f4302d25c758ae3e3ce5dc3c97d";
$octicons-version: "396334ee3da78f4302d25c758ae3e3ce5dc3c97d";
@font-face {
font-family: 'octicons';
src: url('#{$octicons-font-path}/octicons.eot?#iefix&v=#{$octicons-version}') format('embedded-opentype'),
url('#{$octicons-font-path}/octicons.woff?v=#{$octicons-version}') format('woff'),
url('#{$octicons-font-path}/octicons.ttf?v=#{$octicons-version}') format('truetype'),
url('#{$octicons-font-path}/octicons.svg?v=#{$octicons-version}#octicons') format('svg');
font-weight: normal;
font-style: normal;
font-family: "octicons";
src: url("#{$octicons-font-path}/octicons.eot?#iefix&v=#{$octicons-version}")
format("embedded-opentype"),
url("#{$octicons-font-path}/octicons.woff?v=#{$octicons-version}") format("woff"),
url("#{$octicons-font-path}/octicons.ttf?v=#{$octicons-version}") format("truetype"),
url("#{$octicons-font-path}/octicons.svg?v=#{$octicons-version}#octicons") format("svg");
font-weight: normal;
font-style: normal;
}
// .octicon is optimized for 16px.
// .mega-octicon is optimized for 32px but can be used larger.
.octicon, .mega-octicon {
font: normal normal normal 16px/1 octicons;
display: inline-block;
text-decoration: none;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
.octicon,
.mega-octicon {
font: normal normal normal 16px/1 octicons;
display: inline-block;
text-decoration: none;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.mega-octicon {
font-size: 32px;
}
.mega-octicon { font-size: 32px; }
.octicon-alert:before { content: '\f02d'} /*  */
.octicon-arrow-down:before { content: '\f03f'} /*  */
.octicon-arrow-left:before { content: '\f040'} /*  */
.octicon-arrow-right:before { content: '\f03e'} /*  */
.octicon-arrow-small-down:before { content: '\f0a0'} /*  */
.octicon-arrow-small-left:before { content: '\f0a1'} /*  */
.octicon-arrow-small-right:before { content: '\f071'} /*  */
.octicon-arrow-small-up:before { content: '\f09f'} /*  */
.octicon-arrow-up:before { content: '\f03d'} /*  */
.octicon-alert:before {
content: "\f02d";
} /*  */
.octicon-arrow-down:before {
content: "\f03f";
} /*  */
.octicon-arrow-left:before {
content: "\f040";
} /*  */
.octicon-arrow-right:before {
content: "\f03e";
} /*  */
.octicon-arrow-small-down:before {
content: "\f0a0";
} /*  */
.octicon-arrow-small-left:before {
content: "\f0a1";
} /*  */
.octicon-arrow-small-right:before {
content: "\f071";
} /*  */
.octicon-arrow-small-up:before {
content: "\f09f";
} /*  */
.octicon-arrow-up:before {
content: "\f03d";
} /*  */
.octicon-microscope:before,
.octicon-beaker:before { content: '\f0dd'} /*  */
.octicon-bell:before { content: '\f0de'} /*  */
.octicon-book:before { content: '\f007'} /*  */
.octicon-bookmark:before { content: '\f07b'} /*  */
.octicon-briefcase:before { content: '\f0d3'} /*  */
.octicon-broadcast:before { content: '\f048'} /*  */
.octicon-browser:before { content: '\f0c5'} /*  */
.octicon-bug:before { content: '\f091'} /*  */
.octicon-calendar:before { content: '\f068'} /*  */
.octicon-check:before { content: '\f03a'} /*  */
.octicon-checklist:before { content: '\f076'} /*  */
.octicon-chevron-down:before { content: '\f0a3'} /*  */
.octicon-chevron-left:before { content: '\f0a4'} /*  */
.octicon-chevron-right:before { content: '\f078'} /*  */
.octicon-chevron-up:before { content: '\f0a2'} /*  */
.octicon-circle-slash:before { content: '\f084'} /*  */
.octicon-circuit-board:before { content: '\f0d6'} /*  */
.octicon-clippy:before { content: '\f035'} /*  */
.octicon-clock:before { content: '\f046'} /*  */
.octicon-cloud-download:before { content: '\f00b'} /*  */
.octicon-cloud-upload:before { content: '\f00c'} /*  */
.octicon-code:before { content: '\f05f'} /*  */
.octicon-color-mode:before { content: '\f065'} /*  */
.octicon-beaker:before {
content: "\f0dd";
} /*  */
.octicon-bell:before {
content: "\f0de";
} /*  */
.octicon-book:before {
content: "\f007";
} /*  */
.octicon-bookmark:before {
content: "\f07b";
} /*  */
.octicon-briefcase:before {
content: "\f0d3";
} /*  */
.octicon-broadcast:before {
content: "\f048";
} /*  */
.octicon-browser:before {
content: "\f0c5";
} /*  */
.octicon-bug:before {
content: "\f091";
} /*  */
.octicon-calendar:before {
content: "\f068";
} /*  */
.octicon-check:before {
content: "\f03a";
} /*  */
.octicon-checklist:before {
content: "\f076";
} /*  */
.octicon-chevron-down:before {
content: "\f0a3";
} /*  */
.octicon-chevron-left:before {
content: "\f0a4";
} /*  */
.octicon-chevron-right:before {
content: "\f078";
} /*  */
.octicon-chevron-up:before {
content: "\f0a2";
} /*  */
.octicon-circle-slash:before {
content: "\f084";
} /*  */
.octicon-circuit-board:before {
content: "\f0d6";
} /*  */
.octicon-clippy:before {
content: "\f035";
} /*  */
.octicon-clock:before {
content: "\f046";
} /*  */
.octicon-cloud-download:before {
content: "\f00b";
} /*  */
.octicon-cloud-upload:before {
content: "\f00c";
} /*  */
.octicon-code:before {
content: "\f05f";
} /*  */
.octicon-color-mode:before {
content: "\f065";
} /*  */
.octicon-comment-add:before,
.octicon-comment:before { content: '\f02b'} /*  */
.octicon-comment-discussion:before { content: '\f04f'} /*  */
.octicon-credit-card:before { content: '\f045'} /*  */
.octicon-dash:before { content: '\f0ca'} /*  */
.octicon-dashboard:before { content: '\f07d'} /*  */
.octicon-database:before { content: '\f096'} /*  */
.octicon-comment:before {
content: "\f02b";
} /*  */
.octicon-comment-discussion:before {
content: "\f04f";
} /*  */
.octicon-credit-card:before {
content: "\f045";
} /*  */
.octicon-dash:before {
content: "\f0ca";
} /*  */
.octicon-dashboard:before {
content: "\f07d";
} /*  */
.octicon-database:before {
content: "\f096";
} /*  */
.octicon-clone:before,
.octicon-desktop-download:before { content: '\f0dc'} /*  */
.octicon-device-camera:before { content: '\f056'} /*  */
.octicon-device-camera-video:before { content: '\f057'} /*  */
.octicon-device-desktop:before { content: '\f27c'} /*  */
.octicon-device-mobile:before { content: '\f038'} /*  */
.octicon-diff:before { content: '\f04d'} /*  */
.octicon-diff-added:before { content: '\f06b'} /*  */
.octicon-diff-ignored:before { content: '\f099'} /*  */
.octicon-diff-modified:before { content: '\f06d'} /*  */
.octicon-diff-removed:before { content: '\f06c'} /*  */
.octicon-diff-renamed:before { content: '\f06e'} /*  */
.octicon-ellipsis:before { content: '\f09a'} /*  */
.octicon-desktop-download:before {
content: "\f0dc";
} /*  */
.octicon-device-camera:before {
content: "\f056";
} /*  */
.octicon-device-camera-video:before {
content: "\f057";
} /*  */
.octicon-device-desktop:before {
content: "\f27c";
} /*  */
.octicon-device-mobile:before {
content: "\f038";
} /*  */
.octicon-diff:before {
content: "\f04d";
} /*  */
.octicon-diff-added:before {
content: "\f06b";
} /*  */
.octicon-diff-ignored:before {
content: "\f099";
} /*  */
.octicon-diff-modified:before {
content: "\f06d";
} /*  */
.octicon-diff-removed:before {
content: "\f06c";
} /*  */
.octicon-diff-renamed:before {
content: "\f06e";
} /*  */
.octicon-ellipsis:before {
content: "\f09a";
} /*  */
.octicon-eye-unwatch:before,
.octicon-eye-watch:before,
.octicon-eye:before { content: '\f04e'} /*  */
.octicon-file-binary:before { content: '\f094'} /*  */
.octicon-file-code:before { content: '\f010'} /*  */
.octicon-file-directory:before { content: '\f016'} /*  */
.octicon-file-media:before { content: '\f012'} /*  */
.octicon-file-pdf:before { content: '\f014'} /*  */
.octicon-file-submodule:before { content: '\f017'} /*  */
.octicon-file-symlink-directory:before { content: '\f0b1'} /*  */
.octicon-file-symlink-file:before { content: '\f0b0'} /*  */
.octicon-file-text:before { content: '\f011'} /*  */
.octicon-file-zip:before { content: '\f013'} /*  */
.octicon-flame:before { content: '\f0d2'} /*  */
.octicon-fold:before { content: '\f0cc'} /*  */
.octicon-gear:before { content: '\f02f'} /*  */
.octicon-gift:before { content: '\f042'} /*  */
.octicon-gist:before { content: '\f00e'} /*  */
.octicon-gist-secret:before { content: '\f08c'} /*  */
.octicon-eye:before {
content: "\f04e";
} /*  */
.octicon-file-binary:before {
content: "\f094";
} /*  */
.octicon-file-code:before {
content: "\f010";
} /*  */
.octicon-file-directory:before {
content: "\f016";
} /*  */
.octicon-file-media:before {
content: "\f012";
} /*  */
.octicon-file-pdf:before {
content: "\f014";
} /*  */
.octicon-file-submodule:before {
content: "\f017";
} /*  */
.octicon-file-symlink-directory:before {
content: "\f0b1";
} /*  */
.octicon-file-symlink-file:before {
content: "\f0b0";
} /*  */
.octicon-file-text:before {
content: "\f011";
} /*  */
.octicon-file-zip:before {
content: "\f013";
} /*  */
.octicon-flame:before {
content: "\f0d2";
} /*  */
.octicon-fold:before {
content: "\f0cc";
} /*  */
.octicon-gear:before {
content: "\f02f";
} /*  */
.octicon-gift:before {
content: "\f042";
} /*  */
.octicon-gist:before {
content: "\f00e";
} /*  */
.octicon-gist-secret:before {
content: "\f08c";
} /*  */
.octicon-git-branch-create:before,
.octicon-git-branch-delete:before,
.octicon-git-branch:before { content: '\f020'} /*  */
.octicon-git-commit:before { content: '\f01f'} /*  */
.octicon-git-compare:before { content: '\f0ac'} /*  */
.octicon-git-merge:before { content: '\f023'} /*  */
.octicon-git-branch:before {
content: "\f020";
} /*  */
.octicon-git-commit:before {
content: "\f01f";
} /*  */
.octicon-git-compare:before {
content: "\f0ac";
} /*  */
.octicon-git-merge:before {
content: "\f023";
} /*  */
.octicon-git-pull-request-abandoned:before,
.octicon-git-pull-request:before { content: '\f009'} /*  */
.octicon-globe:before { content: '\f0b6'} /*  */
.octicon-graph:before { content: '\f043'} /*  */
.octicon-heart:before { content: '\2665'} /* ♥ */
.octicon-history:before { content: '\f07e'} /*  */
.octicon-home:before { content: '\f08d'} /*  */
.octicon-horizontal-rule:before { content: '\f070'} /*  */
.octicon-hubot:before { content: '\f09d'} /*  */
.octicon-inbox:before { content: '\f0cf'} /*  */
.octicon-info:before { content: '\f059'} /*  */
.octicon-issue-closed:before { content: '\f028'} /*  */
.octicon-issue-opened:before { content: '\f026'} /*  */
.octicon-issue-reopened:before { content: '\f027'} /*  */
.octicon-jersey:before { content: '\f019'} /*  */
.octicon-key:before { content: '\f049'} /*  */
.octicon-keyboard:before { content: '\f00d'} /*  */
.octicon-law:before { content: '\f0d8'} /*  */
.octicon-light-bulb:before { content: '\f000'} /*  */
.octicon-link:before { content: '\f05c'} /*  */
.octicon-link-external:before { content: '\f07f'} /*  */
.octicon-list-ordered:before { content: '\f062'} /*  */
.octicon-list-unordered:before { content: '\f061'} /*  */
.octicon-location:before { content: '\f060'} /*  */
.octicon-git-pull-request:before {
content: "\f009";
} /*  */
.octicon-globe:before {
content: "\f0b6";
} /*  */
.octicon-graph:before {
content: "\f043";
} /*  */
.octicon-heart:before {
content: "\2665";
} /* ♥ */
.octicon-history:before {
content: "\f07e";
} /*  */
.octicon-home:before {
content: "\f08d";
} /*  */
.octicon-horizontal-rule:before {
content: "\f070";
} /*  */
.octicon-hubot:before {
content: "\f09d";
} /*  */
.octicon-inbox:before {
content: "\f0cf";
} /*  */
.octicon-info:before {
content: "\f059";
} /*  */
.octicon-issue-closed:before {
content: "\f028";
} /*  */
.octicon-issue-opened:before {
content: "\f026";
} /*  */
.octicon-issue-reopened:before {
content: "\f027";
} /*  */
.octicon-jersey:before {
content: "\f019";
} /*  */
.octicon-key:before {
content: "\f049";
} /*  */
.octicon-keyboard:before {
content: "\f00d";
} /*  */
.octicon-law:before {
content: "\f0d8";
} /*  */
.octicon-light-bulb:before {
content: "\f000";
} /*  */
.octicon-link:before {
content: "\f05c";
} /*  */
.octicon-link-external:before {
content: "\f07f";
} /*  */
.octicon-list-ordered:before {
content: "\f062";
} /*  */
.octicon-list-unordered:before {
content: "\f061";
} /*  */
.octicon-location:before {
content: "\f060";
} /*  */
.octicon-gist-private:before,
.octicon-mirror-private:before,
.octicon-git-fork-private:before,
.octicon-lock:before { content: '\f06a'} /*  */
.octicon-logo-github:before { content: '\f092'} /*  */
.octicon-mail:before { content: '\f03b'} /*  */
.octicon-mail-read:before { content: '\f03c'} /*  */
.octicon-mail-reply:before { content: '\f051'} /*  */
.octicon-mark-github:before { content: '\f00a'} /*  */
.octicon-markdown:before { content: '\f0c9'} /*  */
.octicon-megaphone:before { content: '\f077'} /*  */
.octicon-mention:before { content: '\f0be'} /*  */
.octicon-milestone:before { content: '\f075'} /*  */
.octicon-lock:before {
content: "\f06a";
} /*  */
.octicon-logo-github:before {
content: "\f092";
} /*  */
.octicon-mail:before {
content: "\f03b";
} /*  */
.octicon-mail-read:before {
content: "\f03c";
} /*  */
.octicon-mail-reply:before {
content: "\f051";
} /*  */
.octicon-mark-github:before {
content: "\f00a";
} /*  */
.octicon-markdown:before {
content: "\f0c9";
} /*  */
.octicon-megaphone:before {
content: "\f077";
} /*  */
.octicon-mention:before {
content: "\f0be";
} /*  */
.octicon-milestone:before {
content: "\f075";
} /*  */
.octicon-mirror-public:before,
.octicon-mirror:before { content: '\f024'} /*  */
.octicon-mortar-board:before { content: '\f0d7'} /*  */
.octicon-mute:before { content: '\f080'} /*  */
.octicon-no-newline:before { content: '\f09c'} /*  */
.octicon-octoface:before { content: '\f008'} /*  */
.octicon-organization:before { content: '\f037'} /*  */
.octicon-package:before { content: '\f0c4'} /*  */
.octicon-paintcan:before { content: '\f0d1'} /*  */
.octicon-pencil:before { content: '\f058'} /*  */
.octicon-mirror:before {
content: "\f024";
} /*  */
.octicon-mortar-board:before {
content: "\f0d7";
} /*  */
.octicon-mute:before {
content: "\f080";
} /*  */
.octicon-no-newline:before {
content: "\f09c";
} /*  */
.octicon-octoface:before {
content: "\f008";
} /*  */
.octicon-organization:before {
content: "\f037";
} /*  */
.octicon-package:before {
content: "\f0c4";
} /*  */
.octicon-paintcan:before {
content: "\f0d1";
} /*  */
.octicon-pencil:before {
content: "\f058";
} /*  */
.octicon-person-add:before,
.octicon-person-follow:before,
.octicon-person:before { content: '\f018'} /*  */
.octicon-pin:before { content: '\f041'} /*  */
.octicon-plug:before { content: '\f0d4'} /*  */
.octicon-person:before {
content: "\f018";
} /*  */
.octicon-pin:before {
content: "\f041";
} /*  */
.octicon-plug:before {
content: "\f0d4";
} /*  */
.octicon-repo-create:before,
.octicon-gist-new:before,
.octicon-file-directory-create:before,
.octicon-file-add:before,
.octicon-plus:before { content: '\f05d'} /*  */
.octicon-primitive-dot:before { content: '\f052'} /*  */
.octicon-primitive-square:before { content: '\f053'} /*  */
.octicon-pulse:before { content: '\f085'} /*  */
.octicon-question:before { content: '\f02c'} /*  */
.octicon-quote:before { content: '\f063'} /*  */
.octicon-radio-tower:before { content: '\f030'} /*  */
.octicon-plus:before {
content: "\f05d";
} /*  */
.octicon-primitive-dot:before {
content: "\f052";
} /*  */
.octicon-primitive-square:before {
content: "\f053";
} /*  */
.octicon-pulse:before {
content: "\f085";
} /*  */
.octicon-question:before {
content: "\f02c";
} /*  */
.octicon-quote:before {
content: "\f063";
} /*  */
.octicon-radio-tower:before {
content: "\f030";
} /*  */
.octicon-repo-delete:before,
.octicon-repo:before { content: '\f001'} /*  */
.octicon-repo-clone:before { content: '\f04c'} /*  */
.octicon-repo-force-push:before { content: '\f04a'} /*  */
.octicon-repo:before {
content: "\f001";
} /*  */
.octicon-repo-clone:before {
content: "\f04c";
} /*  */
.octicon-repo-force-push:before {
content: "\f04a";
} /*  */
.octicon-gist-fork:before,
.octicon-repo-forked:before { content: '\f002'} /*  */
.octicon-repo-pull:before { content: '\f006'} /*  */
.octicon-repo-push:before { content: '\f005'} /*  */
.octicon-rocket:before { content: '\f033'} /*  */
.octicon-rss:before { content: '\f034'} /*  */
.octicon-ruby:before { content: '\f047'} /*  */
.octicon-screen-full:before { content: '\f066'} /*  */
.octicon-screen-normal:before { content: '\f067'} /*  */
.octicon-repo-forked:before {
content: "\f002";
} /*  */
.octicon-repo-pull:before {
content: "\f006";
} /*  */
.octicon-repo-push:before {
content: "\f005";
} /*  */
.octicon-rocket:before {
content: "\f033";
} /*  */
.octicon-rss:before {
content: "\f034";
} /*  */
.octicon-ruby:before {
content: "\f047";
} /*  */
.octicon-screen-full:before {
content: "\f066";
} /*  */
.octicon-screen-normal:before {
content: "\f067";
} /*  */
.octicon-search-save:before,
.octicon-search:before { content: '\f02e'} /*  */
.octicon-server:before { content: '\f097'} /*  */
.octicon-settings:before { content: '\f07c'} /*  */
.octicon-shield:before { content: '\f0e1'} /*  */
.octicon-search:before {
content: "\f02e";
} /*  */
.octicon-server:before {
content: "\f097";
} /*  */
.octicon-settings:before {
content: "\f07c";
} /*  */
.octicon-shield:before {
content: "\f0e1";
} /*  */
.octicon-log-in:before,
.octicon-sign-in:before { content: '\f036'} /*  */
.octicon-sign-in:before {
content: "\f036";
} /*  */
.octicon-log-out:before,
.octicon-sign-out:before { content: '\f032'} /*  */
.octicon-squirrel:before { content: '\f0b2'} /*  */
.octicon-sign-out:before {
content: "\f032";
} /*  */
.octicon-squirrel:before {
content: "\f0b2";
} /*  */
.octicon-star-add:before,
.octicon-star-delete:before,
.octicon-star:before { content: '\f02a'} /*  */
.octicon-stop:before { content: '\f08f'} /*  */
.octicon-star:before {
content: "\f02a";
} /*  */
.octicon-stop:before {
content: "\f08f";
} /*  */
.octicon-repo-sync:before,
.octicon-sync:before { content: '\f087'} /*  */
.octicon-sync:before {
content: "\f087";
} /*  */
.octicon-tag-remove:before,
.octicon-tag-add:before,
.octicon-tag:before { content: '\f015'} /*  */
.octicon-telescope:before { content: '\f088'} /*  */
.octicon-terminal:before { content: '\f0c8'} /*  */
.octicon-three-bars:before { content: '\f05e'} /*  */
.octicon-thumbsdown:before { content: '\f0db'} /*  */
.octicon-thumbsup:before { content: '\f0da'} /*  */
.octicon-tools:before { content: '\f031'} /*  */
.octicon-trashcan:before { content: '\f0d0'} /*  */
.octicon-triangle-down:before { content: '\f05b'} /*  */
.octicon-triangle-left:before { content: '\f044'} /*  */
.octicon-triangle-right:before { content: '\f05a'} /*  */
.octicon-triangle-up:before { content: '\f0aa'} /*  */
.octicon-unfold:before { content: '\f039'} /*  */
.octicon-unmute:before { content: '\f0ba'} /*  */
.octicon-versions:before { content: '\f064'} /*  */
.octicon-watch:before { content: '\f0e0'} /*  */
.octicon-tag:before {
content: "\f015";
} /*  */
.octicon-telescope:before {
content: "\f088";
} /*  */
.octicon-terminal:before {
content: "\f0c8";
} /*  */
.octicon-three-bars:before {
content: "\f05e";
} /*  */
.octicon-thumbsdown:before {
content: "\f0db";
} /*  */
.octicon-thumbsup:before {
content: "\f0da";
} /*  */
.octicon-tools:before {
content: "\f031";
} /*  */
.octicon-trashcan:before {
content: "\f0d0";
} /*  */
.octicon-triangle-down:before {
content: "\f05b";
} /*  */
.octicon-triangle-left:before {
content: "\f044";
} /*  */
.octicon-triangle-right:before {
content: "\f05a";
} /*  */
.octicon-triangle-up:before {
content: "\f0aa";
} /*  */
.octicon-unfold:before {
content: "\f039";
} /*  */
.octicon-unmute:before {
content: "\f0ba";
} /*  */
.octicon-versions:before {
content: "\f064";
} /*  */
.octicon-watch:before {
content: "\f0e0";
} /*  */
.octicon-remove-close:before,
.octicon-x:before { content: '\f081'} /*  */
.octicon-zap:before { content: '\26A1'} /* ⚡ */
.octicon-x:before {
content: "\f081";
} /*  */
.octicon-zap:before {
content: "\26A1";
} /* ⚡ */

View file

@ -1,217 +1,543 @@
@font-face {
font-family: 'octicons';
src: font-url('octicons.eot?#iefix') format('embedded-opentype'),
font-url('octicons.woff') format('woff'),
font-url('octicons.ttf') format('truetype'),
font-url('octicons.svg#octicons') format('svg');
font-weight: normal;
font-style: normal;
font-family: "octicons";
src: font-url("octicons.eot?#iefix") format("embedded-opentype"),
font-url("octicons.woff") format("woff"), font-url("octicons.ttf") format("truetype"),
font-url("octicons.svg#octicons") format("svg");
font-weight: normal;
font-style: normal;
}
// .octicon is optimized for 16px.
// .mega-octicon is optimized for 32px but can be used larger.
.octicon, .mega-octicon {
font: normal normal normal 16px/1 octicons;
display: inline-block;
text-decoration: none;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
.octicon,
.mega-octicon {
font: normal normal normal 16px/1 octicons;
display: inline-block;
text-decoration: none;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.mega-octicon {
font-size: 32px;
}
.mega-octicon { font-size: 32px; }
.octicon-alert:before { content: '\f02d'} /*  */
.octicon-arrow-down:before { content: '\f03f'} /*  */
.octicon-arrow-left:before { content: '\f040'} /*  */
.octicon-arrow-right:before { content: '\f03e'} /*  */
.octicon-arrow-small-down:before { content: '\f0a0'} /*  */
.octicon-arrow-small-left:before { content: '\f0a1'} /*  */
.octicon-arrow-small-right:before { content: '\f071'} /*  */
.octicon-arrow-small-up:before { content: '\f09f'} /*  */
.octicon-arrow-up:before { content: '\f03d'} /*  */
.octicon-alert:before {
content: "\f02d";
} /*  */
.octicon-arrow-down:before {
content: "\f03f";
} /*  */
.octicon-arrow-left:before {
content: "\f040";
} /*  */
.octicon-arrow-right:before {
content: "\f03e";
} /*  */
.octicon-arrow-small-down:before {
content: "\f0a0";
} /*  */
.octicon-arrow-small-left:before {
content: "\f0a1";
} /*  */
.octicon-arrow-small-right:before {
content: "\f071";
} /*  */
.octicon-arrow-small-up:before {
content: "\f09f";
} /*  */
.octicon-arrow-up:before {
content: "\f03d";
} /*  */
.octicon-microscope:before,
.octicon-beaker:before { content: '\f0dd'} /*  */
.octicon-bell:before { content: '\f0de'} /*  */
.octicon-book:before { content: '\f007'} /*  */
.octicon-bookmark:before { content: '\f07b'} /*  */
.octicon-briefcase:before { content: '\f0d3'} /*  */
.octicon-broadcast:before { content: '\f048'} /*  */
.octicon-browser:before { content: '\f0c5'} /*  */
.octicon-bug:before { content: '\f091'} /*  */
.octicon-calendar:before { content: '\f068'} /*  */
.octicon-check:before { content: '\f03a'} /*  */
.octicon-checklist:before { content: '\f076'} /*  */
.octicon-chevron-down:before { content: '\f0a3'} /*  */
.octicon-chevron-left:before { content: '\f0a4'} /*  */
.octicon-chevron-right:before { content: '\f078'} /*  */
.octicon-chevron-up:before { content: '\f0a2'} /*  */
.octicon-circle-slash:before { content: '\f084'} /*  */
.octicon-circuit-board:before { content: '\f0d6'} /*  */
.octicon-clippy:before { content: '\f035'} /*  */
.octicon-clock:before { content: '\f046'} /*  */
.octicon-cloud-download:before { content: '\f00b'} /*  */
.octicon-cloud-upload:before { content: '\f00c'} /*  */
.octicon-code:before { content: '\f05f'} /*  */
.octicon-color-mode:before { content: '\f065'} /*  */
.octicon-beaker:before {
content: "\f0dd";
} /*  */
.octicon-bell:before {
content: "\f0de";
} /*  */
.octicon-book:before {
content: "\f007";
} /*  */
.octicon-bookmark:before {
content: "\f07b";
} /*  */
.octicon-briefcase:before {
content: "\f0d3";
} /*  */
.octicon-broadcast:before {
content: "\f048";
} /*  */
.octicon-browser:before {
content: "\f0c5";
} /*  */
.octicon-bug:before {
content: "\f091";
} /*  */
.octicon-calendar:before {
content: "\f068";
} /*  */
.octicon-check:before {
content: "\f03a";
} /*  */
.octicon-checklist:before {
content: "\f076";
} /*  */
.octicon-chevron-down:before {
content: "\f0a3";
} /*  */
.octicon-chevron-left:before {
content: "\f0a4";
} /*  */
.octicon-chevron-right:before {
content: "\f078";
} /*  */
.octicon-chevron-up:before {
content: "\f0a2";
} /*  */
.octicon-circle-slash:before {
content: "\f084";
} /*  */
.octicon-circuit-board:before {
content: "\f0d6";
} /*  */
.octicon-clippy:before {
content: "\f035";
} /*  */
.octicon-clock:before {
content: "\f046";
} /*  */
.octicon-cloud-download:before {
content: "\f00b";
} /*  */
.octicon-cloud-upload:before {
content: "\f00c";
} /*  */
.octicon-code:before {
content: "\f05f";
} /*  */
.octicon-color-mode:before {
content: "\f065";
} /*  */
.octicon-comment-add:before,
.octicon-comment:before { content: '\f02b'} /*  */
.octicon-comment-discussion:before { content: '\f04f'} /*  */
.octicon-credit-card:before { content: '\f045'} /*  */
.octicon-dash:before { content: '\f0ca'} /*  */
.octicon-dashboard:before { content: '\f07d'} /*  */
.octicon-database:before { content: '\f096'} /*  */
.octicon-comment:before {
content: "\f02b";
} /*  */
.octicon-comment-discussion:before {
content: "\f04f";
} /*  */
.octicon-credit-card:before {
content: "\f045";
} /*  */
.octicon-dash:before {
content: "\f0ca";
} /*  */
.octicon-dashboard:before {
content: "\f07d";
} /*  */
.octicon-database:before {
content: "\f096";
} /*  */
.octicon-clone:before,
.octicon-desktop-download:before { content: '\f0dc'} /*  */
.octicon-device-camera:before { content: '\f056'} /*  */
.octicon-device-camera-video:before { content: '\f057'} /*  */
.octicon-device-desktop:before { content: '\f27c'} /*  */
.octicon-device-mobile:before { content: '\f038'} /*  */
.octicon-diff:before { content: '\f04d'} /*  */
.octicon-diff-added:before { content: '\f06b'} /*  */
.octicon-diff-ignored:before { content: '\f099'} /*  */
.octicon-diff-modified:before { content: '\f06d'} /*  */
.octicon-diff-removed:before { content: '\f06c'} /*  */
.octicon-diff-renamed:before { content: '\f06e'} /*  */
.octicon-ellipsis:before { content: '\f09a'} /*  */
.octicon-desktop-download:before {
content: "\f0dc";
} /*  */
.octicon-device-camera:before {
content: "\f056";
} /*  */
.octicon-device-camera-video:before {
content: "\f057";
} /*  */
.octicon-device-desktop:before {
content: "\f27c";
} /*  */
.octicon-device-mobile:before {
content: "\f038";
} /*  */
.octicon-diff:before {
content: "\f04d";
} /*  */
.octicon-diff-added:before {
content: "\f06b";
} /*  */
.octicon-diff-ignored:before {
content: "\f099";
} /*  */
.octicon-diff-modified:before {
content: "\f06d";
} /*  */
.octicon-diff-removed:before {
content: "\f06c";
} /*  */
.octicon-diff-renamed:before {
content: "\f06e";
} /*  */
.octicon-ellipsis:before {
content: "\f09a";
} /*  */
.octicon-eye-unwatch:before,
.octicon-eye-watch:before,
.octicon-eye:before { content: '\f04e'} /*  */
.octicon-file-binary:before { content: '\f094'} /*  */
.octicon-file-code:before { content: '\f010'} /*  */
.octicon-file-directory:before { content: '\f016'} /*  */
.octicon-file-media:before { content: '\f012'} /*  */
.octicon-file-pdf:before { content: '\f014'} /*  */
.octicon-file-submodule:before { content: '\f017'} /*  */
.octicon-file-symlink-directory:before { content: '\f0b1'} /*  */
.octicon-file-symlink-file:before { content: '\f0b0'} /*  */
.octicon-file-text:before { content: '\f011'} /*  */
.octicon-file-zip:before { content: '\f013'} /*  */
.octicon-flame:before { content: '\f0d2'} /*  */
.octicon-fold:before { content: '\f0cc'} /*  */
.octicon-gear:before { content: '\f02f'} /*  */
.octicon-gift:before { content: '\f042'} /*  */
.octicon-gist:before { content: '\f00e'} /*  */
.octicon-gist-secret:before { content: '\f08c'} /*  */
.octicon-eye:before {
content: "\f04e";
} /*  */
.octicon-file-binary:before {
content: "\f094";
} /*  */
.octicon-file-code:before {
content: "\f010";
} /*  */
.octicon-file-directory:before {
content: "\f016";
} /*  */
.octicon-file-media:before {
content: "\f012";
} /*  */
.octicon-file-pdf:before {
content: "\f014";
} /*  */
.octicon-file-submodule:before {
content: "\f017";
} /*  */
.octicon-file-symlink-directory:before {
content: "\f0b1";
} /*  */
.octicon-file-symlink-file:before {
content: "\f0b0";
} /*  */
.octicon-file-text:before {
content: "\f011";
} /*  */
.octicon-file-zip:before {
content: "\f013";
} /*  */
.octicon-flame:before {
content: "\f0d2";
} /*  */
.octicon-fold:before {
content: "\f0cc";
} /*  */
.octicon-gear:before {
content: "\f02f";
} /*  */
.octicon-gift:before {
content: "\f042";
} /*  */
.octicon-gist:before {
content: "\f00e";
} /*  */
.octicon-gist-secret:before {
content: "\f08c";
} /*  */
.octicon-git-branch-create:before,
.octicon-git-branch-delete:before,
.octicon-git-branch:before { content: '\f020'} /*  */
.octicon-git-commit:before { content: '\f01f'} /*  */
.octicon-git-compare:before { content: '\f0ac'} /*  */
.octicon-git-merge:before { content: '\f023'} /*  */
.octicon-git-branch:before {
content: "\f020";
} /*  */
.octicon-git-commit:before {
content: "\f01f";
} /*  */
.octicon-git-compare:before {
content: "\f0ac";
} /*  */
.octicon-git-merge:before {
content: "\f023";
} /*  */
.octicon-git-pull-request-abandoned:before,
.octicon-git-pull-request:before { content: '\f009'} /*  */
.octicon-globe:before { content: '\f0b6'} /*  */
.octicon-graph:before { content: '\f043'} /*  */
.octicon-heart:before { content: '\2665'} /* ♥ */
.octicon-history:before { content: '\f07e'} /*  */
.octicon-home:before { content: '\f08d'} /*  */
.octicon-horizontal-rule:before { content: '\f070'} /*  */
.octicon-hubot:before { content: '\f09d'} /*  */
.octicon-inbox:before { content: '\f0cf'} /*  */
.octicon-info:before { content: '\f059'} /*  */
.octicon-issue-closed:before { content: '\f028'} /*  */
.octicon-issue-opened:before { content: '\f026'} /*  */
.octicon-issue-reopened:before { content: '\f027'} /*  */
.octicon-jersey:before { content: '\f019'} /*  */
.octicon-key:before { content: '\f049'} /*  */
.octicon-keyboard:before { content: '\f00d'} /*  */
.octicon-law:before { content: '\f0d8'} /*  */
.octicon-light-bulb:before { content: '\f000'} /*  */
.octicon-link:before { content: '\f05c'} /*  */
.octicon-link-external:before { content: '\f07f'} /*  */
.octicon-list-ordered:before { content: '\f062'} /*  */
.octicon-list-unordered:before { content: '\f061'} /*  */
.octicon-location:before { content: '\f060'} /*  */
.octicon-git-pull-request:before {
content: "\f009";
} /*  */
.octicon-globe:before {
content: "\f0b6";
} /*  */
.octicon-graph:before {
content: "\f043";
} /*  */
.octicon-heart:before {
content: "\2665";
} /* ♥ */
.octicon-history:before {
content: "\f07e";
} /*  */
.octicon-home:before {
content: "\f08d";
} /*  */
.octicon-horizontal-rule:before {
content: "\f070";
} /*  */
.octicon-hubot:before {
content: "\f09d";
} /*  */
.octicon-inbox:before {
content: "\f0cf";
} /*  */
.octicon-info:before {
content: "\f059";
} /*  */
.octicon-issue-closed:before {
content: "\f028";
} /*  */
.octicon-issue-opened:before {
content: "\f026";
} /*  */
.octicon-issue-reopened:before {
content: "\f027";
} /*  */
.octicon-jersey:before {
content: "\f019";
} /*  */
.octicon-key:before {
content: "\f049";
} /*  */
.octicon-keyboard:before {
content: "\f00d";
} /*  */
.octicon-law:before {
content: "\f0d8";
} /*  */
.octicon-light-bulb:before {
content: "\f000";
} /*  */
.octicon-link:before {
content: "\f05c";
} /*  */
.octicon-link-external:before {
content: "\f07f";
} /*  */
.octicon-list-ordered:before {
content: "\f062";
} /*  */
.octicon-list-unordered:before {
content: "\f061";
} /*  */
.octicon-location:before {
content: "\f060";
} /*  */
.octicon-gist-private:before,
.octicon-mirror-private:before,
.octicon-git-fork-private:before,
.octicon-lock:before { content: '\f06a'} /*  */
.octicon-logo-github:before { content: '\f092'} /*  */
.octicon-mail:before { content: '\f03b'} /*  */
.octicon-mail-read:before { content: '\f03c'} /*  */
.octicon-mail-reply:before { content: '\f051'} /*  */
.octicon-mark-github:before { content: '\f00a'} /*  */
.octicon-markdown:before { content: '\f0c9'} /*  */
.octicon-megaphone:before { content: '\f077'} /*  */
.octicon-mention:before { content: '\f0be'} /*  */
.octicon-milestone:before { content: '\f075'} /*  */
.octicon-lock:before {
content: "\f06a";
} /*  */
.octicon-logo-github:before {
content: "\f092";
} /*  */
.octicon-mail:before {
content: "\f03b";
} /*  */
.octicon-mail-read:before {
content: "\f03c";
} /*  */
.octicon-mail-reply:before {
content: "\f051";
} /*  */
.octicon-mark-github:before {
content: "\f00a";
} /*  */
.octicon-markdown:before {
content: "\f0c9";
} /*  */
.octicon-megaphone:before {
content: "\f077";
} /*  */
.octicon-mention:before {
content: "\f0be";
} /*  */
.octicon-milestone:before {
content: "\f075";
} /*  */
.octicon-mirror-public:before,
.octicon-mirror:before { content: '\f024'} /*  */
.octicon-mortar-board:before { content: '\f0d7'} /*  */
.octicon-mute:before { content: '\f080'} /*  */
.octicon-no-newline:before { content: '\f09c'} /*  */
.octicon-octoface:before { content: '\f008'} /*  */
.octicon-organization:before { content: '\f037'} /*  */
.octicon-package:before { content: '\f0c4'} /*  */
.octicon-paintcan:before { content: '\f0d1'} /*  */
.octicon-pencil:before { content: '\f058'} /*  */
.octicon-mirror:before {
content: "\f024";
} /*  */
.octicon-mortar-board:before {
content: "\f0d7";
} /*  */
.octicon-mute:before {
content: "\f080";
} /*  */
.octicon-no-newline:before {
content: "\f09c";
} /*  */
.octicon-octoface:before {
content: "\f008";
} /*  */
.octicon-organization:before {
content: "\f037";
} /*  */
.octicon-package:before {
content: "\f0c4";
} /*  */
.octicon-paintcan:before {
content: "\f0d1";
} /*  */
.octicon-pencil:before {
content: "\f058";
} /*  */
.octicon-person-add:before,
.octicon-person-follow:before,
.octicon-person:before { content: '\f018'} /*  */
.octicon-pin:before { content: '\f041'} /*  */
.octicon-plug:before { content: '\f0d4'} /*  */
.octicon-person:before {
content: "\f018";
} /*  */
.octicon-pin:before {
content: "\f041";
} /*  */
.octicon-plug:before {
content: "\f0d4";
} /*  */
.octicon-repo-create:before,
.octicon-gist-new:before,
.octicon-file-directory-create:before,
.octicon-file-add:before,
.octicon-plus:before { content: '\f05d'} /*  */
.octicon-primitive-dot:before { content: '\f052'} /*  */
.octicon-primitive-square:before { content: '\f053'} /*  */
.octicon-pulse:before { content: '\f085'} /*  */
.octicon-question:before { content: '\f02c'} /*  */
.octicon-quote:before { content: '\f063'} /*  */
.octicon-radio-tower:before { content: '\f030'} /*  */
.octicon-plus:before {
content: "\f05d";
} /*  */
.octicon-primitive-dot:before {
content: "\f052";
} /*  */
.octicon-primitive-square:before {
content: "\f053";
} /*  */
.octicon-pulse:before {
content: "\f085";
} /*  */
.octicon-question:before {
content: "\f02c";
} /*  */
.octicon-quote:before {
content: "\f063";
} /*  */
.octicon-radio-tower:before {
content: "\f030";
} /*  */
.octicon-repo-delete:before,
.octicon-repo:before { content: '\f001'} /*  */
.octicon-repo-clone:before { content: '\f04c'} /*  */
.octicon-repo-force-push:before { content: '\f04a'} /*  */
.octicon-repo:before {
content: "\f001";
} /*  */
.octicon-repo-clone:before {
content: "\f04c";
} /*  */
.octicon-repo-force-push:before {
content: "\f04a";
} /*  */
.octicon-gist-fork:before,
.octicon-repo-forked:before { content: '\f002'} /*  */
.octicon-repo-pull:before { content: '\f006'} /*  */
.octicon-repo-push:before { content: '\f005'} /*  */
.octicon-rocket:before { content: '\f033'} /*  */
.octicon-rss:before { content: '\f034'} /*  */
.octicon-ruby:before { content: '\f047'} /*  */
.octicon-screen-full:before { content: '\f066'} /*  */
.octicon-screen-normal:before { content: '\f067'} /*  */
.octicon-repo-forked:before {
content: "\f002";
} /*  */
.octicon-repo-pull:before {
content: "\f006";
} /*  */
.octicon-repo-push:before {
content: "\f005";
} /*  */
.octicon-rocket:before {
content: "\f033";
} /*  */
.octicon-rss:before {
content: "\f034";
} /*  */
.octicon-ruby:before {
content: "\f047";
} /*  */
.octicon-screen-full:before {
content: "\f066";
} /*  */
.octicon-screen-normal:before {
content: "\f067";
} /*  */
.octicon-search-save:before,
.octicon-search:before { content: '\f02e'} /*  */
.octicon-server:before { content: '\f097'} /*  */
.octicon-settings:before { content: '\f07c'} /*  */
.octicon-shield:before { content: '\f0e1'} /*  */
.octicon-search:before {
content: "\f02e";
} /*  */
.octicon-server:before {
content: "\f097";
} /*  */
.octicon-settings:before {
content: "\f07c";
} /*  */
.octicon-shield:before {
content: "\f0e1";
} /*  */
.octicon-log-in:before,
.octicon-sign-in:before { content: '\f036'} /*  */
.octicon-sign-in:before {
content: "\f036";
} /*  */
.octicon-log-out:before,
.octicon-sign-out:before { content: '\f032'} /*  */
.octicon-squirrel:before { content: '\f0b2'} /*  */
.octicon-sign-out:before {
content: "\f032";
} /*  */
.octicon-squirrel:before {
content: "\f0b2";
} /*  */
.octicon-star-add:before,
.octicon-star-delete:before,
.octicon-star:before { content: '\f02a'} /*  */
.octicon-stop:before { content: '\f08f'} /*  */
.octicon-star:before {
content: "\f02a";
} /*  */
.octicon-stop:before {
content: "\f08f";
} /*  */
.octicon-repo-sync:before,
.octicon-sync:before { content: '\f087'} /*  */
.octicon-sync:before {
content: "\f087";
} /*  */
.octicon-tag-remove:before,
.octicon-tag-add:before,
.octicon-tag:before { content: '\f015'} /*  */
.octicon-telescope:before { content: '\f088'} /*  */
.octicon-terminal:before { content: '\f0c8'} /*  */
.octicon-three-bars:before { content: '\f05e'} /*  */
.octicon-thumbsdown:before { content: '\f0db'} /*  */
.octicon-thumbsup:before { content: '\f0da'} /*  */
.octicon-tools:before { content: '\f031'} /*  */
.octicon-trashcan:before { content: '\f0d0'} /*  */
.octicon-triangle-down:before { content: '\f05b'} /*  */
.octicon-triangle-left:before { content: '\f044'} /*  */
.octicon-triangle-right:before { content: '\f05a'} /*  */
.octicon-triangle-up:before { content: '\f0aa'} /*  */
.octicon-unfold:before { content: '\f039'} /*  */
.octicon-unmute:before { content: '\f0ba'} /*  */
.octicon-versions:before { content: '\f064'} /*  */
.octicon-watch:before { content: '\f0e0'} /*  */
.octicon-tag:before {
content: "\f015";
} /*  */
.octicon-telescope:before {
content: "\f088";
} /*  */
.octicon-terminal:before {
content: "\f0c8";
} /*  */
.octicon-three-bars:before {
content: "\f05e";
} /*  */
.octicon-thumbsdown:before {
content: "\f0db";
} /*  */
.octicon-thumbsup:before {
content: "\f0da";
} /*  */
.octicon-tools:before {
content: "\f031";
} /*  */
.octicon-trashcan:before {
content: "\f0d0";
} /*  */
.octicon-triangle-down:before {
content: "\f05b";
} /*  */
.octicon-triangle-left:before {
content: "\f044";
} /*  */
.octicon-triangle-right:before {
content: "\f05a";
} /*  */
.octicon-triangle-up:before {
content: "\f0aa";
} /*  */
.octicon-unfold:before {
content: "\f039";
} /*  */
.octicon-unmute:before {
content: "\f0ba";
} /*  */
.octicon-versions:before {
content: "\f064";
} /*  */
.octicon-watch:before {
content: "\f0e0";
} /*  */
.octicon-remove-close:before,
.octicon-x:before { content: '\f081'} /*  */
.octicon-zap:before { content: '\26A1'} /* ⚡ */
.octicon-x:before {
content: "\f081";
} /*  */
.octicon-zap:before {
content: "\26A1";
} /* ⚡ */

View file

@ -815,7 +815,7 @@
<symbol viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" id="icon-duplicate">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.936 2a2 2 0 0 0-2 2v.649h1V4a1 1 0 0 1 1-1h5.566a1 1 0 0 1 1 1v4.595a1 1 0 0 1-1 1h-.642v1h.642a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H6.936zM3.5 5.402a2 2 0 0 0-2 2v4.595a2 2 0 0 0 2 2h5.566a2 2 0 0 0 2-2V7.402a2 2 0 0 0-2-2H3.5zm-1 2a1 1 0 0 1 1-1h5.566a1 1 0 0 1 1 1v4.595a1 1 0 0 1-1 1H3.5a1 1 0 0 1-1-1V7.402z"
stroke="none" fill="#192734"></path>
stroke="none" fill="var(--icon-stroke)"></path>
</symbol>
<symbol viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-chart">

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 114 KiB

View file

@ -113,10 +113,11 @@ watch(showOptions, (val) => {
.combo-box-options {
width: 100%;
background-color: var(--white);
background-color: var(--fg-color);
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-2xl);
padding: 0;
border: 1px solid var(--subtle-accent);
}
.combo-box-option {

View file

@ -1,113 +1,3 @@
<script setup>
import draggable from "vuedraggable";
import Field from "./Field.vue";
import AddFieldButton from "./AddFieldButton.vue";
import EditableInput from "./EditableInput.vue";
import { computed, ref } from "vue";
import { useStore } from "../store";
import { move_children_to_parent, confirm_dialog, is_touch_screen_device } from "../utils";
import { useMagicKeys, whenever } from "@vueuse/core";
const props = defineProps(["section", "column"]);
const store = useStore();
// delete/backspace to delete the field
const { Backspace } = useMagicKeys();
whenever(Backspace, (value) => {
if (value && selected.value && store.not_using_input) {
remove_column();
}
});
const hovered = ref(false);
const selected = computed(() => store.selected(props.column.df.name));
function add_column() {
// insert new column after the current column
let index = props.section.columns.indexOf(props.column);
props.section.columns.splice(index + 1, 0, {
df: store.get_df("Column Break"),
fields: [],
});
}
function remove_column() {
if (store.is_customize_form && props.column.df.is_custom_field == 0) {
frappe.msgprint(__("Cannot delete standard field. You can hide it if you want"));
throw "cannot delete standard field";
} else if (props.column.fields.length == 0 || store.has_standard_field(props.column)) {
delete_column();
} else {
confirm_dialog(
__("Delete Column", null, "Title of confirmation dialog"),
__(
"Are you sure you want to delete the column? All the fields in the column will be moved to the previous column.",
null,
"Confirmation dialog message"
),
() => delete_column(),
__("Delete column", null, "Button text"),
() => delete_column(true),
__("Delete entire column with fields", null, "Button text")
);
}
}
function delete_column(with_children) {
// move all fields to previous column
let columns = props.section.columns;
let index = columns.indexOf(props.column);
if (with_children && index == 0 && columns.length == 1) {
if (props.column.fields.length == 0) {
frappe.msgprint(__("Section must have at least one column"));
throw "section must have at least one column";
}
columns.unshift({
df: store.get_df("Column Break"),
fields: [],
is_first: true,
});
index++;
}
if (!with_children) {
if (index > 0) {
let prev_column = columns[index - 1];
prev_column.fields = [...prev_column.fields, ...props.column.fields];
} else {
if (props.column.fields.length == 0) {
// set next column as first column
let next_column = columns[index + 1];
if (next_column) {
next_column.is_first = true;
} else {
frappe.msgprint(__("Section must have at least one column"));
throw "section must have at least one column";
}
} else {
// create a new column if current column has fields and push fields to it
columns.unshift({
df: store.get_df("Column Break"),
fields: props.column.fields,
is_first: true,
});
index++;
}
}
}
// remove column
columns.splice(index, 1);
store.form.selected_field = null;
}
function move_columns_to_section() {
move_children_to_parent(props, "section", "column", store.current_tab);
}
</script>
<template>
<div
:class="['column', selected ? 'selected' : hovered ? 'hovered' : '']"
@ -117,35 +7,12 @@ function move_columns_to_section() {
@mouseout.stop="hovered = false"
>
<div
:class="['column-header', column.df.label ? 'has-label' : '']"
v-if="column.df.label"
class="column-header"
:hidden="!column.df.label && store.read_only"
>
<div class="column-label">
<EditableInput
:text="column.df.label"
:placeholder="__('Column Title')"
v-model="column.df.label"
/>
</div>
<div class="column-actions">
<button class="btn btn-xs btn-icon" :title="__('Add Column')" @click="add_column">
<div v-html="frappe.utils.icon('add', 'sm')"></div>
</button>
<button
v-if="section.columns.indexOf(column)"
class="btn btn-xs btn-icon"
:title="__('Move the current column & the following columns to a new section')"
@click="move_columns_to_section"
>
<div v-html="frappe.utils.icon('move', 'sm')"></div>
</button>
<button
class="btn btn-xs btn-icon"
:title="__('Remove Column')"
@click.stop="remove_column"
>
<div v-html="frappe.utils.icon('remove', 'sm')"></div>
</button>
<span>{{ column.df.label }}</span>
</div>
</div>
<div v-if="column.df.description" class="column-description">
@ -169,11 +36,7 @@ function move_columns_to_section() {
/>
</template>
</draggable>
<div
class="empty-column"
:hidden="store.read_only"
:style="store.selected(column.df.name) ? { top: '35px' } : { top: 0 }"
>
<div class="empty-column" :hidden="store.read_only">
<AddFieldButton :column="column" />
</div>
<div v-if="column.fields.length" class="add-new-field-btn">
@ -182,6 +45,30 @@ function move_columns_to_section() {
</div>
</template>
<script setup>
import draggable from "vuedraggable";
import Field from "./Field.vue";
import AddFieldButton from "./AddFieldButton.vue";
import { computed, ref } from "vue";
import { useStore } from "../store";
import { is_touch_screen_device } from "../utils";
import { useMagicKeys, whenever } from "@vueuse/core";
const props = defineProps(["section", "column"]);
const store = useStore();
// delete/backspace to delete the field
const { Backspace } = useMagicKeys();
whenever(Backspace, (value) => {
if (value && selected.value && store.not_using_input) {
remove_column();
}
});
const hovered = ref(false);
const selected = computed(() => store.selected(props.column.df.name));
</script>
<style lang="scss" scoped>
.column {
position: relative;
@ -197,35 +84,17 @@ function move_columns_to_section() {
&.hovered,
&.selected {
border-color: var(--primary);
border-color: var(--border-primary);
border-style: solid;
.btn.btn-icon {
opacity: 1 !important;
}
}
&.selected {
.column-header {
display: flex;
}
.column-container:empty {
height: 80%;
}
}
.column-header {
display: none;
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 0.5rem;
padding-left: 0.3rem;
&.has-label {
display: flex;
}
.column-label {
:deep(span) {
font-weight: 600;
@ -278,8 +147,9 @@ function move_columns_to_section() {
flex-direction: column;
align-items: center;
position: absolute;
left: 0;
top: 0;
bottom: 0;
left: 0;
gap: 5px;
width: 100%;
padding: 15px;
@ -304,7 +174,7 @@ function move_columns_to_section() {
padding: 10px 6px 5px;
button {
background-color: var(--white);
background-color: var(--fg-color);
&:hover {
background-color: var(--btn-default-hover-bg);

View file

@ -0,0 +1,129 @@
<template>
<button
ref="dropdown_btn_ref"
class="dropdown-btn btn btn-xs btn-icon"
@click.stop="toggle_fieldtype_options"
>
<slot>
<div v-html="frappe.utils.icon('dot-horizontal', 'sm')" />
</slot>
<Teleport to="#autocomplete-area">
<div class="dropdown" ref="dropdown_ref">
<div v-show="show" class="dropdown-options">
<div v-for="group in groups" :key="group.key" class="groups">
<div v-if="group.group" class="group-title">
{{ group.group }}
</div>
<div
class="dropdown-option"
v-for="item in group.items"
:key="item.label"
:title="item.tooltip"
>
<button class="dropdown-item" @click.stop="action(item.onClick)">
{{ item.label }}
</button>
</div>
</div>
</div>
</div>
</Teleport>
</button>
</template>
<script setup>
import { createPopper } from "@popperjs/core";
import { nextTick, ref, computed } from "vue";
import { onClickOutside } from "@vueuse/core";
const props = defineProps({
options: {
type: Array,
required: true,
},
placement: {
type: String,
default: "bottom-end",
},
});
const show = ref(false);
const dropdown_btn_ref = ref(null);
const dropdown_ref = ref(null);
const popper = ref(null);
onClickOutside(dropdown_btn_ref, () => (show.value = false), { ignore: [dropdown_ref] });
const groups = computed(() => {
let _groups = props.options[0]?.group ? props.options : [{ group: "", items: props.options }];
return _groups.map((group, i) => {
return {
key: i,
group: group.group,
items: group.items,
};
});
});
function setupPopper() {
if (!popper.value) {
popper.value = createPopper(dropdown_btn_ref.value, dropdown_ref.value, {
placement: props.placement,
modifiers: [
{
name: "offset",
options: {
offset: [0, 4],
},
},
],
});
} else {
popper.value.update();
}
}
function toggle_fieldtype_options() {
show.value = !show.value;
nextTick(() => setupPopper());
}
function action(clickEvent) {
clickEvent && clickEvent();
show.value = false;
}
</script>
<style lang="scss" scoped>
.groups {
padding: 5px;
.group-title {
display: flex;
align-items: center;
padding: 4px 8px;
font-size: smaller;
font-weight: 600;
text-transform: uppercase;
color: var(--disabled-text-color);
}
}
.dropdown-btn {
&:hover {
background-color: var(--bg-light-gray);
}
}
.dropdown-options {
background-color: var(--fg-color);
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-2xl);
padding: 4px;
border: 1px solid var(--subtle-accent);
}
.dropdown {
z-index: 100;
}
</style>

View file

@ -75,6 +75,110 @@ function duplicate_field() {
store.form.selected_field = duplicate_field.df;
}
function make_dialog(frm) {
frm.dialog = new frappe.ui.Dialog({
title: __("Set Filters"),
fields: [
{
fieldtype: "HTML",
fieldname: "filter_area",
},
],
primary_action: () => {
let fieldname = props.field.df.fieldname;
let field_option = props.field.df.options;
let filters = frm.filter_group.get_filters().map((filter) => {
// last element is a boolean which hides the filter hence not required to store in meta
filter.pop();
// filter_group component requires options and frm.set_query requires fieldname so storing both
filter[0] = { fieldname, field_option };
return filter;
});
props.field.df.link_filters = JSON.stringify(filters);
frm.dialog.hide();
},
primary_action_label: __("Apply"),
});
if (frm.doctype === "Customize Form") {
let current_doctype = frm.doc.doc_type;
let fieldname = props.field.df.fieldname;
let property = "link_filters";
let property_setter_id = current_doctype + "-" + fieldname + "-" + property;
frappe.db.exists("Property Setter", property_setter_id).then((exits) => {
if (exits) {
frm.dialog.set_secondary_action_label(__("Reset To Default"));
frm.dialog.set_secondary_action(() => {
frappe.call({
method: "frappe.custom.doctype.customize_form.customize_form.get_link_filters_from_doc_without_customisations",
args: {
doctype: current_doctype,
fieldname: fieldname,
},
callback: function (r) {
if (r.message) {
props.field.df.link_filters = r.message;
frm.filter_group.clear_filters();
add_existing_filter(frm, props.field.df);
// hide the secondary action button
frm.dialog.get_secondary_btn().addClass("hidden");
}
},
});
});
}
});
}
// Setting selected field in store because when we click on the dialog the selected field is set to null
frm.dialog.$wrapper.on("click", () => {
store.form.selected_field = props.field.df;
});
}
function make_filter_area(frm, doctype) {
frm.filter_group = new frappe.ui.FilterGroup({
parent: frm.dialog.get_field("filter_area").$wrapper,
doctype: doctype,
on_change: () => {},
});
}
function add_existing_filter(frm, df) {
if (df.link_filters) {
let filters = JSON.parse(df.link_filters);
filters.map((filter) => {
// filter_group component requires options and frm.set_query requires fieldname
filter[0] = filter[0].field_option;
});
if (filters) {
frm.filter_group.add_filters_to_filter_group(filters);
}
}
}
function edit_filters() {
let field_doctype = props.field.df.options;
const { frm } = store;
make_dialog(frm);
make_filter_area(frm, field_doctype);
frappe.model.with_doctype(field_doctype, () => {
frm.dialog.show();
add_existing_filter(frm, props.field.df);
});
}
function is_filter_applied() {
if (props.field.df.link_filters && JSON.parse(props.field.df.link_filters).length > 0) {
return "btn-filter-applied";
}
}
onMounted(() => selected.value && label_input.value.focus_on_label());
</script>
@ -111,22 +215,17 @@ onMounted(() => selected.value && label_input.value.focus_on_label());
</template>
<template #actions>
<div class="field-actions" :hidden="store.read_only">
<AddFieldButton
v-if="column.fields.indexOf(field) != column.fields.length - 1"
ref="add_field_ref"
:field="field"
:column="column"
:tooltip="__('Add field below')"
<button
v-if="field.df.fieldtype === 'Link'"
class="btn btn-xs btn-icon"
:class="is_filter_applied()"
@click="edit_filters"
>
<div v-html="frappe.utils.icon('filter', 'sm')"></div>
</button>
<AddFieldButton ref="add_field_ref" :column="column" :field="field">
<div v-html="frappe.utils.icon('add', 'sm')" />
</AddFieldButton>
<button
class="btn btn-xs btn-icon"
:title="__('Duplicate field')"
@click.stop="duplicate_field"
>
<div v-html="frappe.utils.icon('duplicate', 'sm')"></div>
</button>
<button
v-if="column.fields.indexOf(field)"
class="btn btn-xs btn-icon"
@ -137,6 +236,13 @@ onMounted(() => selected.value && label_input.value.focus_on_label());
>
<div v-html="frappe.utils.icon('move', 'sm')"></div>
</button>
<button
class="btn btn-xs btn-icon"
:title="__('Duplicate field')"
@click.stop="duplicate_field"
>
<div v-html="frappe.utils.icon('duplicate', 'sm')"></div>
</button>
<button
class="btn btn-xs btn-icon"
:title="__('Remove field')"
@ -166,7 +272,7 @@ onMounted(() => selected.value && label_input.value.focus_on_label());
&.hovered,
&.selected {
border-color: var(--primary);
border-color: var(--border-primary);
.btn.btn-icon {
opacity: 1 !important;
}
@ -211,4 +317,10 @@ onMounted(() => selected.value && label_input.value.focus_on_label());
}
}
}
.btn-filter-applied {
background-color: var(--gray-300) !important;
&:hover {
background-color: var(--gray-400) !important;
}
}
</style>

View file

@ -6,7 +6,7 @@
class="search-input form-control"
type="text"
:placeholder="__('Search properties...')"
@input="event => $emit('update:modelValue', event.target.value)"
@input="(event) => $emit('update:modelValue', event.target.value)"
/>
<span class="search-icon">
<div v-html="frappe.utils.icon('search', 'sm')"></div>

View file

@ -1,10 +1,87 @@
<template>
<div
class="form-section-container"
:style="{ borderBottom: props.section.df.hide_border ? 'none' : '' }"
>
<div
:class="[
'form-section',
hovered ? 'hovered' : '',
store.selected(section.df.name) ? 'selected' : '',
]"
:title="section.df.fieldname"
@click.stop="select_section"
@mouseover.stop="hovered = true"
@mouseout.stop="hovered = false"
>
<div
:class="[
'section-header',
section.df.label || section.df.collapsible ? 'has-label' : '',
collapsed ? 'collapsed' : '',
]"
:hidden="!section.df.label && store.read_only"
>
<div class="section-label">
<EditableInput
:text="section.df.label"
:placeholder="__('Section Title')"
v-model="section.df.label"
/>
<div
v-if="section.df.collapsible"
class="collapse-indicator"
v-html="frappe.utils.icon(collapsed ? 'down' : 'up-line', 'sm')"
></div>
</div>
<Dropdown v-if="!store.read_only" :options="options" @click.stop />
</div>
<div v-if="section.df.description" class="section-description">
{{ section.df.description }}
</div>
<div
class="section-columns"
:class="{
hidden: section.df.collapsible && collapsed,
'has-one-column': section.columns.length === 1,
}"
>
<draggable
class="section-columns-container"
v-model="section.columns"
group="columns"
item-key="id"
:delay="is_touch_screen_device() ? 200 : 0"
:animation="200"
:easing="store.get_animation"
:disabled="store.read_only"
>
<template #item="{ element }">
<Column
:section="section"
:column="element"
:data-is-user-generated="store.is_user_generated_field(element)"
/>
</template>
</draggable>
</div>
</div>
</div>
</template>
<script setup>
import draggable from "vuedraggable";
import Column from "./Column.vue";
import EditableInput from "./EditableInput.vue";
import Dropdown from "./Dropdown.vue";
import { ref, computed } from "vue";
import { useStore } from "../store";
import { section_boilerplate, move_children_to_parent, confirm_dialog, is_touch_screen_device } from "../utils";
import {
section_boilerplate,
move_children_to_parent,
confirm_dialog,
is_touch_screen_device,
} from "../utils";
import { useMagicKeys, whenever } from "@vueuse/core";
const props = defineProps(["tab", "section"]);
@ -21,7 +98,9 @@ whenever(Backspace, (value) => {
const hovered = ref(false);
const collapsed = ref(false);
const selected = computed(() => store.selected(props.section.df.name));
const column = computed(() => props.section.columns[props.section.columns.length - 1]);
// section
function add_section_above() {
let index = props.tab.sections.indexOf(props.section);
props.tab.sections.splice(index, 0, section_boilerplate());
@ -97,103 +176,130 @@ function move_sections_to_tab() {
// activate tab
store.form.active_tab = new_tab;
}
</script>
<template>
<div
class="form-section-container"
:style="{ borderBottom: props.section.df.hide_border ? 'none' : '' }"
>
<div
:class="[
'form-section',
hovered ? 'hovered' : '',
store.selected(section.df.name) ? 'selected' : '',
]"
:title="section.df.fieldname"
@click.stop="select_section"
@mouseover.stop="hovered = true"
@mouseout.stop="hovered = false"
>
<div
:class="[
'section-header',
section.df.label || section.df.collapsible ? 'has-label' : '',
collapsed ? 'collapsed' : '',
]"
:hidden="!section.df.label && store.read_only"
>
<div class="section-label">
<EditableInput
:text="section.df.label"
:placeholder="__('Section Title')"
v-model="section.df.label"
/>
<div
v-if="section.df.collapsible"
class="collapse-indicator"
v-html="frappe.utils.icon(collapsed ? 'down' : 'up-line', 'sm')"
></div>
</div>
<div class="section-actions" :hidden="store.read_only">
<button
class="btn btn-xs btn-section"
:title="__('Add section above')"
@click="add_section_above"
>
<div v-html="frappe.utils.icon('add', 'sm')"></div>
</button>
<button
v-if="tab.sections.indexOf(section)"
class="btn btn-xs btn-section"
:title="
__('Move the current section and the following sections to a new tab')
"
@click="move_sections_to_tab"
>
<div v-html="frappe.utils.icon('move', 'sm')"></div>
</button>
<button
class="btn btn-xs btn-section"
:title="__('Remove section')"
@click.stop="remove_section"
>
<div v-html="frappe.utils.icon('remove', 'sm')"></div>
</button>
</div>
</div>
<div v-if="section.df.description" class="section-description">
{{ section.df.description }}
</div>
<div
class="section-columns"
:class="{
hidden: section.df.collapsible && collapsed,
'has-one-column': section.columns.length === 1,
}"
>
<draggable
class="section-columns-container"
v-model="section.columns"
group="columns"
item-key="id"
:delay="is_touch_screen_device() ? 200 : 0"
:animation="200"
:easing="store.get_animation"
:disabled="store.read_only"
>
<template #item="{ element }">
<Column
:section="section"
:column="element"
:data-is-user-generated="store.is_user_generated_field(element)"
/>
</template>
</draggable>
</div>
</div>
</div>
</template>
// column
function add_column() {
props.section.columns.push({
fields: [],
df: store.get_df("Column Break"),
});
}
function remove_column() {
if (store.is_customize_form && column.value.df.is_custom_field == 0) {
frappe.msgprint(__("Cannot delete standard field. You can hide it if you want"));
throw "cannot delete standard field";
} else if (column.value.fields.length == 0 || store.has_standard_field(column.value)) {
delete_column();
} else {
confirm_dialog(
__("Delete Column", null, "Title of confirmation dialog"),
__(
"Are you sure you want to delete the column? All the fields in the column will be moved to the previous column.",
null,
"Confirmation dialog message"
),
() => delete_column(),
__("Delete column", null, "Button text"),
() => delete_column(true),
__("Delete entire column with fields", null, "Button text")
);
}
}
function delete_column(with_children) {
// move all fields to previous column
let columns = props.section.columns;
let index = columns.length - 1;
if (with_children && index == 0 && columns.length == 1) {
if (column.value.fields.length == 0) {
frappe.msgprint(__("Section must have at least one column"));
throw "section must have at least one column";
}
columns.unshift({
df: store.get_df("Column Break"),
fields: [],
is_first: true,
});
index++;
}
if (!with_children) {
if (index > 0) {
let prev_column = columns[index - 1];
prev_column.fields = [...prev_column.fields, ...column.value.fields];
} else {
if (column.value.fields.length == 0) {
// set next column as first column
let next_column = columns[index + 1];
if (next_column) {
next_column.is_first = true;
} else {
frappe.msgprint(__("Section must have at least one column"));
throw "section must have at least one column";
}
} else {
// create a new column if current column has fields and push fields to it
columns.unshift({
df: store.get_df("Column Break"),
fields: column.value.fields,
is_first: true,
});
index++;
}
}
}
// remove column
columns.splice(index, 1);
store.form.selected_field = null;
}
const options = computed(() => {
let groups = [
{
group: "Section",
items: [
{ label: "Add section above", onClick: add_section_above },
{ label: "Remove section", onClick: remove_section },
],
},
{
group: "Column",
items: [{ label: "Add column", onClick: add_column }],
},
];
// add remove column option if there are more than one columns
if (props.section.columns.length > 1) {
groups[1].items.push({
label: "Remove column",
tooltip: "Remove last column",
onClick: remove_column,
});
} else if (props.section.columns[0].fields.length) {
// add remove all fields option if there is only one column and it has fields
groups[1].items.push({
label: "Empty column",
tooltip: "Remove all fields in the column",
onClick: () => delete_column(true),
});
}
// add move to tab option if the current section is not the first section
if (props.tab.sections.indexOf(props.section) > 0) {
groups[0].items.push({
label: "Move sections to new tab",
tooltip: "Move current and all subsequent sections to a new tab",
onClick: move_sections_to_tab,
});
}
return groups;
});
</script>
<style lang="scss" scoped>
.form-section-container {
@ -218,15 +324,11 @@ function move_sections_to_tab() {
&.hovered,
&.selected {
border-color: var(--primary);
}
&.selected .section-header {
display: flex;
border-color: var(--border-primary);
}
.section-header {
display: none;
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 0.75rem;
@ -254,16 +356,21 @@ function move_sections_to_tab() {
.section-actions {
display: flex;
gap: 4px;
align-items: center;
}
.btn-section {
padding: var(--padding-xs);
box-shadow: none;
// .btn-section {
// padding: var(--padding-xs);
// box-shadow: none;
&:hover {
background-color: var(--bg-light-gray);
}
// &:hover {
// background-color: var(--bg-light-gray);
// }
// }
.btn-section {
display: inline-flex;
gap: 2px;
}
}

View file

@ -13,7 +13,7 @@ const store = useStore();
const { Backspace } = useMagicKeys();
whenever(Backspace, (value) => {
if (value && selected.value && store.not_using_input) {
remove_tab(store.current_tab, '', true);
remove_tab(store.current_tab, "", true);
}
});
@ -45,12 +45,10 @@ function add_new_section() {
function is_tab_empty(tab) {
// check if sections have columns and it contains fields
return !tab.sections.some((section) =>
section.columns.some((column) => column.fields.length)
);
return !tab.sections.some((section) => section.columns.some((column) => column.fields.length));
}
function remove_tab(tab, event, force=false) {
function remove_tab(tab, event, force = false) {
// is remove_tab_btn is not visible then return
if (!event?.currentTarget?.offsetParent && !force) return;
@ -270,7 +268,7 @@ function delete_tab(tab, with_children) {
color: var(--text-color);
&::before {
border-color: var(--primary);
border-color: var(--border-primary);
}
}

View file

@ -4,10 +4,7 @@ const props = defineProps(["df", "value"]);
</script>
<template>
<div
class="control frappe-control editable"
:data-fieldtype="df.fieldtype"
>
<div class="control frappe-control editable" :data-fieldtype="df.fieldtype">
<!-- label -->
<div class="field-controls">
<h4 v-if="df.fieldtype == 'Heading'">
@ -28,5 +25,4 @@ const props = defineProps(["df", "value"]);
h4 {
margin-bottom: 0px;
}
</style>

View file

@ -20,7 +20,7 @@ let slots = useSlots();
type="checkbox"
:checked="value"
:disabled="read_only"
@change="event => $emit('update:modelValue', event.target.checked)"
@change="(event) => $emit('update:modelValue', event.target.checked)"
/>
<span class="label-area" :class="{ reqd: df.reqd }">{{ df.label }}</span>
</label>
@ -31,7 +31,8 @@ let slots = useSlots();
</template>
<style lang="scss" scoped>
label, input {
label,
input {
margin-bottom: 0 !important;
cursor: pointer;
}

View file

@ -7,40 +7,36 @@ let emit = defineEmits(["update:modelValue"]);
let slots = useSlots();
let code = ref(null);
let code_control = ref(null);
let update_control = ref(true);
let code_control = computed(() => {
if (!code.value) return;
code.value.innerHTML = "";
return frappe.ui.form.make_control({
parent: code.value,
df: {
...props.df,
fieldtype: "Code",
hidden: 0,
read_only: props.read_only,
change: () => {
if (update_control.value) {
content.value = code_control.value.get_value();
}
update_control.value = true;
},
},
value: content.value,
disabled: Boolean(slots.label) || props.read_only,
render_input: true,
only_input: Boolean(slots.label),
});
});
let content = computed({
get: () => props.modelValue,
set: (value) => emit("update:modelValue", value),
});
onMounted(() => {
if (code.value) code_control.value;
if (code.value) {
code_control.value = frappe.ui.form.make_control({
parent: code.value,
df: {
...props.df,
fieldtype: "Code",
hidden: 0,
read_only: props.read_only,
change: () => {
if (update_control.value) {
content.value = code_control.value.get_value();
}
update_control.value = true;
},
},
value: content.value,
disabled: Boolean(slots.label) || props.read_only,
render_input: true,
only_input: Boolean(slots.label),
});
}
});
watch(

View file

@ -23,10 +23,7 @@ if (props.df.fieldtype === "Icon") {
</script>
<template>
<div
class="control frappe-control"
:class="{ editable: slots.label }"
>
<div class="control frappe-control" :class="{ editable: slots.label }">
<!-- label -->
<div v-if="slots.label" class="field-controls">
<slot name="label" />
@ -49,7 +46,7 @@ if (props.df.fieldtype === "Icon") {
type="text"
:value="value"
:disabled="read_only || df.read_only"
@input="event => $emit('update:modelValue', event.target.value)"
@input="(event) => $emit('update:modelValue', event.target.value)"
/>
<input
v-if="slots.label && df.fieldtype === 'Barcode'"

View file

@ -13,20 +13,22 @@ function get_options() {
if (typeof options == "string") {
options = options.split("\n") || "";
options = options.map(opt => {
options = options.map((opt) => {
return { label: __(opt), value: opt };
});
}
if (options?.length && typeof options[0] == "string") {
options = options.map(opt => {
options = options.map((opt) => {
return { label: __(opt), value: opt };
});
}
if (props.df.fieldname == "fieldtype") {
if (!in_list(frappe.model.layout_fields, props.modelValue)) {
options = options && options.filter(opt => !in_list(frappe.model.layout_fields, opt.value));
options =
options &&
options.filter((opt) => !in_list(frappe.model.layout_fields, opt.value));
} else {
options = [{ label: __(props.modelValue), value: props.modelValue }];
}
@ -56,17 +58,17 @@ let select_control = computed(() => {
content.value = select_control.value.get_value();
}
update_control.value = true;
}
},
},
value: content.value,
render_input: true,
only_input: Boolean(slots.label) || props.no_label
only_input: Boolean(slots.label) || props.no_label,
});
});
let content = computed({
get: () => props.modelValue,
set: value => emit("update:modelValue", value)
set: (value) => emit("update:modelValue", value),
});
onMounted(() => {
@ -75,15 +77,18 @@ onMounted(() => {
watch(
() => content.value,
value => {
(value) => {
update_control.value = false;
select_control.value?.set_value(value);
}
);
watch(() => props.df.options, () => {
select_control.value;
})
watch(
() => props.df.options,
() => {
select_control.value;
}
);
</script>
<template>

View file

@ -38,7 +38,7 @@ let height = computed(() => {
type="text"
:value="value"
:disabled="read_only || df.read_only"
@input="event => $emit('update:modelValue', event.target.value)"
@input="(event) => $emit('update:modelValue', event.target.value)"
/>
<!-- description -->

View file

@ -201,6 +201,18 @@ export const useStore = defineStore("form-builder-store", () => {
get_field_data(df)
);
}
// check if link_filters format is correct or not
if (df.link_filters) {
try {
let link_filters = JSON.parse(df.link_filters);
} catch (e) {
error_message = __(
`Invalid Filter Format. Try using filter icon on the field to set it correctly`
);
}
}
});
return error_message;

View file

@ -28,9 +28,7 @@ function open_in_editor(location) {
}
function error_component(error, i) {
let location = data.value.error.errors[i].location;
let location_string = `${location.file}:${location.line}:${
location.column
}`;
let location_string = `${location.file}:${location.line}:${location.column}`;
let template = error.replace(
" > " + location_string,
` &gt; <a class="file-link" @click="open">${location_string}</a>`
@ -41,11 +39,11 @@ function error_component(error, i) {
methods: {
open() {
frappe.realtime.emit("open_in_editor", location);
}
}
},
},
};
}
defineExpose({show, hide});
defineExpose({ show, hide });
</script>
<style scoped>
@ -58,8 +56,7 @@ defineExpose({show, hide});
z-index: 9999;
margin: 0;
background: rgba(0, 0, 0, 0.66);
--monospace: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier,
monospace;
--monospace: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
--dim: var(--gray-400);
}
.window {

View file

@ -1,13 +1,7 @@
<template>
<div
v-if="is_shown"
class="flex justify-between build-success-message align-center"
>
<div v-if="is_shown" class="flex justify-between build-success-message align-center">
Compiled successfully
<a
v-if="!live_reload"
class="ml-4 text-white underline" href="/" @click.prevent="reload"
>
<a v-if="!live_reload" class="ml-4 text-white underline" href="/" @click.prevent="reload">
Refresh
</a>
</div>
@ -43,7 +37,7 @@ function reload() {
window.location.reload();
}
defineExpose({show, hide});
defineExpose({ show, hide });
</script>
<style scoped>

View file

@ -1,11 +1,7 @@
<template>
<div class="file-browser">
<div>
<a
href=""
class="text-muted text-medium"
@click.prevent="emit('hide-browser')"
>
<a href="" class="text-muted text-medium" @click.prevent="emit('hide-browser')">
{{ __("← Back to upload files") }}
</a>
</div>
@ -23,8 +19,8 @@
class="tree with-skeleton"
:node="node"
:selected_node="selected_node"
@node-click="n => toggle_node(n)"
@load-more="n => load_more(n)"
@node-click="(n) => toggle_node(n)"
@load-more="(n) => load_more(n)"
/>
</div>
</div>
@ -48,7 +44,7 @@ let node = ref({
fetching: false,
fetched: false,
open: false,
filtered: true
filtered: true,
});
let selected_node = ref({});
let search_text = ref("");
@ -61,16 +57,14 @@ function toggle_node(node) {
node.fetching = true;
node.children_start = 0;
node.children_loading = false;
get_files_in_folder(node.value, 0).then(
({ files, has_more }) => {
node.open = true;
node.children = files;
node.fetched = true;
node.fetching = false;
node.children_start += page_length.value;
node.has_more_children = has_more;
}
);
get_files_in_folder(node.value, 0).then(({ files, has_more }) => {
node.open = true;
node.children = files;
node.fetched = true;
node.fetching = false;
node.children_start += page_length.value;
node.has_more_children = has_more;
});
} else {
node.open = !node.open;
select_node(node);
@ -80,14 +74,12 @@ function load_more(node) {
if (node.has_more_children) {
let start = node.children_start;
node.children_loading = true;
get_files_in_folder(node.value, start).then(
({ files, has_more }) => {
node.children = node.children.concat(files);
node.children_start += page_length.value;
node.has_more_children = has_more;
node.children_loading = false;
}
);
get_files_in_folder(node.value, start).then(({ files, has_more }) => {
node.children = node.children.concat(files);
node.children_start += page_length.value;
node.has_more_children = has_more;
node.children_loading = false;
});
}
}
function select_node(node) {
@ -100,9 +92,9 @@ function get_files_in_folder(folder, start) {
.call("frappe.core.api.file.get_files_in_folder", {
folder,
start,
page_length: page_length.value
page_length: page_length.value,
})
.then(r => {
.then((r) => {
let { files = [], has_more = false } = r.message || {};
files.sort((a, b) => {
if (a.is_folder && b.is_folder) {
@ -116,7 +108,7 @@ function get_files_in_folder(folder, start) {
}
return 0;
});
files = files.map(file => make_file_node(file));
files = files.map((file) => make_file_node(file));
return { files, has_more };
});
}
@ -127,15 +119,12 @@ function search_by_name() {
}
if (search_text.value.length < 3) return;
frappe
.call(
"frappe.core.api.file.get_files_by_search_text",
{
text: search_text.value
}
)
.then(r => {
.call("frappe.core.api.file.get_files_by_search_text", {
text: search_text.value,
})
.then((r) => {
let files = r.message || [];
files = files.map(file => make_file_node(file));
files = files.map((file) => make_file_node(file));
if (!folder_node.value) {
folder_node.value = node.value;
}
@ -145,7 +134,7 @@ function search_by_name() {
children: files,
by_search: true,
open: true,
filtered: true
filtered: true,
};
});
}
@ -164,7 +153,7 @@ function make_file_node(file) {
children_start: 0,
open: false,
fetching: false,
filtered: true
filtered: true,
};
}

View file

@ -1,13 +1,8 @@
<template>
<div class="file-preview">
<div class="file-icon">
<img
v-if="is_image"
:src="src"
:alt="file.name"
>
<div class="fallback" v-else v-html="frappe.utils.icon('file', 'md')">
</div>
<img v-if="is_image" :src="src" :alt="file.name" />
<div class="fallback" v-else v-html="frappe.utils.icon('file', 'md')"></div>
</div>
<div>
<div>
@ -24,8 +19,20 @@
</div>
<div class="flex config-area">
<label v-if="is_optimizable" class="frappe-checkbox"><input type="checkbox" :checked="optimize" @change="emit('toggle_optimize')">{{ __('Optimize') }}</label>
<label class="frappe-checkbox"><input type="checkbox" :checked="file.private" @change="emit('toggle_private')">{{ __('Private') }}</label>
<label v-if="is_optimizable" class="frappe-checkbox"
><input
type="checkbox"
:checked="optimize"
@change="emit('toggle_optimize')"
/>{{ __("Optimize") }}</label
>
<label class="frappe-checkbox"
><input
type="checkbox"
:checked="file.private"
@change="emit('toggle_private')"
/>{{ __("Private") }}</label
>
</div>
<div>
<span v-if="file.error_message" class="file-error text-danger">
@ -45,8 +52,18 @@
<div v-if="uploaded" v-html="frappe.utils.icon('solid-success', 'lg')"></div>
<div v-if="file.failed" v-html="frappe.utils.icon('solid-error', 'lg')"></div>
<div class="file-action-buttons">
<button v-if="is_cropable" class="btn btn-crop muted" @click="emit('toggle_image_cropper')" v-html="frappe.utils.icon('crop', 'md')"></button>
<button v-if="!uploaded && !file.uploading && !file.failed" class="btn muted" @click="emit('remove')" v-html="frappe.utils.icon('delete', 'md')"></button>
<button
v-if="is_cropable"
class="btn btn-crop muted"
@click="emit('toggle_image_cropper')"
v-html="frappe.utils.icon('crop', 'md')"
></button>
<button
v-if="!uploaded && !file.uploading && !file.failed"
class="btn muted"
@click="emit('remove')"
v-html="frappe.utils.icon('delete', 'md')"
></button>
</div>
</div>
</div>
@ -79,15 +96,20 @@ let uploaded = computed(() => {
return props.file.request_succeeded;
});
let is_image = computed(() => {
return props.file.file_obj.type.startsWith('image');
return props.file.file_obj.type.startsWith("image");
});
let is_optimizable = computed(() => {
let is_svg = props.file.file_obj.type == 'image/svg+xml';
let is_svg = props.file.file_obj.type == "image/svg+xml";
return is_image.value && !is_svg && !uploaded.value && !props.file.failed;
});
let is_cropable = computed(() => {
let croppable_types = ['image/jpeg', 'image/png'];
return !uploaded.value && !props.file.uploading && !props.file.failed && croppable_types.includes(props.file.file_obj.type);
let croppable_types = ["image/jpeg", "image/png"];
return (
!uploaded.value &&
!props.file.uploading &&
!props.file.failed &&
croppable_types.includes(props.file.file_obj.type)
);
});
let progress = computed(() => {
let value = Math.round((props.file.progress * 100) / props.file.total);
@ -102,7 +124,7 @@ onMounted(() => {
if (is_image.value) {
if (window.FileReader) {
let fr = new FileReader();
fr.onload = () => src.value = fr.result;
fr.onload = () => (src.value = fr.result);
fr.readAsDataURL(props.file.file_obj);
}
}

View file

@ -1,5 +1,6 @@
<template>
<div class="file-uploader"
<div
class="file-uploader"
@dragover.prevent="dragover"
@dragleave.prevent="dragleave"
@drop.prevent="dropfiles"
@ -10,19 +11,50 @@
>
<div v-if="!is_dragging">
<div class="text-center">
{{ __('Drag and drop files here or upload from') }}
{{ __("Drag and drop files here or upload from") }}
</div>
<div class="mt-2 text-center">
<button class="btn btn-file-upload" @click="browse_files">
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="15" cy="15" r="15" fill="var(--subtle-fg)"/>
<path d="M13.5 22V19" stroke="var(--text-color)" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.5 22V19" stroke="var(--text-color)" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.5 22H19.5" stroke="var(--text-color)" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.5 16H22.5" stroke="var(--text-color)" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 8H9C8.17157 8 7.5 8.67157 7.5 9.5V17.5C7.5 18.3284 8.17157 19 9 19H21C21.8284 19 22.5 18.3284 22.5 17.5V9.5C22.5 8.67157 21.8284 8 21 8Z" stroke="var(--text-color)" stroke-linecap="round" stroke-linejoin="round"/>
<svg
width="30"
height="30"
viewBox="0 0 30 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="15" cy="15" r="15" fill="var(--subtle-fg)" />
<path
d="M13.5 22V19"
stroke="var(--text-color)"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M16.5 22V19"
stroke="var(--text-color)"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M10.5 22H19.5"
stroke="var(--text-color)"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M7.5 16H22.5"
stroke="var(--text-color)"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M21 8H9C8.17157 8 7.5 8.67157 7.5 9.5V17.5C7.5 18.3284 8.17157 19 9 19H21C21.8284 19 22.5 18.3284 22.5 17.5V9.5C22.5 8.67157 21.8284 8 21 8Z"
stroke="var(--text-color)"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<div class="mt-1">{{ __('My Device') }}</div>
<div class="mt-1">{{ __("My Device") }}</div>
</button>
<input
type="file"
@ -31,37 +63,105 @@
@change="on_file_input"
:multiple="allow_multiple"
:accept="(restrictions.allowed_file_types || []).join(', ')"
/>
<button
class="btn btn-file-upload"
v-if="!disable_file_browser"
@click="show_file_browser = true"
>
<button class="btn btn-file-upload" v-if="!disable_file_browser" @click="show_file_browser = true">
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="15" cy="15" r="15" fill="var(--subtle-fg)"/>
<path d="M13.0245 11.5H8C7.72386 11.5 7.5 11.7239 7.5 12V20C7.5 21.1046 8.39543 22 9.5 22H20.5C21.6046 22 22.5 21.1046 22.5 20V14.5C22.5 14.2239 22.2761 14 22 14H15.2169C15.0492 14 14.8926 13.9159 14.8 13.776L13.4414 11.724C13.3488 11.5841 13.1922 11.5 13.0245 11.5Z" stroke="var(--text-color)" stroke-miterlimit="10" stroke-linecap="square"/>
<path d="M8.87939 9.5V8.5C8.87939 8.22386 9.10325 8 9.37939 8H20.6208C20.8969 8 21.1208 8.22386 21.1208 8.5V12" stroke="var(--text-color)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<svg
width="30"
height="30"
viewBox="0 0 30 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="15" cy="15" r="15" fill="var(--subtle-fg)" />
<path
d="M13.0245 11.5H8C7.72386 11.5 7.5 11.7239 7.5 12V20C7.5 21.1046 8.39543 22 9.5 22H20.5C21.6046 22 22.5 21.1046 22.5 20V14.5C22.5 14.2239 22.2761 14 22 14H15.2169C15.0492 14 14.8926 13.9159 14.8 13.776L13.4414 11.724C13.3488 11.5841 13.1922 11.5 13.0245 11.5Z"
stroke="var(--text-color)"
stroke-miterlimit="10"
stroke-linecap="square"
/>
<path
d="M8.87939 9.5V8.5C8.87939 8.22386 9.10325 8 9.37939 8H20.6208C20.8969 8 21.1208 8.22386 21.1208 8.5V12"
stroke="var(--text-color)"
stroke-miterlimit="10"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<div class="mt-1">{{ __('Library') }}</div>
<div class="mt-1">{{ __("Library") }}</div>
</button>
<button class="btn btn-file-upload" v-if="allow_web_link" @click="show_web_link = true">
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="15" cy="15" r="15" fill="var(--subtle-fg)"/>
<path d="M12.0469 17.9543L17.9558 12.0454" stroke="var(--text-color)" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.8184 11.4547L15.7943 9.47873C16.4212 8.85205 17.2714 8.5 18.1578 8.5C19.0443 8.5 19.8945 8.85205 20.5214 9.47873V9.47873C21.1481 10.1057 21.5001 10.9558 21.5001 11.8423C21.5001 12.7287 21.1481 13.5789 20.5214 14.2058L18.5455 16.1818" stroke="var(--text-color)" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.4547 13.8184L9.47873 15.7943C8.85205 16.4212 8.5 17.2714 8.5 18.1578C8.5 19.0443 8.85205 19.8945 9.47873 20.5214V20.5214C10.1057 21.1481 10.9558 21.5001 11.8423 21.5001C12.7287 21.5001 13.5789 21.1481 14.2058 20.5214L16.1818 18.5455" stroke="var(--text-color)" stroke-linecap="round" stroke-linejoin="round"/>
<button
class="btn btn-file-upload"
v-if="allow_web_link"
@click="show_web_link = true"
>
<svg
width="30"
height="30"
viewBox="0 0 30 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="15" cy="15" r="15" fill="var(--subtle-fg)" />
<path
d="M12.0469 17.9543L17.9558 12.0454"
stroke="var(--text-color)"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M13.8184 11.4547L15.7943 9.47873C16.4212 8.85205 17.2714 8.5 18.1578 8.5C19.0443 8.5 19.8945 8.85205 20.5214 9.47873V9.47873C21.1481 10.1057 21.5001 10.9558 21.5001 11.8423C21.5001 12.7287 21.1481 13.5789 20.5214 14.2058L18.5455 16.1818"
stroke="var(--text-color)"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M11.4547 13.8184L9.47873 15.7943C8.85205 16.4212 8.5 17.2714 8.5 18.1578C8.5 19.0443 8.85205 19.8945 9.47873 20.5214V20.5214C10.1057 21.1481 10.9558 21.5001 11.8423 21.5001C12.7287 21.5001 13.5789 21.1481 14.2058 20.5214L16.1818 18.5455"
stroke="var(--text-color)"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<div class="mt-1">{{ __('Link') }}</div>
<div class="mt-1">{{ __("Link") }}</div>
</button>
<button v-if="allow_take_photo" class="btn btn-file-upload" @click="capture_image">
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="15" cy="15" r="15" fill="var(--subtle-fg)"/>
<path d="M11.5 10.5H9.5C8.67157 10.5 8 11.1716 8 12V20C8 20.8284 8.67157 21.5 9.5 21.5H20.5C21.3284 21.5 22 20.8284 22 20V12C22 11.1716 21.3284 10.5 20.5 10.5H18.5L17.3 8.9C17.1111 8.64819 16.8148 8.5 16.5 8.5H13.5C13.1852 8.5 12.8889 8.64819 12.7 8.9L11.5 10.5Z" stroke="var(--text-color)" stroke-linejoin="round"/>
<circle cx="15" cy="16" r="2.5" stroke="var(--text-color)"/>
<button
v-if="allow_take_photo"
class="btn btn-file-upload"
@click="capture_image"
>
<svg
width="30"
height="30"
viewBox="0 0 30 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="15" cy="15" r="15" fill="var(--subtle-fg)" />
<path
d="M11.5 10.5H9.5C8.67157 10.5 8 11.1716 8 12V20C8 20.8284 8.67157 21.5 9.5 21.5H20.5C21.3284 21.5 22 20.8284 22 20V12C22 11.1716 21.3284 10.5 20.5 10.5H18.5L17.3 8.9C17.1111 8.64819 16.8148 8.5 16.5 8.5H13.5C13.1852 8.5 12.8889 8.64819 12.7 8.9L11.5 10.5Z"
stroke="var(--text-color)"
stroke-linejoin="round"
/>
<circle cx="15" cy="16" r="2.5" stroke="var(--text-color)" />
</svg>
<div class="mt-1">{{ __('Camera') }}</div>
<div class="mt-1">{{ __("Camera") }}</div>
</button>
<button v-if="google_drive_settings.enabled" class="btn btn-file-upload" @click="show_google_drive_picker">
<button
v-if="google_drive_settings.enabled"
class="btn btn-file-upload"
@click="show_google_drive_picker"
>
<svg width="30" height="30">
<image href="/assets/frappe/icons/social/google_drive.svg" width="30" height="30"/>
<image
href="/assets/frappe/icons/social/google_drive.svg"
width="30"
height="30"
/>
</svg>
<div class="mt-1">{{ __('Google Drive') }}</div>
<div class="mt-1">{{ __("Google Drive") }}</div>
</button>
</div>
<div class="text-muted text-medium text-center">
@ -69,10 +169,13 @@
</div>
</div>
<div v-else>
{{ __('Drop files here') }}
{{ __("Drop files here") }}
</div>
</div>
<div class="file-preview-area" v-show="files.length && !show_file_browser && !show_web_link">
<div
class="file-preview-area"
v-show="files.length && !show_file_browser && !show_web_link"
>
<div class="file-preview-container" v-if="!show_image_cropper">
<FilePreview
v-for="(file, i) in files"
@ -85,19 +188,16 @@
/>
</div>
<div class="flex align-center" v-if="show_upload_button && currently_uploading === -1">
<button
class="btn btn-primary btn-sm margin-right"
@click="upload_files"
>
<button class="btn btn-primary btn-sm margin-right" @click="upload_files">
<span v-if="files.length === 1">
{{ __('Upload file') }}
{{ __("Upload file") }}
</span>
<span v-else>
{{ __('Upload {0} files', [files.length]) }}
{{ __("Upload {0} files", [files.length]) }}
</span>
</button>
<div class="text-muted text-medium">
{{ __('Click on the lock icon to toggle public/private') }}
{{ __("Click on the lock icon to toggle public/private") }}
</div>
</div>
</div>
@ -106,60 +206,56 @@
:file="files[crop_image_with_index]"
:fixed_aspect_ratio="restrictions.crop_image_aspect_ratio"
@toggle_image_cropper="toggle_image_cropper(-1)"
@upload_after_crop="trigger_upload=true"
@upload_after_crop="trigger_upload = true"
/>
<FileBrowser
ref="file_browser"
v-if="show_file_browser && !disable_file_browser"
@hide-browser="show_file_browser = false"
/>
<WebLink
ref="web_link"
v-if="show_web_link"
@hide-web-link="show_web_link = false"
/>
<WebLink ref="web_link" v-if="show_web_link" @hide-web-link="show_web_link = false" />
</div>
</template>
<script setup>
import { computed, ref, watch } from 'vue';
import FilePreview from './FilePreview.vue';
import FileBrowser from './FileBrowser.vue';
import WebLink from './WebLink.vue';
import GoogleDrivePicker from '../../integrations/google_drive_picker';
import ImageCropper from './ImageCropper.vue';
import { computed, ref, watch } from "vue";
import FilePreview from "./FilePreview.vue";
import FileBrowser from "./FileBrowser.vue";
import WebLink from "./WebLink.vue";
import GoogleDrivePicker from "../../integrations/google_drive_picker";
import ImageCropper from "./ImageCropper.vue";
// props
const props = defineProps({
show_upload_button: {
default: true
default: true,
},
disable_file_browser: {
default: false
default: false,
},
allow_multiple: {
default: true
default: true,
},
as_dataurl: {
default: false
default: false,
},
doctype: {
default: null
default: null,
},
docname: {
default: null
default: null,
},
fieldname: {
default: null
default: null,
},
folder: {
default: 'Home'
default: "Home",
},
method: {
default: null
default: null,
},
on_success: {
default: null
default: null,
},
make_attachments_public: {
default: null,
@ -169,15 +265,15 @@ const props = defineProps({
max_file_size: null, // 2048 -> 2KB
max_number_of_files: null,
allowed_file_types: [], // ['image/*', 'video/*', '.jpg', '.gif', '.pdf'],
crop_image_aspect_ratio: null // 1, 16 / 9, 4 / 3, NaN (free)
})
crop_image_aspect_ratio: null, // 1, 16 / 9, 4 / 3, NaN (free)
}),
},
attach_doc_image: {
default: false
default: false,
},
upload_notes: {
default: null // "Images or video, upto 2MB"
}
default: null, // "Images or video, upto 2MB"
},
});
// variables
@ -197,7 +293,7 @@ let hide_dialog_footer = ref(false);
let allow_take_photo = ref(false);
let allow_web_link = ref(true);
let google_drive_settings = ref({
enabled: false
enabled: false,
});
let wrapper_ready = ref(false);
@ -211,14 +307,13 @@ if (frappe.user_id !== "Guest") {
if (!resp.exc) {
google_drive_settings.value = resp.message;
}
}
},
});
}
if (props.restrictions.max_file_size == null) {
frappe.call('frappe.core.api.file.get_max_file_size')
.then(res => {
props.restrictions.max_file_size = Number(res.message);
});
frappe.call("frappe.core.api.file.get_max_file_size").then((res) => {
props.restrictions.max_file_size = Number(res.message);
});
}
if (props.restrictions.max_number_of_files == null && props.doctype) {
props.restrictions.max_number_of_files = frappe.get_meta(props.doctype)?.max_attachments;
@ -242,7 +337,7 @@ function on_file_input(e) {
add_files(file_input.value.files);
}
function remove_file(file) {
files.value = files.value.filter(f => f !== file);
files.value = files.value.filter((f) => f !== file);
}
function toggle_image_cropper(index) {
crop_image_with_index.value = show_image_cropper.value ? -1 : index;
@ -251,7 +346,7 @@ function toggle_image_cropper(index) {
}
function toggle_all_private() {
let flag;
let private_values = files.value.filter(file => file.private);
let private_values = files.value.filter((file) => file.private);
if (private_values.length < files.value.length) {
// there are some private and some public
// set all to private
@ -260,7 +355,7 @@ function toggle_all_private() {
// all are private, set all to public
flag = false;
}
files.value = files.value.map(file => {
files.value = files.value.map((file) => {
file.private = flag;
return file;
});
@ -268,12 +363,19 @@ function toggle_all_private() {
function show_max_files_number_warning(file) {
console.warn(
`File skipped because it exceeds the allowed specified limit of ${max_number_of_files} uploads`,
file,
file
);
if (props.doctype) {
MSG = __('File "{0}" was skipped because only {1} uploads are allowed for DocType "{2}"', [file.name, max_number_of_files, props.doctype])
MSG = __('File "{0}" was skipped because only {1} uploads are allowed for DocType "{2}"', [
file.name,
max_number_of_files,
props.doctype,
]);
} else {
MSG = __('File "{0}" was skipped because only {1} uploads are allowed', [file.name, max_number_of_files])
MSG = __('File "{0}" was skipped because only {1} uploads are allowed', [
file.name,
max_number_of_files,
]);
}
frappe.show_alert({
message: MSG,
@ -283,14 +385,14 @@ function show_max_files_number_warning(file) {
function add_files(file_array) {
let _files = Array.from(file_array)
.filter(check_restrictions)
.map(file => {
let is_image = file.type.startsWith('image');
.map((file) => {
let is_image = file.type.startsWith("image");
let size_kb = file.size / 1024;
return {
file_obj: file,
cropper_file: file,
crop_box_data: null,
optimize: size_kb > 200 && is_image && !file.type.includes('svg'),
optimize: size_kb > 200 && is_image && !file.type.includes("svg"),
name: file.name,
doc: null,
progress: 0,
@ -306,7 +408,7 @@ function add_files(file_array) {
// pop extra files as per FileUploader.restrictions.max_number_of_files
max_number_of_files = props.restrictions.max_number_of_files;
if (max_number_of_files && _files.length > max_number_of_files) {
_files.slice(max_number_of_files).forEach(file => {
_files.slice(max_number_of_files).forEach((file) => {
show_max_files_number_warning(file, props.doctype);
});
@ -315,8 +417,12 @@ function add_files(file_array) {
files.value = files.value.concat(_files);
// if only one file is allowed and crop_image_aspect_ratio is set, open cropper immediately
if (files.value.length === 1 && !props.allow_multiple && props.restrictions.crop_image_aspect_ratio != null) {
if (!files.value[0].file_obj.type.includes('svg')) {
if (
files.value.length === 1 &&
!props.allow_multiple &&
props.restrictions.crop_image_aspect_ratio != null
) {
if (!files.value[0].file_obj.type.includes("svg")) {
toggle_image_cropper(0);
}
}
@ -330,14 +436,14 @@ function check_restrictions(file) {
if (allowed_file_types && allowed_file_types.length) {
is_correct_type = allowed_file_types.some((type) => {
// is this is a mime-type
if (type.includes('/')) {
if (type.includes("/")) {
if (!file.type) return false;
return file.type.match(type);
}
// otherwise this is likely an extension
if (type[0] === '.') {
return file.name.endsWith(type);
if (type[0] === ".") {
return file.name.toLowerCase().endsWith(type.toLowerCase());
}
return false;
});
@ -348,17 +454,20 @@ function check_restrictions(file) {
}
if (!is_correct_type) {
console.warn('File skipped because of invalid file type', file);
console.warn("File skipped because of invalid file type", file);
frappe.show_alert({
message: __('File "{0}" was skipped because of invalid file type', [file.name]),
indicator: 'orange'
indicator: "orange",
});
}
if (!valid_file_size) {
console.warn('File skipped because of invalid file size', file.size, file);
console.warn("File skipped because of invalid file size", file.size, file);
frappe.show_alert({
message: __('File "{0}" was skipped because size exceeds {1} MB', [file.name, max_file_size / (1024 * 1024)]),
indicator: 'orange'
message: __('File "{0}" was skipped because size exceeds {1} MB', [
file.name,
max_file_size / (1024 * 1024),
]),
indicator: "orange",
});
}
@ -374,17 +483,12 @@ function upload_files() {
if (props.as_dataurl) {
return return_as_dataurl();
}
return frappe.run_serially(
files.value.map(
(file, i) =>
() => upload_file(file, i)
)
);
return frappe.run_serially(files.value.map((file, i) => () => upload_file(file, i)));
}
function upload_via_file_browser() {
let selected_file = file_browser.value.selected_node;
if (!selected_file.value) {
frappe.msgprint(__('Click on a file to select it.'));
frappe.msgprint(__("Click on a file to select it."));
close_dialog.value = true;
return Promise.reject();
}
@ -396,23 +500,22 @@ function upload_via_file_browser() {
function upload_via_web_link() {
let file_url = web_link.value.url;
if (!file_url) {
frappe.msgprint(__('Invalid URL'));
frappe.msgprint(__("Invalid URL"));
close_dialog.value = true;
return Promise.reject();
}
file_url = decodeURI(file_url)
file_url = decodeURI(file_url);
close_dialog.value = true;
return upload_file({
file_url
file_url,
});
}
function return_as_dataurl() {
let promises = files.value.map(file =>
frappe.dom.file_to_base64(file.file_obj)
.then(dataurl => {
file.dataurl = dataurl;
props.on_success && props.on_success(file);
})
let promises = files.value.map((file) =>
frappe.dom.file_to_base64(file.file_obj).then((dataurl) => {
file.dataurl = dataurl;
props.on_success && props.on_success(file);
})
);
close_dialog.value = true;
return Promise.all(promises);
@ -422,23 +525,23 @@ function upload_file(file, i) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.upload.addEventListener('loadstart', (e) => {
xhr.upload.addEventListener("loadstart", (e) => {
file.uploading = true;
})
xhr.upload.addEventListener('progress', (e) => {
});
xhr.upload.addEventListener("progress", (e) => {
if (e.lengthComputable) {
file.progress = e.loaded;
file.total = e.total;
}
})
xhr.upload.addEventListener('load', (e) => {
});
xhr.upload.addEventListener("load", (e) => {
file.uploading = false;
resolve();
})
xhr.addEventListener('error', (e) => {
});
xhr.addEventListener("error", (e) => {
file.failed = true;
reject();
})
});
xhr.onreadystatechange = () => {
if (xhr.readyState == XMLHttpRequest.DONE) {
if (xhr.status === 200) {
@ -447,10 +550,10 @@ function upload_file(file, i) {
let file_doc = null;
try {
r = JSON.parse(xhr.responseText);
if (r.message.doctype === 'File') {
if (r.message.doctype === "File") {
file_doc = r.message;
}
} catch(e) {
} catch (e) {
r = xhr.responseText;
}
@ -460,14 +563,16 @@ function upload_file(file, i) {
props.on_success(file_doc, r);
}
if (i == files.value.length - 1 && files.value.every(file => file.request_succeeded)) {
if (
i == files.value.length - 1 &&
files.value.every((file) => file.request_succeeded)
) {
close_dialog.value = true;
}
} else if (xhr.status === 403) {
file.failed = true;
let response = JSON.parse(xhr.responseText);
file.error_message = `Not permitted. ${response._error_message || ''}.`;
file.error_message = `Not permitted. ${response._error_message || ""}.`;
try {
// Append server messages which are useful hint for perm issues
@ -475,73 +580,73 @@ function upload_file(file, i) {
server_messages.forEach((m) => {
m = JSON.parse(m);
file.error_message += `\n ${m.message} `
})
file.error_message += `\n ${m.message} `;
});
} catch (e) {
console.warning("Failed to parse server message", e)
console.warning("Failed to parse server message", e);
}
} else if (xhr.status === 413) {
file.failed = true;
file.error_message = 'Size exceeds the maximum allowed file size.';
file.error_message = "Size exceeds the maximum allowed file size.";
} else {
file.failed = true;
file.error_message = xhr.status === 0 ? 'XMLHttpRequest Error' : `${xhr.status} : ${xhr.statusText}`;
file.error_message =
xhr.status === 0
? "XMLHttpRequest Error"
: `${xhr.status} : ${xhr.statusText}`;
let error = null;
try {
error = JSON.parse(xhr.responseText);
} catch(e) {
} catch (e) {
// pass
}
frappe.request.cleanup({}, error);
}
}
}
xhr.open('POST', '/api/method/upload_file', true);
xhr.setRequestHeader('Accept', 'application/json');
xhr.setRequestHeader('X-Frappe-CSRF-Token', frappe.csrf_token);
};
xhr.open("POST", "/api/method/upload_file", true);
xhr.setRequestHeader("Accept", "application/json");
xhr.setRequestHeader("X-Frappe-CSRF-Token", frappe.csrf_token);
let form_data = new FormData();
if (file.file_obj) {
form_data.append('file', file.file_obj, file.name);
form_data.append("file", file.file_obj, file.name);
}
form_data.append('is_private', +file.private);
form_data.append('folder', props.folder);
form_data.append("is_private", +file.private);
form_data.append("folder", props.folder);
if (file.file_url) {
form_data.append('file_url', file.file_url);
form_data.append("file_url", file.file_url);
}
if (file.file_name) {
form_data.append('file_name', file.file_name);
form_data.append("file_name", file.file_name);
}
if (file.library_file_name) {
form_data.append('library_file_name', file.library_file_name);
form_data.append("library_file_name", file.library_file_name);
}
if (props.doctype && props.docname) {
form_data.append('doctype', props.doctype);
form_data.append('docname', props.docname);
form_data.append("doctype", props.doctype);
form_data.append("docname", props.docname);
}
if (props.fieldname) {
form_data.append('fieldname', props.fieldname);
form_data.append("fieldname", props.fieldname);
}
if (props.method) {
form_data.append('method', props.method);
form_data.append("method", props.method);
}
if (file.optimize) {
form_data.append('optimize', true);
form_data.append("optimize", true);
}
if (props.attach_doc_image) {
form_data.append('max_width', 200);
form_data.append('max_height', 200);
form_data.append("max_width", 200);
form_data.append("max_height", 200);
}
xhr.send(form_data);
@ -550,23 +655,23 @@ function upload_file(file, i) {
function capture_image() {
const capture = new frappe.ui.Capture({
animate: false,
error: true
error: true,
});
capture.show();
capture.submit(data_urls => {
data_urls.forEach(data_url => {
let filename = `capture_${frappe.datetime.now_datetime().replaceAll(/[: -]/g, '_')}.png`;
url_to_file(data_url, filename, 'image/png').then((file) =>
add_files([file])
);
capture.submit((data_urls) => {
data_urls.forEach((data_url) => {
let filename = `capture_${frappe.datetime
.now_datetime()
.replaceAll(/[: -]/g, "_")}.png`;
url_to_file(data_url, filename, "image/png").then((file) => add_files([file]));
});
});
}
function show_google_drive_picker() {
close_dialog.value = true;
let google_drive = new GoogleDrivePicker({
pickerCallback: data => google_drive_callback(data),
...google_drive_settings.value
pickerCallback: (data) => google_drive_callback(data),
...google_drive_settings.value,
});
google_drive.loadPicker();
}
@ -574,31 +679,36 @@ function google_drive_callback(data) {
if (data.action == google.picker.Action.PICKED) {
upload_file({
file_url: data.docs[0].url,
file_name: data.docs[0].name
file_name: data.docs[0].name,
});
} else if (data.action == google.picker.Action.CANCEL) {
cur_frm.attachments.new_attachment()
cur_frm.attachments.new_attachment();
}
}
function url_to_file(url, filename, mime_type) {
return fetch(url)
.then(res => res.arrayBuffer())
.then(buffer => new File([buffer], filename, { type: mime_type }));
.then((res) => res.arrayBuffer())
.then((buffer) => new File([buffer], filename, { type: mime_type }));
}
// computed
let upload_complete = computed(() => {
return files.value.length > 0
&& files.value.every(
file => file.total !== 0 && file.progress === file.total);
return (
files.value.length > 0 &&
files.value.every((file) => file.total !== 0 && file.progress === file.total)
);
});
// watcher
watch(files, (newvalue, oldvalue) => {
if (!props.allow_multiple && newvalue.length > 1) {
files.value = [newvalue[newvalue.length - 1]];
}
}, { deep: true });
watch(
files,
(newvalue, oldvalue) => {
if (!props.allow_multiple && newvalue.length > 1) {
files.value = [newvalue[newvalue.length - 1]];
}
},
{ deep: true }
);
defineExpose({
files,

View file

@ -13,7 +13,7 @@
:class="{
active: isNaN(aspect_ratio)
? isNaN(button.value)
: button.value === aspect_ratio
: button.value === aspect_ratio,
}"
:key="button.label"
@click="aspect_ratio = button.value"
@ -63,9 +63,9 @@ function crop_image() {
props.file.crop_box_data = cropper.value.getData();
const canvas = cropper.value.getCroppedCanvas();
const file_type = props.file.file_obj.type;
canvas.toBlob(blob => {
canvas.toBlob((blob) => {
var cropped_file_obj = new File([blob], props.file.name, {
type: blob.type
type: blob.type,
});
props.file.file_obj = cropped_file_obj;
emit("toggle_image_cropper");
@ -87,7 +87,7 @@ onMounted(() => {
scalable: false,
viewMode: 1,
data: crop_box,
aspectRatio: aspect_ratio.value
aspectRatio: aspect_ratio.value,
});
window.cropper = cropper.value;
};
@ -98,30 +98,33 @@ let aspect_ratio_buttons = computed(() => {
return [
{
label: __("1:1"),
value: 1
value: 1,
},
{
label: __("4:3"),
value: 4 / 3
value: 4 / 3,
},
{
label: __("16:9"),
value: 16 / 9
value: 16 / 9,
},
{
label: __("Free"),
value: NaN
}
value: NaN,
},
];
});
// watcher
watch(aspect_ratio, (value) => {
if (cropper.value) {
cropper.value.setAspectRatio(value);
}
}, { deep: true });
watch(
aspect_ratio,
(value) => {
if (cropper.value) {
cropper.value.setAspectRatio(value);
}
},
{ deep: true }
);
</script>
<style scoped>

View file

@ -4,7 +4,7 @@
:stroke-dasharray="circumference + ' ' + circumference"
:style="{
stroke: secondary,
strokeDashoffset: 0
strokeDashoffset: 0,
}"
:stroke-width="stroke"
fill="transparent"
@ -16,7 +16,7 @@
:stroke-dasharray="circumference + ' ' + circumference"
:style="{
stroke: primary,
strokeDashoffset: strokeDashoffset
strokeDashoffset: strokeDashoffset,
}"
:stroke-width="stroke"
fill="transparent"
@ -32,7 +32,7 @@
:style="{
color: 'var(--text-color)',
fontSize: 'var(--text-xs)',
fontWeight: 'var(--text-bold)'
fontWeight: 'var(--text-bold)',
}"
>
{{ progress }}%
@ -49,7 +49,7 @@ const props = defineProps({
secondary: String,
radius: Number,
progress: Number,
stroke: Number
stroke: Number,
});
// variables
@ -60,7 +60,6 @@ let circumference = ref(normalizedRadius.value * 2 * Math.PI);
let strokeDashoffset = computed(() => {
return circumference.value - (props.progress / 100) * circumference.value;
});
</script>
<style scoped>

View file

@ -1,9 +1,7 @@
<template>
<div class="file-web-link margin-bottom">
<a href class="text-muted text-medium"
@click.prevent="emit('hide-web-link')"
>
{{ __('← Back to upload files') }}
<a href class="text-muted text-medium" @click.prevent="emit('hide-web-link')">
{{ __("← Back to upload files") }}
</a>
<div class="input-group">
<input
@ -11,7 +9,7 @@
class="form-control"
:placeholder="__('Attach a web link')"
v-model="url"
>
/>
</div>
</div>
</template>

View file

@ -1,17 +1,4 @@
frappe.ui.form.ControlFloat = class ControlFloat extends frappe.ui.form.ControlInt {
make_input() {
super.make_input();
const change_handler = (e) => {
if (this.change) this.change(e);
else {
let value = this.get_input_value();
this.parse_validate_and_set_in_model(value, e);
this.refresh();
}
};
// convert to number format on focusout since focus converts it to flt.
this.$input.on("focusout", change_handler);
}
parse(value) {
value = this.eval_expression(value);
return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision());

View file

@ -13,7 +13,6 @@ frappe.ui.form.ControlInt = class ControlInt extends frappe.ui.form.ControlData
.on("focus", function () {
setTimeout(function () {
if (!document.activeElement) return;
document.activeElement.value = me.validate(document.activeElement.value);
document.activeElement.select();
}, 100);
return false;
@ -24,10 +23,19 @@ frappe.ui.form.ControlInt = class ControlInt extends frappe.ui.form.ControlData
}
eval_expression(value) {
if (typeof value === "string") {
if (value.match(/^[0-9+\-/* ]+$/)) {
const parsed_components = value.match(/[^\d.,]+|[\d.,]+/g);
var parsed_value = value;
if (parsed_components !== null) {
parsed_value = parsed_components
.map((v) => {
return isNaN(parseFloat(v)) ? v : flt(v);
})
.join("");
}
if (parsed_value.match(/^[0-9+\-/*.() ]+$/)) {
// If it is a string containing operators
try {
return eval(value);
return eval(parsed_value);
} catch (e) {
// bad expression
return value;

View file

@ -107,6 +107,7 @@ frappe.ui.form.Form = class FrappeForm {
// 2 column layout
this.setup_std_layout();
this.setup_filters();
// client script must be called after "setup" - there are no fields_dict attached to the frm otherwise
this.script_manager = new frappe.ui.form.ScriptManager({
@ -272,6 +273,41 @@ frappe.ui.form.Form = class FrappeForm {
});
}
setup_filters() {
let fields_with_filters = frappe
.get_meta(this.doctype)
.fields.filter((field) => field.link_filters)
.map((field) => JSON.parse(field.link_filters));
if (fields_with_filters.length === 0) return;
fields_with_filters = this.parse_filters(fields_with_filters);
for (let link_field in fields_with_filters) {
const filters = fields_with_filters[link_field];
this.set_query(link_field, () => filters);
}
}
parse_filters(data) {
const parsed_data = {};
for (const d of data) {
for (const condition of d) {
let [doctype, field, operator, value] = condition;
doctype = doctype.fieldname;
if (!parsed_data[doctype]) {
parsed_data[doctype] = {
filters: {},
};
}
if (!parsed_data[doctype].filters[field]) {
parsed_data[doctype].filters[field] = [operator, value];
}
}
}
return parsed_data;
}
watch_model_updates() {
// watch model updates
var me = this;

View file

@ -1520,9 +1520,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
if (this.filter_area.is_being_edited()) {
return true;
}
// this is set when a bulk operation is called from a list view which might update the list view
// this is to avoid the list view from refreshing a lot of times
// the list view is updated once after the bulk operation is complete
// this flag is left for backward compatibility, there's no need to prevent realtime
// refresh. They are by default debounced now and there's no way to bypass that.
if (this.disable_list_update) {
return true;
}
@ -1673,7 +1672,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
}
},
standard: true,
shortcut: "Ctrl+J",
shortcut: "Ctrl+Y",
});
}
@ -1724,7 +1723,6 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
get_workflow_action_menu_items() {
const workflow_actions = [];
const me = this;
if (frappe.model.has_workflow(this.doctype)) {
const actions = frappe.workflow.get_all_transition_actions(this.doctype);
@ -1733,16 +1731,11 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
label: __(action),
name: action,
action: () => {
me.disable_list_update = true;
frappe
.xcall("frappe.model.workflow.bulk_workflow_approval", {
docnames: this.get_checked_items(true),
doctype: this.doctype,
action: action,
})
.finally(() => {
me.disable_list_update = false;
});
frappe.xcall("frappe.model.workflow.bulk_workflow_approval", {
docnames: this.get_checked_items(true),
doctype: this.doctype,
action: action,
});
},
is_workflow_action: true,
});
@ -1803,9 +1796,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
return {
label: __("Assign To", null, "Button in list view actions menu"),
action: () => {
this.disable_list_update = true;
bulk_operations.assign(this.get_checked_items(true), () => {
this.disable_list_update = false;
this.clear_checked_items();
this.refresh();
});
@ -1818,9 +1809,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
return {
label: __("Apply Assignment Rule", null, "Button in list view actions menu"),
action: () => {
this.disable_list_update = true;
bulk_operations.apply_assignment_rule(this.get_checked_items(true), () => {
this.disable_list_update = false;
this.clear_checked_items();
this.refresh();
});
@ -1833,9 +1822,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
return {
label: __("Add Tags", null, "Button in list view actions menu"),
action: () => {
this.disable_list_update = true;
bulk_operations.add_tags(this.get_checked_items(true), () => {
this.disable_list_update = false;
this.clear_checked_items();
this.refresh();
});
@ -1872,9 +1859,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
);
}
frappe.confirm(message, () => {
this.disable_list_update = true;
bulk_operations.delete(docnames, () => {
this.disable_list_update = false;
this.clear_checked_items();
this.refresh();
});
@ -1897,9 +1882,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
"Title of confirmation dialog"
),
() => {
this.disable_list_update = true;
bulk_operations.submit_or_cancel(docnames, "cancel", () => {
this.disable_list_update = false;
this.clear_checked_items();
this.refresh();
});
@ -1924,9 +1907,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
"Title of confirmation dialog"
),
() => {
this.disable_list_update = true;
bulk_operations.submit_or_cancel(docnames, "submit", () => {
this.disable_list_update = false;
this.clear_checked_items();
this.refresh();
});
@ -1950,9 +1931,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
}
});
this.disable_list_update = true;
bulk_operations.edit(this.get_checked_items(true), field_mappings, () => {
this.disable_list_update = false;
this.refresh();
});
},

View file

@ -1,7 +1,7 @@
frappe.ui.FilterGroup = class {
constructor(opts) {
$.extend(this, opts);
this.filters = [];
this.filters = this.filters || [];
window.fltr = this;
if (!this.filter_button) {
this.wrapper = this.parent;
@ -239,6 +239,7 @@ frappe.ui.FilterGroup = class {
},
filter_list: this.base_list || this,
};
let filter = new frappe.ui.Filter(args);
this.filters.push(filter);
return filter;

View file

@ -600,40 +600,11 @@ frappe.search.utils = {
return { score, marked_string };
},
/**
* @deprecated Use frappe.search.utils.fuzzy_search(subseq, str, true).marked_string instead.
*/
bolden_match_part: function (str, subseq) {
if (fuzzy_match(subseq, str)[0] === false) {
return str;
}
if (str.indexOf(subseq) == 0) {
var tail = str.split(subseq)[1];
return "<mark>" + subseq + "</mark>" + tail;
}
var rendered = "";
var str_orig = str;
var str_len = str.length;
str = str.toLowerCase();
subseq = subseq.toLowerCase();
outer: for (var i = 0, j = 0; i < subseq.length; i++) {
var sub_ch = subseq.charCodeAt(i);
while (j < str_len) {
if (str.charCodeAt(j) === sub_ch) {
var str_char = str_orig.charAt(j);
if (str_char === str_char.toLowerCase()) {
rendered += "<mark>" + subseq.charAt(i) + "</mark>";
} else {
rendered += "<mark>" + subseq.charAt(i).toUpperCase() + "</mark>";
}
j++;
continue outer;
}
rendered += str_orig.charAt(j);
j++;
}
return str_orig;
}
rendered += str_orig.slice(j);
return rendered;
return this.fuzzy_search(subseq, str, true).marked_string;
},
get_executables(keywords) {

View file

@ -13,15 +13,14 @@ frappe.tags.utils = {
return [];
}
for (let i in frappe.tags.tags) {
let tag = frappe.tags.tags[i];
let level = frappe.search.utils.fuzzy_search(txt, tag);
if (level) {
frappe.tags.tags.forEach((tag) => {
const search_result = frappe.search.utils.fuzzy_search(txt, tag, true);
if (search_result.score) {
out.push({
type: "Tag",
label: __("#{0}", [frappe.search.utils.bolden_match_part(__(tag), txt)]),
label: __("#{0}", [search_result.marked_string]),
value: __("#{0}", [__(tag)]),
index: 1 + level,
index: 1 + search_result.score,
match: tag,
onclick() {
// Use Global Search Dialog for tag search too.
@ -29,8 +28,7 @@ frappe.tags.utils = {
},
});
}
}
});
return out;
},

View file

@ -346,7 +346,7 @@ frappe.views.Workspace = class Workspace {
) {
default_page = {
name: localStorage.current_page,
public: localStorage.is_current_page_public == "true",
public: localStorage.is_current_page_public != "false",
};
} else if (Object.keys(this.all_pages).length !== 0) {
default_page = { name: this.all_pages[0].title, public: this.all_pages[0].public };
@ -616,6 +616,8 @@ frappe.views.Workspace = class Workspace {
"options",
this.get_value() ? me.public_parent_pages : me.private_parent_pages
);
d.set_df_property("icon", "hidden", this.get_value() ? 0 : 1);
d.set_df_property("indicator_color", "hidden", this.get_value() ? 1 : 0);
},
},
{
@ -625,24 +627,23 @@ frappe.views.Workspace = class Workspace {
label: __("Icon"),
fieldtype: "Icon",
fieldname: "icon",
default: item.icon,
change: function () {
d.set_df_property("indicator_color", "hidden", this.get_value() ? 1 : 0);
},
default: item.public && item.icon,
hidden: !item.public,
},
{
label: __("Indicator color"),
fieldtype: "Select",
fieldname: "indicator_color",
options: this.indicator_colors,
default: item.indicator_color,
default: !item.public && item.indicator_color,
hidden: item.public,
},
],
primary_action_label: __("Update"),
primary_action: (values) => {
values.title = frappe.utils.escape_html(values.title);
let is_title_changed = values.title != old_item.title;
let is_section_changed = values.is_public != old_item.public;
let is_section_changed = Boolean(values.is_public) != Boolean(old_item.public);
if (
(is_title_changed || is_section_changed) &&
!this.validate_page(values, old_item)
@ -943,6 +944,8 @@ frappe.views.Workspace = class Workspace {
"options",
this.get_value() ? me.public_parent_pages : me.private_parent_pages
);
d.set_df_property("icon", "hidden", this.get_value() ? 0 : 1);
d.set_df_property("indicator_color", "hidden", this.get_value() ? 1 : 0);
},
},
{
@ -952,17 +955,16 @@ frappe.views.Workspace = class Workspace {
label: __("Icon"),
fieldtype: "Icon",
fieldname: "icon",
default: new_page.icon,
change: function () {
d.set_df_property("indicator_color", "hidden", this.get_value() ? 1 : 0);
},
default: new_page.public && new_page.icon,
hidden: !new_page.public,
},
{
label: __("Indicator color"),
fieldtype: "Select",
fieldname: "indicator_color",
options: this.indicator_colors,
default: new_page.indicator_color,
hidden: new_page.public,
default: !new_page.public && new_page.indicator_color,
},
],
primary_action_label: __("Duplicate"),
@ -1187,6 +1189,8 @@ frappe.views.Workspace = class Workspace {
"options",
this.get_value() ? me.public_parent_pages : me.private_parent_pages
);
d.set_df_property("icon", "hidden", this.get_value() ? 0 : 1);
d.set_df_property("indicator_color", "hidden", this.get_value() ? 1 : 0);
},
},
{
@ -1196,9 +1200,7 @@ frappe.views.Workspace = class Workspace {
label: __("Icon"),
fieldtype: "Icon",
fieldname: "icon",
change: function () {
d.set_df_property("indicator_color", "hidden", this.get_value() ? 1 : 0);
},
hidden: 1,
},
{
label: __("Indicator color"),

View file

@ -19,10 +19,7 @@
handle=".icon-drag"
>
<template #item="{ element }">
<div
class="mt-2 row align-center column-row"
v-for="column in df.table_columns"
>
<div class="mt-2 row align-center column-row" v-for="column in df.table_columns">
<div class="col-8">
<div class="column-label d-flex align-center">
<div class="px-2 icon-drag ml-n2">
@ -50,10 +47,7 @@
max="100"
step="5"
/>
<button
class="ml-2 btn btn-xs btn-icon"
@click="remove_column(column)"
>
<button class="ml-2 btn btn-xs btn-icon" @click="remove_column(column)">
<svg class="icon icon-sm">
<use href="#icon-close"></use>
</svg>
@ -74,7 +68,7 @@ const props = defineProps(["df"]);
// methods
function remove_column(column) {
props.df["table_columns"] = props.df.table_columns.filter(_column => _column !== column)
props.df["table_columns"] = props.df.table_columns.filter((_column) => _column !== column);
}
// computed
let help_message = computed(() => {

View file

@ -7,10 +7,7 @@
v-if="df.fieldtype == 'HTML' && df.html"
v-html="df.html"
></div>
<div
class="custom-html"
v-if="df.fieldtype == 'Field Template'"
>
<div class="custom-html" v-if="df.fieldtype == 'Field Template'">
{{ df.label }}
</div>
<input
@ -24,9 +21,7 @@
@blur="editing = false"
/>
<span v-else-if="df.label">{{ df.label }}</span>
<i class="text-muted" v-else>
{{ __("No Label") }} ({{ df.fieldname }})
</i>
<i class="text-muted" v-else> {{ __("No Label") }} ({{ df.fieldname }}) </i>
</div>
<div class="field-actions">
<button
@ -45,10 +40,7 @@
>
Configure columns
</button>
<button
class="btn btn-xs btn-icon"
@click="df['remove'] = true"
>
<button class="btn btn-xs btn-icon" @click="df['remove'] = true">
<svg class="icon icon-sm">
<use href="#icon-close"></use>
</svg>
@ -94,14 +86,14 @@ function edit_html() {
label: __("HTML"),
fieldname: "html",
fieldtype: "Code",
options: "HTML"
}
options: "HTML",
},
],
primary_action: ({ html }) => {
html = frappe.dom.remove_script_and_style(html);
props.df["html"] = html;
d.hide();
}
},
});
d.set_value("html", props.df.html);
d.show();
@ -112,7 +104,7 @@ function configure_columns() {
fields: [
{
fieldtype: "HTML",
fieldname: "columns_area"
fieldname: "columns_area",
},
{
label: "",
@ -130,8 +122,8 @@ function configure_columns() {
dialog.set_value("add_column", "");
}
}
}
}
},
},
],
on_page_show: () => {
createApp(ConfigureColumnsVue, { df: props.df }).mount(
@ -139,8 +131,8 @@ function configure_columns() {
);
},
on_hide: () => {
props.df["table_columns"] = props.df.table_columns.filter(col => !col.invalid_width);
}
props.df["table_columns"] = props.df.table_columns.filter((col) => !col.invalid_width);
},
});
dialog.show();
}
@ -149,18 +141,18 @@ function get_all_columns() {
let more_columns = [
{
label: __("Sr No."),
value: "idx"
}
value: "idx",
},
];
return more_columns.concat(
meta.fields
.map(tf => {
.map((tf) => {
if (frappe.model.no_value_type.includes(tf.fieldtype)) {
return;
}
return {
label: tf.label,
value: tf.fieldname
value: tf.fieldname,
};
})
.filter(Boolean)
@ -172,8 +164,8 @@ function get_column_to_add(fieldname) {
label: __("Sr No."),
fieldtype: "Data",
fieldname: "idx",
width: 10
}
width: 10,
},
};
if (fieldname in standard_columns) {
@ -182,7 +174,7 @@ function get_column_to_add(fieldname) {
return {
...frappe.meta.get_docfield(props.df.options, fieldname),
width: 10
width: 10,
};
}
function validate_table_columns() {
@ -214,7 +206,6 @@ watch(
() => validate_table_columns(),
{ deep: true }
);
</script>
<style scoped>

View file

@ -1,10 +1,7 @@
<template>
<div class="html-editor">
<div class="d-flex justify-content-end">
<button
class="btn btn-default btn-xs btn-edit"
@click="toggle_edit"
>
<button class="btn btn-default btn-xs btn-edit" @click="toggle_edit">
{{ !editing ? buttonLabel : __("Done") }}
</button>
</div>
@ -46,9 +43,9 @@ function toggle_edit() {
max_lines: 30,
change: () => {
emit("change", get_value());
}
},
},
render_input: true
render_input: true,
});
}
control.value.set_value(props.value);

View file

@ -13,11 +13,7 @@
type="button"
class="btn btn-xs"
@click="letterhead.align = direction"
:class="
letterhead.align == direction
? 'btn-secondary'
: 'btn-default'
"
:class="letterhead.align == direction ? 'btn-secondary' : 'btn-default'"
>
{{ direction }}
</button>
@ -30,12 +26,7 @@
min="20"
:max="range_input_field === 'image_width' ? 700 : 500"
:value="letterhead[range_input_field]"
@input="
e =>
(letterhead[range_input_field] = parseFloat(
e.target.value
))
"
@input="(e) => (letterhead[range_input_field] = parseFloat(e.target.value))"
/>
</div>
<div>
@ -58,11 +49,7 @@
class="ml-2 btn btn-default btn-xs btn-edit"
@click="toggle_edit_letterhead"
>
{{
!store.edit_letterhead
? __("Edit Letter Head")
: __("Done")
}}
{{ !store.edit_letterhead ? __("Edit Letter Head") : __("Done") }}
</button>
<button
v-if="!letterhead"
@ -73,10 +60,7 @@
</button>
</div>
</div>
<div
v-if="letterhead && !store.edit_letterhead"
v-html="letterhead.content"
></div>
<div v-if="letterhead && !store.edit_letterhead" v-html="letterhead.content"></div>
<!-- <div v-show="letterhead && store.edit_letterhead" ref="editor"></div> -->
<div
class="edit-letterhead"
@ -85,8 +69,8 @@
justifyContent: {
Left: 'flex-start',
Center: 'center',
Right: 'flex-end'
}[letterhead.align]
Right: 'flex-end',
}[letterhead.align],
}"
>
<div class="edit-image">
@ -101,7 +85,7 @@
height:
range_input_field === 'image_height'
? letterhead.image_height + 'px'
: null
: null,
}"
/>
</div>
@ -143,15 +127,15 @@ function toggle_edit_letterhead() {
change: () => {
letterhead.value._dirty = true;
letterhead.value.content = control.value.get_value();
}
},
},
render_input: true,
only_input: true,
no_wrapper: true
no_wrapper: true,
});
}
control.value.set_value(letterhead.value.content);
};
}
function change_letterhead() {
let d = new frappe.ui.Dialog({
title: __("Change Letter Head"),
@ -160,62 +144,52 @@ function change_letterhead() {
label: __("Letter Head"),
fieldname: "letterhead",
fieldtype: "Link",
options: "Letter Head"
}
options: "Letter Head",
},
],
primary_action: ({ letterhead }) => {
if (letterhead) {
set_letterhead(letterhead);
}
d.hide();
}
},
});
d.show();
};
}
function upload_image() {
new frappe.ui.FileUploader({
folder: "Home/Attachments",
on_success: file_doc => {
get_image_dimensions(file_doc.file_url).then(
({ width, height }) => {
letterhead.value["image"] = file_doc.file_url;
let new_width = width;
let new_height = height;
aspect_ratio.value = width / height;
range_input_field.value =
aspect_ratio.value > 1
? "image_width"
: "image_height";
on_success: (file_doc) => {
get_image_dimensions(file_doc.file_url).then(({ width, height }) => {
letterhead.value["image"] = file_doc.file_url;
let new_width = width;
let new_height = height;
aspect_ratio.value = width / height;
range_input_field.value = aspect_ratio.value > 1 ? "image_width" : "image_height";
if (width > 200) {
new_width = 200;
new_height = new_width / aspect_ratio.value;
}
if (height > 80) {
new_height = 80;
new_width = aspect_ratio.value * new_height;
}
letterhead.value["image_height"] = new_height;
letterhead.value["image_width"] = new_width;
if (width > 200) {
new_width = 200;
new_height = new_width / aspect_ratio.value;
}
);
}
if (height > 80) {
new_height = 80;
new_width = aspect_ratio.value * new_height;
}
letterhead.value["image_height"] = new_height;
letterhead.value["image_width"] = new_width;
});
},
});
};
}
function set_letterhead(_letterhead) {
store.value.change_letterhead(_letterhead).then(() => {
get_image_dimensions(letterhead.value.image).then(
({ width, height }) => {
aspect_ratio.value = width / height;
range_input_field.value =
aspect_ratio.value > 1
? "image_width"
: "image_height";
}
);
get_image_dimensions(letterhead.value.image).then(({ width, height }) => {
aspect_ratio.value = width / height;
range_input_field.value = aspect_ratio.value > 1 ? "image_width" : "image_height";
});
});
};
}
function create_letterhead() {
let d = new frappe.ui.Dialog({
title: __("Create Letter Head"),
@ -223,23 +197,23 @@ function create_letterhead() {
{
label: __("Letter Head Name"),
fieldname: "name",
fieldtype: "Data"
}
fieldtype: "Data",
},
],
primary_action: ({ name }) => {
return frappe.db
.insert({
doctype: "Letter Head",
letter_head_name: name,
source: "Image"
source: "Image",
})
.then(doc => {
.then((doc) => {
d.hide();
store.value.change_letterhead(doc.name).then(() => {
toggle_edit_letterhead();
});
});
}
},
});
d.show();
}
@ -249,34 +223,33 @@ onMounted(() => {
set_letterhead(frappe.boot.sysdefaults.letter_head);
}
watch(() => {
return letterhead.value
? letterhead.value[range_input_field.value]
: null;
}, () => {
if (aspect_ratio.value === null) return;
watch(
() => {
return letterhead.value ? letterhead.value[range_input_field.value] : null;
},
() => {
if (aspect_ratio.value === null) return;
let update_field =
range_input_field.value == "image_width"
? "image_height"
: "image_width";
letterhead.value[update_field] =
update_field == "image_width"
? aspect_ratio.value * letterhead.value.image_height
: letterhead.value.image_width / aspect_ratio.value;
});
let update_field =
range_input_field.value == "image_width" ? "image_height" : "image_width";
letterhead.value[update_field] =
update_field == "image_width"
? aspect_ratio.value * letterhead.value.image_height
: letterhead.value.image_width / aspect_ratio.value;
}
);
});
// watch
watch(letterhead, () => {
if (!letterhead.value) return;
if (letterhead.value.image_width && letterhead.value.image_height) {
let dimension =
letterhead.value.image_width > letterhead.value.image_height
? "width"
: "height";
let dimension_value = letterhead.value["image_" + dimension];
letterhead.value.content = `
watch(
letterhead,
() => {
if (!letterhead.value) return;
if (letterhead.value.image_width && letterhead.value.image_height) {
let dimension =
letterhead.value.image_width > letterhead.value.image_height ? "width" : "height";
let dimension_value = letterhead.value["image_" + dimension];
letterhead.value.content = `
<div style="text-align: ${letterhead.value.align.toLowerCase()};">
<img
src="${letterhead.value.image}"
@ -285,8 +258,11 @@ watch(letterhead, () => {
style="${dimension}: ${dimension_value}px;">
</div>
`;
}
}, { deep: true }, { immediate: true });
}
},
{ deep: true },
{ immediate: true }
);
</script>
<style scoped>

View file

@ -59,7 +59,7 @@ function refresh() {
iframe.value?.contentWindow.location.reload();
}
function get_default_docname() {
return frappe.db.get_list(doctype.value, { limit: 1 }).then(doc => {
return frappe.db.get_list(doctype.value, { limit: 1 }).then((doc) => {
return doc.length > 0 ? doc[0].name : null;
});
}
@ -78,9 +78,7 @@ let url = computed(() => {
params.append("letterhead", store.value.letterhead.name);
}
let _url =
type.value == "PDF"
? `/api/method/frappe.utils.weasyprint.download_pdf`
: "/printpreview";
type.value == "PDF" ? `/api/method/frappe.utils.weasyprint.download_pdf` : "/printpreview";
return `${_url}?${params.toString()}`;
});
@ -95,9 +93,9 @@ onMounted(() => {
options: doctype.value,
change: () => {
docname.value = doc_select.value.get_value();
}
},
},
render_input: true
render_input: true,
});
preview_type.value = frappe.ui.form.make_control({
parent: preview_type_ref.value,
@ -108,12 +106,12 @@ onMounted(() => {
options: ["PDF", "HTML"],
change: () => {
type.value = preview_type.value.get_value();
}
},
},
render_input: true
render_input: true,
});
preview_type.value.set_value(type.value);
get_default_docname().then(doc_name => {
get_default_docname().then((doc_name) => {
doc_name && doc_select.value.set_value(doc_name);
});
});

View file

@ -57,8 +57,8 @@ function add_section_above(section) {
label: "",
columns: [
{ label: "", fields: [] },
{ label: "", fields: [] }
]
{ label: "", fields: [] },
],
});
}
sections.push(_section);
@ -75,12 +75,12 @@ let rootStyles = computed(() => {
margin_top = 0,
margin_bottom = 0,
margin_left = 0,
margin_right = 0
margin_right = 0,
} = print_format.value;
return {
padding: `${margin_top}mm ${margin_right}mm ${margin_bottom}mm ${margin_left}mm`,
width: "210mm",
minHeight: "297mm"
minHeight: "297mm",
};
});
let page_number_style = computed(() => {
@ -89,7 +89,7 @@ let page_number_style = computed(() => {
background: "white",
padding: "4px",
borderRadius: "var(--border-radius)",
border: "1px solid var(--border-color)"
border: "1px solid var(--border-color)",
};
if (print_format.value.page_number.includes("Top")) {
style.top = print_format.value.margin_top / 2 + "mm";

View file

@ -27,14 +27,12 @@ let show_preview = ref(false);
// computed
let $store = computed(() => {
return getStore(props.print_format_name)
return getStore(props.print_format_name);
});
let shouldRender = computed(() => {
return Boolean(
$store.value.print_format.value &&
$store.value.meta.value &&
$store.value.layout.value
$store.value.print_format.value && $store.value.meta.value && $store.value.layout.value
);
});

View file

@ -4,11 +4,7 @@
<div class="sidebar-menu">
<div class="sidebar-label">{{ __("Page Margins") }}</div>
<div class="margin-controls">
<div
class="form-group"
v-for="df in margins"
:key="df.fieldname"
>
<div class="form-group" v-for="df in margins" :key="df.fieldname">
<div class="clearfix">
<label class="control-label">
{{ df.label }}
@ -21,13 +17,7 @@
class="form-control form-control-sm"
:value="print_format[df.fieldname]"
min="0"
@change="
e =>
update_margin(
df.fieldname,
e.target.value
)
"
@change="(e) => update_margin(df.fieldname, e.target.value)"
/>
</div>
</div>
@ -43,10 +33,7 @@
class="form-control form-control-sm"
v-model="print_format.font"
>
<option
v-for="font in google_fonts"
:value="font"
>
<option v-for="font in google_fonts" :value="font">
{{ font }}
</option>
</select>
@ -65,10 +52,7 @@
placeholder="12, 13, 14"
:value="print_format.font_size"
@change="
e =>
(print_format.font_size = parseFloat(
e.target.value
))
(e) => (print_format.font_size = parseFloat(e.target.value))
"
/>
</div>
@ -112,10 +96,7 @@
item-key="id"
>
<template #item="{ element }">
<div
class="field"
:title="element.fieldname"
>
<div class="field" :title="element.fieldname">
{{ element.label }}
</div>
</template>
@ -157,7 +138,7 @@ function clone_field(df) {
"options",
"table_columns",
"html",
"field_template"
"field_template",
]);
if (cloned.custom) {
// generate unique fieldnames for custom blocks
@ -171,16 +152,14 @@ let margins = computed(() => {
return [
{ label: __("Top"), fieldname: "margin_top" },
{ label: __("Bottom"), fieldname: "margin_bottom" },
{ label: __("Left", null, 'alignment'), fieldname: "margin_left" },
{ label: __("Right", null, 'alignment'), fieldname: "margin_right" }
{ label: __("Left", null, "alignment"), fieldname: "margin_left" },
{ label: __("Right", null, "alignment"), fieldname: "margin_right" },
];
});
let fields = computed(() => {
let fields = meta.value.fields
.filter(df => {
if (
["Section Break", "Column Break"].includes(df.fieldtype)
) {
.filter((df) => {
if (["Section Break", "Column Break"].includes(df.fieldtype)) {
return false;
}
if (search_text.value) {
@ -195,12 +174,12 @@ let fields = computed(() => {
return true;
}
})
.map(df => {
.map((df) => {
let out = {
label: df.label,
fieldname: df.fieldname,
fieldtype: df.fieldtype,
options: df.options
options: df.options,
};
if (df.fieldtype == "Table") {
out.table_columns = get_table_columns(df);
@ -214,27 +193,27 @@ let fields = computed(() => {
fieldname: "custom_html",
fieldtype: "HTML",
html: "",
custom: 1
custom: 1,
},
{
label: __("ID (name)"),
fieldname: "name",
fieldtype: "Data"
fieldtype: "Data",
},
{
label: __("Spacer"),
fieldname: "spacer",
fieldtype: "Spacer",
custom: 1
custom: 1,
},
{
label: __("Divider"),
fieldname: "divider",
fieldtype: "Divider",
custom: 1
custom: 1,
},
...print_templates.value,
...fields
...fields,
];
});
let print_templates = computed(() => {
@ -243,21 +222,18 @@ let print_templates = computed(() => {
for (let template of templates) {
let df;
if (template.field) {
df = frappe.meta.get_docfield(
meta.value.name,
template.field
);
df = frappe.meta.get_docfield(meta.value.name, template.field);
} else {
df = {
label: template.name,
fieldname: frappe.scrub(template.name)
fieldname: frappe.scrub(template.name),
};
}
out.push({
label: `${__(df.label)} (${__("Field Template")})`,
fieldname: df.fieldname + "_template",
fieldtype: "Field Template",
field_template: template.name
field_template: template.name,
});
}
return out;
@ -270,7 +246,7 @@ let page_number_positions = computed(() => {
{ label: __("Top Right"), value: "Top Right" },
{ label: __("Bottom Left"), value: "Bottom Left" },
{ label: __("Bottom Center"), value: "Bottom Center" },
{ label: __("Bottom Right"), value: "Bottom Right" }
{ label: __("Bottom Right"), value: "Bottom Right" },
];
});
@ -278,7 +254,7 @@ let page_number_positions = computed(() => {
onMounted(() => {
let method =
"frappe.printing.page.print_format_builder_beta.print_format_builder_beta.get_google_fonts";
frappe.call(method).then(r => {
frappe.call(method).then((r) => {
google_fonts.value = r.message || [];
if (!google_fonts.value.includes(print_format.value.font)) {
google_fonts.value.push(print_format.value.font);

View file

@ -28,10 +28,7 @@
<use href="#icon-dot-horizontal"></use>
</svg>
</button>
<div
class="dropdown-menu dropdown-menu-right"
role="menu"
>
<div class="dropdown-menu dropdown-menu-right" role="menu">
<button
v-for="option in section_options"
class="dropdown-item"
@ -44,17 +41,11 @@
</div>
</div>
<div class="row section-columns">
<div
class="column col"
v-for="(column, i) in section.columns"
:key="i"
>
<div class="column col" v-for="(column, i) in section.columns" :key="i">
<draggable
class="drag-container"
:style="{
backgroundColor: column.fields.length
? null
: 'var(--gray-50)'
backgroundColor: column.fields.length ? null : 'var(--gray-50)',
}"
v-model="column.fields"
group="fields"
@ -68,10 +59,7 @@
</div>
</div>
</div>
<div
class="my-4 text-center text-muted font-italic"
v-if="section.page_break"
>
<div class="my-4 text-center text-muted font-italic" v-if="section.page_break">
{{ __("Page Break") }}
</div>
</div>
@ -93,7 +81,7 @@ function add_column() {
if (props.section.columns.length < 4) {
props.section.columns.push({
label: "",
fields: []
fields: [],
});
}
}
@ -121,46 +109,50 @@ let section_options = computed(() => {
return [
{
label: __("Add section above"),
action: () => emit("add_section_above")
action: () => emit("add_section_above"),
},
{
label: __("Add column"),
action: add_column,
condition: () => props.section.columns.length < 4
condition: () => props.section.columns.length < 4,
},
{
label: __("Remove column"),
action: remove_column,
condition: () => props.section.columns.length > 1
condition: () => props.section.columns.length > 1,
},
{
label: __("Add page break"),
action: add_page_break,
condition: () => !props.section.page_break
condition: () => !props.section.page_break,
},
{
label: __("Remove page break"),
action: remove_page_break,
condition: () => props.section.page_break
condition: () => props.section.page_break,
},
{
label: __("Remove section"),
action: () => { props.section["remove"] = true }
action: () => {
props.section["remove"] = true;
},
},
{
label: __("Field Orientation (Left-Right)"),
condition: () => !props.section.field_orientation,
action: () => { props.section["field_orientation"] = "left-right" }
action: () => {
props.section["field_orientation"] = "left-right";
},
},
{
label: __("Field Orientation (Top-Down)"),
condition: () =>
props.section.field_orientation == "left-right",
action: () => { props.section["field_orientation"] = "" }
}
].filter(option => (option.condition ? option.condition() : true));
})
condition: () => props.section.field_orientation == "left-right",
action: () => {
props.section["field_orientation"] = "";
},
},
].filter((option) => (option.condition ? option.condition() : true));
});
</script>
<style scoped>

View file

@ -128,6 +128,7 @@ onConnect((edge) => {
data: {
action: "",
allowed: "All",
allow_self_approval: 1,
from: source_node.data.state,
to: target_node.data.state,
from_id: source_node.id,

View file

@ -6,8 +6,8 @@ import { useStore } from "../store";
const props = defineProps({
node: {
type: Object,
required: true
}
required: true,
},
});
const isValidConnection = ({ source, target }) => {
@ -26,21 +26,25 @@ let store = useStore();
const { edges, findNode } = useVueFlow();
watch(
() => findNode(props.node.id)?.selected,
val => {
(val) => {
if (val) store.workflow.selected = props.node;
let connected_edges = edges.value.filter(
edge => edge.source === props.node.id || edge.target === props.node.id
(edge) => edge.source === props.node.id || edge.target === props.node.id
);
connected_edges.forEach(edge => edge.selected = val);
connected_edges.forEach((edge) => (edge.selected = val));
}
);
let label = computed(() => findNode(props.node.id)?.data?.action);
watch(() => props.node.data, () => {
store.ref_history.commit();
}, { deep: true });
watch(
() => props.node.data,
() => {
store.ref_history.commit();
},
{ deep: true }
);
</script>
<template>

View file

@ -11,21 +11,21 @@ const props = defineProps({
targetPosition: { type: String, required: false },
sourceHandle: { type: Object, required: false },
targetHandle: { type: Object, required: false },
markerEnd: { type: String, required: false }
markerEnd: { type: String, required: false },
});
let opposite = {
left: "left",
right: "right",
top: "bottom",
bottom: "top"
bottom: "top",
};
const d = computed(() =>
getSmoothStepPath({
...props,
borderRadius: 30,
targetPosition: opposite[props.targetPosition]
targetPosition: opposite[props.targetPosition],
})
);
</script>

View file

@ -9,4 +9,4 @@ $alert-types: info, success, warning, danger;
border: none;
}
}
}
}

View file

@ -8,15 +8,15 @@
clip: rect(0, 0, 0, 0);
}
&> input {
& > input {
display: block;
}
&> ul:empty {
& > ul:empty {
display: none;
}
&> ul {
& > ul {
position: absolute;
width: 100%;
list-style: none;
@ -33,7 +33,7 @@
z-index: 4;
min-width: 250px;
&> li {
& > li {
cursor: pointer;
@include get_textstyle("sm", "regular");
padding: var(--padding-sm);
@ -54,12 +54,13 @@
}
}
&> li .link-option {
& > li .link-option {
font-weight: normal;
color: var(--text-color);
}
&> li:hover, &> li[aria-selected=true] {
& > li:hover,
& > li[aria-selected="true"] {
background-color: var(--awesomplete-hover-bg);
color: var(--text-color);
}
@ -68,4 +69,4 @@
text-decoration: none;
}
}
}
}

View file

@ -24,7 +24,8 @@
height: var(--btn-height);
padding: 0px;
@extend .center-content;
&.btn-default, &.btn-secondary {
&.btn-default,
&.btn-secondary {
min-width: 28px;
}
}
@ -41,7 +42,9 @@
);
color: $white;
&:hover, &:active, &:focus {
&:hover,
&:active,
&:focus {
color: $white;
}
.icon {
@ -61,7 +64,8 @@
);
color: var(--primary);
&:hover, &:active {
&:hover,
&:active {
color: var(--primary);
}
@ -73,7 +77,8 @@
.btn.btn-secondary {
background-color: var(--control-bg);
color: var(--text-color);
&:hover, &:active {
&:hover,
&:active {
background-color: var(--btn-default-hover-bg);
color: var(--text-color);
}
@ -82,7 +87,8 @@
.btn.btn-default {
background-color: var(--control-bg);
color: var(--text-color);
&:hover, &:active {
&:hover,
&:active {
background: var(--btn-default-hover-bg);
color: var(--text-color);
}
@ -120,7 +126,7 @@
box-shadow: none;
}
.btn-primary:active {
color: var(--gray-900) !important;
background-color: var(--invert-neutral) !important;
color: var(--gray-900) !important;
background-color: var(--invert-neutral) !important;
}
}

View file

@ -19,7 +19,8 @@
cursor: pointer;
}
.color-selector, .hue-selector {
.color-selector,
.hue-selector {
width: 12px;
height: 12px;
background: transparent;
@ -27,11 +28,12 @@
border-radius: 50%;
/* box-shadow: 0 0 0 1px gray, 0 0 0 3px white, 0 0 0 4px gray; */
border: 1px solid rgba(0, 0, 0, 0.2);
&::before, &::after {
&::before,
&::after {
position: absolute;
background-color: transparent;
border: 1px solid rgba(0, 0, 0, 0.2);
content: ' ';
content: " ";
border-radius: 50%;
}
@ -74,8 +76,8 @@
position: relative;
background: linear-gradient(
90deg,
hsl(0, 100%, 50%),
hsl(60, 100%, 50%),
hsl(0, 100%, 50%),
hsl(60, 100%, 50%),
hsl(120, 100%, 50%),
hsl(180, 100%, 50%),
hsl(240, 100%, 50%),
@ -92,7 +94,7 @@
}
}
.frappe-control[data-fieldtype='Color'] {
.frappe-control[data-fieldtype="Color"] {
input {
padding-left: 38px;
}
@ -108,9 +110,9 @@
position: absolute;
top: 5px;
left: 8px;
content: ' ';
content: " ";
&.no-value {
background: url('/assets/frappe/images/color-circle.png');
background: url("/assets/frappe/images/color-circle.png");
background-size: contain;
}
}

View file

@ -125,7 +125,8 @@ select.form-control {
}
}
.selectable-item:hover, .selectable-item.highlighted {
.selectable-item:hover,
.selectable-item.highlighted {
background-color: var(--fg-hover-color);
}
@ -142,7 +143,7 @@ select.form-control {
.frappe-control {
@include get_textstyle("base", "regular");
.control-label.reqd:after {
content: ' *';
content: " *";
color: var(--red-400);
}
.help:empty {
@ -193,7 +194,7 @@ select.form-control {
}
}
.frappe-control:not([data-fieldtype='MultiSelectPills']):not([data-fieldtype='Table MultiSelect']) {
.frappe-control:not([data-fieldtype="MultiSelectPills"]):not([data-fieldtype="Table MultiSelect"]) {
&.has-error {
input {
border: 1px solid var(--error-border);
@ -205,8 +206,8 @@ select.form-control {
}
}
.frappe-control[data-fieldtype='MultiSelectPills'],
.frappe-control[data-fieldtype='Table MultiSelect'] {
.frappe-control[data-fieldtype="MultiSelectPills"],
.frappe-control[data-fieldtype="Table MultiSelect"] {
&.has-error {
.control-input {
border: 1px solid var(--error-border);
@ -257,7 +258,8 @@ select.form-control {
}
/* progress bar */
.progress, .progress-bar {
.progress,
.progress-bar {
box-shadow: none;
}
@ -361,7 +363,8 @@ textarea.form-control {
overflow-wrap: break-word;
}
.frappe-control[data-fieldtype="Data"] .control-input, .control-value {
.frappe-control[data-fieldtype="Data"] .control-input,
.control-value {
position: relative;
}
@ -372,26 +375,30 @@ textarea.form-control {
padding: 3px;
}
.markdown-preview, .html-preview {
.markdown-preview,
.html-preview {
padding: var(--padding-md);
min-height: 300px;
max-height: 600px;
overflow: auto;
}
.markdown-toggle, .html-toggle {
.markdown-toggle,
.html-toggle {
margin-bottom: var(--margin-xs);
}
.barcode-scanner {
position: relative;
& > canvas, & > video {
& > canvas,
& > video {
max-width: 100%;
width: 100%;
}
canvas.drawing, canvas.drawingBuffer {
canvas.drawing,
canvas.drawingBuffer {
position: absolute;
left: 0;
top: 0;

View file

@ -19,12 +19,10 @@ $disabled-input-height: 22px;
--margin-xl: 30px;
--margin-2xl: 40px;
--modal-shadow: var(--shadow-md);
--card-shadow: var(--shadow-sm);
--btn-shadow: var(--shadow-xs);
// navbar
--navbar-height: 48px;
@ -50,8 +48,6 @@ $disabled-input-height: 22px;
--bg-pink: var(--pink-50);
--bg-cyan: var(--cyan-50);
--text-on-blue: var(--blue-700);
--text-on-light-blue: var(--blue-600);
--text-on-dark-blue: var(--blue-800);
@ -102,6 +98,9 @@ $disabled-input-height: 22px;
--btn-default-bg: var(--gray-100);
--btn-default-hover-bg: var(--gray-300);
// Border Colors
--border-primary: var(--gray-900);
// Other Colors
--sidebar-select-color: var(--gray-200);
@ -125,7 +124,6 @@ $disabled-input-height: 22px;
--code-block-bg: var(--gray-900);
--code-block-text: var(--gray-400);
--primary-color: var(--gray-900);
--btn-height: 28px;
@ -140,8 +138,11 @@ $disabled-input-height: 22px;
--checkbox-focus-shadow: 0 0 0 2px var(--gray-300);
--checkbox-gradient: linear-gradient(180deg, var(--primary) -124.51%, var(--primary) 100%);
--checkbox-disabled-gradient: linear-gradient(180deg, var(--disabled-control-bg) -124.51%, var(--disabled-control-bg) 100%);
--checkbox-disabled-gradient: linear-gradient(
180deg,
var(--disabled-control-bg) -124.51%,
var(--disabled-control-bg) 100%
);
// switch
--switch-bg: var(--gray-300);

View file

@ -20,11 +20,14 @@
&--nav {
border-bottom: 1px solid var(--border-color);
}
&--nav-title:hover, &--nav-action:hover {
&--nav-title:hover,
&--nav-action:hover {
background-color: var(--fg-hover-color);
}
&--time-current-hours, &--time-current-minutes, &--time-current-seconds {
&--time-current-hours,
&--time-current-minutes,
&--time-current-seconds {
font-family: inherit;
&:after {
color: var(--text-color);
@ -48,12 +51,14 @@
}
}
&.-range-from-, &.-range-to- {
&.-range-from-,
&.-range-to- {
border: 1px solid var(--border-color);
background: var(--date-range-bg);
}
&.-selected-, &.-current-.-selected- {
&.-selected-,
&.-current-.-selected- {
color: var(--date-active-text);
background: var(--date-active-bg);
border-radius: var(--border-radius-tiny);
@ -79,7 +84,8 @@
}
}
&--time, &--buttons {
&--time,
&--buttons {
border-top: 1px solid var(--border-color);
}
@ -109,4 +115,3 @@
}
}
}

View file

@ -24,7 +24,7 @@
}
.justify-between {
justify-content: space-between
justify-content: space-between;
}
.justify-flex-end {
@ -51,7 +51,8 @@
align-items: center;
}
.level-left, .level-right {
.level-left,
.level-right {
display: flex;
flex-basis: auto;
flex-grow: 0;
@ -84,5 +85,5 @@
}
.fill-width {
flex: 1
flex: 1;
}

View file

@ -27,4 +27,4 @@
font-size: var(--text-xl);
font-weight: 700;
color: var(--heading-color);
}
}

View file

@ -1,7 +1,8 @@
@import "../element/checkbox";
@import "../element/radio";
html, body {
html,
body {
height: 100%;
font-family: var(--font-stack);
font-variation-settings: "opsz" 24;
@ -21,7 +22,8 @@ html, body {
color: var(--text-color);
}
.input-area, .disp-area {
.input-area,
.disp-area {
display: flex;
}
}
@ -57,7 +59,6 @@ html, body {
-moz-appearance: none;
/* for Chrome */
-webkit-appearance: none;
}
.select-icon {
@ -134,7 +135,8 @@ html, body {
.btn-link {
box-shadow: none !important;
outline: none;
.icon, &:hover {
.icon,
&:hover {
text-decoration: none !important;
}
}

View file

@ -31,15 +31,17 @@
}
}
.grid-static-col, .row-check, .row-index {
.grid-static-col,
.row-check,
.row-index {
height: 32px;
padding: 6px 8px !important;
}
.grid-static-col {
padding: 6px 8px !important;
.static-area{
.static-area {
&.reqd:after {
content: ' *';
content: " *";
color: var(--red-400);
}
}
@ -69,7 +71,8 @@
}
// hide row index in 6/4 column child tables
.form-column.col-sm-6, .form-column.col-sm-4 {
.form-column.col-sm-6,
.form-column.col-sm-4 {
.form-grid {
.row-index {
display: none;
@ -98,7 +101,7 @@
.grid-body .data-row {
@include get_textstyle("sm", "regular");
color: var(--text-muted)
color: var(--text-muted);
}
.grid-empty,
@ -219,7 +222,7 @@
padding: 0px !important;
}
.frappe-control[data-fieldtype=Select].form-group .select-icon {
.frappe-control[data-fieldtype="Select"].form-group .select-icon {
top: 9px;
}
@ -287,7 +290,8 @@
margin-left: 1rem;
}
.grid-static-col[data-fieldtype="Code"], .grid-static-col[data-fieldtype="HTML Editor"] {
.grid-static-col[data-fieldtype="Code"],
.grid-static-col[data-fieldtype="HTML Editor"] {
overflow: hidden;
.static-area {
@ -450,7 +454,8 @@
}
}
.grid-buttons, .grid-bulk-actions {
.grid-buttons,
.grid-bulk-actions {
display: inline-flex;
}
@ -460,7 +465,8 @@
.grid-footer {
margin-top: var(--margin-sm);
}
.grid-footer, .grid-custom-buttons {
.grid-footer,
.grid-custom-buttons {
padding: var(--padding-sm) 0px;
background-color: var(--fg-color);
.btn {
@ -517,8 +523,7 @@
}
.grid-footer-toolbar {
padding: var(--padding-md) var(--padding-sm) var(--padding-xs)
var(--padding-sm);
padding: var(--padding-md) var(--padding-sm) var(--padding-xs) var(--padding-sm);
// border-top: 1px solid var(--border-color);
span {
margin-right: var(--margin-xs);
@ -565,10 +570,10 @@
@media (min-width: map-get($grid-breakpoints, "md")) {
.form-grid-container {
overflow-x: unset!important;
overflow-x: unset !important;
.form-grid {
position: unset!important;
position: unset !important;
}
}
}
@ -579,4 +584,3 @@
padding-right: 0 !important;
}
}

View file

@ -12,8 +12,8 @@
cursor: pointer;
/* Hide scrollbar for IE, Edge and Firefox */
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
/* Hide scrollbar for Chrome, Safari and Opera */
&::-webkit-scrollbar {
@ -32,7 +32,7 @@
.search-icons {
position: relative;
input[type='search'] {
input[type="search"] {
height: inherit;
padding-left: 30px;
}
@ -50,7 +50,7 @@
}
}
.frappe-control[data-fieldtype='Icon'] {
.frappe-control[data-fieldtype="Icon"] {
input {
padding-left: 40px;
}
@ -62,7 +62,7 @@
position: absolute;
top: calc(50% + 2px);
left: 8px;
content: ' ';
content: " ";
}
.like-disabled-input {
.icon-value {
@ -88,8 +88,9 @@
}
}
.dt-cell__edit, .filter-field {
.dt-cell__edit,
.filter-field {
.selected-icon {
top: 5px !important;
}
}
}

View file

@ -76,4 +76,4 @@ use.like-icon {
.no-stroke {
stroke: none;
}
}

View file

@ -7,7 +7,7 @@
}
.indicator::before {
content: '';
content: "";
height: 8px;
width: 8px;
border-radius: var(--border-radius);
@ -17,7 +17,7 @@
.indicator-pill-right {
@include get_textstyle("sm", "regular");
padding: 4.5px 8px;
border-radius: var( --border-radius-full);
border-radius: var(--border-radius-full);
height: 20px;
}
@ -31,7 +31,7 @@
.indicator-pill:not(.no-indicator-dot)::before,
.indicator-pill-right::after {
content:'';
content: "";
display: inline-table;
height: 6px;
width: 6px;
@ -48,7 +48,8 @@
margin: 0 0 0 4px;
}
$indicator-colors: green, cyan, blue, orange, yellow, gray, grey, red, pink, darkgrey, purple, light-blue;
$indicator-colors: green, cyan, blue, orange, yellow, gray, grey, red, pink, darkgrey, purple,
light-blue;
@each $color in $indicator-colors {
.indicator.#{"" + $color} {
&::before,
@ -72,11 +73,12 @@ $indicator-colors: green, cyan, blue, orange, yellow, gray, grey, red, pink, dar
}
}
.indicator.blink {
animation: blink 1s linear infinite;
}
@keyframes blink {
50% { opacity: 0.5; }
50% {
opacity: 0.5;
}
}

View file

@ -1,9 +1,4 @@
@mixin flex(
$dis: flex,
$x: center,
$y: center,
$dir: row
) {
@mixin flex($dis: flex, $x: center, $y: center, $dir: row) {
display: $dis;
justify-content: $x;
align-items: $y;
@ -43,7 +38,7 @@
$top: 0,
$left: 0,
$background-color: var(--bg-color),
$border-radius: var(--border-radius),
$border-radius: var(--border-radius)
) {
// Deprecated: Does not work as expected anymore. Also, this never worked in Safari.
}
}

Some files were not shown because too many files have changed in this diff Show more