ember-tribe 3.0.2 → 3.0.4
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.
- package/blueprints/ember-tribe/files/app/app.js +25 -0
- package/blueprints/ember-tribe/files/app/services/store.js +837 -0
- package/blueprints/ember-tribe/files/app/services/types.js +18 -38
- package/blueprints/ember-tribe/files/app/templates/application.gjs +6 -0
- package/blueprints/ember-tribe/files/{app/styles → public/assets/scss}/app.scss +2 -17
- package/blueprints/ember-tribe/index.js +0 -3
- package/package.json +1 -1
- package/blueprints/ember-tribe/files/app/templates/application.hbs +0 -2
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import '@warp-drive/ember/install';
|
|
2
|
+
import Application from '@ember/application';
|
|
3
|
+
import compatModules from '@embroider/virtual/compat-modules';
|
|
4
|
+
import Resolver from 'ember-resolver';
|
|
5
|
+
import loadInitializers from 'ember-load-initializers';
|
|
6
|
+
import config from 'new/config/environment';
|
|
7
|
+
import 'ember-power-select/styles';
|
|
8
|
+
import { importSync, isDevelopingApp, macroCondition } from '@embroider/macros';
|
|
9
|
+
import setupInspector from '@embroider/legacy-inspector-support/ember-source-4.12';
|
|
10
|
+
|
|
11
|
+
//SCSS include
|
|
12
|
+
import '../public/assets/scss/app.scss';
|
|
13
|
+
|
|
14
|
+
if (macroCondition(isDevelopingApp())) {
|
|
15
|
+
importSync('./deprecation-workflow');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default class App extends Application {
|
|
19
|
+
modulePrefix = config.modulePrefix;
|
|
20
|
+
podModulePrefix = config.podModulePrefix;
|
|
21
|
+
Resolver = Resolver.withModules(compatModules);
|
|
22
|
+
inspector = setupInspector(this);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
loadInitializers(App, config.modulePrefix, compatModules);
|
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
import Service from '@ember/service';
|
|
2
|
+
import { tracked } from '@glimmer/tracking';
|
|
3
|
+
import { action } from '@ember/object';
|
|
4
|
+
import ENV from '<%= dasherizedPackageName %>/config/environment';
|
|
5
|
+
import { TrackedArray, TrackedObject } from 'tracked-built-ins';
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Utility helpers
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Convert a camelCase or dasherized string to snake_case.
|
|
13
|
+
*/
|
|
14
|
+
function underscore(str) {
|
|
15
|
+
return str
|
|
16
|
+
.replace(/::/g, '/')
|
|
17
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
|
18
|
+
.replace(/([a-z\d])([A-Z])/g, '$1_$2')
|
|
19
|
+
.replace(/-/g, '_')
|
|
20
|
+
.toLowerCase();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Convert snake_case or dasherized to camelCase.
|
|
25
|
+
*/
|
|
26
|
+
function camelize(str) {
|
|
27
|
+
return str
|
|
28
|
+
.replace(/[-_](.)/g, (_, c) => c.toUpperCase())
|
|
29
|
+
.replace(/^(.)/, (_, c) => c.toLowerCase());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Convert between model name representations:
|
|
34
|
+
* 'blogPost' | 'blog-post' | 'blog_post' → canonical snake_case 'blog_post'
|
|
35
|
+
*/
|
|
36
|
+
function normalizeType(type) {
|
|
37
|
+
return underscore(type.replace(/-/g, '_'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Dasherize for path segments: 'blog_post' → 'blog-post'
|
|
42
|
+
*/
|
|
43
|
+
function dasherize(str) {
|
|
44
|
+
return underscore(str).replace(/_/g, '-');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// RecordArray — a tracked array-like that also carries `.meta`
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
class RecordArray {
|
|
52
|
+
@tracked _records;
|
|
53
|
+
@tracked meta;
|
|
54
|
+
|
|
55
|
+
constructor(records = [], meta = {}) {
|
|
56
|
+
this._records = new TrackedArray(records);
|
|
57
|
+
this.meta = meta;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// MutableArray compat
|
|
61
|
+
get length() { return this._records.length; }
|
|
62
|
+
|
|
63
|
+
objectAt(idx) { return this._records[idx]; }
|
|
64
|
+
|
|
65
|
+
forEach(fn) { this._records.forEach(fn); }
|
|
66
|
+
map(fn) { return this._records.map(fn); }
|
|
67
|
+
filter(fn) { return this._records.filter(fn); }
|
|
68
|
+
find(fn) { return this._records.find(fn); }
|
|
69
|
+
reduce(fn, init) { return this._records.reduce(fn, init); }
|
|
70
|
+
some(fn) { return this._records.some(fn); }
|
|
71
|
+
every(fn) { return this._records.every(fn); }
|
|
72
|
+
includes(r) { return this._records.includes(r); }
|
|
73
|
+
indexOf(r) { return this._records.indexOf(r); }
|
|
74
|
+
slice(...a) { return this._records.slice(...a); }
|
|
75
|
+
toArray() { return [...this._records]; }
|
|
76
|
+
get firstObject() { return this._records[0]; }
|
|
77
|
+
get lastObject() { return this._records[this._records.length - 1]; }
|
|
78
|
+
|
|
79
|
+
push(record) { this._records.push(record); }
|
|
80
|
+
pushObject(record) { this._records.push(record); }
|
|
81
|
+
removeObject(record) {
|
|
82
|
+
const idx = this._records.indexOf(record);
|
|
83
|
+
if (idx !== -1) this._records.splice(idx, 1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** ES iterator support — allows `for...of` and spread. */
|
|
87
|
+
[Symbol.iterator]() { return this._records[Symbol.iterator](); }
|
|
88
|
+
|
|
89
|
+
/** Internal: wholesale replace content. */
|
|
90
|
+
_replace(records, meta) {
|
|
91
|
+
this._records.splice(0, this._records.length, ...records);
|
|
92
|
+
if (meta !== undefined) this.meta = meta;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Record — a reactive proxy object representing a single resource
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
let _nextClientId = 1;
|
|
101
|
+
|
|
102
|
+
class Record {
|
|
103
|
+
// ---- internal bookkeeping (not enumerable) ----
|
|
104
|
+
@tracked _type;
|
|
105
|
+
@tracked _id;
|
|
106
|
+
@tracked _clientId;
|
|
107
|
+
@tracked _attributes; // TrackedObject of current attrs
|
|
108
|
+
@tracked _originalAttrs; // snapshot at last clean state
|
|
109
|
+
@tracked _relationships; // TrackedObject { key: id | [ids] }
|
|
110
|
+
@tracked _errors; // TrackedArray
|
|
111
|
+
@tracked _isNew;
|
|
112
|
+
@tracked _isDeleted;
|
|
113
|
+
@tracked _isSaving;
|
|
114
|
+
@tracked _store; // back-reference
|
|
115
|
+
|
|
116
|
+
constructor(store, type, id, attributes = {}, relationships = {}, isNew = false) {
|
|
117
|
+
this._store = store;
|
|
118
|
+
this._type = type;
|
|
119
|
+
this._id = id;
|
|
120
|
+
this._clientId = `client-${_nextClientId++}`;
|
|
121
|
+
this._attributes = new TrackedObject({ ...attributes });
|
|
122
|
+
this._originalAttrs = { ...attributes };
|
|
123
|
+
this._relationships = new TrackedObject({ ...relationships });
|
|
124
|
+
this._errors = new TrackedArray([]);
|
|
125
|
+
this._isNew = isNew;
|
|
126
|
+
this._isDeleted = false;
|
|
127
|
+
this._isSaving = false;
|
|
128
|
+
|
|
129
|
+
// Return a Proxy so arbitrary attribute access works: record.title, record.slug, etc.
|
|
130
|
+
return new Proxy(this, {
|
|
131
|
+
get(target, prop, receiver) {
|
|
132
|
+
// Prioritise explicit Record properties / methods
|
|
133
|
+
if (prop in target || typeof prop === 'symbol') {
|
|
134
|
+
return Reflect.get(target, prop, receiver);
|
|
135
|
+
}
|
|
136
|
+
// Relationship?
|
|
137
|
+
const schema = store._schemaFor(type);
|
|
138
|
+
if (schema && schema.relationships && schema.relationships[prop]) {
|
|
139
|
+
return target._resolveRelationship(prop);
|
|
140
|
+
}
|
|
141
|
+
// Attribute?
|
|
142
|
+
if (target._attributes && prop in target._attributes) {
|
|
143
|
+
return target._attributes[prop];
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
set(target, prop, value) {
|
|
149
|
+
if (prop in target || typeof prop === 'symbol' || prop.startsWith('_')) {
|
|
150
|
+
target[prop] = value;
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
// Relationship?
|
|
154
|
+
const schema = store._schemaFor(type);
|
|
155
|
+
if (schema && schema.relationships && schema.relationships[prop]) {
|
|
156
|
+
target._setRelationship(prop, value);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
// Attribute
|
|
160
|
+
target._attributes[prop] = value;
|
|
161
|
+
return true;
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
has(target, prop) {
|
|
165
|
+
if (prop in target) return true;
|
|
166
|
+
if (target._attributes && prop in target._attributes) return true;
|
|
167
|
+
return false;
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
ownKeys(target) {
|
|
171
|
+
const attrKeys = target._attributes ? Object.keys(target._attributes) : [];
|
|
172
|
+
return [...new Set([...Reflect.ownKeys(target), ...attrKeys])];
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
176
|
+
if (target._attributes && prop in target._attributes) {
|
|
177
|
+
return { configurable: true, enumerable: true, value: target._attributes[prop] };
|
|
178
|
+
}
|
|
179
|
+
return Reflect.getOwnPropertyDescriptor(target, prop);
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---- public computed-style accessors ----
|
|
185
|
+
get id() { return this._id; }
|
|
186
|
+
set id(v) { this._id = v; }
|
|
187
|
+
|
|
188
|
+
get isNew() { return this._isNew; }
|
|
189
|
+
get isDeleted() { return this._isDeleted; }
|
|
190
|
+
get isSaving() { return this._isSaving; }
|
|
191
|
+
get errors() { return this._errors; }
|
|
192
|
+
|
|
193
|
+
get hasDirtyAttributes() {
|
|
194
|
+
const keys = new Set([
|
|
195
|
+
...Object.keys(this._attributes),
|
|
196
|
+
...Object.keys(this._originalAttrs),
|
|
197
|
+
]);
|
|
198
|
+
for (const k of keys) {
|
|
199
|
+
if (this._attributes[k] !== this._originalAttrs[k]) return true;
|
|
200
|
+
}
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
changedAttributes() {
|
|
205
|
+
const diff = {};
|
|
206
|
+
const keys = new Set([
|
|
207
|
+
...Object.keys(this._attributes),
|
|
208
|
+
...Object.keys(this._originalAttrs),
|
|
209
|
+
]);
|
|
210
|
+
for (const k of keys) {
|
|
211
|
+
if (this._attributes[k] !== this._originalAttrs[k]) {
|
|
212
|
+
diff[k] = [this._originalAttrs[k], this._attributes[k]];
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return diff;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
rollbackAttributes() {
|
|
219
|
+
for (const k of Object.keys(this._attributes)) {
|
|
220
|
+
if (!(k in this._originalAttrs)) {
|
|
221
|
+
delete this._attributes[k];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
for (const [k, v] of Object.entries(this._originalAttrs)) {
|
|
225
|
+
this._attributes[k] = v;
|
|
226
|
+
}
|
|
227
|
+
if (this._isNew) {
|
|
228
|
+
this._store._unloadRecord(this);
|
|
229
|
+
}
|
|
230
|
+
this._isDeleted = false;
|
|
231
|
+
this._errors.splice(0, this._errors.length);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ---- persistence ----
|
|
235
|
+
|
|
236
|
+
async save() {
|
|
237
|
+
this._isSaving = true;
|
|
238
|
+
this._errors.splice(0, this._errors.length);
|
|
239
|
+
try {
|
|
240
|
+
if (this._isDeleted) {
|
|
241
|
+
await this._store._deleteRemote(this);
|
|
242
|
+
this._store._unloadRecord(this);
|
|
243
|
+
} else if (this._isNew) {
|
|
244
|
+
await this._store._createRemote(this);
|
|
245
|
+
this._isNew = false;
|
|
246
|
+
} else {
|
|
247
|
+
await this._store._updateRemote(this);
|
|
248
|
+
}
|
|
249
|
+
// Snapshot clean state
|
|
250
|
+
this._originalAttrs = { ...this._attributes };
|
|
251
|
+
} finally {
|
|
252
|
+
this._isSaving = false;
|
|
253
|
+
}
|
|
254
|
+
return this;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
deleteRecord() {
|
|
258
|
+
this._isDeleted = true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async destroyRecord() {
|
|
262
|
+
this.deleteRecord();
|
|
263
|
+
return this.save();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ---- relationships (resolved lazily) ----
|
|
267
|
+
|
|
268
|
+
_resolveRelationship(name) {
|
|
269
|
+
const schema = this._store._schemaFor(this._type);
|
|
270
|
+
const rel = schema.relationships[name];
|
|
271
|
+
const raw = this._relationships[name];
|
|
272
|
+
|
|
273
|
+
if (rel.kind === 'belongsTo') {
|
|
274
|
+
if (!raw) return null;
|
|
275
|
+
// Return a promise that resolves to the related record
|
|
276
|
+
const relType = rel.type;
|
|
277
|
+
const cached = this._store.peekRecord(relType, raw);
|
|
278
|
+
if (cached) return Promise.resolve(cached);
|
|
279
|
+
return this._store.findRecord(relType, raw);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// hasMany — return a promise resolving to a RecordArray
|
|
283
|
+
const ids = Array.isArray(raw) ? raw : [];
|
|
284
|
+
const loaded = ids
|
|
285
|
+
.map((rid) => this._store.peekRecord(rel.type, rid))
|
|
286
|
+
.filter(Boolean);
|
|
287
|
+
if (loaded.length === ids.length) {
|
|
288
|
+
return Promise.resolve(new RecordArray(loaded));
|
|
289
|
+
}
|
|
290
|
+
// Fetch any missing
|
|
291
|
+
return Promise.all(
|
|
292
|
+
ids.map((rid) => {
|
|
293
|
+
const cached = this._store.peekRecord(rel.type, rid);
|
|
294
|
+
return cached ? cached : this._store.findRecord(rel.type, rid);
|
|
295
|
+
}),
|
|
296
|
+
).then((records) => new RecordArray(records));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
_setRelationship(name, value) {
|
|
300
|
+
const schema = this._store._schemaFor(this._type);
|
|
301
|
+
const rel = schema.relationships[name];
|
|
302
|
+
|
|
303
|
+
if (rel.kind === 'belongsTo') {
|
|
304
|
+
if (value === null) {
|
|
305
|
+
this._relationships[name] = null;
|
|
306
|
+
} else {
|
|
307
|
+
this._relationships[name] = value._id ?? value.id ?? value;
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
// hasMany — accept array of records or ids
|
|
311
|
+
if (Array.isArray(value)) {
|
|
312
|
+
this._relationships[name] = value.map((v) => v._id ?? v.id ?? v);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Serialise for the network
|
|
318
|
+
toJSON() {
|
|
319
|
+
return { ...this._attributes };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
// Store Service
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
export default class StoreService extends Service {
|
|
328
|
+
// Identity map: Map<normalizedType, Map<id, Record>>
|
|
329
|
+
_cache = new Map();
|
|
330
|
+
|
|
331
|
+
// Live arrays returned by peekAll (kept in sync)
|
|
332
|
+
_liveArrays = new Map();
|
|
333
|
+
|
|
334
|
+
// Schema registry derived from the Tribe blueprint
|
|
335
|
+
// Map<normalizedType, { attributes: { slug: varType }, relationships: { slug: { kind, type, inverse } } }>
|
|
336
|
+
_schemas = new Map();
|
|
337
|
+
|
|
338
|
+
// Metadata cache per type (from last server response)
|
|
339
|
+
_meta = new Map();
|
|
340
|
+
|
|
341
|
+
// Base networking config (mirrors the old adapter)
|
|
342
|
+
get _host() { return ENV.TribeENV.API_URL; }
|
|
343
|
+
get _namespace() { return 'api.php'; }
|
|
344
|
+
get _headers() {
|
|
345
|
+
return {
|
|
346
|
+
Authorization: `Bearer ${ENV.TribeENV.API_KEY}`,
|
|
347
|
+
'Content-Type': 'application/json',
|
|
348
|
+
Accept: 'application/vnd.api+json',
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ===========================================================================
|
|
353
|
+
// Schema / Blueprint
|
|
354
|
+
// ===========================================================================
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Must be called once at boot (the `types` service can invoke this).
|
|
358
|
+
* Parses the Tribe webapp blueprint and registers schemas.
|
|
359
|
+
*/
|
|
360
|
+
loadBlueprint(webappPayload) {
|
|
361
|
+
if (!webappPayload || !webappPayload.modules) return;
|
|
362
|
+
|
|
363
|
+
for (const [typeSlug, typeData] of Object.entries(webappPayload.modules)) {
|
|
364
|
+
if (
|
|
365
|
+
typeSlug === 'webapp' ||
|
|
366
|
+
typeSlug === 'deleted_record' ||
|
|
367
|
+
typeSlug === 'platform_record' ||
|
|
368
|
+
typeSlug === 'blueprint_record' ||
|
|
369
|
+
typeSlug === 'file_record' ||
|
|
370
|
+
typeSlug === 'apikey_record' ||
|
|
371
|
+
!typeData.modules ||
|
|
372
|
+
!Array.isArray(typeData.modules)
|
|
373
|
+
) {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const attributes = {};
|
|
378
|
+
const relationships = {};
|
|
379
|
+
|
|
380
|
+
typeData.modules.forEach((mod) => {
|
|
381
|
+
const slug = mod.input_slug;
|
|
382
|
+
if (mod.linked_type) {
|
|
383
|
+
// This field is a relationship
|
|
384
|
+
const relType = normalizeType(mod.linked_type);
|
|
385
|
+
const kind = mod.var_type === 'array' || mod.var_type === 'has_many' ? 'hasMany' : 'belongsTo';
|
|
386
|
+
relationships[slug] = { kind, type: relType, inverse: mod.inverse ?? null };
|
|
387
|
+
} else {
|
|
388
|
+
attributes[slug] = mod.var_type ?? 'string';
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
this._schemas.set(normalizeType(typeSlug), { attributes, relationships });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Register a schema manually (for types not in the blueprint).
|
|
398
|
+
*/
|
|
399
|
+
registerSchema(type, schema) {
|
|
400
|
+
this._schemas.set(normalizeType(type), schema);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
_schemaFor(type) {
|
|
404
|
+
return this._schemas.get(normalizeType(type)) || null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ===========================================================================
|
|
408
|
+
// URL building
|
|
409
|
+
// ===========================================================================
|
|
410
|
+
|
|
411
|
+
_urlForType(type) {
|
|
412
|
+
return `${this._host}/${this._namespace}/${underscore(normalizeType(type))}`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
_urlForRecord(type, id) {
|
|
416
|
+
return `${this._urlForType(type)}/${id}`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ===========================================================================
|
|
420
|
+
// Network helpers
|
|
421
|
+
// ===========================================================================
|
|
422
|
+
|
|
423
|
+
async _fetch(url, options = {}) {
|
|
424
|
+
const res = await fetch(url, {
|
|
425
|
+
...options,
|
|
426
|
+
headers: { ...this._headers, ...(options.headers || {}) },
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
if (!res.ok) {
|
|
430
|
+
const body = await res.json().catch(() => ({}));
|
|
431
|
+
const err = new Error(`HTTP ${res.status}`);
|
|
432
|
+
err.status = res.status;
|
|
433
|
+
err.payload = body;
|
|
434
|
+
throw err;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// 204 No Content (typical for DELETE)
|
|
438
|
+
if (res.status === 204) return null;
|
|
439
|
+
return res.json();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ===========================================================================
|
|
443
|
+
// JSON:API normalisation (response → internal)
|
|
444
|
+
// ===========================================================================
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Normalise a JSON:API document and push all resources into the cache.
|
|
448
|
+
* Returns { data: Record | [Record], meta }.
|
|
449
|
+
*/
|
|
450
|
+
_normalizeAndPush(payload) {
|
|
451
|
+
if (!payload) return { data: null, meta: {} };
|
|
452
|
+
|
|
453
|
+
const meta = payload.meta || {};
|
|
454
|
+
|
|
455
|
+
// Side-load included resources first
|
|
456
|
+
if (Array.isArray(payload.included)) {
|
|
457
|
+
payload.included.forEach((resource) => this._pushResource(resource));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
let data;
|
|
461
|
+
if (Array.isArray(payload.data)) {
|
|
462
|
+
data = payload.data.map((r) => this._pushResource(r));
|
|
463
|
+
} else if (payload.data) {
|
|
464
|
+
data = this._pushResource(payload.data);
|
|
465
|
+
} else {
|
|
466
|
+
data = null;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return { data, meta };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Push a single JSON:API resource object into the identity map.
|
|
474
|
+
*/
|
|
475
|
+
_pushResource(resource) {
|
|
476
|
+
const type = normalizeType(resource.type);
|
|
477
|
+
const id = String(resource.id);
|
|
478
|
+
|
|
479
|
+
// Deserialise attributes (snake_case → camelCase keys kept as-is;
|
|
480
|
+
// the Tribe API already uses snake_case which matches model slugs)
|
|
481
|
+
const attrs = {};
|
|
482
|
+
if (resource.attributes) {
|
|
483
|
+
for (const [k, v] of Object.entries(resource.attributes)) {
|
|
484
|
+
attrs[k] = v;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Deserialise relationships → store ids
|
|
489
|
+
const rels = {};
|
|
490
|
+
if (resource.relationships) {
|
|
491
|
+
for (const [k, v] of Object.entries(resource.relationships)) {
|
|
492
|
+
if (v.data === null || v.data === undefined) {
|
|
493
|
+
rels[k] = null;
|
|
494
|
+
} else if (Array.isArray(v.data)) {
|
|
495
|
+
rels[k] = v.data.map((d) => String(d.id));
|
|
496
|
+
} else {
|
|
497
|
+
rels[k] = String(v.data.id);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Upsert into identity map
|
|
503
|
+
const existing = this._peekById(type, id);
|
|
504
|
+
if (existing) {
|
|
505
|
+
// Merge into existing record (preserves object identity)
|
|
506
|
+
Object.assign(existing._attributes, attrs);
|
|
507
|
+
existing._originalAttrs = { ...existing._attributes };
|
|
508
|
+
Object.assign(existing._relationships, rels);
|
|
509
|
+
existing._isNew = false;
|
|
510
|
+
return existing;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const record = new Record(this, type, id, attrs, rels, false);
|
|
514
|
+
this._cacheRecord(record);
|
|
515
|
+
return record;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ===========================================================================
|
|
519
|
+
// JSON:API serialisation (internal → request payload)
|
|
520
|
+
// ===========================================================================
|
|
521
|
+
|
|
522
|
+
_serialise(record) {
|
|
523
|
+
const type = normalizeType(record._type);
|
|
524
|
+
const schema = this._schemaFor(type);
|
|
525
|
+
|
|
526
|
+
const attributes = {};
|
|
527
|
+
for (const [k, v] of Object.entries(record._attributes)) {
|
|
528
|
+
// Use underscore keys on the wire (matches existing serialiser)
|
|
529
|
+
attributes[underscore(k)] = v;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const relationships = {};
|
|
533
|
+
if (schema && schema.relationships) {
|
|
534
|
+
for (const [k, rel] of Object.entries(schema.relationships)) {
|
|
535
|
+
const raw = record._relationships[k];
|
|
536
|
+
if (raw === undefined) continue;
|
|
537
|
+
if (rel.kind === 'belongsTo') {
|
|
538
|
+
relationships[underscore(k)] = {
|
|
539
|
+
data: raw ? { type: underscore(rel.type), id: String(raw) } : null,
|
|
540
|
+
};
|
|
541
|
+
} else {
|
|
542
|
+
relationships[underscore(k)] = {
|
|
543
|
+
data: (Array.isArray(raw) ? raw : []).map((rid) => ({
|
|
544
|
+
type: underscore(rel.type),
|
|
545
|
+
id: String(rid),
|
|
546
|
+
})),
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const payload = {
|
|
553
|
+
data: {
|
|
554
|
+
type: underscore(type),
|
|
555
|
+
attributes,
|
|
556
|
+
},
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
if (record._id) payload.data.id = String(record._id);
|
|
560
|
+
if (Object.keys(relationships).length) payload.data.relationships = relationships;
|
|
561
|
+
|
|
562
|
+
return payload;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ===========================================================================
|
|
566
|
+
// Cache primitives
|
|
567
|
+
// ===========================================================================
|
|
568
|
+
|
|
569
|
+
_cacheRecord(record) {
|
|
570
|
+
const type = normalizeType(record._type);
|
|
571
|
+
if (!this._cache.has(type)) this._cache.set(type, new Map());
|
|
572
|
+
this._cache.get(type).set(String(record._id ?? record._clientId), record);
|
|
573
|
+
|
|
574
|
+
// Update live array
|
|
575
|
+
const live = this._liveArrays.get(type);
|
|
576
|
+
if (live && !live.includes(record)) {
|
|
577
|
+
live.push(record);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
_peekById(type, id) {
|
|
582
|
+
const bucket = this._cache.get(normalizeType(type));
|
|
583
|
+
return bucket ? bucket.get(String(id)) || null : null;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
_unloadRecord(record) {
|
|
587
|
+
const type = normalizeType(record._type);
|
|
588
|
+
const bucket = this._cache.get(type);
|
|
589
|
+
if (bucket) {
|
|
590
|
+
bucket.delete(String(record._id ?? record._clientId));
|
|
591
|
+
}
|
|
592
|
+
const live = this._liveArrays.get(type);
|
|
593
|
+
if (live) live.removeObject(record);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ===========================================================================
|
|
597
|
+
// CRUD — remote operations (called by Record.save())
|
|
598
|
+
// ===========================================================================
|
|
599
|
+
|
|
600
|
+
async _createRemote(record) {
|
|
601
|
+
const url = this._urlForType(record._type);
|
|
602
|
+
const payload = this._serialise(record);
|
|
603
|
+
const json = await this._fetch(url, { method: 'POST', body: JSON.stringify(payload) });
|
|
604
|
+
if (json) {
|
|
605
|
+
const { data } = this._normalizeAndPush(json);
|
|
606
|
+
// The server may assign an id
|
|
607
|
+
if (data && data._id) {
|
|
608
|
+
// Re-key in cache
|
|
609
|
+
const type = normalizeType(record._type);
|
|
610
|
+
const bucket = this._cache.get(type);
|
|
611
|
+
if (bucket) {
|
|
612
|
+
bucket.delete(record._clientId);
|
|
613
|
+
}
|
|
614
|
+
record._id = data._id;
|
|
615
|
+
this._cacheRecord(record);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async _updateRemote(record) {
|
|
621
|
+
const url = this._urlForRecord(record._type, record._id);
|
|
622
|
+
const payload = this._serialise(record);
|
|
623
|
+
const json = await this._fetch(url, { method: 'PATCH', body: JSON.stringify(payload) });
|
|
624
|
+
if (json) this._normalizeAndPush(json);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async _deleteRemote(record) {
|
|
628
|
+
const url = this._urlForRecord(record._type, record._id);
|
|
629
|
+
await this._fetch(url, { method: 'DELETE' });
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// ===========================================================================
|
|
633
|
+
// Public API — Finding Records
|
|
634
|
+
// ===========================================================================
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* store.findRecord('post', 1) → GET /api.php/post/1
|
|
638
|
+
* store.findRecord('post', 1, { include: 'comments' })
|
|
639
|
+
*/
|
|
640
|
+
async findRecord(type, id, options = {}) {
|
|
641
|
+
let url = this._urlForRecord(type, id);
|
|
642
|
+
const params = this._buildQueryParams(options);
|
|
643
|
+
if (params) url += `?${params}`;
|
|
644
|
+
const json = await this._fetch(url);
|
|
645
|
+
const { data, meta } = this._normalizeAndPush(json);
|
|
646
|
+
if (meta) this._meta.set(normalizeType(type), meta);
|
|
647
|
+
return data;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* store.peekRecord('post', 1) → from cache only, no network
|
|
652
|
+
*/
|
|
653
|
+
peekRecord(type, id) {
|
|
654
|
+
return this._peekById(type, id);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* store.findAll('post') → GET /api.php/post
|
|
659
|
+
* store.findAll('post', { include: 'comments' })
|
|
660
|
+
*/
|
|
661
|
+
async findAll(type, options = {}) {
|
|
662
|
+
let url = this._urlForType(type);
|
|
663
|
+
const params = this._buildQueryParams(options);
|
|
664
|
+
if (params) url += `?${params}`;
|
|
665
|
+
const json = await this._fetch(url);
|
|
666
|
+
const { data, meta } = this._normalizeAndPush(json);
|
|
667
|
+
const records = Array.isArray(data) ? data : data ? [data] : [];
|
|
668
|
+
// Update or create live array
|
|
669
|
+
const nType = normalizeType(type);
|
|
670
|
+
let live = this._liveArrays.get(nType);
|
|
671
|
+
if (!live) {
|
|
672
|
+
live = new RecordArray(records, meta);
|
|
673
|
+
this._liveArrays.set(nType, live);
|
|
674
|
+
} else {
|
|
675
|
+
live._replace(records, meta);
|
|
676
|
+
}
|
|
677
|
+
return live;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* store.peekAll('post') → RecordArray from cache, no network
|
|
682
|
+
*/
|
|
683
|
+
peekAll(type) {
|
|
684
|
+
const nType = normalizeType(type);
|
|
685
|
+
if (!this._liveArrays.has(nType)) {
|
|
686
|
+
const bucket = this._cache.get(nType);
|
|
687
|
+
const records = bucket ? [...bucket.values()] : [];
|
|
688
|
+
this._liveArrays.set(nType, new RecordArray(records));
|
|
689
|
+
}
|
|
690
|
+
return this._liveArrays.get(nType);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* store.query('person', { filter: { name: 'Peter' } })
|
|
695
|
+
*/
|
|
696
|
+
async query(type, params = {}) {
|
|
697
|
+
let url = this._urlForType(type);
|
|
698
|
+
const qs = this._buildQueryParams(params);
|
|
699
|
+
if (qs) url += `?${qs}`;
|
|
700
|
+
const json = await this._fetch(url);
|
|
701
|
+
const { data, meta } = this._normalizeAndPush(json);
|
|
702
|
+
const records = Array.isArray(data) ? data : data ? [data] : [];
|
|
703
|
+
return new RecordArray(records, meta);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* store.queryRecord('user', { ... }) → returns a single record
|
|
708
|
+
*/
|
|
709
|
+
async queryRecord(type, params = {}) {
|
|
710
|
+
let url = this._urlForType(type);
|
|
711
|
+
const qs = this._buildQueryParams(params);
|
|
712
|
+
if (qs) url += `?${qs}`;
|
|
713
|
+
const json = await this._fetch(url);
|
|
714
|
+
const { data, meta } = this._normalizeAndPush(json);
|
|
715
|
+
if (Array.isArray(data)) return data[0] || null;
|
|
716
|
+
return data;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// ===========================================================================
|
|
720
|
+
// Public API — Creating Records
|
|
721
|
+
// ===========================================================================
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* store.createRecord('post', { title: 'Hello', body: '...' })
|
|
725
|
+
*/
|
|
726
|
+
createRecord(type, attrs = {}) {
|
|
727
|
+
const nType = normalizeType(type);
|
|
728
|
+
const schema = this._schemaFor(nType);
|
|
729
|
+
|
|
730
|
+
// Separate relationships from plain attributes
|
|
731
|
+
const plainAttrs = {};
|
|
732
|
+
const rels = {};
|
|
733
|
+
|
|
734
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
735
|
+
if (schema && schema.relationships && schema.relationships[k]) {
|
|
736
|
+
// Accept a record or an id
|
|
737
|
+
const rel = schema.relationships[k];
|
|
738
|
+
if (rel.kind === 'belongsTo') {
|
|
739
|
+
rels[k] = v && typeof v === 'object' ? (v._id ?? v.id ?? v) : v;
|
|
740
|
+
} else {
|
|
741
|
+
rels[k] = Array.isArray(v) ? v.map((r) => (r && typeof r === 'object' ? (r._id ?? r.id ?? r) : r)) : v;
|
|
742
|
+
}
|
|
743
|
+
} else {
|
|
744
|
+
plainAttrs[k] = v;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const record = new Record(this, nType, null, plainAttrs, rels, true);
|
|
749
|
+
this._cacheRecord(record);
|
|
750
|
+
return record;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// ===========================================================================
|
|
754
|
+
// Public API — Pushing Records
|
|
755
|
+
// ===========================================================================
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* store.push(jsonApiDocument)
|
|
759
|
+
* Accepts a JSON:API-shaped document with `data` (and optional `included`).
|
|
760
|
+
*/
|
|
761
|
+
push(jsonApiDoc) {
|
|
762
|
+
const { data } = this._normalizeAndPush(jsonApiDoc);
|
|
763
|
+
return data;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* store.pushPayload(rawPayload)
|
|
768
|
+
* Accepts a REST-style payload keyed by type name, normalises to JSON:API, and pushes.
|
|
769
|
+
* Example: { posts: [{ id: 1, title: '...' }] }
|
|
770
|
+
*/
|
|
771
|
+
pushPayload(rawPayload) {
|
|
772
|
+
const resources = [];
|
|
773
|
+
for (const [key, items] of Object.entries(rawPayload)) {
|
|
774
|
+
const type = normalizeType(key);
|
|
775
|
+
const list = Array.isArray(items) ? items : [items];
|
|
776
|
+
for (const item of list) {
|
|
777
|
+
const { id, ...attrs } = item;
|
|
778
|
+
resources.push({ id: String(id), type, attributes: attrs });
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return this.push({ data: resources });
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// ===========================================================================
|
|
785
|
+
// Public API — Unload
|
|
786
|
+
// ===========================================================================
|
|
787
|
+
|
|
788
|
+
unloadRecord(record) {
|
|
789
|
+
this._unloadRecord(record);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
unloadAll(type) {
|
|
793
|
+
if (type) {
|
|
794
|
+
const nType = normalizeType(type);
|
|
795
|
+
this._cache.delete(nType);
|
|
796
|
+
const live = this._liveArrays.get(nType);
|
|
797
|
+
if (live) live._replace([]);
|
|
798
|
+
} else {
|
|
799
|
+
this._cache.clear();
|
|
800
|
+
this._liveArrays.forEach((live) => live._replace([]));
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// ===========================================================================
|
|
805
|
+
// Public API — Metadata
|
|
806
|
+
// ===========================================================================
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* store.metadataFor('post') → last meta received for this type
|
|
810
|
+
*/
|
|
811
|
+
metadataFor(type) {
|
|
812
|
+
return this._meta.get(normalizeType(type)) || {};
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// ===========================================================================
|
|
816
|
+
// Query-param builder
|
|
817
|
+
// ===========================================================================
|
|
818
|
+
|
|
819
|
+
_buildQueryParams(options) {
|
|
820
|
+
if (!options || typeof options !== 'object') return '';
|
|
821
|
+
const parts = [];
|
|
822
|
+
|
|
823
|
+
const serialize = (obj, prefix) => {
|
|
824
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
825
|
+
const key = prefix ? `${prefix}[${k}]` : k;
|
|
826
|
+
if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
|
|
827
|
+
serialize(v, key);
|
|
828
|
+
} else {
|
|
829
|
+
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
serialize(options);
|
|
835
|
+
return parts.join('&');
|
|
836
|
+
}
|
|
837
|
+
}
|
|
@@ -3,53 +3,40 @@ import ENV from '<%= dasherizedPackageName %>/config/environment';
|
|
|
3
3
|
import { service } from '@ember/service';
|
|
4
4
|
import { action } from '@ember/object';
|
|
5
5
|
import { tracked } from '@glimmer/tracking';
|
|
6
|
-
import Model, { attr } from '@ember-data/model';
|
|
7
|
-
import { getOwner } from '@ember/application';
|
|
8
6
|
|
|
9
7
|
export default class TypesService extends Service {
|
|
10
8
|
@service store;
|
|
11
|
-
@tracked json =
|
|
12
|
-
|
|
13
|
-
});
|
|
9
|
+
@tracked json = null;
|
|
10
|
+
@tracked simplifiedJson = null;
|
|
14
11
|
|
|
15
12
|
@action
|
|
16
13
|
async fetchAgain() {
|
|
17
14
|
if (ENV.TribeENV.API_URL !== undefined && ENV.TribeENV.API_URL != '') {
|
|
15
|
+
// First fetch — get the webapp blueprint (no includes yet)
|
|
18
16
|
this.json = await this.store.findRecord('webapp', 0, {});
|
|
19
|
-
let owner = getOwner(this);
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class DynamicModel extends Model {
|
|
25
|
-
@attr slug;
|
|
26
|
-
@attr modules;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (!owner.hasRegistration(`model:${modelDynamicName}`)) {
|
|
30
|
-
owner.register(`model:${modelDynamicName}`, DynamicModel);
|
|
31
|
-
}
|
|
32
|
-
});
|
|
18
|
+
// Feed the blueprint into the store so it knows every type's schema
|
|
19
|
+
this.store.loadBlueprint(this.json);
|
|
33
20
|
|
|
21
|
+
// Second fetch — now with total_objects included
|
|
34
22
|
this.json = await this.store.findRecord('webapp', 0, {
|
|
35
|
-
include:
|
|
23
|
+
include: 'total_objects',
|
|
36
24
|
});
|
|
37
|
-
|
|
25
|
+
|
|
26
|
+
// Re-load blueprint with the enriched response
|
|
27
|
+
this.store.loadBlueprint(this.json);
|
|
28
|
+
|
|
38
29
|
this.simplifiedJson = this.convertTypesToSimplified(this.json);
|
|
39
|
-
//console.log(this.simplifiedJson);
|
|
40
30
|
}
|
|
41
31
|
}
|
|
42
32
|
|
|
43
33
|
convertTypesToSimplified = (typesJson) => {
|
|
44
|
-
// Create the basic structure with a types object
|
|
45
34
|
const simplifiedTypes = {
|
|
46
|
-
project_description: typesJson.modules
|
|
35
|
+
project_description: typesJson.modules?.webapp?.project_description ?? '',
|
|
47
36
|
types: {},
|
|
48
37
|
};
|
|
49
38
|
|
|
50
|
-
|
|
51
|
-
for (const [typeSlug, typeData] of Object.entries(typesJson.modules)) {
|
|
52
|
-
// Skip the webapp info and any types without modules
|
|
39
|
+
for (const [typeSlug, typeData] of Object.entries(typesJson.modules || {})) {
|
|
53
40
|
if (
|
|
54
41
|
typeSlug === 'webapp' ||
|
|
55
42
|
typeSlug === 'deleted_record' ||
|
|
@@ -63,36 +50,29 @@ export default class TypesService extends Service {
|
|
|
63
50
|
continue;
|
|
64
51
|
}
|
|
65
52
|
|
|
66
|
-
// Create a new object for this type
|
|
67
53
|
simplifiedTypes.types[typeSlug] = {};
|
|
68
54
|
|
|
69
|
-
// Process each module in the content type
|
|
70
55
|
typeData.modules.forEach((module) => {
|
|
71
56
|
const slug = module.input_slug;
|
|
72
|
-
let varType =
|
|
57
|
+
let varType =
|
|
58
|
+
(module.var_type ?? 'string') +
|
|
59
|
+
(module.linked_type ? ' | *' + module.linked_type : '');
|
|
73
60
|
|
|
74
|
-
// Handle select options if they exist
|
|
75
61
|
if (
|
|
76
62
|
module.input_options &&
|
|
77
63
|
Array.isArray(module.input_options) &&
|
|
78
64
|
module.input_options.length > 0
|
|
79
65
|
) {
|
|
80
|
-
|
|
81
|
-
const optionSlugs = module.input_options.map(
|
|
82
|
-
(option) => option.slug,
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
// Add the piped extension to the var_type
|
|
66
|
+
const optionSlugs = module.input_options.map((option) => option.slug);
|
|
86
67
|
if (optionSlugs.length > 0) {
|
|
87
68
|
varType += ` | ${optionSlugs.join(', ')}`;
|
|
88
69
|
}
|
|
89
70
|
}
|
|
90
71
|
|
|
91
|
-
// Add the module to the simplified type
|
|
92
72
|
simplifiedTypes.types[typeSlug][slug] = varType;
|
|
93
73
|
});
|
|
94
74
|
}
|
|
95
75
|
|
|
96
76
|
return simplifiedTypes;
|
|
97
|
-
}
|
|
77
|
+
};
|
|
98
78
|
}
|
|
@@ -31,20 +31,5 @@ $spacers: (
|
|
|
31
31
|
10: $spacer * 12,
|
|
32
32
|
) !default;
|
|
33
33
|
|
|
34
|
-
@import "node_modules/bootstrap/scss/bootstrap";
|
|
35
|
-
@import "node_modules/animate.css/animate";
|
|
36
|
-
|
|
37
|
-
.flame-bg {
|
|
38
|
-
background: linear-gradient(0deg, rgba(153,51,153,1) 0%, rgba(255,51,153,1) 100%);
|
|
39
|
-
background-color: rgba(0, 0, 0, 0);
|
|
40
|
-
background-position-x: 0%;
|
|
41
|
-
background-position-y: 0%;
|
|
42
|
-
background-repeat: repeat;
|
|
43
|
-
background-attachment: scroll;
|
|
44
|
-
background-image: linear-gradient(0deg, rgb(153, 51, 153) 0%, rgb(255, 51, 153) 100%);
|
|
45
|
-
background-size: auto;
|
|
46
|
-
background-origin: padding-box;
|
|
47
|
-
background-clip: border-box;
|
|
48
|
-
height: 100vh;
|
|
49
|
-
width: 100vw;
|
|
50
|
-
}
|
|
34
|
+
@import "../../../node_modules/bootstrap/scss/bootstrap";
|
|
35
|
+
@import "../../../node_modules/animate.css/animate";
|
|
@@ -45,10 +45,7 @@ module.exports = {
|
|
|
45
45
|
{ name: 'ember-math-helpers' },
|
|
46
46
|
{ name: 'ember-cli-string-helpers' },
|
|
47
47
|
{ name: 'ember-promise-helpers' },
|
|
48
|
-
{ name: 'ember-tag-input' },
|
|
49
48
|
{ name: 'ember-file-upload' },
|
|
50
|
-
{ name: 'ember-data' },
|
|
51
|
-
{ name: 'ember-basic-dropdown' },
|
|
52
49
|
{ name: 'ember-power-select' },
|
|
53
50
|
{ name: 'ember-click-outside' },
|
|
54
51
|
{ name: 'ember-keyboard' },
|
package/package.json
CHANGED