Kaynağa Gözat

feat: New Print Format Builder

- Print Format Builder Beta page
- Add margin fields in Print Format
- Using vuedraggable for drag and drop
version-14
Faris Ansari 3 yıl önce
ebeveyn
işleme
b8fbed0f66
17 değiştirilmiş dosya ile 815 ekleme ve 12 silme
  1. +37
    -2
      frappe/printing/doctype/print_format/print_format.json
  2. +4
    -0
      frappe/printing/doctype/print_format/print_format.py
  3. +6
    -0
      frappe/printing/page/print/print.js
  4. +11
    -7
      frappe/printing/page/print_format_builder/print_format_builder.js
  5. +7
    -2
      frappe/printing/page/print_format_builder/print_format_builder.py
  6. +0
    -0
      frappe/printing/page/print_format_builder_beta/__init__.py
  7. +3
    -0
      frappe/printing/page/print_format_builder_beta/print_format_builder_beta.css
  8. +31
    -0
      frappe/printing/page/print_format_builder_beta/print_format_builder_beta.js
  9. +22
    -0
      frappe/printing/page/print_format_builder_beta/print_format_builder_beta.json
  10. +73
    -0
      frappe/public/js/print_format_builder/PrintFormat.vue
  11. +110
    -0
      frappe/public/js/print_format_builder/PrintFormatBuilder.vue
  12. +163
    -0
      frappe/public/js/print_format_builder/PrintFormatControls.vue
  13. +203
    -0
      frappe/public/js/print_format_builder/PrintFormatSection.vue
  14. +31
    -0
      frappe/public/js/print_format_builder/print_format_builder.bundle.js
  15. +100
    -0
      frappe/public/js/print_format_builder/utils.js
  16. +2
    -1
      package.json
  17. +12
    -0
      yarn.lock

+ 37
- 2
frappe/printing/doctype/print_format/print_format.json Dosyayı Görüntüle

@@ -19,6 +19,10 @@
"html",
"raw_commands",
"section_break_9",
"margin_top",
"margin_bottom",
"margin_left",
"margin_right",
"align_labels_right",
"show_section_headings",
"line_breaks",
@@ -31,7 +35,8 @@
"section_break_13",
"print_format_help",
"format_data",
"print_format_builder"
"print_format_builder",
"print_format_builder_beta"
],
"fields": [
{
@@ -205,13 +210,43 @@
"fieldname": "absolute_value",
"fieldtype": "Check",
"label": "Show Absolute Values"
},
{
"default": "0",
"fieldname": "print_format_builder_beta",
"fieldtype": "Check",
"label": "Print Format Builder Beta"
},
{
"default": "15",
"fieldname": "margin_top",
"fieldtype": "Float",
"label": "Margin Top"
},
{
"default": "15",
"fieldname": "margin_bottom",
"fieldtype": "Float",
"label": "Margin Bottom"
},
{
"default": "15",
"fieldname": "margin_left",
"fieldtype": "Float",
"label": "Margin Left"
},
{
"default": "15",
"fieldname": "margin_right",
"fieldtype": "Float",
"label": "Margin Right"
}
],
"icon": "fa fa-print",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-03-01 15:25:46.578863",
"modified": "2021-07-11 11:53:52.028982",
"modified_by": "Administrator",
"module": "Printing",
"name": "Print Format",


+ 4
- 0
frappe/printing/doctype/print_format/print_format.py Dosyayı Görüntüle

@@ -38,6 +38,10 @@ class PrintFormat(Document):

def extract_images(self):
from frappe.core.doctype.file.file import extract_images_from_html

if self.print_format_builder_beta:
return

if self.format_data:
data = json.loads(self.format_data)
for df in data:


+ 6
- 0
frappe/printing/page/print/print.js Dosyayı Görüntüle

@@ -258,6 +258,11 @@ frappe.ui.form.PrintView = class {
fieldtype: 'Read Only',
default: print_format.name || 'Standard',
},
{
label: __('Use the new Print Format Builder Beta'),
fieldname: 'beta',
fieldtype: 'Check'
},
],
(data) => {
frappe.route_options = {
@@ -265,6 +270,7 @@ frappe.ui.form.PrintView = class {
doctype: this.frm.doctype,
name: data.print_format_name,
based_on: data.based_on,
beta: data.beta
};
frappe.set_route('print-format-builder');
this.print_sel.val(data.print_format_name);


+ 11
- 7
frappe/printing/page/print_format_builder/print_format_builder.js Dosyayı Görüntüle

@@ -12,9 +12,9 @@ frappe.pages['print-format-builder'].on_page_show = function(wrapper) {
});
} else if(frappe.route_options) {
if(frappe.route_options.make_new) {
let { doctype, name, based_on } = frappe.route_options;
let { doctype, name, based_on, beta } = frappe.route_options;
frappe.route_options = null;
frappe.print_format_builder.setup_new_print_format(doctype, name, based_on);
frappe.print_format_builder.setup_new_print_format(doctype, name, based_on, beta);
} else {
frappe.print_format_builder.print_format = frappe.route_options.doc;
frappe.route_options = null;
@@ -126,18 +126,22 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {

});
}
setup_new_print_format(doctype, name, based_on) {
setup_new_print_format(doctype, name, based_on, beta) {
frappe.call({
method: 'frappe.printing.page.print_format_builder.print_format_builder.create_custom_format',
args: {
doctype: doctype,
name: name,
based_on: based_on
based_on: based_on,
beta: Boolean(beta)
},
callback: (r) => {
if(!r.exc) {
if(r.message) {
this.print_format = r.message;
if(r.message) {
let print_format = r.message;
if (print_format.print_format_builder_beta) {
frappe.set_route('print-format-builder-beta', print_format.name);
} else {
this.print_format = print_format;
this.refresh();
}
}


+ 7
- 2
frappe/printing/page/print_format_builder/print_format_builder.py Dosyayı Görüntüle

@@ -1,11 +1,16 @@
import frappe

@frappe.whitelist()
def create_custom_format(doctype, name, based_on='Standard'):
def create_custom_format(doctype, name, based_on='Standard', beta=False):
doc = frappe.new_doc('Print Format')
doc.doc_type = doctype
doc.name = name
doc.print_format_builder = 1
beta = frappe.parse_json(beta)

if beta:
doc.print_format_builder_beta = 1
else:
doc.print_format_builder = 1
doc.format_data = frappe.db.get_value('Print Format', based_on, 'format_data') \
if based_on != 'Standard' else None
doc.insert()

+ 0
- 0
frappe/printing/page/print_format_builder_beta/__init__.py Dosyayı Görüntüle


+ 3
- 0
frappe/printing/page/print_format_builder_beta/print_format_builder_beta.css Dosyayı Görüntüle

@@ -0,0 +1,3 @@
.layout-main-section-wrapper {
margin-bottom: 0;
}

+ 31
- 0
frappe/printing/page/print_format_builder_beta/print_format_builder_beta.js Dosyayı Görüntüle

@@ -0,0 +1,31 @@
frappe.pages["print-format-builder-beta"].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: __("Print Format Builder"),
single_column: true
});

function load_print_format_builder_beta() {
let route = frappe.get_route();
let $parent = $(wrapper).find(".layout-main-section");
$parent.empty();

if (route.length > 1) {
frappe.require("print_format_builder.bundle.js").then(() => {
frappe.print_format_builder = new frappe.ui.PrintFormatBuilder({
wrapper: $parent,
page,
print_format: route[1]
});
});
}
}

load_print_format_builder_beta();

// hot reload in development
if (frappe.boot.developer_mode) {
frappe.hot_update = frappe.hot_update || [];
frappe.hot_update.push(load_print_format_builder_beta);
}
};

+ 22
- 0
frappe/printing/page/print_format_builder_beta/print_format_builder_beta.json Dosyayı Görüntüle

@@ -0,0 +1,22 @@
{
"content": null,
"creation": "2021-07-10 12:22:16.138485",
"docstatus": 0,
"doctype": "Page",
"idx": 0,
"modified": "2021-07-10 12:22:16.138485",
"modified_by": "Administrator",
"module": "Printing",
"name": "print-format-builder-beta",
"owner": "Administrator",
"page_name": "Print Format Builder Beta",
"roles": [
{
"role": "System Manager"
}
],
"script": null,
"standard": "Yes",
"style": null,
"system_page": 0
}

+ 73
- 0
frappe/public/js/print_format_builder/PrintFormat.vue Dosyayı Görüntüle

@@ -0,0 +1,73 @@
<template>
<div class="print-format-main" :style="rootStyles">
<draggable
v-model="layout.sections"
group="sections"
filter=".section-columns, .column, .field"
:animation="200"
>
<PrintFormatSection
v-for="(section, i) in layout.sections"
:key="i"
:section="section"
@add_section_above="add_section_above(section)"
/>
</draggable>
</div>
</template>

<script>
import draggable from "vuedraggable";
import PrintFormatSection from "./PrintFormatSection.vue";

export default {
name: "PrintFormat",
props: ["print_format", "meta", "layout"],
components: {
draggable,
PrintFormatSection,
},
computed: {
rootStyles() {
let {
margin_top = 0,
margin_bottom = 0,
margin_left = 0,
margin_right = 0,
} = this.print_format;
return {
padding: `${margin_top}mm ${margin_right}mm ${margin_bottom}mm ${margin_left}mm`,
width: "210mm",
minHeight: "297mm",
};
},
},
methods: {
add_section_above(section) {
let sections = [];
for (let _section of this.layout.sections) {
if (_section === section) {
sections.push({
label: "",
columns: [
{ label: "", fields: [] },
{ label: "", fields: [] },
],
});
}
sections.push(_section);
}
this.$set(this.layout, "sections", sections);
},
},
};
</script>

<style scoped>
.print-format-main {
margin-left: auto;
background-color: white;
box-shadow: var(--shadow-lg);
border-radius: var(--border-radius);
}
</style>

+ 110
- 0
frappe/public/js/print_format_builder/PrintFormatBuilder.vue Dosyayı Görüntüle

@@ -0,0 +1,110 @@
<template>
<div class="layout-main-section row" v-if="print_format && meta && layout">
<div class="col-3">
<PrintFormatControls
:print_format="print_format"
:meta="meta"
@update="update($event)"
/>
</div>
<div class="print-format-container col-9">
<PrintFormat :print_format="print_format" :meta="meta" :layout="layout" />
</div>
</div>
</template>

<script>
import PrintFormat from "./PrintFormat.vue";
import PrintFormatControls from "./PrintFormatControls.vue";
import { create_default_layout } from "./utils";

export default {
name: "PrintFormatBuilder",
props: ["print_format_name"],
components: {
PrintFormat,
PrintFormatControls,
},
data() {
return {
print_format: null,
doctype: null,
meta: null,
layout: null,
};
},
mounted() {
this.fetch();
},
methods: {
fetch() {
frappe.dom.freeze(__("Loading..."));
frappe.model.clear_doc("Print Format", this.print_format_name);
frappe.model.with_doc("Print Format", this.print_format_name, () => {
this.print_format = frappe.get_doc(
"Print Format",
this.print_format_name
);
frappe.model.with_doctype(this.print_format.doc_type, () => {
this.meta = frappe.get_meta(this.print_format.doc_type);
this.layout = this.get_layout();
frappe.dom.unfreeze();
});
});
},
update({ fieldname, value }) {
this.$set(this.print_format, fieldname, value);
},
save_changes() {
frappe.dom.freeze();

this.layout.sections = this.layout.sections
.map((section) => {
section.columns = section.columns.map((column) => {
column.fields = column.fields.filter((df) => !df.remove);
return column;
});
return section.remove ? null : section;
})
.filter(Boolean);

this.print_format.format_data = JSON.stringify(this.layout);

frappe
.call("frappe.client.save", {
doc: this.print_format,
})
.then(() => {
this.fetch();
})
.always(() => {
frappe.dom.unfreeze();
});
},
reset_changes() {
this.fetch();
},
get_layout() {
if (this.print_format) {
if (!this.print_format.format_data) {
return create_default_layout(this.meta);
}
if (typeof this.print_format.format_data == "string") {
return JSON.parse(this.print_format.format_data);
}
return this.print_format.format_data;
}
return null;
},
},
};
</script>

<style scoped>
.print-format-container {
height: calc(100vh - 140px);
overflow-y: auto;
padding-top: 0.5rem;
padding-bottom: 4rem;
}
</style>

+ 163
- 0
frappe/public/js/print_format_builder/PrintFormatControls.vue Dosyayı Görüntüle

@@ -0,0 +1,163 @@
<template>
<div class="layout-side-section">
<div class="form-sidebar">
<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="clearfix">
<label class="control-label"> {{ df.label }} </label>
</div>
<div class="control-input-wrapper">
<div class="control-input">
<input
type="number"
class="form-control form-control-sm"
:value="print_format[df.fieldname]"
@change="(e) => update_margin(df.fieldname, e.target.value)"
/>
</div>
</div>
</div>
</div>
</div>
<div class="sidebar-menu">
<div class="sidebar-label">{{ __("Fields") }}</div>
<input
class="form-control form-control-sm mb-2"
type="text"
:placeholder="__('Search fields')"
v-model="search_text"
/>
<draggable
class="fields-container"
:list="fields"
:group="{ name: 'fields', pull: 'clone', put: false }"
:sort="false"
>
<div class="field" v-for="df in fields" :key="df.fieldname">
{{ df.label }}
</div>
</draggable>
</div>
</div>
</div>
</template>

<script>
import draggable from "vuedraggable";
import { get_table_columns } from "./utils";

export default {
name: "PrintFormatControls",
props: ["print_format", "meta"],
data() {
return {
search_text: "",
};
},
components: {
draggable,
},
methods: {
update_margin(fieldname, value) {
value = parseFloat(value);
if (value < 0) {
value = 0;
}
this.$emit("update", { fieldname, value });
},
},
computed: {
margins() {
return [
{ label: __("Top"), fieldname: "margin_top" },
{ label: __("Bottom"), fieldname: "margin_bottom" },
{ label: __("Left"), fieldname: "margin_left" },
{ label: __("Right"), fieldname: "margin_right" },
];
},
fields() {
let fields = this.meta.fields
.filter((df) => {
if (["Section Break", "Column Break"].includes(df.fieldtype)) {
return false;
}
if (this.search_text) {
if (df.fieldname.includes(this.search_text)) {
return true;
}
if (df.label && df.label.includes(this.search_text)) {
return true;
}
return false;
} else {
return true;
}
})
.map((df) => {
let out = {
label: df.label,
fieldname: df.fieldname,
options: df.options,
reqd: df.reqd,
};
if (df.fieldtype == "Table") {
out.table_columns = get_table_columns(df);
}
return out;
});

return [
{
label: "Custom HTML",
fieldname: "custom_html",
fieldtype: "HTML",
html: "",
},
...fields,
];
},
},
};
</script>

<style scoped>
.margin-controls {
display: flex;
}

.margin-controls .form-control {
background: white;
}

.margin-controls > .form-group + .form-group {
margin-left: 0.5rem;
}

.fields-container {
max-height: calc(100vh - 22rem);
overflow-y: auto;
}

.field {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
background-color: var(--bg-light-gray);
border-radius: var(--border-radius);
border: 1px dashed var(--gray-400);
padding: 0.5rem 0.75rem;
font-size: var(--text-sm);
cursor: pointer;
}

.field:not(:first-child) {
margin-top: 0.5rem;
}

.sidebar-menu:last-child {
margin-bottom: 0;
}
</style>

+ 203
- 0
frappe/public/js/print_format_builder/PrintFormatSection.vue Dosyayı Görüntüle

@@ -0,0 +1,203 @@
<template>
<div class="print-format-section" v-if="!section.remove">
<div class="section-header">
<input
class="input-section-label w-50"
type="text"
:placeholder="__('Section Title')"
v-model="section.label"
/>

<div class="dropdown">
<button
class="btn btn-xs btn-section dropdown-button"
data-toggle="dropdown"
>
<svg class="icon icon-sm">
<use xlink:href="#icon-dot-horizontal"></use>
</svg>
</button>
<div class="dropdown-menu dropdown-menu-right" role="menu">
<button
class="dropdown-item"
@click="add_column"
v-if="section.columns.length < 4"
>
{{ __("Add column") }}
</button>
<button
class="dropdown-item"
@click="remove_column"
v-if="section.columns.length > 1"
>
{{ __("Remove column") }}
</button>
<button class="dropdown-item" @click="$emit('add_section_above')">
{{ __("Add section above") }}
</button>
<button class="dropdown-item" @click="$set(section, 'remove', true)">
{{ __("Remove section") }}
</button>
</div>
</div>
</div>
<div class="row section-columns">
<div class="column col" v-for="(column, i) in section.columns" :key="i">
<draggable
class="drag-container"
v-model="column.fields"
group="fields"
:animation="150"
>
<button
class="field"
v-for="df in get_fields(column)"
:key="df.fieldname"
>
<div>
{{ df.label }}
</div>
<button
class="btn btn-xs btn-remove-field"
@click="$set(df, 'remove', true)"
>
<svg class="icon icon-sm">
<use xlink:href="#icon-close"></use>
</svg>
</button>
</button>
</draggable>
</div>
</div>
</div>
</template>

<script>
import draggable from "vuedraggable";

export default {
name: "PrintFormatSection",
props: ["section"],
components: {
draggable,
},
methods: {
add_column() {
if (this.section.columns.length < 4) {
this.section.columns.push({
label: "",
fields: [],
});
}
},
remove_column() {
if (this.section.columns.length <= 1) return;

let columns = this.section.columns.slice();
let last_column_fields = columns.slice(-1)[0].fields.slice();
let index = columns.length - 1;
columns = columns.slice(0, index);
let last_column = columns[index - 1];
last_column.fields = [...last_column.fields, ...last_column_fields];

this.$set(this.section, "columns", columns);
},
get_fields(column) {
return column.fields.filter((df) => !df.remove);
},
},
};
</script>

<style scoped>
.print-format-section {
background-color: white;
border: 1px solid var(--dark-border-color);
border-radius: var(--border-radius);
padding: 1rem;
cursor: pointer;
}

.section-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 0.75rem;
}

.input-section-label {
border: 1px solid transparent;
border-radius: var(--border-radius);
font-size: var(--text-md);
font-weight: 600;
}

.input-section-label:focus {
border-color: var(--border-color);
outline: none;
background-color: var(--control-bg);
}

.input-section-label::placeholder {
font-style: italic;
font-weight: normal;
}

.btn-section {
padding: var(--padding-xs);
box-shadow: none;
}

.btn-section:hover {
background-color: var(--bg-light-gray);
}

.print-format-section:not(:first-child) {
margin-top: 1rem;
}

.section-columns {
margin-left: -8px;
margin-right: -8px;
}

.column {
padding-left: 8px;
padding-right: 8px;
}

.field {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
background-color: var(--bg-light-gray);
border-radius: var(--border-radius);
border: 1px dashed var(--gray-400);
padding: 0.5rem 0.75rem;
font-size: var(--text-sm);
}

.field:not(:first-child) {
margin-top: 0.5rem;
}

.btn-remove-field {
opacity: 0;
padding: 2px;
box-shadow: none;
}

.btn-remove-field:hover {
background-color: white;
}

.field:hover .btn-remove-field {
opacity: 1;
}

.drag-container {
height: 100%;
min-height: 2rem;
}
</style>

+ 31
- 0
frappe/public/js/print_format_builder/print_format_builder.bundle.js Dosyayı Görüntüle

@@ -0,0 +1,31 @@
import PrintFormatBuilderComponent from "./PrintFormatBuilder.vue";

class PrintFormatBuilder {
constructor({ wrapper, page, print_format }) {
this.$wrapper = $(wrapper);
this.page = page;
this.print_format = print_format;
this.page.set_title(__("Editing {0}", [this.print_format]));
this.page.set_primary_action(__("Save changes"), () => {
this.$component.save_changes();
});
this.page.set_secondary_action(__("Reset changes"), () => {
this.$component.reset_changes();
});

let $vm = new Vue({
el: this.$wrapper.get(0),
render: h =>
h(PrintFormatBuilderComponent, {
props: {
print_format_name: print_format
}
})
});
this.$component = $vm.$children[0];
}
}

frappe.provide("frappe.ui");
frappe.ui.PrintFormatBuilder = PrintFormatBuilder;
export default PrintFormatBuilder;

+ 100
- 0
frappe/public/js/print_format_builder/utils.js Dosyayı Görüntüle

@@ -0,0 +1,100 @@
export function create_default_layout(meta) {
let layout = {
sections: []
};

let section = null,
column = null;

function set_column(df) {
if (!section) {
set_section();
}
column = get_new_column(df);
section.columns.push(column);
}

function set_section(df) {
section = get_new_section(df);
column = null;
layout.sections.push(section);
}

function get_new_section(df) {
if (!df) {
df = { label: "" };
}
return {
label: df.label || "",
columns: []
};
}

function get_new_column(df) {
if (!df) {
df = { label: "" };
}
return {
label: df.label || "",
fields: []
};
}

for (let df of meta.fields) {
if (df.fieldname) {
// make a copy to avoid mutation bugs
df = JSON.parse(JSON.stringify(df));
} else {
continue;
}

if (df.fieldtype === "Section Break") {
set_section(df);
} else if (df.fieldtype === "Column Break") {
set_column(df);
} else if (df.label) {
if (!column) set_column();

if (!df.print_hide) {
let field = {
label: df.label,
fieldname: df.fieldname,
options: df.options
};

if (df.fieldtype === "Table") {
field.table_columns = get_table_columns(df);
}

column.fields.push(field);
section.has_fields = true;
}
}
}

// remove empty sections
layout.sections = layout.sections.filter(section => section.has_fields);

return layout;
}

export function get_table_columns(df) {
let table_columns = [];
let table_fields = frappe.get_meta(df.options).fields;

for (let tf of table_fields) {
if (
!in_list(["Section Break", "Column Break"], tf.fieldtype) &&
!tf.print_hide &&
df.label
) {
table_columns.push({
label: tf.label,
fieldname: tf.fieldname,
options: tf.options,
width: tf.width || 0
});
}
}
return table_columns;
}

+ 2
- 1
package.json Dosyayı Görüntüle

@@ -61,7 +61,8 @@
"superagent": "^3.8.2",
"touch": "^3.1.0",
"vue": "2.6.12",
"vue-router": "^2.0.0"
"vue-router": "^2.0.0",
"vuedraggable": "^2.24.3"
},
"devDependencies": {
"chalk": "^2.3.2",


+ 12
- 0
yarn.lock Dosyayı Görüntüle

@@ -6911,6 +6911,11 @@ socket.io@^2.4.0:
socket.io-client "2.4.0"
socket.io-parser "~3.4.0"

sortablejs@1.10.2:
version "1.10.2"
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.10.2.tgz#6e40364d913f98b85a14f6678f92b5c1221f5290"
integrity sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A==

sortablejs@^1.7.0:
version "1.8.3"
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.8.3.tgz#5ae908ef96300966e95440a143340f5dd565a0df"
@@ -7790,6 +7795,13 @@ vue@2.6.12:
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.12.tgz#f5ebd4fa6bd2869403e29a896aed4904456c9123"
integrity sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg==

vuedraggable@^2.24.3:
version "2.24.3"
resolved "https://registry.yarnpkg.com/vuedraggable/-/vuedraggable-2.24.3.tgz#43c93849b746a24ce503e123d5b259c701ba0d19"
integrity sha512-6/HDXi92GzB+Hcs9fC6PAAozK1RLt1ewPTLjK0anTYguXLAeySDmcnqE8IC0xa7shvSzRjQXq3/+dsZ7ETGF3g==
dependencies:
sortablejs "1.10.2"

wcwidth@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"


Yükleniyor…
İptal
Kaydet