crudkit 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. {crudkit-0.2.0 → crudkit-0.2.1}/CHANGELOG.md +9 -1
  2. {crudkit-0.2.0 → crudkit-0.2.1}/PKG-INFO +4 -2
  3. {crudkit-0.2.0 → crudkit-0.2.1}/README.md +3 -1
  4. {crudkit-0.2.0 → crudkit-0.2.1}/pyproject.toml +1 -1
  5. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_frontend/context_processors.py +3 -0
  6. crudkit-0.2.0/src/crudkit_frontend/static/crudkit_frontend/assets/index-BUe8jarF.js → crudkit-0.2.1/src/crudkit_frontend/static/crudkit_frontend/assets/index-Cj0Mdbti.js +1 -1
  7. crudkit-0.2.1/src/crudkit_frontend/templates/crudkit_frontend/index.html +21 -0
  8. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_frontend/tests/templates/crudkit_frontend/index.html +3 -1
  9. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_frontend/tests/test_frontend.py +19 -0
  10. crudkit-0.2.0/src/crudkit_frontend/templates/crudkit_frontend/index.html +0 -17
  11. {crudkit-0.2.0 → crudkit-0.2.1}/.gitignore +0 -0
  12. {crudkit-0.2.0 → crudkit-0.2.1}/manage.py +0 -0
  13. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/__init__.py +0 -0
  14. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/admin.py +0 -0
  15. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/ai_backend.py +0 -0
  16. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/apps.py +0 -0
  17. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/decorators.py +0 -0
  18. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/fields.py +0 -0
  19. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/llm.py +0 -0
  20. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/management/__init__.py +0 -0
  21. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/management/commands/__init__.py +0 -0
  22. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/management/commands/add_default_exchange_rates.py +0 -0
  23. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/management/commands/backfill_ai_fields.py +0 -0
  24. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/management/commands/set_exchange_rate.py +0 -0
  25. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/migrations/0001_squashed.py +0 -0
  26. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/migrations/__init__.py +0 -0
  27. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/models.py +0 -0
  28. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/profile.py +0 -0
  29. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/signals.py +0 -0
  30. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/tasks.py +0 -0
  31. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/templatetags/__init__.py +0 -0
  32. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/templatetags/crm_forms.py +0 -0
  33. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/templatetags/crm_time.py +0 -0
  34. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/templatetags/gravatar.py +0 -0
  35. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/templatetags/model_magic.py +0 -0
  36. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/tests/__init__.py +0 -0
  37. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/tests/test_ai_backend.py +0 -0
  38. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/tests/test_ai_fields.py +0 -0
  39. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/tests/test_ai_tasks.py +0 -0
  40. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/tests/test_backfill_command.py +0 -0
  41. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/tests/test_fields.py +0 -0
  42. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/tests/test_llm.py +0 -0
  43. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/tests/test_money.py +0 -0
  44. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/tests/test_tasks.py +0 -0
  45. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/utils.py +0 -0
  46. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit/widgets.py +0 -0
  47. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_api/__init__.py +0 -0
  48. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_api/authentication.py +0 -0
  49. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_api/filters.py +0 -0
  50. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_api/metadata.py +0 -0
  51. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_api/pagination.py +0 -0
  52. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_api/serializers.py +0 -0
  53. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_api/tests/__init__.py +0 -0
  54. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_api/tests/test_email_id_serialization.py +0 -0
  55. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_api/tests/test_filters.py +0 -0
  56. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_api/tests/test_metadata.py +0 -0
  57. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_api/tests/test_user_profile.py +0 -0
  58. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_api/urls.py +0 -0
  59. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_api/views.py +0 -0
  60. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_assistant/__init__.py +0 -0
  61. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_assistant/admin.py +0 -0
  62. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_assistant/agent.py +0 -0
  63. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_assistant/apps.py +0 -0
  64. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_assistant/consumers.py +0 -0
  65. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_assistant/deps.py +0 -0
  66. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_assistant/execution.py +0 -0
  67. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_assistant/migrations/0001_initial.py +0 -0
  68. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_assistant/migrations/__init__.py +0 -0
  69. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_assistant/models.py +0 -0
  70. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_assistant/routing.py +0 -0
  71. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_assistant/runner.py +0 -0
  72. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_assistant/tests/__init__.py +0 -0
  73. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_assistant/tests/test_proposals.py +0 -0
  74. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_assistant/tools.py +0 -0
  75. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_assistant/utils.py +0 -0
  76. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_frontend/__init__.py +0 -0
  77. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_frontend/apps.py +0 -0
  78. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_frontend/static/crudkit_frontend/assets/favicon-Cit_9t8D.svg +0 -0
  79. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_frontend/static/crudkit_frontend/assets/index-CL5BO7Fx.css +0 -0
  80. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_frontend/tests/__init__.py +0 -0
  81. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_frontend/tests/urls.py +0 -0
  82. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_frontend/urls.py +0 -0
  83. {crudkit-0.2.0 → crudkit-0.2.1}/src/crudkit_frontend/views.py +0 -0
  84. {crudkit-0.2.0 → crudkit-0.2.1}/tests/__init__.py +0 -0
  85. {crudkit-0.2.0 → crudkit-0.2.1}/tests/celery.py +0 -0
  86. {crudkit-0.2.0 → crudkit-0.2.1}/tests/settings.py +0 -0
  87. {crudkit-0.2.0 → crudkit-0.2.1}/tests/testapp/__init__.py +0 -0
  88. {crudkit-0.2.0 → crudkit-0.2.1}/tests/testapp/adapters.py +0 -0
  89. {crudkit-0.2.0 → crudkit-0.2.1}/tests/testapp/ai.py +0 -0
  90. {crudkit-0.2.0 → crudkit-0.2.1}/tests/testapp/apps.py +0 -0
  91. {crudkit-0.2.0 → crudkit-0.2.1}/tests/testapp/migrations/0001_initial.py +0 -0
  92. {crudkit-0.2.0 → crudkit-0.2.1}/tests/testapp/migrations/__init__.py +0 -0
  93. {crudkit-0.2.0 → crudkit-0.2.1}/tests/testapp/models.py +0 -0
  94. {crudkit-0.2.0 → crudkit-0.2.1}/tests/urls.py +0 -0
@@ -1,6 +1,14 @@
1
1
  # Changelog
2
2
 
3
- ## 0.2.0 (2026-08-11)
3
+ ## 0.2.1 (2026-08-11)
4
+
5
+ - `crudkit_frontend`: asset URLs are no longer hardcoded to `/static/`. The
6
+ SPA shell references its assets via `{% static %}` tags (so
7
+ `ManifestStaticFilesStorage` users also get hashed, cache-busted URLs), and
8
+ the JS bundle builds asset/chunk URLs at runtime from Django's
9
+ `STATIC_URL`, exposed by the shell as `window.__CRUDKIT_STATIC_URL__` via
10
+ the `crudkit_config` context processor. Projects with a custom
11
+ `STATIC_URL` (e.g. `assets/`) now work out of the box.
4
12
 
5
13
  - The distribution is published on PyPI as **`crudkit`** (the planned
6
14
  `django-crudkit` name was rejected as too similar to an existing project).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: crudkit
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: A metadata-driven CRUD framework for Django: typed object IDs, soft delete, merge, change log, activity feed, saved views, AI fields and a generic REST API.
5
5
  Project-URL: Homepage, https://github.com/fenerum/crudkit
6
6
  Project-URL: Changelog, https://github.com/fenerum/crudkit/blob/main/backend/CHANGELOG.md
@@ -128,7 +128,9 @@ urlpatterns = [..., path("", include("crudkit_frontend.urls"))]
128
128
  ```
129
129
 
130
130
  Static assets are served by `django.contrib.staticfiles` (or WhiteNoise et
131
- al.) from `crudkit_frontend/static/`. Contributors hacking on the SPA itself
131
+ al.) from `crudkit_frontend/static/`, under whatever `STATIC_URL` the project
132
+ uses — nothing assumes the default `static/` prefix. Contributors hacking on
133
+ the SPA itself
132
134
  run the Vite dev server from [../frontend](../frontend/) against any CrudKit
133
135
  backend; `npm run build` there regenerates the bundled assets.
134
136
 
@@ -94,7 +94,9 @@ urlpatterns = [..., path("", include("crudkit_frontend.urls"))]
94
94
  ```
95
95
 
96
96
  Static assets are served by `django.contrib.staticfiles` (or WhiteNoise et
97
- al.) from `crudkit_frontend/static/`. Contributors hacking on the SPA itself
97
+ al.) from `crudkit_frontend/static/`, under whatever `STATIC_URL` the project
98
+ uses — nothing assumes the default `static/` prefix. Contributors hacking on
99
+ the SPA itself
98
100
  run the Vite dev server from [../frontend](../frontend/) against any CrudKit
99
101
  backend; `npm run build` there regenerates the bundled assets.
100
102
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "crudkit"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "A metadata-driven CRUD framework for Django: typed object IDs, soft delete, merge, change log, activity feed, saved views, AI fields and a generic REST API."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -10,7 +10,10 @@ def crudkit_config(request):
10
10
  config = getattr(settings, "CRUDKIT_FRONTEND_CONFIG", {})
11
11
  # Escape "<" so "</script>" in values cannot break out of the script tag.
12
12
  payload = json.dumps(config).replace("<", "\\u003c")
13
+ # settings.STATIC_URL is already "/"-prefixed by Django on access.
14
+ static_url = json.dumps(settings.STATIC_URL).replace("<", "\\u003c")
13
15
  return {
14
16
  "crudkit_config_json": mark_safe(payload),
15
17
  "crudkit_app_name": config.get("app_name", "CrudKit"),
18
+ "crudkit_static_url_json": mark_safe(static_url),
16
19
  }
@@ -855,7 +855,7 @@ Please change the parent <Route path="${g}"> to <Route path="${g==="/"?"*":`${g}
855
855
  }
856
856
  }
857
857
  `,P6=new Map,WI=(t,e)=>{qw(()=>{if(typeof document>"u")return;let n=document,r=P6.get(n);if(r){e&&r.setAttribute("nonce",e);return}let o=n.createElement("style");o.textContent=t,e&&o.setAttribute("nonce",e),n.head.appendChild(o),P6.set(n,o)},[e])};function vI(t){return WI(yI,t.nonce),L0.createElement(qI,{...t})}class dh extends Error{constructor(n,r,o){super(n);I0(this,"errors");I0(this,"response");this.errors=r,this.response=o}}class F2 extends Error{constructor(n,r){super(n);I0(this,"kind");this.kind=r}}const wm=()=>typeof window>"u"?!1:window.location.hostname==="localhost"||window.location.hostname==="127.0.0.1",xI=()=>typeof window>"u"||wm()?"":window.location.origin;class J1{constructor(e=""){I0(this,"baseUrl");I0(this,"clientId","CrudKitAPIClient");this.baseUrl=e||xI()}async login(e,n){const r=await fetch(`${this.baseUrl}/api/v1/token/`,{method:"POST",headers:{"Content-Type":"application/json","Client-Id":this.clientId},body:JSON.stringify({username:e,password:n})});if(!r.ok){const i=await r.text();throw new Error(`Login failed: ${i}`)}const o=await r.json();return this.storeTokens(o.access,o.refresh),o.user}async refreshToken(){const e=this.getRefreshToken();if(!e)throw new F2("No refresh token available","rejected");let n;try{n=await fetch(`${this.baseUrl}/api/v1/token/refresh/`,{method:"POST",headers:{"Content-Type":"application/json","Client-Id":this.clientId},body:JSON.stringify({refresh:e})})}catch{throw new F2("Token refresh request failed: network error","transient")}if(n.status===401||n.status===403)throw this.clearTokens(),new F2("Token refresh failed: refresh token rejected","rejected");if(!n.ok)throw new F2(`Token refresh failed: server returned ${n.status}`,"transient");const r=await n.json();return this.storeAccessToken(r.access),r.access}async logout(){this.clearTokens()}storeTokens(e,n){localStorage.setItem("accessToken",e),localStorage.setItem("refreshToken",n)}storeAccessToken(e){localStorage.setItem("accessToken",e)}getAccessToken(){return localStorage.getItem("accessToken")}getRefreshToken(){return localStorage.getItem("refreshToken")}clearTokens(){localStorage.removeItem("accessToken"),localStorage.removeItem("refreshToken")}isAuthenticated(){return!!this.getAccessToken()}cleanObject(e,n){const r=Object.entries(e.fields).filter(([,o])=>o.type==="BooleanField"&&o.editable).map(([o])=>o);for(const o of r)n[o]||(n[o]=!1);for(const[o,i]of Object.entries(n)){const s=e.fields[o];if(!s){delete n[o];continue}if((s.type==="ForeignKey"||s.type==="OneToOneField")&&i&&typeof i=="object"&&i.id!==void 0?n[o]=i.id:s.type!=="TextField"&&s.type!=="CharField"&&i===""&&(n[o]=null),s.type==="JSONField"&&i!==""&&i!==null&&typeof i=="string")try{n[o]=JSON.parse(i)}catch(a){throw new dh("Invalid JSON",{[o]:"Invalid JSON - "+a})}s.type==="BooleanField"&&(n[o]=i==="on"||i===!0),s.type==="ImageField"&&(i===""||typeof i=="string"&&!i.startsWith("data:"))&&delete n[o]}return n}async fetch(e,n={}){var s,a;n.headers||(n.headers={}),n.headers["X-CSRFToken"]=((s=document.querySelector('meta[name="csrf-token"]'))==null?void 0:s.getAttribute("content"))||null,n.credentials="include";const r=this.getAccessToken();r&&(n.headers.Authorization=`Bearer ${r}`);const o=`${this.baseUrl}/${e}`;wm()&&console.log(`API request to: ${o}`);let i=await fetch(o,{headers:Object.assign(n.headers,{"Client-Id":this.clientId,Accept:"application/json",charset:"utf-8"}),...n});if(i.status===401&&this.getRefreshToken())try{const c=await this.refreshToken();n.headers.Authorization=`Bearer ${c}`,wm()&&console.log(`Retrying API request to: ${o} after token refresh`),i=await fetch(o,{headers:Object.assign(n.headers,{"Client-Id":this.clientId,Accept:"application/json",charset:"utf-8"}),...n})}catch(c){if(console.error("Token refresh failed",c),c instanceof F2&&c.kind==="transient"){const l=new Error(c.message);throw l.statusCode=0,l.transient=!0,l}}if(i.status===400&&((a=i.headers.get("Content-Type"))!=null&&a.includes("application/json"))){const c=await i.json();if(console.log("API returned validation error:",c),c&&c.errors&&Array.isArray(c.errors)){const l=new dh("Invalid Request",c,i);throw l.errors=c.errors,l}else throw new dh("Invalid Request",c,i)}if(!i.ok){const c=await i.text();let l=null;try{l=JSON.parse(c)}catch{}const u=new Error(`Failed to load data from server
858
- `+i.statusText);throw u.statusCode=i.status,u.statusText=i.statusText,u.errorData=l,u.errorText=c,u}return i.clone()}async httpGet(e,n={}){return n?await this.fetch(`${e}?${new URLSearchParams(n)}`):await this.fetch(`${e}`)}async httpPost(e,n={},r={}){return await this.fetch(`${e}?${new URLSearchParams(r)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})}async httpPut(e,n){return await this.fetch(e,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})}async httpPatch(e,n){return await this.fetch(e,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})}async httpDelete(e){return await this.fetch(e,{method:"DELETE"})}async httpOptions(e){return await this.fetch(e,{method:"OPTIONS"})}async list(e,n={}){for(const i in n)typeof n[i]=="boolean"&&(n[i]=n[i]?"True":"False");const o=await(await this.httpGet(`api/v1/${e}/`,n)).json();return o&&o.results!==void 0?{results:o.results,count:o.count,next:o.next,previous:o.previous,page_size:o.page_size,current_page:o.current_page,total_pages:o.total_pages,isPaginated:!0}:o}async getNextPage(e){if(!e)return null;const n=new URL(e),r=n.pathname+n.search;return await(await this.fetch(r.substring(1))).json()}async getPreviousPage(e){if(!e)return null;const n=new URL(e),r=n.pathname+n.search;return await(await this.fetch(r.substring(1))).json()}async retrieve(e,n){return await this.httpGet(`api/v1/${e}/${n}/`).then(r=>r.json())}async create(e,n,r){return await this.httpPost(`api/v1/${e}/`,n,r).then(o=>o.json())}async update(e,n,r){return await this.httpPut(`api/v1/${e}/${n}/`,r).then(o=>o.json())}async partialUpdate(e,n,r){return await this.httpPatch(`api/v1/${e}/${n}/`,r).then(o=>o.json())}async delete(e,n){return await this.httpDelete(`api/v1/${e}/${n}/`)}async metadata(e){return await this.httpGet(`api/v1/${e}/metadata/`).then(n=>n.json())}async initial(e,n={}){return(await(await this.httpGet(`api/v1/${e}/initial/`,n)).json()).fields||{}}async action(e,n,r){return await this.httpPost(`api/v1/${e}/${n}/action/`,{action:r}).then(o=>o.json())}async merge(e,n,r){try{return await(await this.httpPost(`api/v1/${e}/${n}/merge/`,r)).json()}catch(o){throw console.error("Merge operation failed:",o),o.errorData&&(console.log("Server returned error data:",o.errorData),o.errorData.errors&&(o.serverErrors=o.errorData.errors)),o}}async search(e=""){return await this.httpGet("api/v1/search/",{q:e}).then(n=>n.json())}}const Pu=new J1;async function yw(t){return Pu.metadata(t)}async function RI(t,e){return Pu.retrieve(t,e)}async function Ww(t,e){return Pu.list(t,e||{})}async function LI(t,e,n){return Pu.action(t,e,n)}function NI(){return Pu.getAccessToken()}const vw=v.createContext({user:null,loading:!0,isAuthenticated:!1,login:async()=>{throw new Error("AuthProvider not mounted")},logout:async()=>{}}),gd=new J1;function kI({children:t}){const[e,n]=v.useState(null),[r,o]=v.useState(!0);v.useEffect(()=>{(async()=>{try{try{const c=await gd.httpGet("api/v1/user/me/").then(l=>l.json());n(c)}catch(c){(c==null?void 0:c.statusCode)===401||(c==null?void 0:c.statusCode)===403?(console.log("Server rejected stored credentials, clearing tokens"),gd.clearTokens()):console.log("Auth check failed transiently; keeping tokens",c)}}finally{o(!1)}})()},[]);const i=async(a,c)=>{try{o(!0);const l=await gd.login(a,c);return n(l),l}finally{o(!1)}},s=async()=>{o(!0),await gd.logout(),n(null),o(!1)};return h.jsx(vw.Provider,{value:{user:e,login:i,logout:s,loading:r,isAuthenticated:!!e},children:t})}function No(){return v.useContext(vw)}const wI="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCIgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0Ij4KICA8cmVjdCB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHJ4PSIxNCIgZmlsbD0iIzYzNjZmMSIgLz4KICA8dGV4dAogICAgeD0iMzIiCiAgICB5PSIzNCIKICAgIHRleHQtYW5jaG9yPSJtaWRkbGUiCiAgICBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCIKICAgIGZvbnQtZmFtaWx5PSJzeXN0ZW0tdWksIC1hcHBsZS1zeXN0ZW0sICdTZWdvZSBVSScsIHNhbnMtc2VyaWYiCiAgICBmb250LXNpemU9IjI2IgogICAgZm9udC13ZWlnaHQ9IjcwMCIKICAgIGxldHRlci1zcGFjaW5nPSItMSIKICAgIGZpbGw9IiNmZmZmZmYiCiAgPkNLPC90ZXh0Pgo8L3N2Zz4K",SI="/static/crudkit_frontend/assets/favicon-Cit_9t8D.svg",Mh={app_name:"CrudKit",org_name:"CrudKit",logo_url:null,auth_mode:"password",storage_prefix:"crudkit",conversation_link_pattern:"deal|opportunity|case"};function CI(){try{const t=document.getElementById("crudkit-config");if(!t)return Mh;const e=JSON.parse(t.textContent||"");if(e&&typeof e=="object")return{...Mh,...e}}catch{}return Mh}const ws=CI();function TI(t){const e=Math.floor(t/3600),n=Math.floor(t%3600/60),r=t%60,o=e>0?`${e}:`:"",i=n<10&&e>0?`0${n}`:`${n}`,s=r<10?`0${r}`:`${r}`;return`${o}${i}:${s}`}const _I=()=>{const t=new J1,e=yr(),{user:n}=No(),{pathname:r}=et(),[o,i]=v.useState(!1),[s,a]=v.useState(""),[c,l]=v.useState(!1),[u,b]=v.useState(null),[M,f]=v.useState(0),[z,A]=v.useState(null),[m,g]=v.useState(0),[y,x]=v.useState(0),[L,R]=v.useState(0);v.useEffect(()=>{const P=r.split("/").filter(I=>I!=="");if(P.length===1&&/^[A-Z]{3}\d+$/.test(P[0])){const I=P[0],j=I.substring(0,3);l(!0),b({modelType:j,objectId:I})}else l(!1),b(null)},[r]);const{data:k,isLoading:w}=j1({queryKey:["activeWorkLog",n==null?void 0:n.id],queryFn:async()=>{try{const P=await t.list("WLG",{created_by:n==null?void 0:n.id,end_at__isnull:"True",deleted:"False",ordering:"-created_at"}),I=P!=null&&P.isPaginated?P.results:P;if(I&&Array.isArray(I)&&I.length>0){const j=I.filter(J=>J.end_at===null);return j.length>0?[j[0]]:j}return[]}catch(P){throw console.error("Error fetching active work logs:",P),P}},enabled:!!(n!=null&&n.id),refetchInterval:3e4});v.useEffect(()=>{Array.isArray(k)&&k.length>0&&k[0].notes&&a(k[0].notes)},[k]);const S=()=>{const P=Math.floor(M/3600),I=Math.floor(M%3600/60),j=M%60;g(P),x(I),R(j)};v.useEffect(()=>{if(Array.isArray(k)&&k.length>0&&k[0].end_at===null){const P=new Date(k[0].created_at),I=setInterval(()=>{const J=Math.floor((new Date-P)/1e3);f(J)},1e3);return A(I),()=>clearInterval(I)}else clearInterval(z),A(null),f(0)},[k]);const D=fi({mutationFn:()=>{if(u)return t.create("WLG",{related_object:u.objectId,notes:"",end_at:null})},onSuccess:P=>{P&&e.setQueryData(["activeWorkLog",n==null?void 0:n.id],[P]),e.refetchQueries({queryKey:["activeWorkLog"]}),U0.success("Time tracking started")},onError:P=>{console.error("Failed to start time tracking:",P),U0.error("Failed to start time tracking")}}),E=fi({mutationFn:()=>{if(!Array.isArray(k)||k.length===0)return;const P=k[0].id;return t.partialUpdate("WLG",P,{end_at:new Date().toISOString(),notes:s})},onSuccess:()=>{e.setQueryData(["activeWorkLog",n==null?void 0:n.id],[]),e.refetchQueries({queryKey:["activeWorkLog"]}),a(""),U0.success("Time tracking stopped")},onError:P=>{console.error("Failed to stop time tracking:",P),U0.error("Failed to stop time tracking")}}),H=fi({mutationFn:()=>{if(!Array.isArray(k)||k.length===0)return;const P=k[0].id,I=new Date(k[0].created_at),j=m*3600+y*60+L,J=M,G={notes:s};if(j!==J){const a0=new Date(I.getTime()+j*1e3);G.end_at=a0.toISOString()}return t.partialUpdate("WLG",P,G)},onSuccess:P=>{P&&P.end_at?e.setQueryData(["activeWorkLog",n==null?void 0:n.id],[]):P&&e.setQueryData(["activeWorkLog",n==null?void 0:n.id],[P]),e.refetchQueries({queryKey:["activeWorkLog"]}),i(!1),U0.success("Time tracking updated")},onError:P=>{console.error("Failed to update time tracking:",P),U0.error("Failed to update time tracking")}});if(w)return h.jsx("span",{className:"text-fg-3 text-xs px-1.5",children:"…"});const V=Array.isArray(k)&&k.length>0,_=V?k[0].related_object:"";return h.jsxs(h.Fragment,{children:[V?h.jsxs("span",{className:"inline-flex items-center gap-1.5 h-[26px] px-2 rounded-md bg-bg-2 border border-border-1 text-xs",children:[h.jsx("span",{className:"ck-mono text-fg-1 font-medium",children:TI(M)}),h.jsx(h1,{to:`/${_}`,className:"text-primary-300 hover:text-primary-200 ck-mono",children:_}),h.jsx("button",{type:"button",className:"ck-icon-btn ck-icon-btn-sm",onClick:()=>{S(),i(!0)},"aria-label":"Adjust tracked time",title:"Adjust tracked time",children:h.jsxs("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round",children:[h.jsx("path",{d:"M12 20h9"}),h.jsx("path",{d:"M16.5 3.5a2.121 2.121 0 1 1 3 3L7 19l-4 1 1-4 12.5-12.5z"})]})}),h.jsx("button",{type:"button",className:"ck-icon-btn ck-icon-btn-sm",onClick:()=>E.mutate(),"aria-label":"Stop time tracking",title:"Stop",style:{color:"var(--danger)"},children:h.jsx("svg",{width:"10",height:"10",viewBox:"0 0 24 24",fill:"currentColor",children:h.jsx("rect",{x:"6",y:"6",width:"12",height:"12",rx:"1"})})})]}):h.jsxs("button",{type:"button",className:"ck-btn ck-btn-secondary ck-btn-sm",disabled:!c,onClick:()=>D.mutate(),title:c?"Start time tracking":"Open a record to track time",children:[h.jsxs("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round",children:[h.jsx("circle",{cx:"12",cy:"12",r:"10"}),h.jsx("polyline",{points:"12 6 12 12 16 14"})]}),h.jsx("span",{children:"Start"})]}),o&&h.jsx("div",{className:"fixed inset-0 z-50 flex items-center justify-center",style:{background:"rgba(0,0,0,0.6)"},onClick:()=>i(!1),children:h.jsxs("div",{className:"rounded-lg border border-border-1 bg-bg-2 p-5 w-full max-w-md",onClick:P=>P.stopPropagation(),children:[h.jsx("h2",{className:"text-lg font-semibold text-fg-1 mb-4",children:"Complete with Custom Time"}),h.jsx("div",{className:"eyebrow mb-2",children:"Duration"}),h.jsx("div",{className:"flex gap-2 mb-3",children:[{label:"hours",value:m,set:g,max:null},{label:"min",value:y,set:x,max:59},{label:"sec",value:L,set:R,max:59}].map(({label:P,value:I,set:j,max:J})=>h.jsxs("div",{className:"flex-1 flex flex-col items-center",children:[h.jsx("input",{type:"number",inputMode:"numeric",value:String(I),onChange:G=>{const a0=parseInt(G.target.value)||0;j(J!=null?Math.min(a0,J):a0)},className:"ck-input font-mono text-center w-full",placeholder:"0"}),h.jsx("span",{className:"text-xs text-fg-3 mt-1",children:P})]},P))}),h.jsx("div",{className:"eyebrow mb-2",children:"Notes"}),h.jsx("textarea",{value:s,onChange:P=>a(P.target.value),className:"ck-input w-full",rows:4,placeholder:"Add notes about this work session"}),h.jsxs("div",{className:"mt-4 flex justify-end gap-2",children:[h.jsx("button",{type:"button",className:"ck-btn ck-btn-ghost ck-btn-sm",onClick:()=>i(!1),children:"Cancel"}),h.jsx("button",{type:"button",className:"ck-btn ck-btn-primary ck-btn-sm",onClick:()=>H.mutate(),children:"Save"})]})]})})]})},xw=new RegExp("([A-Z]{3})"),Iu=new RegExp("([A-Z]{3}\\d+)"),I6=t=>t?t.charAt(0).toUpperCase()+t.slice(1):"",W1=(t,e=null,n={},r="")=>{let o="";if(Iu.test(t)){const s=`/${t}`;if(e==="edit")o=`${s}/edit`;else if(e==="delete")o=`${s}/delete`;else{if(e!==null)throw Error(`Invalid action: ${e}`);o=s}}else if(xw.test(t))if(o=`/${t}/`,e==="create")o+="create";else if(e==="merge")o+="merge";else{if(e!==null)throw Error(`Invalid action: ${e}`);r!==""&&(o+="VIW/"+r)}else throw Error(`Invalid object name: ${t}`);const i=n?new URLSearchParams(n).toString():"";return i?`${o}?${i}`:o},X6=(t,e=null)=>{try{return W1(t,e),!0}catch{return!1}},j6={"all-objects":"All Objects",profile:"Profile",inbox:"Inbox",search:"Search"};function Rw(t,e={}){const n=[];if(!t)return n;const r=t.split("/").filter(Boolean);if(r.length===0)return n;const o=r[0];if(j6[o])return n.push({path:`/${o}`,text:j6[o]}),n;const i=o.match(/^([A-Z]{3})(\d+)$/),s=o.match(/^([A-Z]{3})$/);let a=null,c=null;if(i)a=i[1],c=o;else if(s)a=o;else return n.push({path:`/${o}`,text:I6(o)}),n;n.push({path:`/${a}`,text:I6(a),modelType:a}),c&&n.push({path:`/${c}`,text:c});const l=r[1];if(l==="edit")n.push({path:`/${c||a}/edit`,text:"Edit"});else if(l==="delete")n.push({path:`/${c||a}/delete`,text:"Delete"});else if(l==="create")n.push({path:`/${a}/create`,text:"Create"});else if(l==="merge")n.push({path:`/${a}/merge`,text:"Merge"});else if(l==="VIW"&&r.length>2){const u=r[2];n.push({path:`/${a}/VIW/${u}`,text:u,viewId:u})}return n}const F6=new J1;function EI(){var s,a;const{pathname:t}=et(),e=Rw(t),n=(s=e.find(c=>c.modelType))==null?void 0:s.modelType,{data:r}=j1({queryKey:["metadata",n],queryFn:()=>F6.metadata(n),enabled:!!n,staleTime:6e4}),o=(a=e.find(c=>c.viewId))==null?void 0:a.viewId,{data:i}=j1({queryKey:["detail","VIW",o],queryFn:()=>F6.retrieve("VIW",o),enabled:!!o,staleTime:6e4});return e.map((c,l)=>{const u=l===e.length-1;let b=c.text;return c.modelType&&(r!=null&&r.verbose_name_plural)&&(b=r.verbose_name_plural),c.viewId&&(i!=null&&i.name)&&(b=i.name),h.jsx("li",{"data-breadcrumbs-title":u?"true":"false",children:h.jsxs("div",{className:"flex items-center gap-1.5",children:[h.jsx("svg",{className:"h-3 w-3 flex-shrink-0 text-fg-4",fill:"none",viewBox:"0 0 24 24",strokeWidth:"1.5",stroke:"currentColor",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M8.25 4.5l7.5 7.5-7.5 7.5"})}),h.jsx(h1,{to:c.path,className:"text-sm truncate capitalize transition-colors duration-fast "+(u?"text-fg-1 font-semibold":"text-fg-2 hover:text-fg-1"),"aria-current":u?"page":void 0,children:b})]})},l)})}/**
858
+ `+i.statusText);throw u.statusCode=i.status,u.statusText=i.statusText,u.errorData=l,u.errorText=c,u}return i.clone()}async httpGet(e,n={}){return n?await this.fetch(`${e}?${new URLSearchParams(n)}`):await this.fetch(`${e}`)}async httpPost(e,n={},r={}){return await this.fetch(`${e}?${new URLSearchParams(r)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})}async httpPut(e,n){return await this.fetch(e,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})}async httpPatch(e,n){return await this.fetch(e,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})}async httpDelete(e){return await this.fetch(e,{method:"DELETE"})}async httpOptions(e){return await this.fetch(e,{method:"OPTIONS"})}async list(e,n={}){for(const i in n)typeof n[i]=="boolean"&&(n[i]=n[i]?"True":"False");const o=await(await this.httpGet(`api/v1/${e}/`,n)).json();return o&&o.results!==void 0?{results:o.results,count:o.count,next:o.next,previous:o.previous,page_size:o.page_size,current_page:o.current_page,total_pages:o.total_pages,isPaginated:!0}:o}async getNextPage(e){if(!e)return null;const n=new URL(e),r=n.pathname+n.search;return await(await this.fetch(r.substring(1))).json()}async getPreviousPage(e){if(!e)return null;const n=new URL(e),r=n.pathname+n.search;return await(await this.fetch(r.substring(1))).json()}async retrieve(e,n){return await this.httpGet(`api/v1/${e}/${n}/`).then(r=>r.json())}async create(e,n,r){return await this.httpPost(`api/v1/${e}/`,n,r).then(o=>o.json())}async update(e,n,r){return await this.httpPut(`api/v1/${e}/${n}/`,r).then(o=>o.json())}async partialUpdate(e,n,r){return await this.httpPatch(`api/v1/${e}/${n}/`,r).then(o=>o.json())}async delete(e,n){return await this.httpDelete(`api/v1/${e}/${n}/`)}async metadata(e){return await this.httpGet(`api/v1/${e}/metadata/`).then(n=>n.json())}async initial(e,n={}){return(await(await this.httpGet(`api/v1/${e}/initial/`,n)).json()).fields||{}}async action(e,n,r){return await this.httpPost(`api/v1/${e}/${n}/action/`,{action:r}).then(o=>o.json())}async merge(e,n,r){try{return await(await this.httpPost(`api/v1/${e}/${n}/merge/`,r)).json()}catch(o){throw console.error("Merge operation failed:",o),o.errorData&&(console.log("Server returned error data:",o.errorData),o.errorData.errors&&(o.serverErrors=o.errorData.errors)),o}}async search(e=""){return await this.httpGet("api/v1/search/",{q:e}).then(n=>n.json())}}const Pu=new J1;async function yw(t){return Pu.metadata(t)}async function RI(t,e){return Pu.retrieve(t,e)}async function Ww(t,e){return Pu.list(t,e||{})}async function LI(t,e,n){return Pu.action(t,e,n)}function NI(){return Pu.getAccessToken()}const vw=v.createContext({user:null,loading:!0,isAuthenticated:!1,login:async()=>{throw new Error("AuthProvider not mounted")},logout:async()=>{}}),gd=new J1;function kI({children:t}){const[e,n]=v.useState(null),[r,o]=v.useState(!0);v.useEffect(()=>{(async()=>{try{try{const c=await gd.httpGet("api/v1/user/me/").then(l=>l.json());n(c)}catch(c){(c==null?void 0:c.statusCode)===401||(c==null?void 0:c.statusCode)===403?(console.log("Server rejected stored credentials, clearing tokens"),gd.clearTokens()):console.log("Auth check failed transiently; keeping tokens",c)}}finally{o(!1)}})()},[]);const i=async(a,c)=>{try{o(!0);const l=await gd.login(a,c);return n(l),l}finally{o(!1)}},s=async()=>{o(!0),await gd.logout(),n(null),o(!1)};return h.jsx(vw.Provider,{value:{user:e,login:i,logout:s,loading:r,isAuthenticated:!!e},children:t})}function No(){return v.useContext(vw)}const wI="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCIgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0Ij4KICA8cmVjdCB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHJ4PSIxNCIgZmlsbD0iIzYzNjZmMSIgLz4KICA8dGV4dAogICAgeD0iMzIiCiAgICB5PSIzNCIKICAgIHRleHQtYW5jaG9yPSJtaWRkbGUiCiAgICBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCIKICAgIGZvbnQtZmFtaWx5PSJzeXN0ZW0tdWksIC1hcHBsZS1zeXN0ZW0sICdTZWdvZSBVSScsIHNhbnMtc2VyaWYiCiAgICBmb250LXNpemU9IjI2IgogICAgZm9udC13ZWlnaHQ9IjcwMCIKICAgIGxldHRlci1zcGFjaW5nPSItMSIKICAgIGZpbGw9IiNmZmZmZmYiCiAgPkNLPC90ZXh0Pgo8L3N2Zz4K",SI=""+(window.__CRUDKIT_STATIC_URL__||"/static/")+"crudkit_frontend/assets/favicon-Cit_9t8D.svg",Mh={app_name:"CrudKit",org_name:"CrudKit",logo_url:null,auth_mode:"password",storage_prefix:"crudkit",conversation_link_pattern:"deal|opportunity|case"};function CI(){try{const t=document.getElementById("crudkit-config");if(!t)return Mh;const e=JSON.parse(t.textContent||"");if(e&&typeof e=="object")return{...Mh,...e}}catch{}return Mh}const ws=CI();function TI(t){const e=Math.floor(t/3600),n=Math.floor(t%3600/60),r=t%60,o=e>0?`${e}:`:"",i=n<10&&e>0?`0${n}`:`${n}`,s=r<10?`0${r}`:`${r}`;return`${o}${i}:${s}`}const _I=()=>{const t=new J1,e=yr(),{user:n}=No(),{pathname:r}=et(),[o,i]=v.useState(!1),[s,a]=v.useState(""),[c,l]=v.useState(!1),[u,b]=v.useState(null),[M,f]=v.useState(0),[z,A]=v.useState(null),[m,g]=v.useState(0),[y,x]=v.useState(0),[L,R]=v.useState(0);v.useEffect(()=>{const P=r.split("/").filter(I=>I!=="");if(P.length===1&&/^[A-Z]{3}\d+$/.test(P[0])){const I=P[0],j=I.substring(0,3);l(!0),b({modelType:j,objectId:I})}else l(!1),b(null)},[r]);const{data:k,isLoading:w}=j1({queryKey:["activeWorkLog",n==null?void 0:n.id],queryFn:async()=>{try{const P=await t.list("WLG",{created_by:n==null?void 0:n.id,end_at__isnull:"True",deleted:"False",ordering:"-created_at"}),I=P!=null&&P.isPaginated?P.results:P;if(I&&Array.isArray(I)&&I.length>0){const j=I.filter(J=>J.end_at===null);return j.length>0?[j[0]]:j}return[]}catch(P){throw console.error("Error fetching active work logs:",P),P}},enabled:!!(n!=null&&n.id),refetchInterval:3e4});v.useEffect(()=>{Array.isArray(k)&&k.length>0&&k[0].notes&&a(k[0].notes)},[k]);const S=()=>{const P=Math.floor(M/3600),I=Math.floor(M%3600/60),j=M%60;g(P),x(I),R(j)};v.useEffect(()=>{if(Array.isArray(k)&&k.length>0&&k[0].end_at===null){const P=new Date(k[0].created_at),I=setInterval(()=>{const J=Math.floor((new Date-P)/1e3);f(J)},1e3);return A(I),()=>clearInterval(I)}else clearInterval(z),A(null),f(0)},[k]);const D=fi({mutationFn:()=>{if(u)return t.create("WLG",{related_object:u.objectId,notes:"",end_at:null})},onSuccess:P=>{P&&e.setQueryData(["activeWorkLog",n==null?void 0:n.id],[P]),e.refetchQueries({queryKey:["activeWorkLog"]}),U0.success("Time tracking started")},onError:P=>{console.error("Failed to start time tracking:",P),U0.error("Failed to start time tracking")}}),E=fi({mutationFn:()=>{if(!Array.isArray(k)||k.length===0)return;const P=k[0].id;return t.partialUpdate("WLG",P,{end_at:new Date().toISOString(),notes:s})},onSuccess:()=>{e.setQueryData(["activeWorkLog",n==null?void 0:n.id],[]),e.refetchQueries({queryKey:["activeWorkLog"]}),a(""),U0.success("Time tracking stopped")},onError:P=>{console.error("Failed to stop time tracking:",P),U0.error("Failed to stop time tracking")}}),H=fi({mutationFn:()=>{if(!Array.isArray(k)||k.length===0)return;const P=k[0].id,I=new Date(k[0].created_at),j=m*3600+y*60+L,J=M,G={notes:s};if(j!==J){const a0=new Date(I.getTime()+j*1e3);G.end_at=a0.toISOString()}return t.partialUpdate("WLG",P,G)},onSuccess:P=>{P&&P.end_at?e.setQueryData(["activeWorkLog",n==null?void 0:n.id],[]):P&&e.setQueryData(["activeWorkLog",n==null?void 0:n.id],[P]),e.refetchQueries({queryKey:["activeWorkLog"]}),i(!1),U0.success("Time tracking updated")},onError:P=>{console.error("Failed to update time tracking:",P),U0.error("Failed to update time tracking")}});if(w)return h.jsx("span",{className:"text-fg-3 text-xs px-1.5",children:"…"});const V=Array.isArray(k)&&k.length>0,_=V?k[0].related_object:"";return h.jsxs(h.Fragment,{children:[V?h.jsxs("span",{className:"inline-flex items-center gap-1.5 h-[26px] px-2 rounded-md bg-bg-2 border border-border-1 text-xs",children:[h.jsx("span",{className:"ck-mono text-fg-1 font-medium",children:TI(M)}),h.jsx(h1,{to:`/${_}`,className:"text-primary-300 hover:text-primary-200 ck-mono",children:_}),h.jsx("button",{type:"button",className:"ck-icon-btn ck-icon-btn-sm",onClick:()=>{S(),i(!0)},"aria-label":"Adjust tracked time",title:"Adjust tracked time",children:h.jsxs("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round",children:[h.jsx("path",{d:"M12 20h9"}),h.jsx("path",{d:"M16.5 3.5a2.121 2.121 0 1 1 3 3L7 19l-4 1 1-4 12.5-12.5z"})]})}),h.jsx("button",{type:"button",className:"ck-icon-btn ck-icon-btn-sm",onClick:()=>E.mutate(),"aria-label":"Stop time tracking",title:"Stop",style:{color:"var(--danger)"},children:h.jsx("svg",{width:"10",height:"10",viewBox:"0 0 24 24",fill:"currentColor",children:h.jsx("rect",{x:"6",y:"6",width:"12",height:"12",rx:"1"})})})]}):h.jsxs("button",{type:"button",className:"ck-btn ck-btn-secondary ck-btn-sm",disabled:!c,onClick:()=>D.mutate(),title:c?"Start time tracking":"Open a record to track time",children:[h.jsxs("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round",children:[h.jsx("circle",{cx:"12",cy:"12",r:"10"}),h.jsx("polyline",{points:"12 6 12 12 16 14"})]}),h.jsx("span",{children:"Start"})]}),o&&h.jsx("div",{className:"fixed inset-0 z-50 flex items-center justify-center",style:{background:"rgba(0,0,0,0.6)"},onClick:()=>i(!1),children:h.jsxs("div",{className:"rounded-lg border border-border-1 bg-bg-2 p-5 w-full max-w-md",onClick:P=>P.stopPropagation(),children:[h.jsx("h2",{className:"text-lg font-semibold text-fg-1 mb-4",children:"Complete with Custom Time"}),h.jsx("div",{className:"eyebrow mb-2",children:"Duration"}),h.jsx("div",{className:"flex gap-2 mb-3",children:[{label:"hours",value:m,set:g,max:null},{label:"min",value:y,set:x,max:59},{label:"sec",value:L,set:R,max:59}].map(({label:P,value:I,set:j,max:J})=>h.jsxs("div",{className:"flex-1 flex flex-col items-center",children:[h.jsx("input",{type:"number",inputMode:"numeric",value:String(I),onChange:G=>{const a0=parseInt(G.target.value)||0;j(J!=null?Math.min(a0,J):a0)},className:"ck-input font-mono text-center w-full",placeholder:"0"}),h.jsx("span",{className:"text-xs text-fg-3 mt-1",children:P})]},P))}),h.jsx("div",{className:"eyebrow mb-2",children:"Notes"}),h.jsx("textarea",{value:s,onChange:P=>a(P.target.value),className:"ck-input w-full",rows:4,placeholder:"Add notes about this work session"}),h.jsxs("div",{className:"mt-4 flex justify-end gap-2",children:[h.jsx("button",{type:"button",className:"ck-btn ck-btn-ghost ck-btn-sm",onClick:()=>i(!1),children:"Cancel"}),h.jsx("button",{type:"button",className:"ck-btn ck-btn-primary ck-btn-sm",onClick:()=>H.mutate(),children:"Save"})]})]})})]})},xw=new RegExp("([A-Z]{3})"),Iu=new RegExp("([A-Z]{3}\\d+)"),I6=t=>t?t.charAt(0).toUpperCase()+t.slice(1):"",W1=(t,e=null,n={},r="")=>{let o="";if(Iu.test(t)){const s=`/${t}`;if(e==="edit")o=`${s}/edit`;else if(e==="delete")o=`${s}/delete`;else{if(e!==null)throw Error(`Invalid action: ${e}`);o=s}}else if(xw.test(t))if(o=`/${t}/`,e==="create")o+="create";else if(e==="merge")o+="merge";else{if(e!==null)throw Error(`Invalid action: ${e}`);r!==""&&(o+="VIW/"+r)}else throw Error(`Invalid object name: ${t}`);const i=n?new URLSearchParams(n).toString():"";return i?`${o}?${i}`:o},X6=(t,e=null)=>{try{return W1(t,e),!0}catch{return!1}},j6={"all-objects":"All Objects",profile:"Profile",inbox:"Inbox",search:"Search"};function Rw(t,e={}){const n=[];if(!t)return n;const r=t.split("/").filter(Boolean);if(r.length===0)return n;const o=r[0];if(j6[o])return n.push({path:`/${o}`,text:j6[o]}),n;const i=o.match(/^([A-Z]{3})(\d+)$/),s=o.match(/^([A-Z]{3})$/);let a=null,c=null;if(i)a=i[1],c=o;else if(s)a=o;else return n.push({path:`/${o}`,text:I6(o)}),n;n.push({path:`/${a}`,text:I6(a),modelType:a}),c&&n.push({path:`/${c}`,text:c});const l=r[1];if(l==="edit")n.push({path:`/${c||a}/edit`,text:"Edit"});else if(l==="delete")n.push({path:`/${c||a}/delete`,text:"Delete"});else if(l==="create")n.push({path:`/${a}/create`,text:"Create"});else if(l==="merge")n.push({path:`/${a}/merge`,text:"Merge"});else if(l==="VIW"&&r.length>2){const u=r[2];n.push({path:`/${a}/VIW/${u}`,text:u,viewId:u})}return n}const F6=new J1;function EI(){var s,a;const{pathname:t}=et(),e=Rw(t),n=(s=e.find(c=>c.modelType))==null?void 0:s.modelType,{data:r}=j1({queryKey:["metadata",n],queryFn:()=>F6.metadata(n),enabled:!!n,staleTime:6e4}),o=(a=e.find(c=>c.viewId))==null?void 0:a.viewId,{data:i}=j1({queryKey:["detail","VIW",o],queryFn:()=>F6.retrieve("VIW",o),enabled:!!o,staleTime:6e4});return e.map((c,l)=>{const u=l===e.length-1;let b=c.text;return c.modelType&&(r!=null&&r.verbose_name_plural)&&(b=r.verbose_name_plural),c.viewId&&(i!=null&&i.name)&&(b=i.name),h.jsx("li",{"data-breadcrumbs-title":u?"true":"false",children:h.jsxs("div",{className:"flex items-center gap-1.5",children:[h.jsx("svg",{className:"h-3 w-3 flex-shrink-0 text-fg-4",fill:"none",viewBox:"0 0 24 24",strokeWidth:"1.5",stroke:"currentColor",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M8.25 4.5l7.5 7.5-7.5 7.5"})}),h.jsx(h1,{to:c.path,className:"text-sm truncate capitalize transition-colors duration-fast "+(u?"text-fg-1 font-semibold":"text-fg-2 hover:text-fg-1"),"aria-current":u?"page":void 0,children:b})]})},l)})}/**
859
859
  * @license lucide-react v1.14.0 - ISC
860
860
  *
861
861
  * This source code is licensed under the ISC license.
@@ -0,0 +1,21 @@
1
+ {% load static %}
2
+ <!doctype html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <link rel="icon" type="image/svg+xml" href="{% static "crudkit_frontend/assets/favicon-Cit_9t8D.svg" %}" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
+ <!-- Rendered by Django in prod; ignored in Vite dev. -->
9
+ <meta name="csrf-token" content="{{ csrf_token }}" />
10
+ <script id="crudkit-config" type="application/json">{{ crudkit_config_json }}</script>
11
+ <!-- Non-module script, so Vite leaves it alone and it runs before the
12
+ deferred bundle: runtime-built asset URLs resolve against it. -->
13
+ <script>window.__CRUDKIT_STATIC_URL__ = {{ crudkit_static_url_json }};</script>
14
+ <title>{{ crudkit_app_name|default:"CrudKit" }}</title>
15
+ <script type="module" crossorigin src="{% static "crudkit_frontend/assets/index-Cj0Mdbti.js" %}"></script>
16
+ <link rel="stylesheet" crossorigin href="{% static "crudkit_frontend/assets/index-CL5BO7Fx.css" %}">
17
+ </head>
18
+ <body>
19
+ <div id="root"></div>
20
+ </body>
21
+ </html>
@@ -1,8 +1,10 @@
1
- <!doctype html>
1
+ {% load static %}<!doctype html>
2
2
  <html lang="en">
3
3
  <head>
4
+ <link rel="icon" href="{% static "crudkit_frontend/assets/favicon.svg" %}" />
4
5
  <meta name="csrf-token" content="{{ csrf_token }}" />
5
6
  <script id="crudkit-config" type="application/json">{{ crudkit_config_json }}</script>
7
+ <script>window.__CRUDKIT_STATIC_URL__ = {{ crudkit_static_url_json }};</script>
6
8
  <title>{{ crudkit_app_name|default:"CrudKit" }}</title>
7
9
  </head>
8
10
  <body>
@@ -54,6 +54,25 @@ class CrudkitConfigContextProcessorTests(TestCase):
54
54
  response = self.client.get("/")
55
55
  self.assertContains(response, "<title>CrudKit</title>", html=False)
56
56
 
57
+ def test_static_url_in_context(self):
58
+ context = crudkit_config(request=None)
59
+ self.assertEqual(context["crudkit_static_url_json"], '"/static/"')
60
+
61
+ response = self.client.get("/")
62
+ self.assertContains(response, 'window.__CRUDKIT_STATIC_URL__ = "/static/"')
63
+ self.assertContains(response, "/static/crudkit_frontend/assets/favicon.svg")
64
+
65
+ @override_settings(STATIC_URL="media-x/")
66
+ def test_nonstandard_static_url_prefix(self):
67
+ # Django auto-prefixes STATIC_URL with "/" on access.
68
+ context = crudkit_config(request=None)
69
+ self.assertEqual(context["crudkit_static_url_json"], '"/media-x/"')
70
+
71
+ response = self.client.get("/")
72
+ self.assertContains(response, 'window.__CRUDKIT_STATIC_URL__ = "/media-x/"')
73
+ self.assertContains(response, "/media-x/crudkit_frontend/assets/favicon.svg")
74
+ self.assertNotContains(response, "/static/")
75
+
57
76
 
58
77
  @override_settings(TEMPLATES=STUB_TEMPLATES, ROOT_URLCONF="crudkit_frontend.tests.urls")
59
78
  class SpaViewTests(TestCase):
@@ -1,17 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="/static/crudkit_frontend/assets/favicon-Cit_9t8D.svg" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <!-- Rendered by Django in prod; ignored in Vite dev. -->
8
- <meta name="csrf-token" content="{{ csrf_token }}" />
9
- <script id="crudkit-config" type="application/json">{{ crudkit_config_json }}</script>
10
- <title>{{ crudkit_app_name|default:"CrudKit" }}</title>
11
- <script type="module" crossorigin src="/static/crudkit_frontend/assets/index-BUe8jarF.js"></script>
12
- <link rel="stylesheet" crossorigin href="/static/crudkit_frontend/assets/index-CL5BO7Fx.css">
13
- </head>
14
- <body>
15
- <div id="root"></div>
16
- </body>
17
- </html>
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes