kopia lustrzana https://github.com/deathbeds/ipydrawio
rodzic
ea4f7b88ea
commit
82ab297197
|
@ -14,13 +14,14 @@
|
|||
|
||||
#### @deathbeds/ipydrawio 1.2.0
|
||||
|
||||
- adds support for data URI-encoded shape libraries (incompatible with `stealth`) [#80]
|
||||
- fixes selection in widget diagrams with more than 10 shapes [#85]
|
||||
|
||||
#### @deathbeds/ipydrawio-notebook 1.2.0
|
||||
|
||||
#### @deathbeds/ipydrawio-webpack 16.4.0
|
||||
#### @deathbeds/ipydrawio-webpack 16.4.500
|
||||
|
||||
- updates to drawio v16.4.0
|
||||
- updates to drawio v16.4.5
|
||||
|
||||
#### @deathbeds/ipydrawio-jupyter-templates 1.2.0
|
||||
|
||||
|
@ -31,6 +32,7 @@
|
|||
#### @deathbeds/ipydrawio-pdf 1.2.0
|
||||
|
||||
[#63]: https://github.com/deathbeds/ipydrawio/issues/63
|
||||
[#80]: https://github.com/deathbeds/ipydrawio/issues/80
|
||||
[#85]: https://github.com/deathbeds/ipydrawio/issues/85
|
||||
[#88]: https://github.com/deathbeds/ipydrawio/issues/88
|
||||
|
||||
|
|
|
@ -97,3 +97,6 @@ ${XP DIO PAGE SIZE} //div[contains(@class, "geFormatSection")][contains(., "P
|
|||
${MIME STDERR} application/vnd.jupyter.stderr
|
||||
# retro
|
||||
${CSS RETRO TREE DIO BTN} css:button[title='${CREATE A BLANK}']
|
||||
# docs
|
||||
${TUTORIALS} ${ROOT}${/}docs${/}tutorials
|
||||
${CLIB TUTORIAL} ${TUTORIALS}${/}working-with-custom-libraries${/}index.ipynb
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
# Copyright 2022 ipydrawio contributors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
*** Settings ***
|
||||
Documentation Do the tutorials work?
|
||||
Resource ../_Keywords.robot
|
||||
Library OperatingSystem
|
||||
Force Tags component:tutorials
|
||||
Suite Setup Set Screenshot Directory ${OUTPUT DIR}${/}screenshots${/}tutorials
|
||||
|
||||
*** Variables ***
|
||||
${XP MY LIBRARY TITLE} xpath://a[starts-with(@title, 'my+library')]
|
||||
${XP MY LIBRARY SHAPES} ${XP MY LIBRARY TITLE}/following-sibling::div[1]//a[contains(@class, 'geItem')]
|
||||
${XP SHAPE TOOLTIP} xpath://*[contains(@class, 'geSidebarTooltip')]
|
||||
@{SHAPE TITLES} Exit Machine Queue Source
|
||||
|
||||
*** Test Cases ***
|
||||
Custom Library URL Hack
|
||||
[Documentation] Does using a custom library work?
|
||||
[Tags] component:widget
|
||||
Copy File ${CLIB TUTORIAL} ${HOME}${/}clib.ipynb
|
||||
Wait Until Keyword Succeeds 5x 5s Open clib.ipynb in ${MENU NOTEBOOK}
|
||||
Lab Command Restart Kernel and Run All Cells
|
||||
Accept Default Dialog Option
|
||||
Click Element css:.jp-Cell:last-child
|
||||
Wait Until Keyword Succeeds 5x 5s Wait for a Diagram to be Ready
|
||||
Wait Until Page Contains Element ${XP MY LIBRARY TITLE}
|
||||
${shapes} = Get WebElements ${XP MY LIBRARY SHAPES}
|
||||
Should be Equal as Integers ${shapes.__len__()} 4
|
||||
FOR ${idx} ${shape} IN ENUMERATE ${shapes}
|
||||
Mouse Over ${shape}
|
||||
Wait Until Page Contains Element
|
||||
... ${XP SHAPE TOOLTIP}\[contains(., '${SHAPE TITLES[${idx}]}')]
|
||||
END
|
||||
[Teardown] Tear Down Custom Library Tutorial
|
||||
|
||||
*** Keywords ***
|
||||
Tear Down Custom Library Tutorial
|
||||
Unselect Frame
|
||||
Remove File ${HOME}${/}clib.ipynb
|
|
@ -3,6 +3,7 @@
|
|||
```{toctree}
|
||||
:maxdepth: 2
|
||||
designing-jupyter-extensions/index
|
||||
working-with-custom-libraries/index
|
||||
```
|
||||
|
||||
<!--
|
||||
|
|
|
@ -0,0 +1,300 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "e9738510-4036-46e9-b728-4590dbd2c738",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Working with Custom Shape Libraries\n",
|
||||
"\n",
|
||||
"When working with the `ipydrawio` TypeScript API, custom shape libraries can be added by providing a fully-resolved, absolute URL to an `.xml` file in `clibs`. \n",
|
||||
"\n",
|
||||
"From the [Widget API](../../Diagram%20Widget.ipynb), this is somewhat more complicated, but might be worth it for certain custom cases such as [issue #80](https://github.com/deathbeds/ipydrawio/issues/80), where a fully-kernel-driven solution is desirable, despite the _gotchas_ (see below)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "ff64bdc4-2dd5-4738-affc-36dab7c82584",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import base64, json, urllib.parse, zlib\n",
|
||||
"import ipydrawio"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "6f54b3ce-a88a-4a89-bebb-3074ad6dfee4",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## A Library\n",
|
||||
"A library is, at its core, a list of shape descriptions. The best way to learn more about these is investigating the [drawio documentation](https://www.diagrams.net/blog/custom-libraries), which covers building them interactively. When done, you'd end up with some data like this."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "8c582ec1-23ca-4e76-8733-cf8d8d7d8b46",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"library = [\n",
|
||||
" {\n",
|
||||
" \"w\": 80,\n",
|
||||
" \"h\": 80,\n",
|
||||
" \"aspect\": \"fixed\",\n",
|
||||
" \"title\": \"Source\",\n",
|
||||
" \"xml\": '<mxGraphModel><root><mxCell id=\"0\"/><mxCell id=\"1\" parent=\"0\"/><object label=\"\" type=\"Source\" interArrivalTime=\"\" id=\"2\"><mxCell style=\"rhombus;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\"><mxGeometry width=\"80\" height=\"80\" as=\"geometry\"/></mxCell></object></root></mxGraphModel>',\n",
|
||||
" },\n",
|
||||
" {\n",
|
||||
" \"w\": 80,\n",
|
||||
" \"h\": 80,\n",
|
||||
" \"aspect\": \"fixed\",\n",
|
||||
" \"title\": \"Queue\",\n",
|
||||
" \"xml\": '<mxGraphModel><root><mxCell id=\"0\"/><mxCell id=\"1\" parent=\"0\"/><object label=\"\" type=\"Queue\" capacity=\"\" id=\"2\"><mxCell style=\"ellipse;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\"><mxGeometry width=\"80\" height=\"80\" as=\"geometry\"/></mxCell></object></root></mxGraphModel>',\n",
|
||||
" },\n",
|
||||
" {\n",
|
||||
" \"w\": 80,\n",
|
||||
" \"h\": 80,\n",
|
||||
" \"aspect\": \"fixed\",\n",
|
||||
" \"title\": \"Machine\",\n",
|
||||
" \"xml\": '<mxGraphModel><root><mxCell id=\"0\"/><mxCell id=\"1\" parent=\"0\"/><object label=\"\" type=\"Machine\" processingTime=\"\" id=\"2\"><mxCell style=\"whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\"><mxGeometry width=\"80\" height=\"80\" as=\"geometry\"/></mxCell></object></root></mxGraphModel>',\n",
|
||||
" },\n",
|
||||
" {\n",
|
||||
" \"w\": 80,\n",
|
||||
" \"h\": 80,\n",
|
||||
" \"aspect\": \"fixed\",\n",
|
||||
" \"title\": \"Exit\",\n",
|
||||
" \"xml\": '<mxGraphModel><root><mxCell id=\"0\"/><mxCell id=\"1\" parent=\"0\"/><object label=\"\" type=\"Exit\" id=\"2\"><mxCell style=\"rhombus;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\"><mxGeometry width=\"80\" height=\"80\" as=\"geometry\"/></mxCell></object></root></mxGraphModel>',\n",
|
||||
" },\n",
|
||||
"]\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "708fb970-bade-40a2-a495-b861ebadfbd3",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## JSON Encoding"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "3c5665cf-9a04-4f28-b27d-7bd9cf880b2a",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Each element in the list has an `xml` attribute which is, as it suggests, XML, and must be carefully escaped. Because this will go through a URL parser, an XML parser, a JSON parser, and then another XML parser, the recommended approach is to use drawio's semi-convoluted base64/zlib technique."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "3e9a519c-a868-4d71-885e-fcafe41915fa",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"zlib_opts = dict(wbits=-15)\n",
|
||||
"\n",
|
||||
"def inflate(deflated):\n",
|
||||
" infl = zlib.decompressobj(**zlib_opts)\n",
|
||||
" return urllib.parse.unquote(infl.decompress(base64.b64decode(deflated)) + infl.flush())\n",
|
||||
"\n",
|
||||
"def deflate(inflated):\n",
|
||||
" defl = zlib.compressobj(**zlib_opts)\n",
|
||||
" return base64.b64encode(defl.compress(urllib.parse.quote(inflated).encode(\"utf-8\")) + defl.flush()).decode('utf-8')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "0a33277f-abd0-4b97-a803-ad2ed82a6222",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Again, due to the number of parsers involved, it's best to avoid extra spaces in the data URI."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "63445943-ecea-441e-87f5-7d2f361b7c4d",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"library_json = json.dumps(\n",
|
||||
" [dict(shape.items(), xml=deflate(shape[\"xml\"])) for shape in library],\n",
|
||||
" separators=(\",\", \":\")\n",
|
||||
")\n",
|
||||
"library_json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "ad6be53d-0d24-4e5e-99b4-33c9aaf531de",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## XML Encoding\n",
|
||||
"\n",
|
||||
"The JSON is wrapped inside an XML document, with a top-level tag of `mxlibrary`."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "82d193a9-2366-4df4-9263-84b65fa96c68",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"library_xml = f\"\"\"<mxlibrary>{library_json}</mxlibrary><!-- /my library -->\"\"\"\n",
|
||||
"library_xml"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "76502de9-f2cb-4485-a438-acb6a06a8ca5",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"This, in turn, must be transformed into a [Data URI](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs). The whole thing can't be `base64` encoded, because drawio expects a semicolon-separated list of ids. \n",
|
||||
"\n",
|
||||
"> **GOTCHA**: Use of data URIs relies on a **NASTY PATCH** applied when packaging `@deathbeds/jupyterlab-drawio-webpack`: by default, the upstream would rewrite this into a proxied request, which `ipydrawio` don't support. Usually, the name of the library will be derived from the filename, which is usually the last path component after the `/` ... in this case, the _whole document_ is the path, so we make do with some hacks."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "bcd588f0-100e-4dd9-96a7-ea0b16bc6a0f",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## URL Encoding"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "3f062362-8691-42d9-b9d7-f4a29cf6c405",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"library_data_uri = f\"data:application/xml,{library_xml}\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "c59eeed7-8599-429e-a869-3964b4709964",
|
||||
"metadata": {},
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "ca0d0acc-7186-4f81-83cf-fd93da83f59c",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## URL Params\n",
|
||||
"Finally, the most reliable means of communicating with drawio is via its [URL parameters](https://www.diagrams.net/doc/faq/supported-url-parameters), exposed on the widget as `url_params`\n",
|
||||
"\n",
|
||||
"> **GOTCHA** `url_params` should be set before the widget is displayed to avoid extra dialogs. \n",
|
||||
"\n",
|
||||
"The `clibs` parameters accepts a list of \"library keys,\" each with different formats. We are interest in `U` (for `URL`) library.\n",
|
||||
"\n",
|
||||
"> Some others that might be worth exploring some time include `L` for `Local`, which works with an `IndexedDB` instance... but is not guaranteed to be configured by the time a document loads.\n",
|
||||
"\n",
|
||||
"Note, we also override the `stealth` default... `stealth` isn't _strictly_ going to worsen the privacy posture, as all of the other providers are still disabled."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "5a89ad37-caf4-4174-90f0-ba51f7c98c3b",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"url_params = dict(ipydrawio.Diagram._default_url_params(None))\n",
|
||||
"url_params.update(clibs=f\"U{library_data_uri}\", stealth=\"0\",)\n",
|
||||
"url_params"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "40668d7d-0d2d-4727-add8-ef8d494f0b23",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### More URL Params\n",
|
||||
"\n",
|
||||
"A number of other parameters can be useful for custom embedding purposes, such as using a `min`imal `ui`, hiding the default `libs`, disabling additional `p`lugins."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "938af1fe-a2b5-42a0-beee-c93d910b7dfd",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"url_params.update(ui=\"min\", libs=\"0\", p=\"\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "7b7169ce-668f-426c-8146-1c27aa4ef66d",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## The Widget"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "91835331-2f68-48af-9ca1-c2512f25ded3",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"d = ipydrawio.Diagram(url_params=url_params, layout=dict(height=\"800px\"))\n",
|
||||
"d"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "bbb0e474-649d-4e33-96c3-5f4f3bad56bd",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Use The Source\n",
|
||||
"\n",
|
||||
"We should now be able to use the desired shapes in the diagram. Unlike `url_params`, the `value` of the diagram's `source` can be updated immediately."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "5daa299b-08d8-4ad2-b35e-d599157612e4",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"d.source.value = '''<mxfile version=\"15.8.7\" type=\"embed\">\n",
|
||||
" <diagram id=\"x\" name=\"My Diagram\">\n",
|
||||
" <mxGraphModel dx=\"1687\" dy=\"681\" grid=\"1\" gridSize=\"10\" guides=\"1\" tooltips=\"1\" connect=\"1\" arrows=\"1\" fold=\"1\" page=\"1\" pageScale=\"1\" pageWidth=\"850\" pageHeight=\"1100\" math=\"0\" shadow=\"0\"><root><mxCell id=\"0\"/><mxCell id=\"1\" parent=\"0\"/><object label=\"apple\" type=\"Source\" interArrivalTime=\"\" id=\"2\"><mxCell style=\"rhombus;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\"><mxGeometry x=\"100\" y=\"90\" width=\"80\" height=\"80\" as=\"geometry\"/></mxCell></object><object label=\"banana\" type=\"Queue\" capacity=\"\" id=\"3\"><mxCell style=\"ellipse;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\"><mxGeometry x=\"250\" y=\"90\" width=\"80\" height=\"80\" as=\"geometry\"/></mxCell></object><object label=\"cherry\" type=\"Machine\" processingTime=\"\" id=\"4\"><mxCell style=\"whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\"><mxGeometry x=\"385\" y=\"90\" width=\"80\" height=\"80\" as=\"geometry\"/></mxCell></object><object label=\"date\" type=\"Exit\" id=\"5\"><mxCell style=\"rhombus;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\"><mxGeometry x=\"510\" y=\"90\" width=\"80\" height=\"80\" as=\"geometry\"/></mxCell></object></root>\n",
|
||||
" </mxGraphModel>\n",
|
||||
" </diagram>\n",
|
||||
"</mxfile>'''"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.10.1"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
|
@ -1 +1 @@
|
|||
Subproject commit bbf17d5f5a1d73cb6c955b579cd3691fc55ebae7
|
||||
Subproject commit bdfb2a3944aaef1a9f95097a6c30d98fbefce1c4
|
|
@ -43,5 +43,5 @@
|
|||
"build:pre": "python scripts/patch.py && python scripts/static.py"
|
||||
},
|
||||
"types": "lib/index.d.ts",
|
||||
"version": "16.4.0"
|
||||
"version": "16.4.500"
|
||||
}
|
||||
|
|
|
@ -29,7 +29,12 @@ PATCHES = {
|
|||
"name": "global ref so we can get at the App at runtime",
|
||||
"before": "new App(new Editor",
|
||||
"after": "window.IPYDRAWIO_APP = new App(new Editor",
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "patch cors check for data URI",
|
||||
"before": "this.editor.isCorsEnabledForUrl(n)||(n=",
|
||||
"after": "(n.match(/^data:/) || this.editor.isCorsEnabledForUrl(n))||(n=",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"@jupyterlab/application": "^3.1.0",
|
||||
"@jupyterlab/launcher": "^3.1.0",
|
||||
"@jupyterlab/mainmenu": "^3.1.0",
|
||||
"@deathbeds/ipydrawio-webpack": "^16.4.0"
|
||||
"@deathbeds/ipydrawio-webpack": "^16.4.500"
|
||||
},
|
||||
"description": "A JupyterLab extension for embedding interactive drawio / mxgraph diagrams.",
|
||||
"devDependencies": {
|
||||
|
|
|
@ -106,14 +106,14 @@
|
|||
"@jupyterlab/application" "^3.1.0"
|
||||
|
||||
"@deathbeds/ipydrawio-webpack@file:packages/ipydrawio-webpack":
|
||||
version "16.4.0"
|
||||
version "16.4.500"
|
||||
dependencies:
|
||||
"@jupyterlab/application" "^3.1.0"
|
||||
|
||||
"@deathbeds/ipydrawio@file:packages/ipydrawio":
|
||||
version "1.2.0"
|
||||
dependencies:
|
||||
"@deathbeds/ipydrawio-webpack" "^16.4.0"
|
||||
"@deathbeds/ipydrawio-webpack" "^16.4.500"
|
||||
"@jupyterlab/application" "^3.1.0"
|
||||
"@jupyterlab/launcher" "^3.1.0"
|
||||
"@jupyterlab/mainmenu" "^3.1.0"
|
||||
|
|
Ładowanie…
Reference in New Issue