Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

111 changes: 110 additions & 1 deletion l10n/en.js

Large diffs are not rendered by default.

111 changes: 110 additions & 1 deletion l10n/en.json

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions l10n/nl.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
OC.L10N.register(
"pipelinq",
{
"Example data fills the lists, dashboards and the 360 degree client view so you can see the app working straight away. Pick \"None\" on a production install.": "Voorbeeldgegevens vullen de lijsten, dashboards en het 360 graden klantbeeld, zodat je de app meteen ziet werken. Kies \"Geen\" op een productieomgeving.",
"Loads what you picked. Every object is marked [Demo], it is safe to run more than once, and `occ pipelinq:demo:seed --remove` takes it away again.": "Laadt wat je koos. Elk object is gemarkeerd met [Demo], je kunt dit meer dan een keer uitvoeren en `occ pipelinq:demo:seed --remove` haalt het weer weg.",
"Nothing is seeded. You start with an empty app and add your own data.": "Er wordt niets klaargezet. Je begint met een lege app en voegt zelf gegevens toe.",
"A worked CRM: clients and contacts, pipelines, products and leads, requests, complaints and contact moments, tasks and contracts. It shows the lists, detail pages and dashboards working. Safe to run more than once, and `occ pipelinq:demo:remove` takes it away again.": "Een uitgewerkt CRM: klanten en contactpersonen, pipelines, producten en leads, verzoeken, klachten en contactmomenten, taken en contracten. Het laat de lijsten, detailpagina’s en dashboards werkend zien. Veilig om vaker uit te voeren, en `occ pipelinq:demo:remove` haalt het weer weg.",
"Load example data?": "Voorbeeldgegevens laden?",
"Example data fills the lists, detail pages and dashboards so you can see the app working straight away. Pick \"None\" on a production install.": "Voorbeeldgegevens vullen de lijsten, detailpagina’s en dashboards, zodat je de app meteen ziet werken. Kies \"Geen\" op een productieomgeving.",
"Load the example data": "Laad de voorbeeldgegevens",
"Loads what you picked. The data is obviously sample data, it is safe to run more than once, and you can delete it afterwards.": "Laadt wat je koos. De gegevens zijn herkenbaar voorbeeldgegevens, je kunt dit meer dan een keer uitvoeren en je kunt ze daarna verwijderen.",
"None, I will set this up myself": "Geen, ik richt dit zelf in",
"Nothing is imported. You start with an empty app and add your own data.": "Er wordt niets geïmporteerd. Je begint met een lege app en voegt zelf gegevens toe.",
"Example data": "Voorbeeldgegevens",
"Sample values for every schema this app supplies, generated from the schemas themselves. It shows the lists, detail pages and dashboards working rather than telling a story. Safe to run more than once, and you can delete it afterwards.": "Voorbeeldwaarden voor elk schema dat deze app levert, gegenereerd uit de schema’s zelf. Het laat de lijsten, detailpagina’s en dashboards werkend zien in plaats van een verhaal te vertellen. Veilig om vaker uit te voeren, en je kunt het daarna verwijderen.",
"\"Vanaf\" datum moet voor of gelijk zijn aan \"tot en met\" datum.": "\"Vanaf\" datum moet voor of gelijk zijn aan \"tot en met\" datum.",
"#": "#",
"%n contact in this segment is missing {channel} consent. Choose how to proceed.": ["%n contact in dit segment mist toestemming voor {channel}. Kies hoe u verdergaat.","%n contacten in dit segment missen toestemming voor {channel}. Kies hoe u verdergaat."],
Expand Down
12 changes: 12 additions & 0 deletions l10n/nl.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
{
"translations": {
"Example data fills the lists, dashboards and the 360 degree client view so you can see the app working straight away. Pick \"None\" on a production install.": "Voorbeeldgegevens vullen de lijsten, dashboards en het 360 graden klantbeeld, zodat je de app meteen ziet werken. Kies \"Geen\" op een productieomgeving.",
"Loads what you picked. Every object is marked [Demo], it is safe to run more than once, and `occ pipelinq:demo:seed --remove` takes it away again.": "Laadt wat je koos. Elk object is gemarkeerd met [Demo], je kunt dit meer dan een keer uitvoeren en `occ pipelinq:demo:seed --remove` haalt het weer weg.",
"Nothing is seeded. You start with an empty app and add your own data.": "Er wordt niets klaargezet. Je begint met een lege app en voegt zelf gegevens toe.",
"A worked CRM: clients and contacts, pipelines, products and leads, requests, complaints and contact moments, tasks and contracts. It shows the lists, detail pages and dashboards working. Safe to run more than once, and `occ pipelinq:demo:remove` takes it away again.": "Een uitgewerkt CRM: klanten en contactpersonen, pipelines, producten en leads, verzoeken, klachten en contactmomenten, taken en contracten. Het laat de lijsten, detailpagina’s en dashboards werkend zien. Veilig om vaker uit te voeren, en `occ pipelinq:demo:remove` haalt het weer weg.",
"Load example data?": "Voorbeeldgegevens laden?",
"Example data fills the lists, detail pages and dashboards so you can see the app working straight away. Pick \"None\" on a production install.": "Voorbeeldgegevens vullen de lijsten, detailpagina’s en dashboards, zodat je de app meteen ziet werken. Kies \"Geen\" op een productieomgeving.",
"Load the example data": "Laad de voorbeeldgegevens",
"Loads what you picked. The data is obviously sample data, it is safe to run more than once, and you can delete it afterwards.": "Laadt wat je koos. De gegevens zijn herkenbaar voorbeeldgegevens, je kunt dit meer dan een keer uitvoeren en je kunt ze daarna verwijderen.",
"None, I will set this up myself": "Geen, ik richt dit zelf in",
"Nothing is imported. You start with an empty app and add your own data.": "Er wordt niets geïmporteerd. Je begint met een lege app en voegt zelf gegevens toe.",
"Example data": "Voorbeeldgegevens",
"Sample values for every schema this app supplies, generated from the schemas themselves. It shows the lists, detail pages and dashboards working rather than telling a story. Safe to run more than once, and you can delete it afterwards.": "Voorbeeldwaarden voor elk schema dat deze app levert, gegenereerd uit de schema’s zelf. Het laat de lijsten, detailpagina’s en dashboards werkend zien in plaats van een verhaal te vertellen. Veilig om vaker uit te voeren, en je kunt het daarna verwijderen.",
"\"Vanaf\" datum moet voor of gelijk zijn aan \"tot en met\" datum.": "\"Vanaf\" datum moet voor of gelijk zijn aan \"tot en met\" datum.",
"#": "#",
"%n contact in this segment is missing {channel} consent. Choose how to proceed.": [
Expand Down
86 changes: 84 additions & 2 deletions lib/Controller/SetupController.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,19 @@ class SetupController extends Controller {
*/
private const DEMO_DATA_DECIDED_KEY = 'demo_data_decided';

/**
* App-config key holding the dataset the operator picked.
*
* The wizard's `choice` step writes it through `POST /api/setup/config`, and
* the `run-action` step that follows reads it back. Two steps rather than
* one because `CnSetupWizard::runAction()` posts to
* `/api/setup/action/{action}` with no body: an action cannot carry the
* answer, so the answer has to be stored before the action runs.
*
* @var string
*/
private const DATASET_KEY = 'demo_dataset';

/**
* Constructor.
*
Expand Down Expand Up @@ -136,6 +149,7 @@ public function status(): DataResponse {
// the marker when it runs, and `occ pipelinq:demo:seed --remove` leaving
// the marker in place is correct: the decision was still made.
$demoDataDone = $this->config(key: self::DEMO_DATA_DECIDED_KEY) !== '';
$pickedDataset = $this->config(key: self::DATASET_KEY);

// Organisation step done once the operator has named the organisation.
$organisationDone = $this->config(key: 'receipt_company_name') !== '';
Expand All @@ -160,11 +174,20 @@ public function status(): DataResponse {
[
'version' => self::SETUP_VERSION,
'completed' => $currencyDone,
// The choice step reads its options from here: it declares
// `optionsSource: datasets` and no options of its own, so a
// dataset missing from this list is a dataset nobody can pick.
'datasets' => $this->demoSeedService->listChoices(),
'steps' => [
'welcome' => ['done' => true],
'currency' => ['done' => $currencyDone],
'provision' => ['done' => $registerDone],
'demo-data' => ['done' => $demoDataDone],
'demo-data' => ['done' => ($pickedDataset !== '')],
// "None" is an ANSWER, so the seed step is finished the
// moment it is chosen: there is nothing left to run.
'load-demo-data' => [
'done' => ($demoDataDone === true || $pickedDataset === DemoSeedService::NONE_DATASET),
],
'organisation' => ['done' => $organisationDone],
'integrations' => ['done' => $integrationsDone],
'done' => ['done' => true],
Expand All @@ -187,6 +210,11 @@ public function status(): DataResponse {
* @spec openspec/specs/first-time-setup/spec.md#requirement-req-setup-pip-008-optional-demo-data-seed
*/
private function skipDemoData(): DataResponse {
// 🔴 IT ANSWERS *BOTH* STEPS. The wizard now has a choice step and a
// run-action step; closing only the second leaves the first
// outstanding, and CnAppRoot opens the wizard while ANY optional step
// is outstanding.
$this->appConfig->setValueString(Application::APP_ID, self::DATASET_KEY, DemoSeedService::NONE_DATASET);
$this->appConfig->setValueString(Application::APP_ID, self::DEMO_DATA_DECIDED_KEY, 'skipped');

return new DataResponse(
Expand All @@ -203,6 +231,24 @@ private function skipDemoData(): DataResponse {
*/
#[AuthorizedAdminSetting(AdminSettings::class)]
public function saveConfig(): DataResponse {
// 🔴 THE DATASET IS VALIDATED BEFORE IT IS STORED. Everything else here
// is written as posted, because a `config-fields` step declares its own
// keys and this endpoint cannot know them. The dataset is different:
// the seed step reads it back and acts on it, so an unknown value would
// surface a step later with no clue why.
$dataset = $this->request->getParam(self::DATASET_KEY);
if ($dataset !== null) {
$named = 'that';
if (is_scalar($dataset) === true) {
$named = (string)$dataset;
}

$known = array_column($this->demoSeedService->listChoices(), 'id');
if (in_array($named, $known, true) === false) {
return new DataResponse(['success' => false, 'message' => 'No dataset is called "' . $named . '".']);
}
}

foreach ($this->request->getParams() as $key => $value) {
if ($key === '_route') {
continue;
Expand Down Expand Up @@ -234,6 +280,13 @@ public function runAction(string $actionId): DataResponse {
return $this->provisionRegister();
}

// `seed-demo-data` is the id the step used before it asked WHICH
// dataset, and it still means "seed the one this app builds". Kept so
// an older manifest, a runbook or a script that posts it keeps working.
if ($actionId === 'load-demo-data') {
return $this->loadDataset();
}

if ($actionId === 'seed-demo-data') {
return $this->seedDemoData();
}
Expand Down Expand Up @@ -298,7 +351,7 @@ private function provisionRegister(): DataResponse {
}//end provisionRegister()

/**
* Seed the optional demo dataset (ADR-042 optional action `seed-demo-data`).
* Seed the dataset the operator picked in the previous step.
*
* Invokes the same DemoSeedService the `occ pipelinq:demo:seed` command
* uses (one write path). Idempotent — re-running creates no duplicates.
Expand All @@ -309,6 +362,30 @@ private function provisionRegister(): DataResponse {
*
* @spec openspec/specs/first-time-setup/spec.md#requirement-req-setup-pip-008-optional-demo-data-seed
*/
private function loadDataset(): DataResponse {
$picked = $this->config(key: self::DATASET_KEY);

// 🔴 NO SILENT DEFAULT. Seeding here because the operator clicked Run
// one step early would plant example objects nobody asked for.
if ($picked === '') {
return new DataResponse(['success' => false, 'message' => 'Pick a dataset first.']);
}

if ($picked === DemoSeedService::NONE_DATASET) {
$this->appConfig->setValueString(Application::APP_ID, self::DEMO_DATA_DECIDED_KEY, 'skipped');

return new DataResponse(['success' => true, 'message' => 'No example data was seeded.']);
}

return $this->seedDemoData();

}//end loadDataset()

/**
* Seed the dataset this app builds.
*
* @return DataResponse `{ success, message }`.
*/
private function seedDemoData(): DataResponse {
try {
$result = $this->demoSeedService->seed();
Expand All @@ -323,6 +400,11 @@ private function seedDemoData(): DataResponse {
// Record the decision so `status()` can report the step done. See
// DEMO_DATA_DECIDED_KEY — an optional step the server can never
// report done covers the whole app with the setup wizard.
// 🔴 BOTH KEYS. The step is a choice followed by a run-action now, and
// CnAppRoot opens the wizard while ANY optional step is outstanding
// — so recording only the decision would leave the choice open and
// the wizard covering every page. Seeding IS choosing the set.
$this->appConfig->setValueString(Application::APP_ID, self::DATASET_KEY, DemoSeedService::DEMO_DATASET);
$this->appConfig->setValueString(Application::APP_ID, self::DEMO_DATA_DECIDED_KEY, 'seeded');

$created = array_sum($result['created']);
Expand Down
61 changes: 61 additions & 0 deletions lib/Service/DemoSeedService.php
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,67 @@ public function __construct(
) {
}//end __construct()

/**
* The answer that means "seed nothing".
*
* 🔴 NOT THE ABSENCE OF AN ANSWER. An operator who declines has FINISHED the
* step; a step that can never be marked done reopens the wizard over every
* page (nextcloud-vue#806).
*
* @var string
*/
public const NONE_DATASET = 'none';

/**
* The id of the dataset this app seeds.
*
* @var string
*/
public const DEMO_DATASET = 'demo';

/**
* Every answer the wizard's choice step may offer, declining included.
*
* 🔴 THE SERVER OWNS THIS LIST, AND THAT IS THE POINT. The step declares
* `optionsSource: datasets` and no options of its own, so what the card
* says is what this service will actually create.
*
* `objectCount` is 0 because this seeder BUILDS its objects rather than
* importing a file, so there is no honest number until it has run. The card
* shows no count rather than a made-up one.
*
* @return array<int, array{id: string, label: string, description: string, objectCount: integer, icon: string}> The answers.
*
* @spec exclude Demo-data choice list; ADR-111 rule 1 has no per-app behavioural spec.
*/
public function listChoices(): array {
return [
[
'id' => self::NONE_DATASET,
'label' => 'None, I will set this up myself',
'description' => 'Nothing is seeded. You start with an empty app and add your own data.',
'objectCount' => 0,
'icon' => 'CloseCircleOutline',
],
[
'id' => self::DEMO_DATASET,
'label' => 'Example data',
// 🔴 NAMES WHAT IT CREATES. The sections below are the ones
// SECTIONS declares, so the sentence and the seeder cannot
// drift apart without this list changing too.
'description' => (
'A worked CRM: clients and contacts, pipelines, products and leads, requests, '
. 'complaints and contact moments, tasks and contracts. It shows the lists, '
. 'detail pages and dashboards working. Safe to run more than once, and '
. '`occ pipelinq:demo:remove` takes it away again.'
),
'objectCount' => 0,
'icon' => 'DatabaseOutline',
],
];

}//end listChoices()

/**
* Seed the demo dataset (idempotent: existing demo objects are reused).
*
Expand Down
4 changes: 2 additions & 2 deletions lib/Settings/demo_seed_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
],
"leads": [
{ "key": "lead-website", "pipelineKey": "pipeline-sales", "clientKey": "bakkerij", "data": { "title": "[Demo] Webshop vernieuwing Gouden Korst", "source": "website", "value": 12500, "probability": 20, "expectedCloseDate": "@days:45", "priority": "normal", "stage": "New", "stageOrder": 1, "status": "open", "description": "Demo lead: replace the bakery webshop." } },
{ "key": "lead-koffie", "pipelineKey": "pipeline-sales", "clientKey": "jansen", "data": { "title": "[Demo] Koffieabonnement kantoor", "source": "referral", "value": 3600, "probability": 40, "expectedCloseDate": "@days:30", "priority": "low", "stage": "Contacted", "stageOrder": 2, "stageEnteredAt": "@days:-3", "status": "open", "description": "Demo lead: office coffee subscription." } },
{ "key": "lead-intranet", "pipelineKey": "pipeline-sales", "clientKey": "gemeente", "data": { "title": "[Demo] Intranet migratie Zonnedael", "source": "tender", "value": 48000, "probability": 55, "expectedCloseDate": "@days:60", "priority": "high", "stage": "Qualified", "stageOrder": 3, "stageEnteredAt": "@days:-31", "status": "open", "description": "Demo lead: municipal intranet migration." } },
{ "key": "lead-koffie", "pipelineKey": "pipeline-sales", "clientKey": "jansen", "data": { "title": "[Demo] Koffieabonnement kantoor", "source": "referral", "value": 3600, "probability": 40, "expectedCloseDate": "@days:30", "priority": "low", "stage": "Contacted", "stageOrder": 2, "stageEnteredAt": "@datetime:-3", "status": "open", "description": "Demo lead: office coffee subscription." } },
{ "key": "lead-intranet", "pipelineKey": "pipeline-sales", "clientKey": "gemeente", "data": { "title": "[Demo] Intranet migratie Zonnedael", "source": "tender", "value": 48000, "probability": 55, "expectedCloseDate": "@days:60", "priority": "high", "stage": "Qualified", "stageOrder": 3, "stageEnteredAt": "@datetime:-31", "status": "open", "description": "Demo lead: municipal intranet migration." } },
{ "key": "lead-catering", "pipelineKey": "pipeline-sales", "clientKey": "devries", "data": { "title": "[Demo] Catering jaarcontract", "source": "phone", "value": 8900, "probability": 70, "expectedCloseDate": "@days:14", "priority": "normal", "stage": "Proposal", "stageOrder": 4, "status": "open", "description": "Demo lead: yearly catering contract proposal." } },
{ "key": "lead-won", "pipelineKey": "pipeline-sales", "clientKey": "elharrak", "data": { "title": "[Demo] Onderhoudscontract gewonnen", "source": "email", "value": 5200, "probability": 100, "expectedCloseDate": "@days:-7", "priority": "normal", "stage": "Won", "stageOrder": 5, "status": "won", "description": "Demo lead: won maintenance contract." } },
{ "key": "lead-lost", "pipelineKey": "pipeline-sales", "clientKey": "bakkerij", "data": { "title": "[Demo] Kassasysteem upgrade verloren", "source": "website", "value": 6700, "probability": 0, "expectedCloseDate": "@days:-14", "priority": "low", "stage": "Lost", "stageOrder": 6, "status": "lost", "description": "Demo lead: lost POS upgrade to competitor." } }
Expand Down
Loading
Loading