Merge branch 'develop' of github.com:frappe/frappe into at-queries

This commit is contained in:
Gavin D'souza 2021-07-28 16:00:46 +05:30
commit a02848849d
52 changed files with 708 additions and 2208 deletions

17
.github/semantic.yml vendored
View file

@ -11,3 +11,20 @@ allowRevertCommits: true
# For allowed PR types: https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json
# Tool Reference: https://github.com/zeke/semantic-pull-requests
# By default types specified in commitizen/conventional-commit-types is used.
# See: https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json
# You can override the valid types
types:
- BREAKING CHANGE
- feat
- fix
- docs
- style
- refactor
- perf
- test
- build
- ci
- chore
- revert

View file

@ -0,0 +1,14 @@
context('Navigation', () => {
before(() => {
cy.login();
cy.visit('/app/website');
});
it('Navigate to route with hash in document name', () => {
cy.insert_doc('ToDo', {'__newname': 'ABC#123', 'description': 'Test this', 'ignore_duplicate': true});
cy.visit('/app/todo/ABC#123');
cy.title().should('eq', 'Test this - ABC#123');
cy.get_field('description', 'Text Editor').contains('Test this');
cy.go('back');
cy.title().should('eq', 'Website');
});
});

View file

@ -8,6 +8,7 @@ let yargs = require("yargs");
let cliui = require("cliui")();
let chalk = require("chalk");
let html_plugin = require("./frappe-html");
let rtlcss = require('rtlcss');
let postCssPlugin = require("esbuild-plugin-postcss2").default;
let ignore_assets = require("./ignore-assets");
let sass_options = require("./sass_options");
@ -96,9 +97,9 @@ async function execute() {
await clean_dist_folders(APPS);
}
let result;
let results;
try {
result = await build_assets_for_apps(APPS, FILES_TO_BUILD);
results = await build_assets_for_apps(APPS, FILES_TO_BUILD);
} catch (e) {
log_error("There were some problems during build");
log();
@ -107,13 +108,15 @@ async function execute() {
}
if (!WATCH_MODE) {
log_built_assets(result.metafile);
log_built_assets(results);
console.timeEnd(TOTAL_BUILD_TIME);
log();
} else {
log("Watching for changes...");
}
return await write_assets_json(result.metafile);
for (const result of results) {
await write_assets_json(result.metafile);
}
}
function build_assets_for_apps(apps, files) {
@ -125,6 +128,8 @@ function build_assets_for_apps(apps, files) {
let output_path = assets_path;
let file_map = {};
let style_file_map = {};
let rtl_style_file_map = {};
for (let file of files) {
let relative_app_path = path.relative(apps_path, file);
let app = relative_app_path.split(path.sep)[0];
@ -140,19 +145,32 @@ function build_assets_for_apps(apps, files) {
}
output_name = path.join(app, "dist", output_name);
if (Object.keys(file_map).includes(output_name)) {
if (Object.keys(file_map).includes(output_name) || Object.keys(style_file_map).includes(output_name)) {
log_warn(
`Duplicate output file ${output_name} generated from ${file}`
);
}
file_map[output_name] = file;
if ([".css", ".scss", ".less", ".sass", ".styl"].includes(extension)) {
style_file_map[output_name] = file;
rtl_style_file_map[output_name.replace('/css/', '/css-rtl/')] = file;
} else {
file_map[output_name] = file;
}
}
return build_files({
let build = build_files({
files: file_map,
outdir: output_path
});
let style_build = build_style_files({
files: style_file_map,
outdir: output_path
});
let rtl_style_build = build_style_files({
files: rtl_style_file_map,
outdir: output_path,
rtl_style: true
});
return Promise.all([build, style_build, rtl_style_build]);
});
}
@ -203,7 +221,33 @@ function get_files_to_build(files) {
}
function build_files({ files, outdir }) {
return esbuild.build({
let build_plugins = [
html_plugin,
vue(),
];
return esbuild.build(get_build_options(files, outdir, build_plugins));
}
function build_style_files({ files, outdir, rtl_style=false }) {
let plugins = [];
if (rtl_style) {
plugins.push(rtlcss);
}
let build_plugins = [
ignore_assets,
postCssPlugin({
plugins: plugins,
sassOptions: sass_options
})
];
plugins.push(require("autoprefixer"));
return esbuild.build(get_build_options(files, outdir, build_plugins));
}
function get_build_options(files, outdir, plugins) {
return {
entryPoints: files,
entryNames: "[dir]/[name].[hash]",
outdir,
@ -217,17 +261,9 @@ function build_files({ files, outdir }) {
PRODUCTION ? "production" : "development"
)
},
plugins: [
html_plugin,
ignore_assets,
vue(),
postCssPlugin({
plugins: [require("autoprefixer")],
sassOptions: sass_options
})
],
plugins: plugins,
watch: get_watch_config()
});
};
}
function get_watch_config() {
@ -260,7 +296,8 @@ async function clean_dist_folders(apps) {
let public_path = get_public_path(app);
let paths = [
path.resolve(public_path, "dist", "js"),
path.resolve(public_path, "dist", "css")
path.resolve(public_path, "dist", "css"),
path.resolve(public_path, "dist", "css-rtl")
];
for (let target of paths) {
if (fs.existsSync(target)) {
@ -272,7 +309,11 @@ async function clean_dist_folders(apps) {
}
}
function log_built_assets(metafile) {
function log_built_assets(results) {
let outputs = {};
for (const result of results) {
outputs = Object.assign(outputs, result.metafile.outputs);
}
let column_widths = [60, 20];
cliui.div(
{
@ -287,9 +328,9 @@ function log_built_assets(metafile) {
cliui.div("");
let output_by_dist_path = {};
for (let outfile in metafile.outputs) {
for (let outfile in outputs) {
if (outfile.endsWith(".map")) continue;
let data = metafile.outputs[outfile];
let data = outputs[outfile];
outfile = path.resolve(outfile);
outfile = path.relative(assets_path, outfile);
let filename = path.basename(outfile);
@ -344,7 +385,11 @@ async function write_assets_json(metafile) {
let info = metafile.outputs[output];
let asset_path = "/" + path.relative(sites_path, output);
if (info.entryPoint) {
out[path.basename(info.entryPoint)] = asset_path;
let key = path.basename(info.entryPoint);
if (key.endsWith('.css') && asset_path.includes('/css-rtl/')) {
key = `rtl_${key}`;
}
out[key] = asset_path;
}
}
@ -483,4 +528,4 @@ function log_rebuilt_assets(prev_assets, new_assets) {
log(" " + filename);
}
log();
}
}

View file

@ -1,31 +1,58 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
import datetime
from frappe import _
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See LICENSE
from urllib.parse import quote
import frappe
import frappe.database
import frappe.utils
from frappe.utils import cint, flt, get_datetime, datetime, date_diff, today
import frappe.utils.user
from frappe import conf
from frappe.sessions import Session, clear_sessions, delete_session
from frappe.modules.patch_handler import check_session_stopped
from frappe.translate import get_lang_code
from frappe.utils.password import check_password, delete_login_failed_cache
from frappe import _, conf
from frappe.core.doctype.activity_log.activity_log import add_authentication_log
from frappe.twofactor import (should_run_2fa, authenticate_for_2factor,
confirm_otp_token, get_cached_user_pass)
from frappe.modules.patch_handler import check_session_stopped
from frappe.sessions import Session, clear_sessions, delete_session
from frappe.translate import get_language
from frappe.twofactor import authenticate_for_2factor, confirm_otp_token, get_cached_user_pass, should_run_2fa
from frappe.utils import cint, date_diff, datetime, get_datetime, today
from frappe.utils.password import check_password
from frappe.website.utils import get_home_page
from urllib.parse import quote
class HTTPRequest:
def __init__(self):
# Get Environment variables
self.domain = frappe.request.host
if self.domain and self.domain.startswith('www.'):
self.domain = self.domain[4:]
# set frappe.local.request_ip
self.set_request_ip()
# load cookies
self.set_cookies()
# set frappe.local.db
self.connect()
# login and start/resume user session
self.set_session()
# set request language
self.set_lang()
# match csrf token from current session
self.validate_csrf_token()
# write out latest cookies
frappe.local.cookie_manager.init_cookies()
# check session status
check_session_stopped()
@property
def domain(self):
if not getattr(self, "_domain", None):
self._domain = frappe.request.host
if self._domain and self._domain.startswith('www.'):
self._domain = self._domain[4:]
return self._domain
def set_request_ip(self):
if frappe.get_request_header('X-Forwarded-For'):
frappe.local.request_ip = (frappe.get_request_header('X-Forwarded-For').split(",")[0]).strip()
@ -35,37 +62,21 @@ class HTTPRequest:
else:
frappe.local.request_ip = '127.0.0.1'
# language
self.set_lang()
# load cookies
def set_cookies(self):
frappe.local.cookie_manager = CookieManager()
# set db
self.connect()
# login
def set_session(self):
frappe.local.login_manager = LoginManager()
if frappe.form_dict._lang:
lang = get_lang_code(frappe.form_dict._lang)
if lang:
frappe.local.lang = lang
self.validate_csrf_token()
# write out latest cookies
frappe.local.cookie_manager.init_cookies()
# check status
check_session_stopped()
def validate_csrf_token(self):
if frappe.local.request and frappe.local.request.method in ("POST", "PUT", "DELETE"):
if not frappe.local.session: return
if not frappe.local.session.data.csrf_token \
or frappe.local.session.data.device=="mobile" \
or frappe.conf.get('ignore_csrf', None):
if not frappe.local.session:
return
if (
not frappe.local.session.data.csrf_token
or frappe.local.session.data.device == "mobile"
or frappe.conf.get('ignore_csrf', None)
):
# not via boot
return
@ -79,17 +90,18 @@ class HTTPRequest:
frappe.throw(_("Invalid Request"), frappe.CSRFTokenError)
def set_lang(self):
from frappe.translate import guess_language
frappe.local.lang = guess_language()
frappe.local.lang = get_language()
def get_db_name(self):
"""get database name from conf"""
return conf.db_name
def connect(self, ac_name = None):
def connect(self):
"""connect to db, from ac_name or db_name"""
frappe.local.db = frappe.database.get_db(user = self.get_db_name(), \
password = getattr(conf, 'db_password', ''))
frappe.local.db = frappe.database.get_db(
user=self.get_db_name(),
password=getattr(conf, 'db_password', '')
)
class LoginManager:
def __init__(self):
@ -142,8 +154,9 @@ class LoginManager:
self.make_session()
self.setup_boot_cache()
self.set_user_info()
self.clear_preferred_language()
def get_user_info(self, resume=False):
def get_user_info(self):
self.info = frappe.db.get_value("User", self.user,
["user_type", "first_name", "last_name", "user_image"], as_dict=1)
@ -181,11 +194,13 @@ class LoginManager:
frappe.local.response["redirect_to"] = redirect_to
frappe.cache().hdel('redirect_after_login', self.user)
frappe.local.cookie_manager.set_cookie("full_name", self.full_name)
frappe.local.cookie_manager.set_cookie("user_id", self.user)
frappe.local.cookie_manager.set_cookie("user_image", self.info.user_image or "")
def clear_preferred_language(self):
frappe.local.cookie_manager.delete_cookie("preferred_language")
def make_session(self, resume=False):
# start session
frappe.local.session_obj = Session(user=self.user, resume=resume,

View file

@ -551,32 +551,16 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), profile=Fal
if coverage:
from coverage import Coverage
from frappe.coverage import STANDARD_INCLUSIONS, STANDARD_EXCLUSIONS, FRAPPE_EXCLUSIONS
# Generate coverage report only for app that is being tested
source_path = os.path.join(get_bench_path(), 'apps', app or 'frappe')
incl = [
'*.py',
]
omit = [
'*.js',
'*.xml',
'*.pyc',
'*.css',
'*.less',
'*.scss',
'*.vue',
'*.html',
'*/test_*',
'*/node_modules/*',
'*/doctype/*/*_dashboard.py',
'*/patches/*',
]
omit = STANDARD_EXCLUSIONS[:]
if not app or app == 'frappe':
omit.append('*/tests/*')
omit.append('*/commands/*')
omit.extend(FRAPPE_EXCLUSIONS)
cov = Coverage(source=[source_path], omit=omit, include=incl)
cov = Coverage(source=[source_path], omit=omit, include=STANDARD_INCLUSIONS)
cov.start()
ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests,
@ -786,7 +770,7 @@ def get_version(output):
"table": lambda: render_table(
[["App", "Version", "Branch", "Commit"]] +
[
[app_info.app, app_info.version, app_info.branch, app_info.commit]
[app_info.app, app_info.version, app_info.branch, app_info.commit]
for app_info in data
]
),

View file

@ -348,6 +348,7 @@ class TestDocType(unittest.TestCase):
dump_docs = json.dumps(docs.get('docs'))
cancel_all_linked_docs(dump_docs)
data_link_doc.cancel()
data_doc.name = '{}-CAN-0'.format(data_doc.name)
data_doc.load_from_db()
self.assertEqual(data_link_doc.docstatus, 2)
self.assertEqual(data_doc.docstatus, 2)
@ -371,7 +372,7 @@ class TestDocType(unittest.TestCase):
for data in link_doc.get('permissions'):
data.submit = 1
data.cancel = 1
link_doc.insert()
link_doc.insert(ignore_if_duplicate=True)
#create first parent doctype
test_doc_1 = new_doctype('Test Doctype 1')
@ -386,7 +387,7 @@ class TestDocType(unittest.TestCase):
for data in test_doc_1.get('permissions'):
data.submit = 1
data.cancel = 1
test_doc_1.insert()
test_doc_1.insert(ignore_if_duplicate=True)
#crete second parent doctype
doc = new_doctype('Test Doctype 2')
@ -401,7 +402,7 @@ class TestDocType(unittest.TestCase):
for data in link_doc.get('permissions'):
data.submit = 1
data.cancel = 1
doc.insert()
doc.insert(ignore_if_duplicate=True)
# create doctype data
data_link_doc_1 = frappe.new_doc('Test Linked Doctype 1')
@ -432,6 +433,7 @@ class TestDocType(unittest.TestCase):
# checking that doc for Test Doctype 2 is not canceled
self.assertRaises(frappe.LinkExistsError, data_link_doc_1.cancel)
data_doc_2.name = '{}-CAN-0'.format(data_doc_2.name)
data_doc.load_from_db()
data_doc_2.load_from_db()
self.assertEqual(data_link_doc_1.docstatus, 2)

35
frappe/coverage.py Normal file
View file

@ -0,0 +1,35 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See LICENSE
"""
frappe.coverage
~~~~~~~~~~~~~~~~
Coverage settings for frappe
"""
STANDARD_INCLUSIONS = ["*.py"]
STANDARD_EXCLUSIONS = [
'*.js',
'*.xml',
'*.pyc',
'*.css',
'*.less',
'*.scss',
'*.vue',
'*.html',
'*/test_*',
'*/node_modules/*',
'*/doctype/*/*_dashboard.py',
'*/patches/*',
]
FRAPPE_EXCLUSIONS = [
"*/tests/*",
"*/commands/*",
"*/frappe/change_log/*",
"*/frappe/exceptions*",
"*frappe/setup.py",
"*/doctype/*/*_dashboard.py",
"*/patches/*",
]

View file

@ -15,14 +15,14 @@ frappe.ui.form.on('Form Tour', {
frm.add_custom_button(__('Show Tour'), async () => {
const issingle = await check_if_single(frm.doc.reference_doctype);
let route_changed = null;
if (issingle) {
frappe.set_route('Form', frm.doc.reference_doctype);
route_changed = frappe.set_route('Form', frm.doc.reference_doctype);
} else {
const new_name = 'new-' + frappe.scrub(frm.doc.reference_doctype) + '-1';
frappe.set_route('Form', frm.doc.reference_doctype, new_name);
route_changed = frappe.set_route('Form', frm.doc.reference_doctype, 'new');
}
frappe.utils.sleep(500).then(() => {
route_changed.then(() => {
const tour_name = frm.doc.name;
cur_frm.tour
.init({ tour_name })

View file

@ -127,7 +127,7 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
'''
transformed_html = '''
<h3>Hi John</h3>
<p style="margin:5px 0 !important">This is a test email</p>
<p style="margin:1em 0 !important">This is a test email</p>
'''
self.assertTrue(transformed_html in inline_style_in_html(html))

View file

@ -5,7 +5,7 @@ import time
from frappe import _, msgprint, is_whitelisted
from frappe.utils import flt, cstr, now, get_datetime_str, file_lock, date_diff
from frappe.model.base_document import BaseDocument, get_controller
from frappe.model.naming import set_new_name
from frappe.model.naming import set_new_name, gen_new_name_for_cancelled_doc
from werkzeug.exceptions import NotFound, Forbidden
import hashlib, json
from frappe.model import optional_fields, table_fields
@ -708,7 +708,6 @@ class Document(BaseDocument):
else:
tmp = frappe.db.sql("""select modified, docstatus from `tab{0}`
where name = %s for update""".format(self.doctype), self.name, as_dict=True)
if not tmp:
frappe.throw(_("Record does not exist"))
else:
@ -919,8 +918,12 @@ class Document(BaseDocument):
@whitelist.__func__
def _cancel(self):
"""Cancel the document. Sets `docstatus` = 2, then saves."""
"""Cancel the document. Sets `docstatus` = 2, then saves.
"""
self.docstatus = 2
new_name = gen_new_name_for_cancelled_doc(self)
frappe.rename_doc(self.doctype, self.name, new_name, force=True, show_alert=False)
self.name = new_name
self.save()
@whitelist.__func__

View file

@ -28,7 +28,7 @@ def set_new_name(doc):
doc.name = None
if getattr(doc, "amended_from", None):
_set_amended_name(doc)
doc.name = _get_amended_name(doc)
return
elif getattr(doc.meta, "issingle", False):
@ -221,6 +221,15 @@ def revert_series_if_last(key, name, doc=None):
* prefix = #### and hashes = 2021 (hash doesn't exist)
* will search hash in key then accordingly get prefix = ""
"""
if hasattr(doc, 'amended_from'):
# do not revert if doc is amended, since cancelled docs still exist
if doc.docstatus != 2 and doc.amended_from:
return
# for first cancelled doc
if doc.docstatus == 2 and not doc.amended_from:
name, _ = NameParser.parse_docname(doc.name, sep='-CAN-')
if ".#" in key:
prefix, hashes = key.rsplit(".", 1)
if "#" not in hashes:
@ -303,16 +312,9 @@ def append_number_if_name_exists(doctype, value, fieldname="name", separator="-"
return value
def _set_amended_name(doc):
am_id = 1
am_prefix = doc.amended_from
if frappe.db.get_value(doc.doctype, doc.amended_from, "amended_from"):
am_id = cint(doc.amended_from.split("-")[-1]) + 1
am_prefix = "-".join(doc.amended_from.split("-")[:-1]) # except the last hyphen
doc.name = am_prefix + "-" + str(am_id)
return doc.name
def _get_amended_name(doc):
name, _ = NameParser(doc).parse_amended_from()
return name
def _field_autoname(autoname, doc, skip_slicing=None):
"""
@ -323,7 +325,6 @@ def _field_autoname(autoname, doc, skip_slicing=None):
name = (cstr(doc.get(fieldname)) or "").strip()
return name
def _prompt_autoname(autoname, doc):
"""
Generate a name using Prompt option. This simply means the user will have to set the name manually.
@ -354,3 +355,61 @@ def _format_autoname(autoname, doc):
name = re.sub(r"(\{[\w | #]+\})", get_param_value_for_match, autoname_value)
return name
class NameParser:
"""Parse document name and return all the parts of it.
NOTE: It handles cancellend and amended doc parsing for now. It can be expanded.
"""
def __init__(self, doc):
self.doc = doc
def parse_name(self):
if not hasattr(self.doc, "amended_from"):
return (self.doc.name, None, None)
#If document is cancelled document
if hasattr(self.doc, "amended_from") and self.doc.docstatus == 2:
return self.parse_docname(self.doc.name, sep='-CAN-')
return self.parse_docname(self.doc.name)
def parse_amended_from(self):
if not getattr(self.doc, 'amended_from', None):
return (None, None)
return self.parse_docname(self.doc.amended_from, '-CAN-')
@classmethod
def parse_docname(cls, name, sep='-'):
split_list = name.rsplit(sep, 1)
if len(split_list) == 1:
return (name, None)
return (split_list[0], split_list[1])
def get_cancelled_doc_latest_counter(tname, docname):
"""Get the latest counter used for cancelled docs of given docname.
"""
name_prefix = f'{docname}-CAN-'
rows = frappe.db.sql("""
select
name
from `tab{tname}`
where
name like %(name_prefix)s and docstatus=2
""".format(tname=tname), {'name_prefix': name_prefix+'%'}, as_dict=1)
if not rows:
return -1
return max([int(row.name.replace(name_prefix, '') or -1) for row in rows])
def gen_new_name_for_cancelled_doc(doc):
"""Generate a new name for cancelled document.
"""
if getattr(doc, "amended_from", None):
name, _ = NameParser(doc).parse_amended_from()
else:
name = doc.name
counter = get_cancelled_doc_latest_counter(doc.doctype, name)
return f'{name}-CAN-{counter+1}'

View file

@ -111,33 +111,16 @@ class ParallelTestRunner():
if self.with_coverage:
from coverage import Coverage
from frappe.utils import get_bench_path
from frappe.coverage import STANDARD_INCLUSIONS, STANDARD_EXCLUSIONS, FRAPPE_EXCLUSIONS
# Generate coverage report only for app that is being tested
source_path = os.path.join(get_bench_path(), 'apps', self.app)
incl = [
'*.py',
]
omit = [
'*.js',
'*.xml',
'*.pyc',
'*.css',
'*.less',
'*.scss',
'*.vue',
'*.pyc',
'*.html',
'*/test_*',
'*/node_modules/*',
'*/doctype/*/*_dashboard.py',
'*/patches/*',
]
omit = STANDARD_EXCLUSIONS[:]
if self.app == 'frappe':
omit.append('*/tests/*')
omit.append('*/commands/*')
omit.extend(FRAPPE_EXCLUSIONS)
self.coverage = Coverage(source=[source_path], omit=omit, include=incl)
self.coverage = Coverage(source=[source_path], omit=omit, include=STANDARD_INCLUSIONS)
self.coverage.start()
def save_coverage(self):

View file

@ -180,3 +180,4 @@ frappe.patches.v12_0.rename_uploaded_files_with_proper_name
frappe.patches.v13_0.queryreport_columns
frappe.patches.v13_0.jinja_hook
frappe.patches.v13_0.update_notification_channel_if_empty
frappe.patches.v13_0.rename_cancelled_docs

View file

@ -0,0 +1,27 @@
import frappe
from frappe.model.naming import NameParser
from frappe.model.rename_doc import rename_doc
def execute():
"""Rename already cancelled documents by adding `CAN-X` postfix instead of `-X`.
"""
for doctype in frappe.db.get_all('DocType'):
doctype = frappe.get_doc('DocType', doctype.name)
if doctype.is_submittable and frappe.db.table_exists(doctype.name):
cancelled_docs = frappe.db.get_all(doctype.name, ['amended_from', 'name'], {'docstatus':2})
for doc in cancelled_docs:
if '-CAN-' in doc.name:
continue
current_name = doc.name
if getattr(doc, "amended_from", None):
orig_name, counter = NameParser.parse_docname(doc.name)
else:
orig_name, counter = doc.name, 0
new_name = f'{orig_name}-CAN-{counter or 0}'
print(f"Renaming {doctype.name} record from {current_name} to {new_name}")
rename_doc(doctype.name, current_name, new_name, ignore_permissions=True, show_alert=False)
frappe.db.commit()

View file

@ -403,19 +403,14 @@ frappe.ui.form.PrintView = class {
setup_print_format_dom(out, $print_format) {
this.print_wrapper.find('.print-format-skeleton').remove();
let base_url = frappe.urllib.get_base_url();
let print_css = frappe.assets.bundled_asset('print.bundle.css');
let print_css = frappe.assets.bundled_asset('print.bundle.css', frappe.utils.is_rtl(this.lang_code));
this.$print_format_body.find('html').attr('dir', frappe.utils.is_rtl(this.lang_code) ? 'rtl': 'ltr');
this.$print_format_body.find('html').attr('lang', this.lang_code);
this.$print_format_body.find('head').html(
`<style type="text/css">${out.style}</style>
<link href="${base_url}${print_css}" rel="stylesheet">`
);
if (frappe.utils.is_rtl(this.lang_code)) {
let rtl_css = frappe.assets.bundled_asset('frappe-rtl.bundle.css');
this.$print_format_body.find('head').append(
`<link type="text/css" rel="stylesheet" href="${base_url}${rtl_css}"></link>`
);
}
this.$print_format_body.find('body').html(
`<div class="print-format print-format-preview">${out.html}</div>`
);

View file

@ -1,299 +0,0 @@
{
"css/frappe-web-b4.css": "public/scss/website.scss",
"css/frappe-chat-web.css": [
"public/css/font-awesome.css",
"public/css/octicons/octicons.css",
"public/less/chat.less"
],
"concat:js/moment-bundle.min.js": [
"node_modules/moment/min/moment-with-locales.min.js",
"node_modules/moment-timezone/builds/moment-timezone-with-data.min.js"
],
"js/chat.js": "public/js/frappe/chat.js",
"js/frappe-recorder.min.js": "public/js/frappe/recorder/recorder.js",
"js/checkout.min.js": "public/js/integrations/razorpay.js",
"js/frappe-web.min.js": [
"public/js/frappe/class.js",
"public/js/frappe/polyfill.js",
"public/js/lib/md5.min.js",
"public/js/frappe/provide.js",
"public/js/frappe/format.js",
"public/js/frappe/utils/number_format.js",
"public/js/frappe/utils/utils.js",
"public/js/frappe/utils/common.js",
"public/js/frappe/ui/messages.js",
"public/js/frappe/translate.js",
"public/js/frappe/utils/pretty_date.js",
"public/js/frappe/microtemplate.js",
"public/js/frappe/query_string.js",
"public/js/frappe/upload.js",
"public/js/frappe/model/meta.js",
"public/js/frappe/model/model.js",
"public/js/frappe/model/perm.js",
"website/js/website.js",
"public/js/frappe/socketio_client.js"
],
"js/bootstrap-4-web.min.js": "website/js/bootstrap-4.js",
"js/control.min.js": [
"node_modules/air-datepicker/dist/js/datepicker.min.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.cs.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.da.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.de.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.en.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.es.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.fi.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.fr.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.hu.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.nl.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.pl.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.pt-BR.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.pt.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.ro.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.sk.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.zh.js",
"public/js/frappe/ui/capture.js",
"public/js/frappe/form/controls/control.js"
],
"js/dialog.min.js": [
"public/js/frappe/dom.js",
"public/js/frappe/form/formatters.js",
"public/js/frappe/form/layout.js",
"public/js/frappe/ui/field_group.js",
"public/js/frappe/form/link_selector.js",
"public/js/frappe/form/multi_select_dialog.js",
"public/js/frappe/ui/dialog.js"
],
"css/desk.min.css": [
"public/js/lib/leaflet/leaflet.css",
"public/js/lib/leaflet/leaflet.draw.css",
"public/js/lib/leaflet/L.Control.Locate.css",
"public/js/lib/leaflet/easy-button.css",
"public/css/font-awesome.css",
"public/css/octicons/octicons.css",
"public/less/desk.less",
"public/less/module.less",
"public/less/mobile.less",
"public/less/controls.less",
"public/less/chat.less",
"public/css/fonts/inter/inter.css",
"node_modules/frappe-charts/dist/frappe-charts.min.css",
"node_modules/plyr/dist/plyr.css",
"public/scss/desk.scss"
],
"css/frappe-rtl.css": [
"public/css/bootstrap-rtl.css",
"public/css/desk-rtl.css",
"public/css/report-rtl.css"
],
"css/printview.css": [
"public/css/bootstrap.css",
"public/scss/print.scss"
],
"concat:js/libs.min.js": [
"public/js/lib/Sortable.min.js",
"public/js/lib/jquery/jquery.hotkeys.js",
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js",
"node_modules/vue/dist/vue.min.js",
"node_modules/moment/min/moment-with-locales.min.js",
"node_modules/moment-timezone/builds/moment-timezone-with-data.min.js",
"node_modules/socket.io-client/dist/socket.io.slim.js",
"node_modules/localforage/dist/localforage.min.js",
"public/js/lib/jSignature.min.js",
"public/js/lib/leaflet/leaflet.js",
"public/js/lib/leaflet/leaflet.draw.js",
"public/js/lib/leaflet/L.Control.Locate.js",
"public/js/lib/leaflet/easy-button.js"
],
"js/desk.min.js": [
"public/js/frappe/translate.js",
"public/js/frappe/class.js",
"public/js/frappe/polyfill.js",
"public/js/frappe/provide.js",
"public/js/frappe/assets.js",
"public/js/frappe/format.js",
"public/js/frappe/form/formatters.js",
"public/js/frappe/dom.js",
"public/js/frappe/ui/messages.js",
"public/js/frappe/ui/keyboard.js",
"public/js/frappe/ui/colors.js",
"public/js/frappe/ui/sidebar.js",
"public/js/frappe/ui/link_preview.js",
"public/js/frappe/request.js",
"public/js/frappe/socketio_client.js",
"public/js/frappe/utils/utils.js",
"public/js/frappe/event_emitter.js",
"public/js/frappe/router.js",
"public/js/frappe/router_history.js",
"public/js/frappe/defaults.js",
"public/js/frappe/roles_editor.js",
"public/js/frappe/module_editor.js",
"public/js/frappe/microtemplate.js",
"public/js/frappe/ui/page.html",
"public/js/frappe/ui/page.js",
"public/js/frappe/ui/slides.js",
"public/js/frappe/ui/onboarding_dialog.js",
"public/js/frappe/ui/find.js",
"public/js/frappe/ui/iconbar.js",
"public/js/frappe/form/layout.js",
"public/js/frappe/ui/field_group.js",
"public/js/frappe/form/link_selector.js",
"public/js/frappe/form/multi_select_dialog.js",
"public/js/frappe/ui/dialog.js",
"public/js/frappe/ui/capture.js",
"public/js/frappe/ui/app_icon.js",
"public/js/frappe/ui/theme_switcher.js",
"public/js/frappe/model/model.js",
"public/js/frappe/db.js",
"public/js/frappe/model/meta.js",
"public/js/frappe/model/sync.js",
"public/js/frappe/model/create_new.js",
"public/js/frappe/model/perm.js",
"public/js/frappe/model/workflow.js",
"public/js/frappe/model/user_settings.js",
"public/js/lib/md5.min.js",
"public/js/frappe/utils/user.js",
"public/js/frappe/utils/common.js",
"public/js/frappe/utils/urllib.js",
"public/js/frappe/utils/pretty_date.js",
"public/js/frappe/utils/tools.js",
"public/js/frappe/utils/datetime.js",
"public/js/frappe/utils/number_format.js",
"public/js/frappe/utils/help.js",
"public/js/frappe/utils/help_links.js",
"public/js/frappe/utils/address_and_contact.js",
"public/js/frappe/utils/preview_email.js",
"public/js/frappe/utils/file_manager.js",
"public/js/frappe/upload.js",
"public/js/frappe/ui/tree.js",
"public/js/frappe/views/container.js",
"public/js/frappe/views/breadcrumbs.js",
"public/js/frappe/views/factory.js",
"public/js/frappe/views/pageview.js",
"public/js/frappe/ui/toolbar/awesome_bar.js",
"public/js/frappe/ui/toolbar/energy_points_notifications.js",
"public/js/frappe/ui/notifications/notifications.js",
"public/js/frappe/ui/toolbar/search.js",
"public/js/frappe/ui/toolbar/tag_utils.js",
"public/js/frappe/ui/toolbar/search.html",
"public/js/frappe/ui/toolbar/search_utils.js",
"public/js/frappe/ui/toolbar/about.js",
"public/js/frappe/ui/toolbar/navbar.html",
"public/js/frappe/ui/toolbar/toolbar.js",
"public/js/frappe/ui/toolbar/notifications.js",
"public/js/frappe/views/communication.js",
"public/js/frappe/views/translation_manager.js",
"public/js/frappe/views/workspace/workspace.js",
"public/js/frappe/widgets/widget_group.js",
"public/js/frappe/ui/sort_selector.html",
"public/js/frappe/ui/sort_selector.js",
"public/js/frappe/change_log.html",
"public/js/frappe/ui/workspace_loading_skeleton.html",
"public/js/frappe/desk.js",
"public/js/frappe/query_string.js",
"public/js/frappe/ui/comment.js",
"public/js/frappe/chat.js",
"public/js/frappe/utils/energy_point_utils.js",
"public/js/frappe/utils/dashboard_utils.js",
"public/js/frappe/ui/chart.js",
"public/js/frappe/ui/datatable.js",
"public/js/frappe/ui/driver.js",
"public/js/frappe/ui/plyr.js",
"public/js/frappe/barcode_scanner/index.js"
],
"js/form.min.js": [
"public/js/frappe/form/templates/**.html",
"public/js/frappe/form/controls/control.js",
"public/js/frappe/views/formview.js",
"public/js/frappe/form/form.js",
"public/js/frappe/meta_tag.js"
],
"js/list.min.js": [
"public/js/frappe/ui/listing.html",
"public/js/frappe/model/indicator.js",
"public/js/frappe/ui/filters/filter.js",
"public/js/frappe/ui/filters/filter_list.js",
"public/js/frappe/ui/filters/field_select.js",
"public/js/frappe/ui/filters/edit_filter.html",
"public/js/frappe/ui/tags.js",
"public/js/frappe/ui/tag_editor.js",
"public/js/frappe/ui/like.js",
"public/js/frappe/ui/liked_by.html",
"public/html/print_template.html",
"public/js/frappe/list/base_list.js",
"public/js/frappe/list/list_view.js",
"public/js/frappe/list/list_factory.js",
"public/js/frappe/list/list_view_select.js",
"public/js/frappe/list/list_sidebar.js",
"public/js/frappe/list/list_sidebar.html",
"public/js/frappe/list/list_sidebar_stat.html",
"public/js/frappe/list/list_sidebar_group_by.js",
"public/js/frappe/list/list_view_permission_restrictions.html",
"public/js/frappe/views/gantt/gantt_view.js",
"public/js/frappe/views/calendar/calendar.js",
"public/js/frappe/views/dashboard/dashboard_view.js",
"public/js/frappe/views/image/image_view.js",
"public/js/frappe/views/map/map_view.js",
"public/js/frappe/views/kanban/kanban_view.js",
"public/js/frappe/views/inbox/inbox_view.js",
"public/js/frappe/views/file/file_view.js",
"public/js/frappe/views/treeview.js",
"public/js/frappe/views/interaction.js",
"public/js/frappe/views/image/image_view_item_row.html",
"public/js/frappe/views/image/photoswipe_dom.html",
"public/js/frappe/views/kanban/kanban_board.html",
"public/js/frappe/views/kanban/kanban_column.html",
"public/js/frappe/views/kanban/kanban_card.html"
],
"css/report.min.css": [
"node_modules/frappe-datatable/dist/frappe-datatable.css",
"public/css/tree_grid.css"
],
"js/report.min.js": [
"public/js/lib/clusterize.min.js",
"public/js/frappe/views/reports/report_factory.js",
"public/js/frappe/views/reports/report_view.js",
"public/js/frappe/views/reports/query_report.js",
"public/js/frappe/views/reports/print_grid.html",
"public/js/frappe/views/reports/print_tree.html",
"public/js/frappe/ui/group_by/group_by.html",
"public/js/frappe/ui/group_by/group_by.js",
"public/js/frappe/views/reports/report_utils.js"
],
"js/web_form.min.js": [
"public/js/frappe/utils/datetime.js",
"public/js/frappe/web_form/webform_script.js"
],
"css/web_form.css": [
"website/css/web_form.css",
"public/css/octicons/octicons.css",
"public/scss/controls.scss",
"node_modules/frappe-datatable/dist/frappe-datatable.css"
],
"css/email.css": "public/scss/email.scss",
"js/barcode_scanner.min.js": "public/js/frappe/barcode_scanner/quagga.js",
"js/user_profile_controller.min.js": "desk/page/user_profile/user_profile_controller.js",
"css/login.css": "public/scss/login.scss",
"js/data_import_tools.min.js": "public/js/frappe/data_import/index.js"
}

File diff suppressed because it is too large Load diff

View file

@ -1,118 +0,0 @@
.navbar .navbar-search-icon{
right: auto;
left: 24px;
}
.navbar > .container > .navbar-header{
float: right !important;
}
body[data-sidebar="0"] .navbar-home {
margin-left: auto !important;
margin-right: 15px !important;
}
.navbar-desk ~ ul > li {
float: right !important;
}
body.no-breadcrumbs .navbar .navbar-home:before {
margin-right: auto;
margin-left: 10px !important;
-ms-transform:rotate(180deg); /* Internet Explorer 9 */
-webkit-transform:rotate(180deg); /* Chrome, Safari, Opera */
transform:rotate(180deg); /* Standard syntax */
}
.layout-side-section .overlay-sidebar {
left: auto !important;
right: 0 !important;
}
.layout-side-section .overlay-sidebar.opened {
transform:translateX(0) !important;
}
.navbar-right {
float: left !important;
}
#navbar-breadcrumbs > li > a:before {
margin-right: auto;
margin-left: 10px;
top: 6px;
-ms-transform:rotate(180deg); /* Internet Explorer 9 */
-webkit-transform:rotate(180deg); /* Chrome, Safari, Opera */
transform:rotate(180deg); /* Standard syntax */
}
.case-wrapper {
float: right;
}
.link-btn {
right: auto;
left: 4px;
transform:rotate(180deg); /* Rotate icon*/
}
.sidebar-menu .badge {
right: auto;
left: 0px;
}
.indicator::before {
margin: 0 0 0 4px;
}
.pull-left {
float: right !important;
}
.grid-row > .row .col:last-child {
margin-right: auto;
margin-left: -10px;
}
.text-right {
text-align: left;
}
.list-row-head .octicon-heart {
margin-right: auto;
margin-left: 13px;
}
.list-id {
margin-left: 7px !important;
}
.avatar-small .avatar-sm {
margin-left: 5px;
margin-right: auto;
}
.list-row-right .list-row-modified {
margin-right: auto;
margin-left: 9px;
}
.list-comment-count {
text-align: right;
}
ul.tree-children {
padding-right: 20px;
padding-left: inherit !important;
}
.balance-area {
float: left !important;
}
.tree.opened::before, .tree-node.opened::before, .tree:last-child::after, .tree-node:last-child::after {
left: inherit !important;
right: 8px;
}
.tree.opened > .tree-children > .tree-node > .tree-link::before, .tree-node.opened > .tree-children > .tree-node > .tree-link::before {
left: inherit !important;
right: -11px;
}
.tree:last-child::after, .tree-node:last-child::after {
right: -13px !important;
}
.tree.opened::before {
left: auto !important;
right: 23px;
}
.results {
direction: ltr;
}
.data-table {
direction: ltr;
}
.section-header {
direction: ltr;
}
.ql-editor {
direction: rtl;
text-align: right;
}

View file

@ -1,15 +0,0 @@
.grid-report {
direction: ltr;
}
.page-form .awesomplete > ul {
left: auto;
}
.chart_area{
direction: ltr;
}
.grid-report .show-zero{
direction: rtl;
}

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="{{ lang }}" dir="{{ layout_direction }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
@ -7,7 +7,7 @@
<meta name="description" content="">
<meta name="author" content="">
<title>{{ title }}</title>
<link href="{{ base_url }}{{ frappe.assets.bundled_asset('print.bundle.css') }}" rel="stylesheet">
<link href="{{ base_url }}{{ frappe.assets.bundled_asset('print.bundle.css', frappe.utils.is_rtl(lang)) }}" rel="stylesheet">
<style>
{{ print_css }}
</style>

View file

@ -168,9 +168,13 @@ frappe.assets = {
}
},
bundled_asset(path) {
bundled_asset(path, is_rtl=null) {
if (!path.startsWith('/assets') && path.includes('.bundle.')) {
return frappe.boot.assets_json[path] || path;
if (path.endsWith('.css') && is_rtl) {
path = `rtl_${path}`;
}
path = frappe.boot.assets_json[path] || path;
return path;
}
return path;
}

View file

@ -64,8 +64,6 @@ frappe.Application = class Application {
}
});
this.set_rtl();
// page container
this.make_page_container();
this.set_route();
@ -489,16 +487,6 @@ frappe.Application = class Application {
}, 100);
}
set_rtl() {
if (frappe.utils.is_rtl()) {
var ls = document.createElement('link');
ls.rel="stylesheet";
ls.type = "text/css";
ls.href= frappe.assets.bundled_asset("frappe-rtl.bundle.css");
document.getElementsByTagName('head')[0].appendChild(ls);
$('body').addClass('frappe-rtl');
}
}
show_change_log() {
var me = this;

View file

@ -53,13 +53,15 @@ frappe.ui.form.ControlSignature = class ControlSignature extends frappe.ui.form.
this.img = $("<img class='img-responsive attach-image-display'>")
.appendTo(this.img_wrapper).toggle(false);
}
refresh_input(e) {
refresh_input() {
// signature dom is not ready
if (!this.body) return;
// prevent to load the second time
this.make_pad();
this.$wrapper.find(".control-input").toggle(false);
this.set_editable(this.get_status()=="Write");
this.load_pad();
if(this.get_status()=="Read") {
if (this.get_status() == "Read") {
$(this.disp_area).toggle(false);
}
}

View file

@ -770,32 +770,36 @@ frappe.ui.form.Form = class FrappeForm {
}
_cancel(btn, callback, on_error, skip_confirm) {
const me = this;
const cancel_doc = () => {
frappe.validated = true;
me.script_manager.trigger("before_cancel").then(() => {
this.script_manager.trigger("before_cancel").then(() => {
if (!frappe.validated) {
return me.handle_save_fail(btn, on_error);
return this.handle_save_fail(btn, on_error);
}
var after_cancel = function(r) {
const original_name = this.docname;
const after_cancel = (r) => {
if (r.exc) {
me.handle_save_fail(btn, on_error);
this.handle_save_fail(btn, on_error);
} else {
frappe.utils.play_sound("cancel");
me.refresh();
callback && callback();
me.script_manager.trigger("after_cancel");
this.script_manager.trigger("after_cancel");
frappe.run_serially([
() => this.rename_notify(this.doctype, original_name, r.docs[0].name),
() => frappe.router.clear_re_route(this.doctype, original_name),
() => this.refresh(),
]);
}
};
frappe.ui.form.save(me, "cancel", after_cancel, btn);
frappe.ui.form.save(this, "cancel", after_cancel, btn);
});
}
if (skip_confirm) {
cancel_doc();
} else {
frappe.confirm(__("Permanently Cancel {0}?", [this.docname]), cancel_doc, me.handle_save_fail(btn, on_error));
frappe.confirm(__("Permanently Cancel {0}?", [this.docname]), cancel_doc, this.handle_save_fail(btn, on_error));
}
};
@ -817,7 +821,7 @@ frappe.ui.form.Form = class FrappeForm {
'docname': this.doc.name
}).then(is_amended => {
if (is_amended) {
frappe.throw(__('This document is already amended, you cannot ammend it again'));
frappe.throw(__('This document is already amended, you cannot amend it again'));
}
this.validate_form_action("Amend");
var me = this;

View file

@ -2,8 +2,6 @@ frappe.ui.form.FormTour = class FormTour {
constructor({ frm }) {
this.frm = frm;
this.driver_steps = [];
this.init_driver();
}
init_driver() {
@ -37,11 +35,17 @@ frappe.ui.form.FormTour = class FormTour {
if (tour_name) {
this.tour = await frappe.db.get_doc('Form Tour', tour_name);
} else {
this.tour = { steps: frappe.tour[this.frm.doctype] };
const doctype_tour_exists = await frappe.db.exists('Form Tour', this.frm.doctype);
if (doctype_tour_exists) {
this.tour = await frappe.db.get_doc('Form Tour', this.frm.doctype);
} else {
this.tour = { steps: frappe.tour[this.frm.doctype] };
}
}
if (on_finish) this.on_finish = on_finish;
this.init_driver();
this.build_steps();
this.update_driver_steps();
}
@ -232,7 +236,7 @@ frappe.ui.form.FormTour = class FormTour {
}
add_step_to_save() {
const page_id = `#page-${this.frm.doctype}`;
const page_id = `[id="page-${this.frm.doctype}"]`;
const $save_btn = `${page_id} .standard-actions .primary-action`;
const save_step = {
element: $save_btn,
@ -244,6 +248,9 @@ frappe.ui.form.FormTour = class FormTour {
description: "",
position: "left",
doneBtnText: __("Save")
},
onNext: () => {
this.frm.save();
}
};
this.driver_steps.push(save_step);

View file

@ -119,6 +119,10 @@ frappe.render_grid = function(opts) {
// render HTML wrapper page
opts.base_url = frappe.urllib.get_base_url();
opts.print_css = frappe.boot.print_css;
opts.lang = opts.lang || frappe.boot.lang,
opts.layout_direction = opts.layout_direction || frappe.utils.is_rtl() ? "rtl" : "ltr";
var html = frappe.render_template("print_template", opts);
var w = window.open();

View file

@ -14,11 +14,10 @@ frappe.view_factories = [];
frappe.route_options = null;
frappe.route_hooks = {};
$(window).on('hashchange', function() {
$(window).on('hashchange', function(e) {
// v1 style routing, route is in hash
if (window.location.hash) {
if (window.location.hash && !frappe.router.is_app_route(e.currentTarget.pathname)) {
let sub_path = frappe.router.get_sub_path(window.location.hash);
window.location.hash = '';
frappe.router.push_state(sub_path);
return false;
}
@ -48,18 +47,18 @@ $('body').on('click', 'a', function(e) {
return;
}
if (href==='') {
if (href === '') {
return override('/app');
}
// target has "#" ,this is a v1 style route, so remake it.
if (e.currentTarget.hash) {
if (href.startsWith('#')) {
// target startswith "#", this is a v1 style route, so remake it.
return override(e.currentTarget.hash);
}
// target has "/app, this is a v2 style route.
if (e.currentTarget.pathname && frappe.router.is_app_route(e.currentTarget.pathname)) {
return override(e.currentTarget.pathname);
if (frappe.router.is_app_route(e.currentTarget.pathname)) {
// target has "/app, this is a v2 style route.
return override(e.currentTarget.pathname + e.currentTarget.hash);
}
});
@ -172,7 +171,7 @@ frappe.router = {
standard_route = ['List', doctype_route.doctype, frappe.utils.to_title_case(route[2])];
if (route[3]) {
// calendar / kanban / dashboard / folder name
standard_route.push(...route.splice(3, route.length));
standard_route.push([...route].splice(3, route.length));
}
}
return standard_route;
@ -235,6 +234,12 @@ frappe.router = {
}
},
clear_re_route(doctype, docname) {
delete frappe.re_route[
`${encodeURIComponent(frappe.router.slug(doctype))}/${encodeURIComponent(docname)}`
];
},
set_title(sub_path) {
if (frappe.route_titles[sub_path]) {
frappe.utils.set_title(frappe.route_titles[sub_path]);
@ -298,7 +303,7 @@ frappe.router = {
new_route = [this.slug(route[1]), 'view', route[2].toLowerCase()];
// calendar / inbox / file folder
if (route[3]) new_route.push(...route.slice(3, route.length));
if (route[3]) new_route.push([...route].slice(3, route.length));
} else {
if ($.isPlainObject(route[2])) {
frappe.route_options = route[2];
@ -349,8 +354,6 @@ frappe.router = {
push_state(url) {
// change the URL and call the router
if (window.location.pathname !== url) {
// cleanup any remenants of v1 routing
window.location.hash = '';
// push state so the browser looks fine
history.pushState(null, null, url);
@ -364,7 +367,11 @@ frappe.router = {
// return clean sub_path from hash or url
// supports both v1 and v2 routing
if (!route) {
route = window.location.hash || (window.location.pathname + window.location.search);
route = window.location.pathname + window.location.hash + window.location.search;
if (route.includes('app#')) {
// to support v1
route = window.location.hash;
}
}
return this.strip_prefix(route);

View file

@ -40,14 +40,16 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
this.catch_enter_as_submit();
}
$(this.wrapper).find('input, select').on('change', () => {
this.dirty = true;
frappe.run_serially([
() => frappe.timeout(0.1),
() => me.refresh_dependency()
]);
});
$(this.wrapper).find('input, select').on(
'change awesomplete-selectcomplete',
() => {
this.dirty = true;
frappe.run_serially([
() => frappe.timeout(0.1),
() => me.refresh_dependency()
]);
}
);
}
}

View file

@ -1264,7 +1264,9 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
print_css: print_css,
print_settings: print_settings,
landscape: landscape,
columns: columns
columns: columns,
lang: frappe.boot.lang,
layout_direction: frappe.utils.is_rtl() ? "rtl" : "ltr"
});
frappe.render_pdf(html, print_settings);

View file

@ -179,9 +179,20 @@
--text-on-pink: var(--pink-500);
--text-on-cyan: var(--cyan-600);
// Layout Colors
--bg-color: var(--gray-50);
--fg-color: white;
--navbar-bg: white;
--fg-hover-color: var(--gray-100);
--card-bg: var(--fg-color);
--disabled-text-color: var(--gray-700);
--disabled-control-bg: var(--gray-50);
--control-bg: var(--gray-100);
--control-bg-on-gray: var(--gray-200);
--awesomebar-focus-bg: var(--fg-color);
--modal-bg: white;
--toast-bg: var(--modal-bg);
--popover-bg: white;
--awesomplete-hover-bg: var(--control-bg);

View file

@ -4,7 +4,7 @@
background-color: white;
font-size: var(--text-sm);
}
/*! This comment will be included even in compressed mode. */
#navbar-breadcrumbs {
margin-left: var(--margin-md);
font-size: var(--text-sm);
@ -12,7 +12,7 @@
font-size: var(--text-md);
margin-right: 10px;
&:before {
content: var(--right-arrow-svg);
content: #{"/*!rtl:var(--left-arrow-svg);*/"}var(--right-arrow-svg);
display: inline-block;
margin-right: 10px;
}

View file

@ -25,20 +25,7 @@ $input-height: 28px !default;
--navbar-height: 60px;
// Layout Colors
--bg-color: var(--gray-50);
--fg-color: white;
--navbar-bg: white;
--fg-hover-color: var(--gray-100);
--card-bg: var(--fg-color);
--disabled-text-color: var(--gray-700);
--disabled-control-bg: var(--gray-50);
--control-bg: var(--gray-100);
--control-bg-on-gray: var(--gray-200);
--awesomebar-focus-bg: var(--fg-color);
--modal-bg: white;
--toast-bg: var(--modal-bg);
--popover-bg: white;
--appreciation-color: var(--dark-green-600);
--appreciation-bg: var(--dark-green-100);
@ -77,4 +64,6 @@ $input-height: 28px !default;
--skeleton-bg: var(--gray-100);
--right-arrow-svg: url("data: image/svg+xml;utf8, <svg width='6' height='8' viewBox='0 0 6 8' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M1.25 7.5L4.75 4L1.25 0.5' stroke='%231F272E' stroke-linecap='round' stroke-linejoin='round'/></svg>");
--left-arrow-svg: url("data: image/svg+xml;utf8, <svg width='6' height='8' viewBox='0 0 6 8' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M7.5 9.5L4 6l3.5-3.5' stroke='%231F272E' stroke-linecap='round' stroke-linejoin='round'></path></svg>");
}

View file

@ -157,4 +157,6 @@
--skeleton-bg: var(--gray-800);
--right-arrow-svg: url("data: image/svg+xml;utf8, <svg width='6' height='8' viewBox='0 0 6 8' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M1.25 7.5L4.75 4L1.25 0.5' stroke='white' stroke-linecap='round' stroke-linejoin='round'/></svg>");
--left-arrow-svg: url("data: image/svg+xml;utf8, <svg width='6' height='8' viewBox='0 0 6 8' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M7.5 9.5L4 6l3.5-3.5' stroke='white' stroke-linecap='round' stroke-linejoin='round'></path></svg>");
}

View file

@ -574,3 +574,15 @@ details > summary:focus {
// font-family: 'Octicons';
// content: "\f00b";
// }
/*rtl:raw:
.dropdown-menu {
right: auto;
}
.popover {
right: auto;
}
.chart-container {
direction: ltr;
}
*/

View file

@ -36,7 +36,13 @@ a {
}
p {
margin: 5px 0 !important;
margin: 1em 0 !important;
}
.with-container {
p {
margin: 5px 0 !important;
}
}
.ql-editor {

View file

@ -1,3 +0,0 @@
@import "frappe/public/css/bootstrap-rtl.css";
@import "frappe/public/css/desk-rtl.css";
@import "frappe/public/css/report-rtl.css";

View file

@ -1,8 +0,0 @@
#!/usr/bin/env python2.7
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
import os
import frappe
frappe.connect(site=os.environ.get("site"))

View file

@ -1,4 +1,4 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
"""
Boot session from cache or build
@ -248,7 +248,6 @@ class Session:
data = self.get_session_record()
if data:
# set language
self.data.update({'data': data, 'user':data.user, 'sid': self.sid})
self.user = data.user
validate_ip_address(self.user)

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang={{ lang }} dir={{ layout_direction }}>
<head>
<meta charset="utf-8">

View file

@ -4,8 +4,9 @@ import time
import unittest
import frappe
from frappe.auth import LoginAttemptTracker
from frappe.auth import HTTPRequest, LoginAttemptTracker
from frappe.frappeclient import FrappeClient, AuthError
from frappe.utils import set_request
class TestAuth(unittest.TestCase):
def __init__(self, *args, **kwargs):

View file

@ -116,3 +116,37 @@ class TestNaming(unittest.TestCase):
self.assertEqual(current_index.get('current'), 2)
frappe.db.sql("""delete from `tabSeries` where name = %s""", series)
def test_naming_for_cancelled_and_amended_doc(self):
submittable_doctype = frappe.get_doc({
"doctype": "DocType",
"module": "Core",
"custom": 1,
"is_submittable": 1,
"permissions": [{
"role": "System Manager",
"read": 1
}],
"name": 'Submittable Doctype'
}).insert(ignore_if_duplicate=True)
doc = frappe.new_doc('Submittable Doctype')
doc.save()
original_name = doc.name
doc.submit()
doc.cancel()
cancelled_name = doc.name
self.assertEqual(cancelled_name, "{}-CAN-0".format(original_name))
amended_doc = frappe.copy_doc(doc)
amended_doc.docstatus = 0
amended_doc.amended_from = doc.name
amended_doc.save()
self.assertEqual(amended_doc.name, original_name)
amended_doc.submit()
amended_doc.cancel()
self.assertEqual(amended_doc.name, "{}-CAN-1".format(original_name))
submittable_doctype.delete()

View file

@ -1,13 +1,26 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
import frappe, unittest, os
import os
import unittest
from random import choices
from unittest.mock import patch
import frappe
import frappe.translate
from frappe import _
from frappe.translate import get_language, get_parent_language
from frappe.utils import set_request
dirname = os.path.dirname(__file__)
translation_string_file = os.path.join(dirname, 'translation_test_file.txt')
first_lang, second_lang, third_lang, fourth_lang, fifth_lang = choices(
frappe.get_all("Language", pluck="name"), k=5
)
class TestTranslate(unittest.TestCase):
def tearDown(self):
frappe.form_dict.pop("_lang", None)
def test_extract_message_from_file(self):
data = frappe.translate.get_messages_from_file(translation_string_file)
self.assertListEqual(data, expected_output)
@ -20,6 +33,41 @@ class TestTranslate(unittest.TestCase):
finally:
frappe.local.lang = 'en'
def test_request_language_resolution_with_form_dict(self):
"""Test for frappe.translate.get_language
Case 1: frappe.form_dict._lang is set
"""
frappe.form_dict._lang = first_lang
with patch.object(frappe.translate, "get_preferred_language_cookie", return_value=second_lang):
return_val = get_language()
self.assertIn(return_val, [first_lang, get_parent_language(first_lang)])
def test_request_language_resolution_with_cookie(self):
"""Test for frappe.translate.get_language
Case 2: frappe.form_dict._lang is not set, but preferred_language cookie is
"""
with patch.object(frappe.translate, "get_preferred_language_cookie", return_value=second_lang):
set_request(method="POST", path="/", headers=[("Accept-Language", third_lang)])
return_val = get_language()
self.assertIn(return_val, [second_lang, get_parent_language(second_lang)])
def test_request_language_resolution_with_request_header(self):
"""Test for frappe.translate.get_language
Case 3: frappe.form_dict._lang & preferred_language cookie is not set, but Accept-Language header is
"""
set_request(method="POST", path="/", headers=[("Accept-Language", third_lang)])
return_val = get_language()
self.assertIn(return_val, [third_lang, get_parent_language(third_lang)])
expected_output = [
('apps/frappe/frappe/tests/translation_test_file.txt', 'Warning: Unable to find {0} in any table related to {1}', 'This is some context', 2),
('apps/frappe/frappe/tests/translation_test_file.txt', 'Warning: Unable to find {0} in any table related to {1}', None, 4),

View file

@ -1,4 +1,4 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
"""
frappe.translate
@ -11,71 +11,100 @@ import io
import itertools
import json
import operator
import functools
import os
import re
from csv import reader
from typing import List, Union, Tuple
import frappe
from frappe.model.utils import InvalidIncludePath, render_include
from frappe.utils import is_html, strip, strip_html_tags
from frappe.utils import get_bench_path, is_html, strip, strip_html_tags
def guess_language(lang_list=None):
"""Set `frappe.local.lang` from HTTP headers at beginning of request"""
user_preferred_language = frappe.request.cookies.get('preferred_language')
is_guest_user = not frappe.session.user or frappe.session.user == 'Guest'
if is_guest_user and user_preferred_language:
return user_preferred_language
def get_language(lang_list: List = None) -> str:
"""Set `frappe.local.lang` from HTTP headers at beginning of request
lang_codes = frappe.request.accept_languages.values()
if not lang_codes:
return frappe.local.lang
Order of priority for setting language:
1. Form Dict => _lang
2. Cookie => preferred_language
3. Request Header => Accept-Language
4. User document => language
5. System Settings => language
"""
guess = None
if not lang_list:
lang_list = get_all_languages() or []
# fetch language from form_dict
if frappe.form_dict._lang:
language = get_lang_code(
frappe.form_dict._lang or get_parent_language(frappe.form_dict._lang)
)
if language:
return language
for l in lang_codes:
code = l.strip()
if not isinstance(code, str):
code = str(code, 'utf-8')
if code in lang_list or code == "en":
guess = code
break
lang_set = set(lang_list or get_all_languages() or [])
# check if parent language (pt) is setup, if variant (pt-BR)
if "-" in code:
code = code.split("-")[0]
if code in lang_list:
guess = code
break
# fetch language from cookie
preferred_language_cookie = get_preferred_language_cookie()
return guess or frappe.local.lang
if preferred_language_cookie:
if preferred_language_cookie in lang_set:
return preferred_language_cookie
def get_user_lang(user=None):
parent_language = get_parent_language(language)
if parent_language in lang_set:
return parent_language
# fetch language from request headers
accept_language = list(frappe.request.accept_languages.values())
for language in accept_language:
if language in lang_set:
return language
parent_language = get_parent_language(language)
if parent_language in lang_set:
return parent_language
# fallback to language set in User or System Settings
return frappe.local.lang
@functools.lru_cache()
def get_parent_language(lang: str) -> str:
"""If the passed language is a variant, return its parent
Eg:
1. zh-TW -> zh
2. sr-BA -> sr
"""
is_language_variant = "-" in lang
if is_language_variant:
return lang[:lang.index("-")]
def get_user_lang(user: str = None) -> str:
"""Set frappe.local.lang from user preferences on session beginning or resumption"""
if not user:
user = frappe.session.user
# via cache
user = user or frappe.session.user
lang = frappe.cache().hget("lang", user)
if not lang:
# if defined in user profile
lang = frappe.db.get_value("User", user, "language")
if not lang:
lang = frappe.db.get_default("lang")
if not lang:
lang = frappe.local.lang or 'en'
# User.language => Session Defaults => frappe.local.lang => 'en'
lang = (
frappe.db.get_value("User", user, "language")
or frappe.db.get_default("lang")
or frappe.local.lang
or "en"
)
frappe.cache().hset("lang", user, lang)
return lang
def get_lang_code(lang):
return frappe.db.get_value('Language', {'language_name': lang}) or lang
def get_lang_code(lang: str) -> Union[str, None]:
return (
frappe.db.get_value("Language", {"name": lang})
or frappe.db.get_value("Language", {"language_name": lang})
)
def set_default_language(lang):
"""Set Global default language"""
@ -529,7 +558,7 @@ def get_all_messages_from_js_files(app_name=None):
return messages
def get_messages_from_file(path):
def get_messages_from_file(path: str) -> List[Tuple[str, str, str, str]]:
"""Returns a list of transatable strings from a code file
:param path: path of the code file
@ -542,7 +571,7 @@ def get_messages_from_file(path):
frappe.flags.scanned_files.append(path)
apps_path = get_bench_dir()
bench_path = get_bench_path()
if os.path.exists(path):
with open(path, 'r') as sourcefile:
try:
@ -550,11 +579,12 @@ def get_messages_from_file(path):
except Exception:
print("Could not scan file for translation: {0}".format(path))
return []
data = [(os.path.relpath(path, apps_path), message, context, line) \
for line, message, context in extract_messages_from_code(file_contents)]
return data
return [
(os.path.relpath(path, bench_path), message, context, line)
for (line, message, context) in extract_messages_from_code(file_contents)
]
else:
# print "Translate: {0} missing".format(os.path.abspath(path))
return []
def extract_messages_from_code(code):
@ -768,9 +798,6 @@ def deduplicate_messages(messages):
ret.append(next(g))
return ret
def get_bench_dir():
return os.path.join(frappe.__file__, '..', '..', '..', '..')
def rename_language(old_name, new_name):
if not frappe.db.exists('Language', new_name):
return
@ -879,3 +906,6 @@ def get_all_languages(with_language_name=False):
@frappe.whitelist(allow_guest=True)
def set_preferred_language_cookie(preferred_language):
frappe.local.cookie_manager.set_cookie("preferred_language", preferred_language)
def get_preferred_language_cookie():
return frappe.request.cookies.get("preferred_language")

View file

@ -73,17 +73,25 @@ def include_script(path):
return f'<script type="text/javascript" src="{path}"></script>'
def include_style(path):
def include_style(path, rtl=None):
path = bundled_asset(path)
return f'<link type="text/css" rel="stylesheet" href="{path}">'
def bundled_asset(path):
def bundled_asset(path, rtl=None):
from frappe.utils import get_assets_json
from frappe.website.utils import abs_url
if ".bundle." in path and not path.startswith("/assets"):
bundled_assets = get_assets_json()
if path.endswith('.css') and is_rtl(rtl):
path = f"rtl_{path}"
path = bundled_assets.get(path) or path
return abs_url(path)
def is_rtl(rtl=None):
from frappe import local
if rtl is None:
return local.lang in ["ar", "he", "fa", "ps"]
return rtl

View file

@ -13,7 +13,7 @@ from PyPDF2 import PdfFileReader, PdfFileWriter
import frappe
from frappe import _
from frappe.utils import scrub_urls
from frappe.utils.jinja_globals import bundled_asset
from frappe.utils.jinja_globals import bundled_asset, is_rtl
PDF_CONTENT_ERRORS = ["ContentNotFoundError", "ContentOperationNotPermittedError",
"UnknownContentError", "RemoteHostClosedError"]
@ -177,7 +177,9 @@ def prepare_header_footer(soup):
"content": content,
"styles": styles,
"html_id": html_id,
"css": css
"css": css,
"lang": frappe.local.lang,
"layout_direction": "rtl" if is_rtl else "ltr"
})
# create temp file

View file

@ -542,7 +542,7 @@ def get_form_data(doctype, docname=None, web_form_name=None):
# For Table fields, server-side processing for meta
for field in out.web_form.web_form_fields:
if field.fieldtype == "Table":
field.fields = get_in_list_view_fields(field.options)
field.fields = frappe.get_meta(field.options).fields
out.update({field.fieldname: field.fields})
if field.fieldtype == "Link":

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html data-theme="{{ desk_theme.lower() }}">
<html data-theme="{{ desk_theme.lower() }}" dir={{ layout_direction }} lang="{{ lang }}">
<head>
<!-- Chrome, Firefox OS and Opera -->
<meta name="theme-color" content="#0089FF">

View file

@ -6,6 +6,7 @@ import os, re
import frappe
from frappe import _
import frappe.sessions
from frappe.utils.jinja_globals import is_rtl
def get_context(context):
if frappe.session.user == "Guest":
@ -40,6 +41,8 @@ def get_context(context):
"build_version": frappe.utils.get_build_version(),
"include_js": hooks["app_include_js"],
"include_css": hooks["app_include_css"],
"layout_direction": "rtl" if is_rtl() else "ltr",
"lang": frappe.local.lang,
"sounds": hooks["sounds"],
"boot": boot if context.get("for_mobile") else boot_json,
"desk_theme": desk_theme or "Light",

View file

@ -1,14 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<html lang="{{ lang }}" dir="{{ layout_direction }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
<meta name="generator" content="frappe">
{{ include_style('print.bundle.css') }}
{%- if has_rtl -%}
{{ include_style('frappe-rtl.bundle.css') }}
{%- endif -%}
<style>
{{ css }}
</style>

View file

@ -7,6 +7,7 @@ from frappe import _
from frappe.modules import get_doc_path
from frappe.core.doctype.access_log.access_log import make_access_log
from frappe.utils import cint, sanitize_html, strip_html
from frappe.utils.jinja_globals import is_rtl
no_cache = 1
@ -44,7 +45,8 @@ def get_context(context):
"css": get_print_style(frappe.form_dict.style, print_format),
"comment": frappe.session.user,
"title": doc.get(meta.title_field) if meta.title_field else doc.name,
"has_rtl": True if frappe.local.lang in ["ar", "he", "fa", "ps"] else False
"lang": frappe.local.lang,
"layout_direction": "rtl" if is_rtl() else "ltr"
}
def get_print_format_doc(print_format_name, meta):

View file

@ -67,6 +67,7 @@
"fast-glob": "^3.2.5",
"launch-editor": "^2.2.1",
"md5": "^2.3.0",
"rtlcss": "^3.2.1",
"yargs": "^16.2.0"
},
"snyk": true

View file

@ -2413,6 +2413,14 @@ find-up@^3.0.0:
dependencies:
locate-path "^3.0.0"
find-up@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
dependencies:
locate-path "^6.0.0"
path-exists "^4.0.0"
follow-redirects@^1.10.0:
version "1.13.3"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267"
@ -3690,6 +3698,13 @@ locate-path@^3.0.0:
p-locate "^3.0.0"
path-exists "^3.0.0"
locate-path@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
dependencies:
p-locate "^5.0.0"
lodash.assign@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
@ -4222,6 +4237,11 @@ nanoid@^3.1.22:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844"
integrity sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ==
nanoid@^3.1.23:
version "3.1.23"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81"
integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==
native-request@^1.0.5:
version "1.0.8"
resolved "https://registry.yarnpkg.com/native-request/-/native-request-1.0.8.tgz#8f66bf606e0f7ea27c0e5995eb2f5d03e33ae6fb"
@ -4571,6 +4591,13 @@ p-limit@^2.0.0:
dependencies:
p-try "^2.0.0"
p-limit@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
dependencies:
yocto-queue "^0.1.0"
p-locate@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
@ -4578,6 +4605,13 @@ p-locate@^3.0.0:
dependencies:
p-limit "^2.0.0"
p-locate@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
dependencies:
p-limit "^3.0.2"
p-map@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
@ -4700,6 +4734,11 @@ path-exists@^3.0.0:
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
@ -5158,6 +5197,15 @@ postcss@^7.0.32:
source-map "^0.6.1"
supports-color "^6.1.0"
postcss@^8.2.4:
version "8.3.5"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.5.tgz#982216b113412bc20a86289e91eb994952a5b709"
integrity sha512-NxTuJocUhYGsMiMFHDUkmjSKT3EdH4/WbGF6GCi1NDGk+vbcUTun4fpbOqaPtD8IIsztA2ilZm2DhYCuyN58gA==
dependencies:
colorette "^1.2.2"
nanoid "^3.1.23"
source-map-js "^0.6.2"
prepend-http@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
@ -5840,6 +5888,17 @@ roarr@^2.15.3:
semver-compare "^1.0.0"
sprintf-js "^1.1.2"
rtlcss@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/rtlcss/-/rtlcss-3.2.1.tgz#654e55ea2f46991f9738d952ba77ba0aa94a670d"
integrity sha512-S9bh35JXwPIhfun7nFu/HjlNrwELL5nvTJqA1suLfbnqY/mauIL5sBkrJNHziVppX9PA2rJ7NV82+RtzB71mJA==
dependencies:
chalk "^4.1.0"
find-up "^5.0.0"
mkdirp "^1.0.4"
postcss "^8.2.4"
strip-json-comments "^3.1.1"
run-async@^2.4.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
@ -6447,6 +6506,11 @@ sortablejs@^1.7.0:
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.8.3.tgz#5ae908ef96300966e95440a143340f5dd565a0df"
integrity sha512-AftvD4hdKcR5QlGi7L/JST506zGNGrysE8/QohDpwKXJarHWqCt+TUlrtoMk/wkECB607Q019/OZlJViyWiD6A==
source-map-js@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e"
integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==
source-map-resolve@^0.5.2:
version "0.5.3"
resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
@ -6744,6 +6808,11 @@ strip-indent@^1.0.1:
dependencies:
get-stdin "^4.0.1"
strip-json-comments@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
strip-json-comments@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
@ -7556,3 +7625,8 @@ yeast@0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk=
yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==